import type { GoodVibesConfig, ConfigKey, ConfigValue, ConfigSetting } from './schema.js'; import type { HookDispatcher } from '../hooks/index.js'; import { type ConfigKeySource } from './manager-key-source.js'; import { type ConfigProfileFallbackReader } from './profile-fallback.js'; import { type SettingsIngestionNotice } from './manager-ingestion.js'; /** Deep immutable type, prevents mutation of nested objects returned from getAll(). */ export type DeepReadonly = { readonly [K in keyof T]: T[K] extends object ? DeepReadonly : T[K]; }; /** Constructor overrides for CLI args and programmatic instantiation. */ interface ConfigCliOverrides { model?: string | undefined; autoApprove?: boolean | undefined; systemPromptFile?: string | undefined; workingDir?: string | undefined; surfaceRoot?: string | undefined; /** * True only in the daemon composition, which OWNS the daemon tier file. A * client READS it and migrates its own view, never the bytes; the default is * that non-writing answer. See MigrationOwnership in ./manager-migration-passes.ts. */ ownsDaemonTier?: boolean | undefined; } export type ConfigOverrides = ConfigCliOverrides & ({ configDir: string; homeDir?: string | undefined; sharedConfigPath?: string | undefined; sharedTierPath?: string | undefined; daemonTierPath?: string | undefined; } | { homeDir: string; configDir?: string | undefined; sharedConfigPath?: string | undefined; sharedTierPath?: string | undefined; daemonTierPath?: string | undefined; }); /** * The tier a value resolved from, and the full source report. `daemon` is the * daemon's own store, the single home of every daemon-owned key (see * config-ownership.ts), overlaid last so a value left behind in a surface silo * can never shadow it. Defined in manager-key-source.ts; re-exported here so * existing importers keep working. */ export type { ConfigKeyTier, ConfigKeySource } from './manager-key-source.js'; export interface ConfigSetOptions { bypassManagedLock?: boolean | undefined; } /** Callback invoked when a watched config key changes. */ export type ConfigChangeCallback = (newValue: ConfigValue, oldValue: ConfigValue) => void; /** Unsubscribe handle returned by ConfigManager.subscribe(). */ export type ConfigUnsubscribe = () => void; /** * ConfigManager, Layered, mutable, persistent config system. * * Load order: defaults < global surface settings < project surface settings < CLI overrides * API keys are never persisted, loaded from env vars only. */ export declare class ConfigManager { private config; private readonly configDir; private readonly configPath; private readonly projectConfigPath; private readonly workingDirectory; private readonly homeDirectory; /** Surface-root-independent shared settings file (~/.goodvibes/shared/settings.json), or null. */ private readonly sharedTierPath; /** The daemon's own settings store (`~/.goodvibes/daemon/settings.json`), or null. */ private readonly daemonTierPath; /** True only in the daemon composition, the runtime allowed to REWRITE that store. */ private readonly daemonTierOwner; /** Shared keys whose value the last load actually sourced from the shared tier file. */ private readonly sharedKeysPresent; /** Daemon-owned keys the last load sourced from the daemon store. */ private readonly daemonKeysPresent; private hookDispatcher; /** Owner-profile read fallback for UNSET keys. Injected; null unless installed. */ private profileFallback; private readonly _listeners; /** Active config-file watch handle (external-edit live reload), or null. */ private _fileWatch; /** Settings the last load could not ingest. See ./settings-ingestion.ts. */ private ingestionNotices; constructor(overrides: ConfigOverrides); getControlPlaneConfigDir(): string; getWorkingDirectory(): string | null; getHomeDirectory(): string | null; /** * Returns the absolute path to the global (surface-level) settings.json file. * Consumers should use this instead of casting through `as unknown` to access * the private `configPath` field. */ getConfigPath(): string; /** * Returns the absolute path to the project-level settings.json file, or * `undefined` if no `workingDir` was provided at construction time. */ getProjectConfigPath(): string | undefined; attachHookDispatcher(hookDispatcher: Pick | null): void; /** Install (or clear) the owner-profile read fallback. See ./profile-fallback.ts. */ attachProfileFallback(reader: ConfigProfileFallbackReader | null): void; private resolvePath; /** * Get a config value by dot-path key. * * An UNSET key may resolve from the owner profile when a fallback reader is * installed, one keyed read by a consumer that needs the value. Deliberately * not applied by `getAll()` or any category/dump path: see ./profile-fallback.ts. */ get(key: K): ConfigValue; /** Set a config value by dot-path key and auto-save to disk. */ set(key: K, value: ConfigValue, options?: ConfigSetOptions): void; /** * Set a single key and persist it to the PROJECT settings overlay (merged * into the raw on-disk shape, keeping only explicit keys), leaving the global * file untouched, so an approval like fetch.allowLocalhost scopes to this * project and survives restarts. Falls back to set() with no project path. */ setProjectValue(key: K, value: ConfigValue, options?: ConfigSetOptions): void; /** Subscribe to changes on a config key; returns an unsubscribe function. */ subscribe(key: K, cb: ConfigChangeCallback): ConfigUnsubscribe; /** * Watch the on-disk config files (global, project, shared-tier) for EXTERNAL * edits and apply them live through the same subscribe() pipeline an * in-process set() uses, no restart. Returns a stop function. */ watchConfigFiles(options?: { intervalMs?: number; }): () => void; /** Stop watching all config files opened by watchConfigFiles(). */ stopWatchingConfigFiles(): void; /** Re-read config from disk and fire subscribers for every watched key that changed. */ private reloadFromDiskAndNotify; /** Notify synchronous subscribers of a key change. */ private notifyListeners; /** Fire the Change:config hook for a config key change. */ private emitConfigHook; /** * Set a config value from a validated ConfigKey with unknown value type (when * iterating schema entries). Runtime validation still applies via set(). */ setDynamic(key: ConfigKey, value: unknown, options?: ConfigSetOptions): void; /** Return a deep-readonly snapshot of the full config. Nested objects are immutable. */ getAll(): DeepReadonly; /** Return a deep-cloned snapshot of a config category. */ getCategory(category: C): Readonly; /** Return a deep-cloned snapshot of the live config (read-only consumers). */ getRaw(): Readonly; /** Return the full schema. */ getSchema(): ConfigSetting[]; /** * Persist a single key to the global settings file by read-merge-write, so * hand edits and other keys survive and no default reaches disk unless set. */ private persistGlobalKey; private writeRawGlobal; /** * Persist current config to the global settings file, writing only the keys * that differ from the shipped defaults (plus unknown keys), no default is * frozen onto disk; resolved config is unchanged on reload. */ save(): void; /** * Drop every daemon-owned key from a whole-config dump. A surface file must * never carry a daemon-owned value again, one writer per key means a * whole-config save cannot quietly re-seed the duplication the daemon config * migration just removed. */ private withoutDaemonOwned; /** Persist current config to the project-level surface settings file. */ saveProject(): void; /** * Every setting the last load could not ingest, with the file, the key and * the reason, the owner-visible signal behind the startup notice. Empty when * every settings file was read whole. See ./settings-ingestion.ts. */ getIngestionQuarantine(): readonly SettingsIngestionNotice[]; /** Where an ingestion notice is filed; see ./manager-ingestion.ts. */ private ingestionSink; private ingest; private loadFailure; /** Load config from disk: global then project (project wins). Deep-merges with defaults. */ load(): void; /** * Overlay the daemon store's daemon-owned keys onto the resolved config, * recording which keys came from there so describeConfigKeySource is honest. */ private loadDaemonTier; /** The daemon store path, or null when no daemon tier is configured. */ getDaemonTierPath(): string | null; /** * Overlay shared-tier values for the shared keys onto the resolved config; a * shared key absent from the file is left at its surface-local value. Records * which keys were sourced from the shared tier so describeConfigKeySource is * honest. */ private loadSharedTier; /** The shared-tier settings file path, or null when no shared tier is configured. */ getSharedTierPath(): string | null; /** * Report which tier a key's live value resolves from (daemon / shared / * project / global / default). Reads the on-disk layers on demand so the * resolution order is inspectable, see manager-key-source.ts. */ describeConfigKeySource(key: ConfigKey): ConfigKeySource; /** * Run the load-time settings migrations over a parsed file. * * The passes and their ORDER live together in manager-migration-passes.ts, * the sequence is a property of the passes, not of this caller. All this * supplies is the receipt sink, which is the one part that needs the manager: * a receipt is announce-once, keyed to this config's own announcement file. */ private applyLoadMigrations; /** File a receipt against this config's own announce-once store. */ private migrationReceipt; /** * Merge a partial patch into a config category and auto-save, the correct * way to update array/object fields that cannot be expressed as a scalar * dot-path key (e.g. notifications.webhookUrls). Shallow-merged. */ mergeCategory(category: C, patch: Partial): void; /** * Remove a key from an object-shaped category and auto-save. mergeCategory * can only set keys, so clearing an override (e.g. a feature-flag entry back * to its default) requires this explicit removal. */ removeCategoryKey(category: C, key: string): void; private categoryIoDeps; /** * Reset a specific key to its default, or reset all config. * Saves to disk after reset. */ reset(key?: ConfigKey): void; } //# sourceMappingURL=manager.d.ts.map