openpetswithchatandmcp/apps/desktop/src/app-state.ts
2026-06-19 07:56:56 +00:00

247 lines
7.2 KiB
TypeScript

import { rmSync } from "node:fs";
import { join } from "node:path";
import { app } from "electron";
import { defaultPetScale, markOnboardingCompleted, normalizeFamiliarName, normalizePetScale, petScaleOptions, type PetScaleValue } from "./app-state-core.js";
import {
buildRecordedActivityAnalytics,
type FamiliarOSActivityRecord,
} from "./app-state-analytics.js";
import {
cloneState,
normalizeState,
normalizePosition,
type FamiliarOSStateV1,
type InstalledPetState,
} from "./app-state-normalization.js";
import { builtInPet } from "./built-in-familiar.js";
import type { Point } from "./display.js";
import {
normalizePreferences,
} from "./app-state-preferences.js";
import { publishPluginAgentActivity } from "./plugin-events-source.js";
import {
acquireStartupInstallLock,
migrateLegacyStateFilenames,
readStateJsonFile,
writeStateJsonFile,
} from "./app-state-storage.js";
export { defaultOpenApiChatEndpoint, normalizeOpenApiChatEndpoint } from "./app-state-preferences.js";
export type { FamiliarOSActivityRecord, FamiliarOSAnalyticsState } from "./app-state-analytics.js";
export type { FamiliarOSStateV1, InstalledPetState } from "./app-state-normalization.js";
export type { FamiliarOSPreferences } from "./app-state-preferences.js";
export { defaultPetScale, normalizeFamiliarName, normalizePetScale, petScaleOptions, type PetScaleValue };
const stateFileName = "familiaros-state.json";
let statePath: string | null = null;
let currentState: FamiliarOSStateV1 | null = null;
let startupInstallLockPath: string | null = null;
export function initializeAppState(): void {
const userDataPath = app.getPath("userData");
migrateLegacyStateFilenames(userDataPath);
startupInstallLockPath = acquireStartupInstallLock(userDataPath);
statePath = join(userDataPath, stateFileName);
const nextState = normalizeState(readStateJsonFile(statePath));
writeStateToDisk(nextState);
currentState = nextState;
console.log(`FamiliarOS state initialized at ${statePath}.`);
}
export function releaseStartupInstallLock(): void {
const lockPath = startupInstallLockPath;
startupInstallLockPath = null;
if (lockPath) rmSync(lockPath, { recursive: true, force: true });
}
export function getAppStateSnapshot(): FamiliarOSStateV1 {
return cloneState(getInitializedState());
}
export function updatePreferences(patch: Partial<FamiliarOSStateV1["preferences"]>): FamiliarOSStateV1 {
const state = getInitializedState();
const preferences = normalizePreferences({ ...state.preferences, ...patch });
const nextState = normalizeState({
...state,
preferences,
});
commitState(nextState);
return getAppStateSnapshot();
}
export function isOnboardingCompleted(): boolean {
return getInitializedState().preferences.onboardingCompleted;
}
export function completeOnboarding(): FamiliarOSStateV1 {
const state = getInitializedState();
const nextState = normalizeState(markOnboardingCompleted(state));
commitState(nextState);
return getAppStateSnapshot();
}
export function setDefaultPet(defaultPetId: string): FamiliarOSStateV1 {
const state = getInitializedState();
const targetPet = state.familiars.installed.find((familiar) => familiar.id === defaultPetId);
if (!targetPet) {
throw new Error(`Cannot set unknown familiar as default: ${defaultPetId}`);
}
if (targetPet.broken) {
throw new Error(`Cannot set broken familiar as default: ${defaultPetId}`);
}
const nextState = normalizeState({
...state,
preferences: {
...state.preferences,
defaultPetId,
},
});
commitState(nextState);
return getAppStateSnapshot();
}
export function setDefaultPetPosition(position: Point): FamiliarOSStateV1 {
const state = getInitializedState();
const nextState = normalizeState({
...state,
defaultPet: {
...state.defaultPet,
position: normalizePosition(position),
},
});
commitState(nextState);
return getAppStateSnapshot();
}
export function resetDefaultPetPosition(position: Point): FamiliarOSStateV1 {
return setDefaultPetPosition(position);
}
export function getDefaultPetPosition(): Point | undefined {
return getInitializedState().defaultPet.position;
}
export function recordFamiliarOSActivity(activity: FamiliarOSActivityRecord, now: number = Date.now()): FamiliarOSStateV1 {
publishPluginAgentActivity({ kind: activity.kind, reaction: activity.reaction });
const state = getInitializedState();
const nextState = normalizeState({
...state,
analytics: buildRecordedActivityAnalytics(state.analytics, activity, now),
});
commitState(nextState);
return getAppStateSnapshot();
}
export function installPetState(familiar: Omit<InstalledPetState, "builtIn" | "protected" | "installed">): FamiliarOSStateV1 {
const state = getInitializedState();
if (state.familiars.installed.some((installedPet) => installedPet.id === familiar.id)) {
throw new Error(`Familiar is already installed: ${familiar.id}`);
}
const nextState = normalizeState({
...state,
familiars: {
installed: [
...state.familiars.installed,
{
...familiar,
builtIn: false,
protected: false,
installed: true,
},
],
},
});
commitState(nextState);
return getAppStateSnapshot();
}
export function removePetState(petId: string): FamiliarOSStateV1 {
if (petId === builtInPet.id) {
throw new Error("Built-in familiar cannot be removed.");
}
const state = getInitializedState();
const existing = state.familiars.installed.find((familiar) => familiar.id === petId);
if (!existing) {
throw new Error(`Familiar is not installed: ${petId}`);
}
const nextDefaultPetId = state.preferences.defaultPetId === petId ? builtInPet.id : state.preferences.defaultPetId;
const nextState = normalizeState({
...state,
preferences: {
...state.preferences,
defaultPetId: nextDefaultPetId,
},
familiars: {
installed: state.familiars.installed.filter((familiar) => familiar.id !== petId),
},
});
commitState(nextState);
return getAppStateSnapshot();
}
export function markPetBroken(petId: string, brokenReason: string): FamiliarOSStateV1 {
const state = getInitializedState();
if (petId === builtInPet.id) {
return getAppStateSnapshot();
}
const nextState = normalizeState({
...state,
preferences: {
...state.preferences,
defaultPetId: state.preferences.defaultPetId === petId ? builtInPet.id : state.preferences.defaultPetId,
},
familiars: {
installed: state.familiars.installed.map((familiar) => familiar.id === petId ? { ...familiar, broken: true, brokenReason } : familiar),
},
});
commitState(nextState);
return getAppStateSnapshot();
}
export function getStateFilePath(): string {
if (!statePath) {
throw new Error("FamiliarOS app state has not been initialized.");
}
return statePath;
}
function getInitializedState(): FamiliarOSStateV1 {
if (!currentState) {
throw new Error("FamiliarOS app state has not been initialized.");
}
return currentState;
}
function commitState(nextState: FamiliarOSStateV1): void {
writeStateToDisk(nextState);
currentState = nextState;
}
function writeStateToDisk(state: FamiliarOSStateV1): void {
writeStateJsonFile(getStateFilePath(), state);
}