/** * Shared config-file resolution for pi extensions. * * Every extension looks for `config.json` in the same two places, in this order: * * 1. Project: `/.pi/extensions//config.json` * 2. Global: `/extensions//config.json` * * What happens when both exist is the extension author's call, via `strategy`: * `"first-match"` reads only the project file, while `"shallow-merge"` and * `"deep-merge"` layer it over the global one — the first key by top-level key, the * second recursively. Defaults always sit underneath whatever is read. */ export declare const CONFIG_FILENAME = "config.json"; /** * Find the root of the git repository containing `startPath`. * * `.git` is a directory in a normal clone but a *file* in worktrees and submodules, * so mere existence is what marks the root — never check `isDirectory()` here. * * @returns the repository root, or `null` when `startPath` is not inside a repository. */ export declare function findGitRoot(startPath: string): string | null; export interface ConfigLocationOptions { /** Where to start looking for the repository root. Defaults to `process.cwd()`. */ cwd?: string; /** * Overrides the agent directory. Defaults to pi's own `getAgentDir()`, which already * honours `PI_CODING_AGENT_DIR` and expands a leading `~`. Intended for tests. */ agentDir?: string; /** Whether to consider the project config tier. Defaults to `true`. */ includeProject?: boolean; } /** * Candidate config paths in priority order, whether or not they exist. * * The project path is omitted entirely when `cwd` is not inside a git repository — * a bare `./.pi/extensions/...` relative to wherever the agent happens to have been * started is not something anyone means to configure. */ export declare function getConfigPaths(extensionId: string, options?: ConfigLocationOptions): string[]; /** * The config file that would be read, or `null` when none of the candidates exist. * * This only checks for existence; a file that exists but holds malformed JSON is still * returned. Use {@link loadConfig} to actually read one. */ export declare function resolveConfigPath(extensionId: string, options?: ConfigLocationOptions): string | null; /** * How to combine config files when more than one exists. * * - `"first-match"` (the default) reads only the highest-priority file. A project * config replaces the global one, so it has to be complete. Which file is in * effect is always obvious. * - `"shallow-merge"` layers higher-priority files over lower ones by top-level key, * so a project config can override one setting and inherit the rest. A nested * object in a project file replaces its global counterpart wholesale rather than * being merged into it — which is what you want when its keys travel together. * - `"deep-merge"` layers files recursively, so nested objects are merged key by * key at every level. Arrays are still replaced wholesale — they are rarely * intended to be concatenated. */ export type ConfigStrategy = "first-match" | "shallow-merge" | "deep-merge"; export interface LoadConfigOptions extends ConfigLocationOptions { /** Defaults to `"first-match"`. */ strategy?: ConfigStrategy; } export interface LoadedConfig { /** Defaults with every contributing file applied over them. */ config: T; /** * The files that contributed, lowest priority first, so the last entry is the one * that had the final say. Empty when nothing readable was found. */ sources: string[]; /** Every candidate path in priority order, whether or not it existed. */ candidates: string[]; /** * Problems worth surfacing to the user: files that existed but could not be used. * Reporting is left to the caller — a library has no business writing to the console. */ diagnostics: string[]; } /** * Read config from disk, applied over `defaults`. * * A file that exists but holds malformed JSON (or a non-object) is reported in * `diagnostics` and skipped, falling through to the next location: a stray project * config should not strand an extension with no configuration at all. * * The returned config shares nothing with `defaults`, so mutating it is safe even * when `defaults` is the module-level constant it usually is. That costs a * `structuredClone`, so `defaults` must be structured-cloneable: JSON-shaped values * plus `Date`, `Map`, `Set` and friends. A function in there throws, and a class * instance comes back as a plain object with its prototype gone — neither belongs in * something that mirrors a `config.json`. */ export declare function loadConfig(extensionId: string, defaults: T, options?: LoadConfigOptions): LoadedConfig;