UNPKG

@phren/cli

Version:

Knowledge layer for AI agents - CLI, MCP server, and data layer

127 lines (126 loc) • 5.3 kB
import * as fs from "fs"; import * as path from "path"; import { listInvalidProjectDirs, normalizeProjectNameForCreate } from "./phren-paths.js"; import { isValidProjectName, errorMessage } from "./utils.js"; import { listProfiles, addProjectToProfile, removeProjectFromProfile, } from "./profile-store.js"; import { logger } from "./logger.js"; // Rename on-disk project directories whose names fail `isValidProjectName` // (typically uppercase) to their lowercase form, and update any profile YAML // references. Skips entries where the target name would collide with an // existing directory or where lowercasing alone doesn't yield a valid name. export function migrateInvalidProjectNames(phrenPath) { const invalid = listInvalidProjectDirs(phrenPath); const outcomes = []; for (const from of invalid) { const to = normalizeProjectNameForCreate(from); if (!isValidProjectName(to)) { outcomes.push({ from, to, action: "skipped-invalid-slug", reason: `lowercasing does not yield a valid name (got "${to}"); rename manually`, }); continue; } if (to === from) { outcomes.push({ from, to, action: "skipped-invalid-slug", reason: "already lowercase but still fails validation; rename manually", }); continue; } const oldDir = path.join(phrenPath, from); const newDir = path.join(phrenPath, to); // Case-insensitive filesystems (macOS APFS/HFS+, default Windows) resolve // both paths to the same inode. Treat that as "rename casing in place" // rather than a collision. let sameInode = false; try { if (fs.existsSync(newDir)) { const oldStat = fs.statSync(oldDir); const newStat = fs.statSync(newDir); sameInode = oldStat.ino === newStat.ino && oldStat.dev === newStat.dev; } } catch (err) { logger.debug("project-migrate", `stat compare ${from}->${to}: ${errorMessage(err)}`); } if (fs.existsSync(newDir) && !sameInode) { outcomes.push({ from, to, action: "skipped-collision", reason: `target "${to}" already exists as a separate directory; merge manually`, }); continue; } try { if (sameInode) { // Two-step rename forces the case change on case-insensitive filesystems // without letting an observer see a missing directory. const tmp = path.join(phrenPath, `${from}.phren-case-tmp-${process.pid}`); fs.renameSync(oldDir, tmp); fs.renameSync(tmp, newDir); } else { fs.renameSync(oldDir, newDir); } } catch (err) { outcomes.push({ from, to, action: "error", reason: `rename failed: ${errorMessage(err)}`, }); continue; } updateProfileReferences(phrenPath, from, to); outcomes.push({ from, to, action: "renamed" }); } return { outcomes }; } function updateProfileReferences(phrenPath, oldName, newName) { const profiles = listProfiles(phrenPath); if (!profiles.ok) { logger.debug("project-migrate", `updateProfileReferences listProfiles: ${profiles.error}`); return; } for (const profile of profiles.data) { if (!profile.projects.includes(oldName)) continue; const removed = removeProjectFromProfile(phrenPath, profile.name, oldName); if (!removed.ok) { logger.debug("project-migrate", `remove ${oldName} from ${profile.name}: ${removed.error}`); continue; } const added = addProjectToProfile(phrenPath, profile.name, newName); if (!added.ok) { logger.debug("project-migrate", `add ${newName} to ${profile.name}: ${added.error}`); } } } export function formatMigrationSummary(result) { if (result.outcomes.length === 0) return "no invalid project directories found"; const parts = []; const renamed = result.outcomes.filter((o) => o.action === "renamed"); const collisions = result.outcomes.filter((o) => o.action === "skipped-collision"); const invalidSlugs = result.outcomes.filter((o) => o.action === "skipped-invalid-slug"); const errors = result.outcomes.filter((o) => o.action === "error"); if (renamed.length > 0) { parts.push(`renamed ${renamed.length}: ${renamed.map((o) => `${o.from}->${o.to}`).join(", ")}`); } if (collisions.length > 0) { parts.push(`${collisions.length} collision(s): ${collisions.map((o) => o.from).join(", ")}`); } if (invalidSlugs.length > 0) { parts.push(`${invalidSlugs.length} unfixable: ${invalidSlugs.map((o) => o.from).join(", ")}`); } if (errors.length > 0) { parts.push(`${errors.length} error(s): ${errors.map((o) => `${o.from} (${o.reason})`).join(", ")}`); } return parts.join("; "); }