mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-10 22:20:14 +00:00
fix(cli): upgrade and update notice use the detected install method
* fix(cli): upgrade + update-notice use the detected install method
hyperframes upgrade hardcoded 'npm install -g', so bun/pnpm/brew users either
saw it fail or silently got a shadowed npm copy while their real (older) binary
kept running. Route the install through detectInstaller() via a new
installInvocation() argv helper; for skip kinds (ephemeral npx/bunx,
project-local, unknown) print 'npx hyperframes@latest' instead of guessing.
The passive update notice now shows the detected manager's command too. Semver
safety guard consolidated into a shared isSafeVersion(). Suppression gates and
the background auto-update flow are unchanged.
* test(cli): pin the shell:false contract of the --yes install path
Export runDetectedInstall and add a mocked-execFileSync test asserting the
detected manager binary is spawned with the exact installInvocation argv,
{stdio:inherit, shell:false}, and that an install failure sets a non-zero exit
code without throwing. Addresses review nit on the untested --yes path.
* fix(cli): guard the registry version at the boundary; execFile the auto-installer
Security (addresses review): a poisoned registry data.version (e.g.
'1.2.3; rm -rf /') was cached unvalidated and flowed into the background
auto-updater, which ran it via exec() -- a shell -- so a registry compromise
meant RCE on the next CLI run. isSafeVersion only covered the two touched
consumers (upgrade, notice), not this third sibling (scheduleBackgroundInstall).
- Guard at the registry boundary in checkForUpdate: only a strict-semver STRING
is trusted; a non-string or metachar-bearing data.version is never cached and
falls back to the last known-good version. The cache-read and fallback paths
re-validate too, so a pre-existing poisoned cache can't leak through. One gate
closes all three consumers and any future one; per-consumer checks stay as
defense in depth.
- The detached auto-installer now runs via execFile(bin, args, shell:false),
reusing installInvocation, matching the interactive runDetectedInstall path --
the shell is gone from that path entirely.
Tests: reject poisoned / non-string registry version (never cached); accept a
valid semver.
This commit is contained in:
@@ -156,3 +156,34 @@ export function detectInstaller(): InstallerInfo {
|
||||
reason: `Unknown install layout at ${realEntry}`,
|
||||
};
|
||||
}
|
||||
|
||||
/** Argv-shaped install command for a no-shell `execFile`. */
|
||||
export interface InstallInvocation {
|
||||
bin: string;
|
||||
args: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* The argv form of {@link InstallerInfo.installCommand}, kept next to the
|
||||
* detector so the command we *run* (execFile, no shell) and the command we
|
||||
* *display* (installCommand string) can never drift. Returns `null` for `skip`
|
||||
* kinds (ephemeral npx/bunx, workspace links, project-local, unknown layouts):
|
||||
* the caller must print a manual instruction rather than run a guessed command
|
||||
* (running the wrong manager is worse than running nothing).
|
||||
*/
|
||||
export function installInvocation(kind: InstallerKind, version: string): InstallInvocation | null {
|
||||
switch (kind) {
|
||||
case "npm":
|
||||
return { bin: "npm", args: ["install", "-g", `hyperframes@${version}`] };
|
||||
case "bun":
|
||||
return { bin: "bun", args: ["add", "-g", `hyperframes@${version}`] };
|
||||
case "pnpm":
|
||||
return { bin: "pnpm", args: ["add", "-g", `hyperframes@${version}`] };
|
||||
case "brew":
|
||||
// brew has no per-version install; `brew upgrade` moves to the tap's
|
||||
// current formula (a no-op if the tap hasn't caught up).
|
||||
return { bin: "brew", args: ["upgrade", "hyperframes"] };
|
||||
case "skip":
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user