fix(desktop): show 'hermes update' guidance for CLI installs instead of dead-end error

A user who installed via the CLI (irm|iex / install.sh) then ran
`hermes desktop` has no staged hermes-setup.exe, so clicking Update
in-app hit resolveUpdaterBinary()=null and showed a misleading error
('re-run the Hermes installer') with a Try-again button that could
never succeed — a dead loop for a perfectly valid install.

Treat the no-updater case as an intentional outcome, not a failure:
- main.cjs applyUpdates returns { ok:true, manual:true, command:'hermes update' }
  (no throw, no 'error' stage) when no updater binary exists.
- New 'manual' update stage + apply-state.command thread the command to the UI.
- updates-overlay ManualView: a polished terminal-native card with the
  exact command and a copy button, framed as the correct path for a CLI
  user rather than an error.

GUI-installer users are unaffected — hermes-setup.exe present => seamless
auto-update runs as before. Zero new process orchestration; can't fail
the update demo.
This commit is contained in:
emozilla
2026-05-29 01:53:43 -04:00
parent 006136c4ab
commit ce31ec09b9
4 changed files with 125 additions and 14 deletions
+29 -3
View File
@@ -22,10 +22,21 @@ export interface UpdateApplyState {
message: string
percent: number | null
error: string | null
/** When the stage is 'manual': the exact command the user should run
* (CLI install with no staged updater). */
command: string | null
log: readonly { stage: DesktopUpdateStage; message: string; at: number }[]
}
const IDLE: UpdateApplyState = { applying: false, stage: 'idle', message: '', percent: null, error: null, log: [] }
const IDLE: UpdateApplyState = {
applying: false,
stage: 'idle',
message: '',
percent: null,
error: null,
command: null,
log: []
}
export const $desktopVersion = atom<DesktopVersionInfo | null>(null)
export const $updateApply = atom<UpdateApplyState>(IDLE)
@@ -142,7 +153,20 @@ export async function applyUpdates(opts: DesktopUpdateApplyOptions = {}): Promis
$updateApply.set({ ...IDLE, applying: true, stage: 'prepare', message: 'Starting update…' })
try {
return await bridge.apply(opts)
const result = await bridge.apply(opts)
// CLI install with no staged updater: not an error — the user just runs
// `hermes update` themselves. Land on a dedicated manual state so the
// overlay shows the command + copy button instead of a dead retry loop.
if (result?.manual) {
$updateApply.set({
...IDLE,
applying: false,
stage: 'manual',
message: result.command ?? 'hermes update',
command: result.command ?? 'hermes update'
})
}
return result
} catch (error) {
const message = error instanceof Error ? error.message : String(error)
$updateApply.set({ ...$updateApply.get(), applying: false, stage: 'error', error: 'apply-failed', message })
@@ -154,7 +178,7 @@ export async function applyUpdates(opts: DesktopUpdateApplyOptions = {}): Promis
function ingestProgress(payload: DesktopUpdateProgress): void {
const current = $updateApply.get()
const log = [...current.log, { stage: payload.stage, message: payload.message, at: payload.at }].slice(-50)
const terminal = payload.stage === 'error' || payload.stage === 'restart'
const terminal = payload.stage === 'error' || payload.stage === 'restart' || payload.stage === 'manual'
$updateApply.set({
applying: !terminal,
@@ -162,6 +186,8 @@ function ingestProgress(payload: DesktopUpdateProgress): void {
message: payload.message,
percent: payload.percent,
error: payload.error,
// 'manual' carries the command to run in its message field.
command: payload.stage === 'manual' ? payload.message : current.command,
log
})
}