dsh-plugin-syncup 4.0.0 → 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/lib/sync.js +142 -13
- package/package.json +1 -1
package/lib/sync.js
CHANGED
|
@@ -7,8 +7,9 @@ import { emptyManifest, localHostEntry, mergeHost, pickInstallSpec, summarizeMan
|
|
|
7
7
|
import { buildPreview, selectPreviewEntries } from './preview.js'
|
|
8
8
|
import { applyPushPlan, buildPushPlan } from './pushplan.js'
|
|
9
9
|
import { manualUpdateInstructions, resolveProfileDir } from './manual.js'
|
|
10
|
+
import { parseVersion } from './spec.js'
|
|
10
11
|
import { readState, resolveStatePath, writeState } from './state.js'
|
|
11
|
-
import { createVersionChecker, gitHubTargetOf, registryNameOf } from './versions.js'
|
|
12
|
+
import { createVersionChecker, gitHubTargetOf, isPrereleaseVersion, registryNameOf } from './versions.js'
|
|
12
13
|
|
|
13
14
|
const PREVIEW_TTL_MS = 10 * 60 * 1000
|
|
14
15
|
const MAX_HISTORY = 20
|
|
@@ -228,6 +229,94 @@ export function createSyncRuntime({
|
|
|
228
229
|
}
|
|
229
230
|
}
|
|
230
231
|
|
|
232
|
+
/**
|
|
233
|
+
* Replace an installed registry bundle through the plugin manager's own update
|
|
234
|
+
* path, when this DSH version has one.
|
|
235
|
+
*
|
|
236
|
+
* dsh 0.2.2 added `updateBundle(name, version)`: it checks the target on the
|
|
237
|
+
* registry, removes the running copy under HMR, installs the new version with
|
|
238
|
+
* the same selection, and reinstalls the old version when that fails. That is
|
|
239
|
+
* exactly the remove-then-install dance this plugin used to perform by hand,
|
|
240
|
+
* but done atomically inside the manager, so it is preferred whenever it is
|
|
241
|
+
* available and applies.
|
|
242
|
+
*
|
|
243
|
+
* It only handles bundles installed from a registry under their own name, and
|
|
244
|
+
* requires a strictly newer semver. Anything else returns `null` and the caller
|
|
245
|
+
* falls back to the generic install path.
|
|
246
|
+
*/
|
|
247
|
+
function registryUpdateFor(name, version) {
|
|
248
|
+
if (typeof pluginManager?.updateBundle !== 'function') return null
|
|
249
|
+
const wanted = String(version ?? '').trim()
|
|
250
|
+
if (wanted === '' || parseVersion(wanted) === null || isPrereleaseVersion(wanted)) return null
|
|
251
|
+
return { name, version: wanted }
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** The exact version inside `name@1.2.3`, or `''` for any other spec shape. */
|
|
255
|
+
function versionFromSpec(spec) {
|
|
256
|
+
if (typeof spec !== 'string' || /[:\s]/.test(spec)) return ''
|
|
257
|
+
const at = spec.lastIndexOf('@')
|
|
258
|
+
if (at <= 0) return ''
|
|
259
|
+
const version = spec.slice(at + 1)
|
|
260
|
+
return parseVersion(version) === null ? '' : version
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
async function updateViaManager({ name, version, job }) {
|
|
264
|
+
jobLog(job, 'info', `${name}: updating to ${version} through the plugin manager`)
|
|
265
|
+
try {
|
|
266
|
+
const outcome = await pluginManager.updateBundle(name, version)
|
|
267
|
+
const step = outcome?.step ?? 'update'
|
|
268
|
+
const result = outcome?.result ?? outcome
|
|
269
|
+
const failure = step === 'install' || step === 'remove' || step === 'check'
|
|
270
|
+
? changeError(result ?? {})
|
|
271
|
+
: null
|
|
272
|
+
if (failure !== null) {
|
|
273
|
+
jobLog(job, 'error', `${name}: update failed — ${failure.message}`)
|
|
274
|
+
return {
|
|
275
|
+
name,
|
|
276
|
+
spec: `${name}@${version}`,
|
|
277
|
+
status: 'failed',
|
|
278
|
+
error: failure.message,
|
|
279
|
+
...(failure.code ? { code: failure.code } : {}),
|
|
280
|
+
...(outcome?.restore === undefined ? {} : { rollback: 'failed' }),
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
if (result?.application === 'failed' || result?.application === 'cancelled') {
|
|
284
|
+
jobLog(job, 'error', `${name}: update ${result.application}`)
|
|
285
|
+
return {
|
|
286
|
+
name,
|
|
287
|
+
spec: `${name}@${version}`,
|
|
288
|
+
status: 'failed',
|
|
289
|
+
error: `the plugin manager ${result.application} the update`,
|
|
290
|
+
...(outcome?.restore === undefined ? {} : { rollback: 'failed' }),
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
const restored = outcome?.restore !== undefined && outcome.restore.application !== 'failed'
|
|
294
|
+
jobLog(
|
|
295
|
+
job,
|
|
296
|
+
'info',
|
|
297
|
+
`${name}: ${restored ? 'restored the previous version' : 'updated'} to ${version}`
|
|
298
|
+
+ `${result?.application === 'restart-required' ? ' (restart required)' : ''}`,
|
|
299
|
+
)
|
|
300
|
+
return {
|
|
301
|
+
name,
|
|
302
|
+
spec: `${name}@${version}`,
|
|
303
|
+
status: 'installed',
|
|
304
|
+
version,
|
|
305
|
+
...(outcome?.from === undefined ? {} : { replaced: outcome.from }),
|
|
306
|
+
...(outcome?.from === undefined ? {} : { upgraded: true }),
|
|
307
|
+
...(restored ? { rollback: 'restored' } : {}),
|
|
308
|
+
...(result?.application === 'restart-required' ? { restartRequired: true } : {}),
|
|
309
|
+
...(Array.isArray(result?.warnings) && result.warnings.length > 0
|
|
310
|
+
? { warnings: result.warnings.slice(0, 10) }
|
|
311
|
+
: {}),
|
|
312
|
+
}
|
|
313
|
+
} catch (error) {
|
|
314
|
+
const info = errorInfo(error)
|
|
315
|
+
jobLog(job, 'error', `${name}: update failed — ${info.message}`)
|
|
316
|
+
return { name, spec: `${name}@${version}`, status: 'failed', ...info }
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
231
320
|
/**
|
|
232
321
|
* Install a bundle, replacing an installed copy when the manager refuses it.
|
|
233
322
|
*
|
|
@@ -276,14 +365,36 @@ export function createSyncRuntime({
|
|
|
276
365
|
|
|
277
366
|
const known = await findManualUpdate(name)
|
|
278
367
|
if (known !== null) {
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
368
|
+
// A remembered failure predates this DSH's own update path. When the manager
|
|
369
|
+
// can now replace the bundle itself, that memory is stale — retry instead of
|
|
370
|
+
// skipping forever.
|
|
371
|
+
const retry = registryUpdateFor(name, versionFromSpec(spec))
|
|
372
|
+
if (retry === null) {
|
|
373
|
+
jobLog(job, 'warn', `${name}: previously required a manual update; skipping`)
|
|
374
|
+
return {
|
|
375
|
+
...first,
|
|
376
|
+
code: 'manual-update',
|
|
377
|
+
error: `${first.error ?? 'already-installed'} This bundle could not be unloaded by HMR before (${known.at}), so it was left untouched.`,
|
|
378
|
+
hint: 'manual-terminal',
|
|
379
|
+
manual: instructionsFor(name, spec),
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
jobLog(job, 'info', `${name}: the plugin manager can update it now; retrying instead of skipping`)
|
|
383
|
+
await forgetManualUpdate(name)
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// The manager's own update path handles the swap atomically; prefer it.
|
|
387
|
+
const native = registryUpdateFor(name, versionFromSpec(spec))
|
|
388
|
+
if (native !== null) {
|
|
389
|
+
const updated = await updateViaManager({ ...native, job })
|
|
390
|
+
if (updated.status !== 'failed' && updated.rollback === undefined) {
|
|
391
|
+
await clearPendingUpgrade(name)
|
|
392
|
+
await forgetManualUpdate(name)
|
|
393
|
+
return { ...updated, upgraded: updated.rollback === undefined, replaced: record.fromVersion || record.fromSpec }
|
|
286
394
|
}
|
|
395
|
+
// A failed native update already restored the previous version; report it
|
|
396
|
+
// rather than trying the manual dance on top.
|
|
397
|
+
return updated
|
|
287
398
|
}
|
|
288
399
|
|
|
289
400
|
jobLog(job, 'warn', `${name}: already installed; replacing ${record.fromVersion || record.fromSpec} with ${spec}`)
|
|
@@ -1163,11 +1274,20 @@ export function createSyncRuntime({
|
|
|
1163
1274
|
return raw === 'npm' || raw === 'github' ? raw : null
|
|
1164
1275
|
}
|
|
1165
1276
|
|
|
1166
|
-
// One job for the whole action: the
|
|
1167
|
-
//
|
|
1277
|
+
// One job for the whole action: the check writes its lines into it, so the
|
|
1278
|
+
// page shows a single log whose counts are the selected bundles.
|
|
1168
1279
|
const job = jobs.start('apply-latest', { total: selected.length })
|
|
1169
|
-
|
|
1170
|
-
|
|
1280
|
+
// Reuse the result the user just looked at. Re-checking here made an update
|
|
1281
|
+
// look like "check again, change nothing": the registry was queried a second
|
|
1282
|
+
// time and the row's outcome was recomputed instead of acted on. Only ask
|
|
1283
|
+
// for entries the cache does not already cover.
|
|
1284
|
+
const cached = latestCache === null ? null : latestCache.result
|
|
1285
|
+
const covered = new Set((cached?.entries ?? []).map((entry) => entry.name))
|
|
1286
|
+
const missing = selected.filter((name) => !covered.has(name))
|
|
1287
|
+
const checked = missing.length === 0 && cached !== null
|
|
1288
|
+
? cached
|
|
1289
|
+
: await runLatestCheck({ signal, only: missing.length === 0 ? selected : missing, job })
|
|
1290
|
+
const byName = new Map([...(cached?.entries ?? []), ...checked.entries].map((entry) => [entry.name, entry]))
|
|
1171
1291
|
const targeted = []
|
|
1172
1292
|
const skipped = []
|
|
1173
1293
|
for (const name of selected) {
|
|
@@ -1232,7 +1352,16 @@ export function createSyncRuntime({
|
|
|
1232
1352
|
continue
|
|
1233
1353
|
}
|
|
1234
1354
|
jobLog(job, 'info', `${entry.name}: installing ${target} (${chosenChannel ?? 'auto'})`)
|
|
1235
|
-
|
|
1355
|
+
// Prefer the manager's own update path: it removes, installs, and restores
|
|
1356
|
+
// on failure inside one call, which is exactly what the manual dance below
|
|
1357
|
+
// approximates. It only applies to a registry package updated to a strictly
|
|
1358
|
+
// newer version, so anything else falls through.
|
|
1359
|
+
const nativeUpdate = chosenChannel === null || chosenChannel === 'npm'
|
|
1360
|
+
? registryUpdateFor(entry.name, entry.channels?.npm ?? entry.npmLatest ?? null)
|
|
1361
|
+
: null
|
|
1362
|
+
const summary = nativeUpdate !== null
|
|
1363
|
+
? await updateViaManager({ ...nativeUpdate, job })
|
|
1364
|
+
: await installOrUpgrade({ name: entry.name, spec: target, previous: current, job })
|
|
1236
1365
|
if (summary.status === 'failed') {
|
|
1237
1366
|
failed.push({
|
|
1238
1367
|
name: entry.name,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin-syncup",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.1.0",
|
|
4
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
5
|
"type": "module",
|
|
6
6
|
"icon": "icon.svg",
|