Files
aiturk-hermes-ide/apps/desktop/electron/first-run-setup-gate.ts
T

175 lines
4.7 KiB
TypeScript

import path from 'node:path'
interface FirstRunSetupBackend {
activeRoot?: string
kind?: string
platform?: string
}
interface FirstRunSetupGateOptions {
isRuntimeAvailable?: (backend: FirstRunSetupBackend) => boolean
hideChoice?: () => void
log?: (message: string) => void
onStuck?: (backend: FirstRunSetupBackend, stuckAfterMs: number) => void
promptChoice?: (backend: FirstRunSetupBackend) => void
stuckAfterMs?: number
}
export type FirstRunSetupDecision = 'continue-local' | 'remote-applied' | 'reset' | 'runtime-ready'
export function createFirstRunSetupGate({
isRuntimeAvailable,
hideChoice,
log,
onStuck,
promptChoice,
stuckAfterMs = 120000
}: FirstRunSetupGateOptions = {}) {
let localBootstrapConfirmed = false
let waiter: {
activeRoot?: string
promise: Promise<FirstRunSetupDecision>
resolve: (decision: FirstRunSetupDecision) => void
} | null = null
let stuckTimer: ReturnType<typeof setTimeout> | null = null
const clearStuckTimer = () => {
if (stuckTimer) {
clearTimeout(stuckTimer)
stuckTimer = null
}
}
const armStuckTimer = (backend: FirstRunSetupBackend) => {
clearStuckTimer()
if (!Number.isFinite(stuckAfterMs) || stuckAfterMs <= 0 || typeof log !== 'function') {
return
}
stuckTimer = setTimeout(() => {
onStuck?.(backend, stuckAfterMs)
log(
`[bootstrap] still waiting for first-run setup choice after ${Math.round(stuckAfterMs / 1000)}s ` +
`(platform=${backend?.platform || 'unknown'})`
)
}, stuckAfterMs)
if (typeof stuckTimer.unref === 'function') {
stuckTimer.unref()
}
}
const shouldGate = (backend?: FirstRunSetupBackend | null) =>
Boolean(backend && backend.kind === 'bootstrap-needed' && !localBootstrapConfirmed)
const wait = async (backend?: FirstRunSetupBackend | null) => {
if (!shouldGate(backend)) {
return 'continue-local' as const
}
// Recheck synchronously before registering the waiter: another profile
// may have completed installation before this stale descriptor arrived.
if (backend && isRuntimeAvailable?.(backend)) {
if (backend.activeRoot) runtimeAvailable(backend.activeRoot)
return 'runtime-ready' as const
}
if (waiter) {
return waiter.promise
}
promptChoice?.(backend)
armStuckTimer(backend)
let resolveWaiter: (decision: FirstRunSetupDecision) => void = () => {}
const promise = new Promise<FirstRunSetupDecision>(resolve => {
resolveWaiter = resolve
})
waiter = { activeRoot: backend?.activeRoot, promise, resolve: resolveWaiter }
return promise
}
const settleWaiter = (decision: FirstRunSetupDecision) => {
clearStuckTimer()
if (!waiter) {
return false
}
const activeWaiter = waiter
waiter = null
activeWaiter.resolve(decision)
return true
}
const continueLocal = () => {
localBootstrapConfirmed = true
settleWaiter('continue-local')
}
// A sibling profile may finish installing the SAME shared runtime while the
// primary boot is awaiting a setup choice. Re-resolve it; do not interpret
// this as permission for another installation, or reuse the sibling's
// profile/backend descriptor.
const runtimeAvailable = (root: string) => {
const canonical = (value: string) => {
const absolute = path.resolve(value)
return process.platform === 'win32' ? absolute.toLowerCase() : absolute
}
if (!waiter?.activeRoot || canonical(waiter.activeRoot) !== canonical(root)) return false
settleWaiter('runtime-ready')
hideChoice?.()
return true
}
const resetForRetry = () => {
// Reset paths are followed by a renderer reload / fresh startHermes() call.
// Settle the old boot explicitly so it cannot fall through into local
// bootstrap and cannot leak a forever-pending connection promise.
settleWaiter('reset')
}
const resetForRepair = () => {
resetForRetry()
localBootstrapConfirmed = false
}
const abandonForRemoteApply = () => {
// Resume the gated startHermes() with an explicit remote decision. The
// caller re-resolves the newly-persisted remote config instead of falling
// through into local bootstrap or leaking the original connection promise.
const resumedWaiter = settleWaiter('remote-applied')
if (!resumedWaiter) {
return false
}
localBootstrapConfirmed = false
hideChoice?.()
return true
}
const isLocalBootstrapConfirmed = () => localBootstrapConfirmed
const hasWaiter = () => Boolean(waiter)
return {
abandonForRemoteApply,
continueLocal,
hasWaiter,
isLocalBootstrapConfirmed,
resetForRepair,
resetForRetry,
runtimeAvailable,
shouldGate,
wait
}
}