export interface ProjectEntry { /** Primary ID (usually directory name) */ id: string; /** Human-readable name */ name?: string; /** Absolute path to project root (contains .decibel/) */ path: string; /** Optional aliases (e.g., "senken" -> "senken-trading-agent") */ aliases?: string[]; /** If true, this is the default project when none specified */ default?: boolean; /** * Per-device path overrides, keyed by device ID (see getDeviceId). * The same registry file can serve multiple machines: `path` stays the * canonical location (external drives mount at the same /Volumes/... path on * any Mac, so they live here), while machine-local checkouts go in * devicePaths. Resolution prefers this device's override, then the canonical * path, then any other device's copy that happens to exist here. */ devicePaths?: Record; /** Display-only (set on entries returned by listProjects): the canonical registered path. */ canonicalPath?: string; /** Display-only: whether the effective path exists with a .decibel folder on this device. */ available?: boolean; /** Display-only: which candidate won — 'device' | 'canonical' | 'other_device'. */ pathSource?: EffectivePathSource; } export type EffectivePathSource = 'device' | 'canonical' | 'other_device'; export interface EffectivePath { path: string; source: EffectivePathSource; available: boolean; } export interface ProjectRegistry { version: 1; projects: ProjectEntry[]; } export declare function getDeviceId(): string; /** * All paths an entry might live at, most-specific first: * this device's override → canonical path → other devices' copies. */ export declare function listCandidatePaths(entry: ProjectEntry): string[]; /** * Pick the path this entry actually lives at ON THIS DEVICE: the first * candidate whose .decibel/ folder exists. External drives resolve via the * canonical path whenever mounted; machine-local checkouts via devicePaths. * If nothing exists (drive unplugged, no local copy), returns the * most-specific candidate with available=false so error messages can point * at the right location. */ export declare function getEffectivePath(entry: ProjectEntry): EffectivePath; /** * Walk up directory tree looking for a .decibel folder. * * The global config directory ENDS the walk rather than merely being skipped. * Skipping it was the first fix and it was not enough: the Windows CI leg then * climbed straight past the fake HOME the test had set up and returned the real * one above it (`C:\Users\runneradmin` -> project "RUNNER~1"). The general * case is the same shape — a stray `.decibel` in an ancestor of every home * directory, `/Users` or `C:\Users`, would be handed to every user on the * machine as their project. Nothing above your home directory is your project, * so reaching it means the answer is "no project", not "keep looking". * * Without this, any cwd under $HOME that is not itself a project resolved to a * "project" rooted at HOME with an id of basename(HOME), and every write then * landed in the directory all projects share (ISS-0177). * * This constrains discovery only. Explicit resolution — a registry entry, * DECIBEL_PROJECT_ROOT, an absolute path — still resolves HOME for anyone who * genuinely means it, and a real checkout under $HOME is still found, because * the walk reaches it long before it reaches home. */ export declare function findDecibelDir(start: string): string | undefined; /** * Resolve a project ID or alias to its filesystem path. * * Resolution order: * 1. Check registry for exact ID match * 2. Check registry for alias match * 3. Check DECIBEL_PROJECT_ROOT env var (if ID matches basename) * 4. Check if ID is an absolute path with .decibel * 5. Walk up from cwd looking for .decibel (if ID matches discovered project) * * @param projectId - Project ID, alias, or path * @returns Absolute path to the project root * @throws Error if project cannot be resolved */ export declare function resolveProject(projectId: string): ProjectEntry; /** * List all registered projects. Entries carry the device-effective `path` * (so every consumer reads the right location for this machine), plus * display fields: canonicalPath, available, pathSource. */ export declare function listProjects(): ProjectEntry[]; export interface RegisterResult { action: 'added' | 'updated_canonical' | 'device_linked' | 'kept_existing_device_link'; id: string; /** The path now effective for this device. */ path: string; /** For kept_existing_device_link: the path that was NOT recorded. */ skippedPath?: string; } /** * Register a new project or update an existing one — device-aware. * * New IDs register their path as canonical. For an existing ID with a * DIFFERENT path, the new path is recorded as this device's override * (devicePaths[deviceId]) so machines don't clobber each other's locations — * the scenario that broke the laptop when the registry held desktop-only * /Volumes/Ashitaka paths. Pass opts.canonical=true to deliberately rewrite * the canonical path (e.g. the project genuinely moved). * * If this device is ALREADY linked to a valid copy, a differing path is NOT * recorded unless opts.repoint=true — otherwise a second copy of the same * project (e.g. a backup on an external drive) discovered later in a scan * would silently steal the link from the working copy. registry_add passes * repoint (explicit user intent); registry_scan apply does not. */ export declare function registerProject(entry: ProjectEntry, opts?: { canonical?: boolean; repoint?: boolean; }): RegisterResult; /** * Remove a project from the registry */ export declare function unregisterProject(projectId: string): boolean; /** * Add an alias to an existing project */ export declare function addProjectAlias(projectId: string, alias: string): void; /** * Get the registry file path (for display/debugging) */ export declare function getRegistryFilePath(): string; export interface ScanFinding { id: string; path: string; registered: boolean; registeredAs?: string; } export interface ScanResult { roots: string[]; found: ScanFinding[]; unregistered: ScanFinding[]; orphans: Array<{ id: string; path: string; reason: string; }>; } export declare function getScanRoots(explicit?: string[] | string): string[]; /** * Scan the given roots for `.decibel/` directories and reconcile against the * registered project list. * * Does not mutate the registry. Callers use `registerProject` to apply. */ export declare function scanForProjects(explicitRoots?: string[] | string): ScanResult; /** * Get the default project. * Resolution order: * 1. Project explicitly marked as default * 2. DECIBEL_DEFAULT_PROJECT env var * 3. If only one project registered, use it * 4. Discover from cwd * * @returns The default project entry or undefined if none can be determined */ export declare function getDefaultProject(): ProjectEntry | undefined; /** * Set a project as the default */ export declare function setDefaultProject(projectId: string): void; /** * Normalize projectId from various input formats. * Handles both camelCase (projectId) and snake_case (project_id). * This allows tools to accept inputs from Claude regardless of which format it uses. */ export declare function normalizeProjectId(input: Record): string | undefined; export interface ResolvedProjectPaths { /** Project ID */ id: string; /** Absolute path to project root */ projectPath: string; /** Absolute path to .decibel folder */ decibelPath: string; /** Helper to get a subdirectory under .decibel */ subPath: (...segments: string[]) => string; } /** * Resolve project and return useful paths. * This is the primary helper for tools to get project paths. * * @param projectId - Project ID, alias, or path (optional, uses default if not provided) * @returns Resolved project paths * @throws Error if project cannot be resolved */ export declare function resolveProjectPaths(projectId?: string): ResolvedProjectPaths; /** * Validate that a write path is within the project's .decibel folder. * Call this before any file write to prevent escaping project boundaries. * * SECURITY: Uses realpath to resolve symlinks, preventing symlink-based escapes. * If the target doesn't exist yet, validates the parent directory instead. */ export declare function validateWritePath(targetPath: string, resolved: ResolvedProjectPaths): void; //# sourceMappingURL=projectRegistry.d.ts.map