/** * Write-once portable project identity: decide, re-link, adopt (T12325 · T12716). * * The tracked identity — `.cleo/project.json` `{schemaVersion, id, name}`, * with the ADR-094 `.cleo/project-id` kept as a legacy mirror (both read by * `@cleocode/paths` `readPortableProjectId`) — is the one project identifier * that survives a fresh clone, a move, and a new device. This module owns the * write-side rules: * * 1. **Write once.** The id is created with `O_EXCL` and never rewritten. A * conflicting or malformed file is reported, not repaired. Only the * manifest's `name` may change, through {@link renameProjectManifest}. * 2. **Never silently re-mint.** When no tracked or local id exists, the * global registry is searched for the project's previous identity (same * path, its canonical-fingerprint alias, or a live checkout of the same remote). A * unique candidate is re-linked; several candidates refuse to guess; a new * id is minted only when nothing can be re-linked or when the caller asks * for one explicitly. * 3. **Never migrate on open.** Tracked files are created here only for a * project that has NONE yet. Turning a legacy `project-id` into * `project.json` is `cleo doctor project-identity --resolve` alone. * * Precedence when the tracked id and the `project-info.json` cache disagree * (T12716, reversing the T12325 rule): the TRACKED id wins, as it already did * for every reader. The disagreement is reported. The cache is not rewritten * here: local state (registry row, aliases) keyed by the old id is re-keyed, * with the old id kept as an alias, only by `cleo doctor project-identity * --resolve`. Two ids for one repository means two lineages (typically two * devices that each ran `cleo init` before the id was tracked). * * @task T12325 * @task T12716 * @see ADR-096 — one committed `.cleo/project.json` (amends ADR-094) */ /** * Where the identity a scaffold step settled on came from. * * - `tracked` — the committed `.cleo/project.json` (or legacy `.cleo/project-id`); * it wins over the `project-info.json` cache (T12716). * - `project-info` — the `project-info.json` cache, used only when no tracked * file exists yet (a project from before ADR-094). * - `registry-path` / `registry-alias` / `registry-remote` — re-linked from the * global registry because neither file was present. * - `minted` — a new random id; only when nothing could be re-linked or the * caller explicitly asked for a new identity. */ export type ProjectIdentitySource = 'project-info' | 'tracked' | 'registry-path' | 'registry-alias' | 'registry-remote' | 'minted'; /** How a registry row was matched as a re-link candidate. */ export type RelinkVia = 'registry-path' | 'registry-alias' | 'registry-remote'; /** One previous identity the global registry offers for a project root. */ export interface RelinkCandidate { /** The registered immutable project id. */ projectId: string; /** The registry's recorded path for that id (may no longer exist). */ projectPath: string; /** Which evidence matched it. */ via: RelinkVia; } /** The identity a scaffold step should use, and why. */ export interface ProjectIdentityDecision { /** The id to write into `project-info.json`. */ projectId: string; /** Provenance of {@link ProjectIdentityDecision.projectId}. */ source: ProjectIdentitySource; /** Human-readable facts to surface (conflicts, re-links, coverage gaps). */ diagnostics: string[]; } /** Outcome of making sure the tracked files record the project's id. */ export type PortableIdFileOutcome = /** `project.json` already records the same id. */ 'present' /** * Only the legacy `project-id` records the same id. Reported, not migrated: * `cleo doctor project-identity --resolve` writes `project.json` (T12716). */ | 'legacy' /** Created now (first adoption): `project.json` plus the `project-id` mirror. */ | 'written' /** Present with a DIFFERENT id — reported, not rewritten. */ | 'conflict' /** Present but unreadable/malformed — reported, not rewritten. */ | 'invalid' /** No `/.cleo/` directory to write into. */ | 'no-cleo-dir'; /** Options for {@link decideProjectIdentity}. */ export interface DecideProjectIdentityOptions { /** * Explicitly request a new identity instead of re-linking. The only way to * mint while the registry offers candidates. */ mintNewIdentity?: boolean; /** Global CLEO home whose registry is searched (defaults to the current one). */ cleoHome?: string; } /** Re-exported from its home in `nexus/identity` (moved there by T12470). */ export { normalizeRemoteUrl } from '../nexus/identity.js'; /** * Search the global registry for identities this project root previously had. * * Tiers, strongest first: a row registered at this exact path; an alias row * for this path's derived ids; a row whose LIVE checkout declares the same * git remote. Stranded rows (path gone) cannot be matched by remote because * the registry stores no remote — their checkout is the only record of it. * * @param projectRoot - Absolute project root. * @param cleoHome - Global CLEO home whose registry to read. * @returns Candidates plus diagnostics; an unreadable registry yields no * candidates and a diagnostic saying coverage is missing. */ export declare function findRelinkCandidates(projectRoot: string, cleoHome?: string): Promise<{ candidates: RelinkCandidate[]; diagnostics: string[]; }>; /** * Decide the identity a scaffold step writes into `project-info.json`. * * @param projectRoot - Absolute project root. * @param localInfoId - The id already in `project-info.json`, if any. * @param options - Explicit-mint request and registry home. * @returns The decision with provenance and diagnostics. * @throws {CleoError} `CONFIG_ERROR` when the tracked file is malformed, or * when the registry offers several candidates and no explicit mint was asked. * * @example * ```ts * const { projectId, source } = await decideProjectIdentity(root, undefined); * ``` */ export declare function decideProjectIdentity(projectRoot: string, localInfoId: string | undefined, options?: DecideProjectIdentityOptions): Promise; /** * Default display name for a project root: its directory basename, made a * valid name (control characters and separators replaced, a leading `~` * dropped, capped at {@link PROJECT_DISPLAY_NAME_MAX}). `project` when nothing * usable is left. * * @param projectRoot - Project root. * @returns A name that passes `isValidProjectDisplayName`. * * @example * ```ts * defaultProjectDisplayName('/work/cleocode'); // 'cleocode' * ``` * @task T12716 */ export declare function defaultProjectDisplayName(projectRoot: string): string; /** * Create `.cleo/project.json` for `projectId` — create-only (`O_EXCL`). * * @param projectRoot - Absolute project root. * @param projectId - The identity to record; never changed afterwards. * @param name - Display name; must pass `isValidProjectDisplayName`. * @returns `written`, or — for a file that already exists — `present` (same * id), `conflict` (different id) or `invalid` (unparseable). Never rewrites. * * @example * ```ts * await createProjectManifest(root, 'c78d09c3a8ee', 'cleocode'); // 'written' * ``` * @task T12716 */ export declare function createProjectManifest(projectRoot: string, projectId: string, name: string): Promise<'written' | 'present' | 'conflict' | 'invalid' | 'no-cleo-dir'>; /** * Create the legacy `.cleo/project-id` mirror for `projectId` — create-only. * Older builds read only this file, so it is kept beside `project.json` * until a later removal (T12716). * * @param projectRoot - Absolute project root. * @param projectId - The identity to mirror. * @returns `written`, or for an existing file `present` / `conflict` / `invalid`. * * @example * ```ts * await createProjectIdMirror(root, 'c78d09c3a8ee'); * ``` * @task T12716 */ export declare function createProjectIdMirror(projectRoot: string, projectId: string): Promise<'written' | 'present' | 'conflict' | 'invalid' | 'no-cleo-dir'>; /** * Make sure the tracked files record `projectId` — write-once. * * Only a project with NO tracked identity gets files: `project.json` and the * legacy `project-id` mirror, both created with `O_EXCL`. A present file is * compared, never rewritten, and a legacy-only project is reported as * `legacy` rather than migrated (migration is `cleo doctor project-identity * --resolve`, T12716). * * @param projectRoot - Absolute project root. * @param projectId - The identity to adopt. * @param name - Display name for a new `project.json`; defaults to * {@link defaultProjectDisplayName}. Ignored when a file already exists. * @returns What happened; `legacy`/`conflict`/`invalid` are for the caller to report. * * @example * ```ts * const outcome = await ensurePortableProjectId(root, info.projectId); * ``` */ export declare function ensurePortableProjectId(projectRoot: string, projectId: string, name?: string): Promise; /** * Rename a project in its committed `.cleo/project.json`: only `name` changes; * the id is carried over byte-identical and keys this build does not know are * kept (`writeProjectManifestName`: a tmp file unique to the call, renamed * into place, so a crash never leaves a half-written identity file). * * @param projectRoot - Absolute project root. * @param name - New display name (trimmed; must pass `isValidProjectDisplayName`). * @returns The previous and new manifest names. * @throws {CleoError} `VALIDATION_ERROR` for an invalid name; `CONFIG_ERROR` * when `project.json` is absent (migrate first) or unusable. * * @example * ```ts * await renameProjectManifest(root, 'cleo-platform'); * ``` * @task T12716 */ export declare function renameProjectManifest(projectRoot: string, name: string): Promise<{ oldName: string; newName: string; projectId: string; }>; //# sourceMappingURL=project-identity.d.ts.map