93 lines
2.9 KiB
TypeScript
93 lines
2.9 KiB
TypeScript
import type { OpencodeStatus } from './manager';
|
|
|
|
type ProjectConfigReadResult = {
|
|
status: 'valid' | 'missing' | 'invalid';
|
|
config?: { initialized?: boolean };
|
|
};
|
|
|
|
export type OpencodeStartupWarmupSkipReason =
|
|
| 'no-session'
|
|
| 'runtime-active'
|
|
| 'no-active-project'
|
|
| 'project-not-ready'
|
|
| 'no-provider'
|
|
| 'eligibility-check-failed'
|
|
| 'start-failed';
|
|
|
|
export type OpencodeStartupWarmupResult =
|
|
| { started: true; status: OpencodeStatus }
|
|
| { started: false; reason: OpencodeStartupWarmupSkipReason; error?: unknown };
|
|
|
|
export interface OpencodeStartupWarmupDependencies {
|
|
hasAuthenticatedSession: () => boolean;
|
|
getStatus: () => OpencodeStatus;
|
|
getActiveProject: () => Promise<{ path: string } | null>;
|
|
readProjectConfig: (projectPath: string) => Promise<ProjectConfigReadResult>;
|
|
getConfiguredProviderCount: () => Promise<number>;
|
|
start: () => Promise<OpencodeStatus>;
|
|
onError?: (error: unknown, phase: 'eligibility' | 'start') => void;
|
|
}
|
|
|
|
function reportError(
|
|
dependencies: OpencodeStartupWarmupDependencies,
|
|
error: unknown,
|
|
phase: 'eligibility' | 'start',
|
|
): void {
|
|
try {
|
|
dependencies.onError?.(error, phase);
|
|
} catch {
|
|
// Logging must never turn a background warmup into an unhandled rejection.
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Starts the local runtime in the background when the persisted app state is
|
|
* ready for a Code session. This deliberately does not throw: startup
|
|
* warmup is an optimization and the normal Chat-page lazy start remains the
|
|
* recovery path when it is unavailable.
|
|
*/
|
|
export async function warmupOpencodeRuntime(
|
|
dependencies: OpencodeStartupWarmupDependencies,
|
|
): Promise<OpencodeStartupWarmupResult> {
|
|
if (!dependencies.hasAuthenticatedSession()) {
|
|
return { started: false, reason: 'no-session' };
|
|
}
|
|
|
|
const status = dependencies.getStatus();
|
|
if (status.state !== 'stopped') {
|
|
return { started: false, reason: 'runtime-active' };
|
|
}
|
|
|
|
let project: { path: string } | null;
|
|
let projectConfig: ProjectConfigReadResult;
|
|
let providerCount: number;
|
|
try {
|
|
project = await dependencies.getActiveProject();
|
|
if (!project) {
|
|
return { started: false, reason: 'no-active-project' };
|
|
}
|
|
|
|
projectConfig = await dependencies.readProjectConfig(project.path);
|
|
if (projectConfig.status !== 'valid' || projectConfig.config?.initialized !== true) {
|
|
return { started: false, reason: 'project-not-ready' };
|
|
}
|
|
|
|
providerCount = await dependencies.getConfiguredProviderCount();
|
|
} catch (error) {
|
|
reportError(dependencies, error, 'eligibility');
|
|
return { started: false, reason: 'eligibility-check-failed', error };
|
|
}
|
|
|
|
if (!Number.isFinite(providerCount) || providerCount <= 0) {
|
|
return { started: false, reason: 'no-provider' };
|
|
}
|
|
|
|
try {
|
|
const startedStatus = await dependencies.start();
|
|
return { started: true, status: startedStatus };
|
|
} catch (error) {
|
|
reportError(dependencies, error, 'start');
|
|
return { started: false, reason: 'start-failed', error };
|
|
}
|
|
}
|