/** * Service for managing saved/favorite locations * Stores locations in ~/.weather-mcp/locations.json */ import type { SavedLocation, SavedLocationsStore } from '../types/savedLocations.js'; /** * Thrown when `~/.weather-mcp/locations.json` exists but cannot be read, parsed, * or does not hold a plain JSON object at the top level. * * The store is **contract**, not garnish: it is user data that only this server * writes, so "empty" and "unreadable" must never render as the same answer. An * unreadable file is refused by every read and every write — it is never renamed, * copied, or overwritten. `ENOENT`, and only `ENOENT`, means an empty store. * * The message is fixed apart from the path. The underlying cause goes to the log, * never into the message. */ export declare class LocationStoreUnreadableError extends Error { readonly storePath: string; constructor(storePath: string); } export declare class LocationStore { private readonly storePath; private readonly storeDir; constructor(customPath?: string); /** * Ensure the storage directory exists * @private */ private ensureDirectoryExists; /** * Load all saved locations from disk. * * Reads the file on **every** call — there is no cache. Two clients on one * machine share this file, so a cached copy written back whole silently deletes * the other client's saves. Every call returns a fresh object. * * @throws {LocationStoreUnreadableError} when the file exists but cannot be * read, cannot be parsed, or is not a plain JSON object at the top level. * A zero-byte or whitespace-only file is a parse failure like any other. */ load(): SavedLocationsStore; /** * Save all locations to disk * @private */ private save; /** * Resolve the pathname this store should actually write, by walking the symlink * chain from `storePath`. * * `realpathSync` cannot be used here: it reports `ENOENT` both for a path that is * absent and for a symlink whose target is absent, so falling back to the literal * path on `ENOENT` renames **over the symlink**, destroying it and writing the * JSON at the link's own pathname. Dotfile managers and synced folders make a * not-yet-created link target an ordinary first-run state. * * @private */ private resolveWriteTarget; /** * Replace the store file atomically: write a uniquely named temp file in the same * directory, fsync it, then rename it over the target. A reader sees the old file * or the new one, never a partial — today's in-place write truncates first, and a * reader landing in that window destroys the file through the parse-failure path. * * On any failure the temp file is removed and the target is left untouched. There * is deliberately **no** fallback to an in-place write: that would reintroduce the * truncation window exactly where the environment is already unusual. * * @private */ private writeAtomically; /** * Get a saved location by alias */ get(alias: string): SavedLocation | undefined; /** * Get all saved locations */ getAll(): SavedLocationsStore; /** * Save or update a location */ set(alias: string, location: Omit): SavedLocation; /** * Remove a saved location */ remove(alias: string): boolean; /** * Check if a location exists */ has(alias: string): boolean; /** * Get the number of saved locations */ count(): number; /** * Clear all saved locations */ clear(): void; /** * Get the storage file path */ getStorePath(): string; } //# sourceMappingURL=locationStore.d.ts.map