77 * disk stayed on the old version, so the footer advertised
88 * "restart to apply" forever and the recorded outcome was a lie.
99 *
10- * This module answers the only question that matters after an install — does
11- * the thing that runs next report the version we installed? — and it answers
12- * it from the same artifact the source updates:
10+ * Only a `native` install is verified, and only against the artifact the
11+ * installer replaces — the packaged binary at `process.execPath`, probed with
12+ * `--version` (Commander prints and exits before any preflight runs). The
13+ * npm family is deliberately left unverified: a global reinstall rewrites the
14+ * very directory this process was loaded from, so a read there proves nothing
15+ * about the next launch and a wrong answer would park a healthy version.
1316 *
14- * - native: the packaged binary at `process.execPath`, probed with
15- * `--version` (Commander prints and exits before any preflight runs).
16- * - npm/pnpm/yarn/bun: the host `package.json`, re-read from disk.
17- * - homebrew: nothing — its update lands through the prepare-on-restart
18- * lifecycle, not through this install path.
19- *
20- * It fails **open**: an unreadable package, a probe that times out or a
21- * version string it cannot parse all report `ok`. A slow antivirus scan must
22- * never turn a good install into a recorded failure. Only a version it read
23- * successfully *and* that disagrees with the target is reported as a mismatch.
17+ * It fails **open**: a probe that times out, cannot run, or prints no version
18+ * reports `ok` with an `unverified` note for the caller to log. A slow
19+ * antivirus scan must never turn a good install into a recorded failure. Only
20+ * a version read successfully *and* disagreeing with the target is a mismatch.
2421 */
2522
2623import { execFile } from 'node:child_process' ;
27- import { readFile } from 'node:fs/promises' ;
28-
2924import { valid } from 'semver' ;
3025
31- import { findHostPackageJsonPath } from '#/cli/version' ;
32-
26+ import { formatErrorMessage } from './format-error' ;
3327import type { InstallSource } from './types' ;
3428
3529/** Bound on the `--version` probe: a native binary starts in well under this. */
3630const VERSION_PROBE_TIMEOUT_MS = 20_000 ;
3731
3832export type InstallVerification =
39- | { readonly ok : true }
33+ /** Installed as expected, or not checkable — `unverified` says which. */
34+ | { readonly ok : true ; readonly unverified ?: string }
4035 | { readonly ok : false ; readonly reason : string } ;
4136
4237export interface VerifyInstalledVersionDeps {
43- /** Path of the packaged binary to probe (native sources only). */
38+ /** Path of the packaged binary to probe (native installs only). */
4439 readonly execPath : string ;
4540 /** Runs `<exe> --version` and resolves its stdout. */
4641 readonly probeExecutableVersion : ( execPath : string ) => Promise < string > ;
47- /** Reads the installed host `package.json`, or null when there is none. */
48- readonly readPackageVersion : ( ) => Promise < string | null > ;
4942}
5043
51- const OK : InstallVerification = { ok : true } ;
44+ function unverified ( note : string ) : InstallVerification {
45+ return { ok : true , unverified : note } ;
46+ }
5247
5348/**
5449 * Extract the first `x.y.z` from a `--version` output. Commander prints the
@@ -90,13 +85,6 @@ async function defaultProbeExecutableVersion(execPath: string): Promise<string>
9085 } ) ;
9186}
9287
93- async function defaultReadPackageVersion ( ) : Promise < string | null > {
94- const path = findHostPackageJsonPath ( ) ;
95- if ( path === null ) return null ;
96- const parsed = JSON . parse ( await readFile ( path , 'utf-8' ) ) as { version ?: unknown } ;
97- return typeof parsed . version === 'string' ? parsed . version : null ;
98- }
99-
10088/**
10189 * Verify that `expectedVersion` is what an install of `source` actually left
10290 * behind. See the module comment for the fail-open rule.
@@ -106,51 +94,28 @@ export async function verifyInstalledVersion(
10694 expectedVersion : string ,
10795 overrides : Partial < VerifyInstalledVersionDeps > = { } ,
10896) : Promise < InstallVerification > {
109- if ( valid ( expectedVersion ) === null ) return OK ;
97+ if ( source !== 'native' ) return unverified ( `not verified for ${ source } installs` ) ;
98+ if ( valid ( expectedVersion ) === null ) {
99+ return unverified ( `not a version to verify against: ${ expectedVersion } ` ) ;
100+ }
110101
111- const deps : VerifyInstalledVersionDeps = {
112- execPath : overrides . execPath ?? process . execPath ,
113- probeExecutableVersion : overrides . probeExecutableVersion ?? defaultProbeExecutableVersion ,
114- readPackageVersion : overrides . readPackageVersion ?? defaultReadPackageVersion ,
115- } ;
102+ const execPath = overrides . execPath ?? process . execPath ;
103+ const probe = overrides . probeExecutableVersion ?? defaultProbeExecutableVersion ;
116104
117- switch ( source ) {
118- case 'native' : {
119- let output : string ;
120- try {
121- output = await deps . probeExecutableVersion ( deps . execPath ) ;
122- } catch {
123- return OK ;
124- }
125- const found = parseVersionOutput ( output ) ;
126- if ( found === null || sameVersion ( found , expectedVersion ) ) return OK ;
127- return {
128- ok : false ,
129- reason :
130- `the installer reported success but ${ deps . execPath } still reports ` +
131- `${ found } (expected ${ expectedVersion } )` ,
132- } ;
133- }
134- case 'npm-global' :
135- case 'pnpm-global' :
136- case 'yarn-global' :
137- case 'bun-global' : {
138- let found : string | null ;
139- try {
140- found = await deps . readPackageVersion ( ) ;
141- } catch {
142- return OK ;
143- }
144- if ( found === null || sameVersion ( found , expectedVersion ) ) return OK ;
145- return {
146- ok : false ,
147- reason :
148- `the installer reported success but the installed package is still ` +
149- `${ found } (expected ${ expectedVersion } )` ,
150- } ;
151- }
152- case 'homebrew' :
153- case 'unsupported' :
154- return OK ;
105+ let output : string ;
106+ try {
107+ output = await probe ( execPath ) ;
108+ } catch ( error ) {
109+ return unverified ( `${ execPath } could not be run: ${ formatErrorMessage ( error ) } ` ) ;
155110 }
111+
112+ const found = parseVersionOutput ( output ) ;
113+ if ( found === null ) return unverified ( `${ execPath } printed no version` ) ;
114+ if ( sameVersion ( found , expectedVersion ) ) return { ok : true } ;
115+ return {
116+ ok : false ,
117+ reason :
118+ `the installer reported success but ${ execPath } still reports ` +
119+ `${ found } (expected ${ expectedVersion } )` ,
120+ } ;
156121}
0 commit comments