@linxin666/dsh-remote-web-ui 0.1.15 → 0.1.16

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/src/update.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  * Remote update support for the dsh-web-ui family — host half. Detects the
3
3
  * installed aggregate package (@linxin666/dsh-web-ui-all) and its family
4
4
  * children, probes the npm registry for newer releases, and runs the actual
5
- * update as `pnpm update` inside the owning dsh profile directory.
5
+ * update as `pnpm update --latest` inside the owning dsh profile directory.
6
6
  *
7
7
  * Pure logic with injected seams (manifest reading, registry fetches, process
8
8
  * spawning) so the whole surface is unit-testable without touching disk,
@@ -184,16 +184,19 @@ export interface UpdateCheckDeps {
184
184
 
185
185
  /**
186
186
  * Resolve the anchor package's manifest path. The aggregate package is the
187
- * canonical entry point; this plugin's own package is the fallback.
187
+ * canonical entry point; this plugin's own package is the fallback. Both a
188
+ * throwing resolve and an undefined return mean "not installed" and move on
189
+ * to the next candidate.
188
190
  * @param resolve - a Node resolve implementation scoped to the host process.
189
191
  * @returns the absolute manifest path, or undefined when neither is installed.
190
192
  */
191
193
  export function resolveAnchorManifest(
192
- resolve: (specifier: string) => string,
194
+ resolve: (specifier: string) => string | undefined,
193
195
  ): string | undefined {
194
196
  for (const name of [AGGREGATE_PACKAGE, SELF_PACKAGE]) {
195
197
  try {
196
- return resolve(name + '/package.json')
198
+ const path = resolve(name + '/package.json')
199
+ if (path !== undefined) return path
197
200
  } catch {
198
201
  // Not installed — try the next candidate.
199
202
  }
@@ -280,14 +283,22 @@ export async function fetchLatestVersion(
280
283
  }
281
284
  }
282
285
 
286
+ /**
287
+ * Sentinel for an installed version that could not be read (missing manifest
288
+ * or resolve failure). It is a real-looking version so `checkUpdates` can
289
+ * render the affected row, but the verified-update comparison must never
290
+ * treat it as evidence — a read failure is not a version that "moved".
291
+ */
292
+ const VERSION_UNKNOWN = '0.0.0'
293
+
283
294
  /** The resolved current version of one family package (probe failure tolerated). */
284
295
  function readInstalledVersion(resolve: (specifier: string) => string | undefined, name: string): string {
285
296
  try {
286
297
  const path = resolve(name + '/package.json')
287
298
  const version = path === undefined ? undefined : readManifest(path)?.version
288
- return typeof version === 'string' ? version : '0.0.0'
299
+ return typeof version === 'string' ? version : VERSION_UNKNOWN
289
300
  } catch {
290
- return '0.0.0'
301
+ return VERSION_UNKNOWN
291
302
  }
292
303
  }
293
304
 
@@ -359,6 +370,10 @@ export type UpdateErrorCode =
359
370
  | 'link'
360
371
  /** pnpm exited non-zero. */
361
372
  | 'pnpm-failed'
373
+ /** pnpm exited 0 but the installed versions did not move. */
374
+ | 'stale'
375
+ /** pnpm exited 0 but the post-run version check could not verify the install. */
376
+ | 'verify-failed'
362
377
 
363
378
  /** Result of one update run. */
364
379
  export interface UpdateRunResult {
@@ -419,10 +434,14 @@ export function runUpdate(deps: UpdateRunDeps): Promise<UpdateRunResult> {
419
434
  }
420
435
  // Ordered fallback chain: each is tried only when the previous one is
421
436
  // missing on PATH (ENOENT on the spawn error event, never on close).
437
+ // `--latest` is required: dsh plugin writes exact-version specs (e.g.
438
+ // "0.1.12" without a range), and plain `pnpm update` treats an exact spec
439
+ // as pinned — it prints "Already up to date" and exits 0 without moving
440
+ // the installed version, so the panel would report a false success.
422
441
  const candidates: ReadonlyArray<{ command: string; args: string[] }> = [
423
- { command: 'pnpm', args: ['update', ...packages] },
424
- { command: 'corepack', args: ['pnpm', 'update', ...packages] },
425
- { command: 'npx', args: ['--yes', 'pnpm', 'update', ...packages] },
442
+ { command: 'pnpm', args: ['update', '--latest', ...packages] },
443
+ { command: 'corepack', args: ['pnpm', 'update', '--latest', ...packages] },
444
+ { command: 'npx', args: ['--yes', 'pnpm', 'update', '--latest', ...packages] },
426
445
  ]
427
446
  // `output` accumulates across candidates for UI display; `currentOutput`
428
447
  // is reset per candidate and carries only that candidate's own diagnostics
@@ -523,3 +542,103 @@ export function runUpdate(deps: UpdateRunDeps): Promise<UpdateRunResult> {
523
542
  runCandidate(0)
524
543
  })
525
544
  }
545
+
546
+ /** Seam set for the verified update run (run + post-run status check). */
547
+ export interface UpdateRunVerifiedDeps {
548
+ /** The pnpm run (profile dir + package list). */
549
+ run: UpdateRunDeps
550
+ /** The post-run status check (same seams as checkUpdates). */
551
+ check: UpdateCheckDeps
552
+ }
553
+
554
+ /**
555
+ * Run the update, then verify the installed versions actually moved. pnpm
556
+ * exits 0 even when it silently kept the installed versions — the pnpm 11
557
+ * `minimumReleaseAge` gate refuses same-day releases by default — so a green
558
+ * exit alone would report a misleading "update complete". Re-read the
559
+ * installed versions afterwards and surface a `stale` failure (with the
560
+ * captured pnpm output) when nothing moved, so the panel can tell the user
561
+ * how to unblock the gate instead of claiming success.
562
+ *
563
+ * The stale decision anchors on the pre-run installed versions, not on the
564
+ * registry latest: under a lenient gate pnpm may move 0.1.12 -> 0.1.13 while
565
+ * latest stays 0.1.15 — that is a real update, not stale. The post-run anchor
566
+ * path is re-resolved rather than reused from boot time: pnpm removes the old
567
+ * version's .pnpm directory on update, so a boot-time captured path would
568
+ * fail to read and collapse a successful update into a bogus 'missing'.
569
+ *
570
+ * A green exit whose verification has nothing to compare (registry probe
571
+ * outage, missing anchor, non-npm mode) is reported as `verify-failed` — it
572
+ * must never read as success. A green exit where no package moved but the
573
+ * post-run check could not prove the install is fully current (a partial
574
+ * probe failure hides whether a gate kept something back) is also
575
+ * `verify-failed`: it must not collapse into a success either.
576
+ * @param deps - the run and check seams.
577
+ * @returns the run result; `stale` when exit 0 left every version in place,
578
+ * `verify-failed` when the post-run check could not verify anything.
579
+ */
580
+ export async function runUpdateVerified(deps: UpdateRunVerifiedDeps): Promise<UpdateRunResult> {
581
+ // Pre-run snapshot: the installed versions the update starts from. Only
582
+ // successfully-read versions are recorded — a read failure (VERSION_UNKNOWN)
583
+ // is not a baseline and must not look like a version that "moved", or a
584
+ // green exit that left everything in place could be mistaken for success.
585
+ const before = new Map<string, string>()
586
+ for (const name of deps.run.packages) {
587
+ const version = readInstalledVersion(deps.check.resolve, name)
588
+ if (version !== VERSION_UNKNOWN) before.set(name, version)
589
+ }
590
+ const result = await runUpdate(deps.run)
591
+ if (!result.ok) return result
592
+ // Re-resolve the anchor after the run: pnpm removes the old version's
593
+ // .pnpm directory, so a boot-time captured path no longer reads. Fall back
594
+ // to the provided path only when nothing resolves now.
595
+ const anchorManifestPath = resolveAnchorManifest(deps.check.resolve) ?? deps.check.anchorManifestPath
596
+ const status = await checkUpdates({ ...deps.check, anchorManifestPath })
597
+ // A green exit with no comparable result is not a success: registry probe
598
+ // outage, missing anchor, or a non-npm install mode all mean the post-run
599
+ // check could not verify anything.
600
+ if (status.error !== undefined || status.mode !== 'npm') {
601
+ return {
602
+ ...result,
603
+ ok: false,
604
+ errorCode: 'verify-failed',
605
+ error: 'pnpm exited 0 but the post-run version check could not verify the install',
606
+ }
607
+ }
608
+ // A package "moved" only when a known pre-run version differs from a known
609
+ // post-run version. A read failure on either side (VERSION_UNKNOWN) is not
610
+ // evidence of movement and must not turn a no-op update into a success; a
611
+ // package without a pre-run baseline is ignored — only the packages pnpm
612
+ // was told to update count as evidence.
613
+ const moved = status.packages.some(packageStatus => {
614
+ const beforeVersion = before.get(packageStatus.name)
615
+ if (beforeVersion === undefined || packageStatus.current === VERSION_UNKNOWN) return false
616
+ return packageStatus.current !== beforeVersion
617
+ })
618
+ if (moved) return result
619
+ // Nothing moved. A green exit is only a true no-op when the post-run check
620
+ // proves the install is already current: every probe succeeded and nothing
621
+ // is outdated. If a probe failed we cannot tell "already up to date" from
622
+ // "a gate silently kept it back", so report verify-failed rather than a
623
+ // false success.
624
+ if (status.outdated) {
625
+ // stale: the check ran, a newer release exists, and no package moved
626
+ // (e.g. the minimumReleaseAge gate kept everything in place).
627
+ return {
628
+ ...result,
629
+ ok: false,
630
+ errorCode: 'stale',
631
+ error: 'pnpm exited 0 but the installed versions did not change',
632
+ }
633
+ }
634
+ const unverifiable = status.packages.some(packageStatus => packageStatus.latest === undefined)
635
+ if (unverifiable) {
636
+ return {
637
+ ...result,
638
+ ok: false,
639
+ errorCode: 'verify-failed',
640
+ error: 'pnpm exited 0 but the post-run version check could not verify the install',
641
+ }
642
+ }
643
+ return result
644
+ }