UNPKG

convex

Version:

Client for the Convex Cloud

164 lines (137 loc) 4.49 kB
// eslint-disable-next-line no-restricted-imports import { promises as fs } from "fs"; import { chalkStderr } from "chalk"; import { logMessage } from "../../../bundler/log.js"; import { hashSha256 } from "../utils/hash.js"; export function isInInteractiveTerminal(): boolean { return process.stdin.isTTY === true; } export async function readFileSafe(filePath: string): Promise<string | null> { try { return await fs.readFile(filePath, "utf8"); } catch { return null; } } /** * Attempt to delete a file. Returns `true` if the file was deleted, * `false` if it didn't exist or the deletion failed. */ export async function safelyDeleteFile(filePath: string): Promise<boolean> { try { await fs.unlink(filePath); return true; } catch { return false; } } // --------------------------------------------------------------------------- // Generic managed-section helpers // // Several files (AGENTS.md, CLAUDE.md) contain a Convex-managed section // delimited by start/end markers. These helpers provide the common logic // for injecting, stripping, and detecting those sections. // --------------------------------------------------------------------------- export type ManagedSectionTarget = { filePath: string; startMarker: string; endMarker: string; }; export type InjectResult = { sectionHash: string; didWrite: boolean; }; export const iife = <T>(fn: () => T): T => fn(); /** * Inject a managed section into a file. If the file already contains the * markers, the section between them is replaced. Otherwise the section is * appended (or the file is created). Only writes when content actually * changes. */ export async function injectManagedSection( opts: ManagedSectionTarget & { section: string }, ): Promise<InjectResult> { const { filePath, startMarker, endMarker, section } = opts; const existing = (await readFileSafe(filePath)) ?? ""; const startIdx = existing.indexOf(startMarker); const endIdx = existing.indexOf(endMarker); const updated = iife(() => { if (startIdx !== -1 && endIdx !== -1) return ( existing.slice(0, startIdx) + section + existing.slice(endIdx + endMarker.length) ); if (existing.length > 0) return existing.trimEnd() + "\n\n" + section + "\n"; return section + "\n"; }); const didWrite = updated !== existing; if (didWrite) await fs.writeFile(filePath, updated, "utf8"); return { sectionHash: hashSha256(section), didWrite }; } export type StripResult = "none" | "section" | "file"; /** * Remove the managed section (between start/end markers) from a file. * If the file is empty after removal, it is deleted. * * Returns `"none"` if the file doesn't exist or has no markers, * `"section"` if the section was stripped, or `"file"` if the entire * file was deleted. */ export async function stripManagedSection( opts: ManagedSectionTarget, ): Promise<StripResult> { const { filePath, startMarker, endMarker } = opts; const content = await readFileSafe(filePath); if (content === null) return "none"; const startIdx = content.indexOf(startMarker); const endIdx = content.indexOf(endMarker); if (startIdx === -1 || endIdx === -1) { return "none"; } const before = content.slice(0, startIdx).trimEnd(); const after = content.slice(endIdx + endMarker.length).trimStart(); const updated = [before, after].filter(Boolean).join("\n\n"); if (!updated.trim()) { await safelyDeleteFile(filePath); return "file"; } await fs.writeFile(filePath, updated + "\n", "utf8"); return "section"; } export async function removeMarkdownSection({ projectDir, strip, fileName, }: { projectDir: string; strip: (dir: string) => Promise<StripResult>; fileName: string; }): Promise<boolean> { const result = await strip(projectDir); if (result === "section") { logMessage( `${chalkStderr.green("✔")} Removed Convex section from ${fileName}.`, ); return true; } if (result === "file") { logMessage(`${chalkStderr.green("✔")} Deleted ${fileName}.`); return true; } return false; } /** * Check whether a file contains a managed section (both markers present). */ export async function hasManagedSection( opts: ManagedSectionTarget, ): Promise<boolean> { const content = await readFileSafe(opts.filePath); return ( content !== null && content.includes(opts.startMarker) && content.includes(opts.endMarker) ); }