UNPKG

@reliverse/rse

Version:

@reliverse/rse is your all-in-one companion for bootstrapping and improving any kind of projects (especially web apps built with frameworks like Next.js) — whether you're kicking off something new or upgrading an existing app. It is also a little AI-power

168 lines (167 loc) 5.25 kB
import path from "@reliverse/pathkit"; import fs from "@reliverse/relifso"; import { relinka } from "@reliverse/relinka"; import { confirmPrompt, inputPrompt } from "@reliverse/rempts"; import { generateText } from "ai"; import { execaCommand } from "execa"; import { glob } from "tinyglobby"; import { MODEL } from "../ai-const.js"; async function getUserPrompt() { const response = await inputPrompt({ title: "AI Code Generation", content: "Describe the code you want to generate:" }); return response; } async function generateAiContent(userDescription) { const result = await generateText({ model: MODEL, system: `ALWAYS follow these rules: 1. First line must be filepath comment: "// {filepath}" 2. Use absolute paths from project root 3. For modifications, include original code context 4. Support TS, JS, JSON and MD formats 5. Add TypeScript types where possible 6. Use "~/" paths relative from "src" 7. Prefer async/await over sync 8. Your name is rse AI`, prompt: userDescription }); return { text: result.text }; } function parseAiOutput(aiText) { const [pathLine = "", ...codeLines] = aiText.split("\n"); const filePath = pathLine.replace(/^\/\/\s*/, "").trim(); const codeContent = codeLines.join("\n"); return { filePath, codeContent }; } async function isFileModified(filePath) { try { const status = (await execaCommand(`git status --porcelain ${filePath}`)).stdout; return status.trim().startsWith("M"); } catch { return false; } } async function confirmOverwrite(filePath) { const modified = await isFileModified(filePath); if (!modified) { return true; } const confirmed = await confirmPrompt({ title: "Git Modification Warning", content: `File ${path.basename(filePath)} has uncommitted changes. Overwrite?`, defaultValue: false }); return confirmed; } async function writeFileSafe(filePath, content) { const absolutePath = path.resolve(process.cwd(), filePath); const dir = path.dirname(absolutePath); if (!await fs.pathExists(dir)) { await fs.mkdir(dir, { recursive: true }); } await fs.writeFile(absolutePath, content, { flag: "wx" }); } async function maybeFormatFile(filePath) { const doFormat = await confirmPrompt({ title: "Code Formatting", content: `Would you like to format ${path.basename(filePath)} with Biome?`, defaultValue: false }); if (!doFormat) { return; } await execaCommand(`bun x biome check --write "${filePath}"`, { stdio: "inherit" }); } async function maybeCommitFile(filePath) { const shouldCommit = await confirmPrompt({ title: "Git Commit", content: "Do you want to commit these changes?", defaultValue: false }); if (!shouldCommit) { return; } const commitMessage = await inputPrompt({ title: "Commit Message", content: "Enter a commit message:" }); await commitChanges(filePath, { commitMessage }); } async function commitChanges(filePath, options) { await execaCommand(`git add ${filePath}`, { stdio: "inherit" }); await execaCommand(`git commit -m "${options.commitMessage}"`, { stdio: "inherit" }); } export async function aiCodeCommand(fileOrFolderPatterns = [], notifyPerFile = false) { const userPrompt = await getUserPrompt(); const { text: aiText } = await generateAiContent(userPrompt); const { filePath, codeContent } = parseAiOutput(aiText); if (!filePath) { throw new Error("AI failed to provide valid file path"); } const canOverwrite = await confirmOverwrite(filePath); if (!canOverwrite) { relinka("info", "Operation cancelled"); process.exit(0); } let fileExists = false; if (await fs.pathExists(filePath)) { fileExists = true; await fs.rm(filePath); } await writeFileSafe(filePath, codeContent); await maybeFormatFile(filePath); await maybeCommitFile(filePath); relinka( "info", `Successfully ${fileExists ? "updated" : "created"} ${filePath}` ); if (!fileOrFolderPatterns.length) { return; } const matchedPaths = await glob(fileOrFolderPatterns, { absolute: false }); if (!matchedPaths.length) { relinka("info", "No files matched the given patterns."); return; } if (!notifyPerFile) { let anyModified = false; for (const p of matchedPaths) { if (await isFileModified(p)) { anyModified = true; break; } } if (anyModified) { const confirmAll = await confirmPrompt({ title: "Git Modification Warning", content: "One or more matched files are modified. Continue anyway?", defaultValue: false }); if (!confirmAll) { relinka("info", "Operation cancelled for matched files."); return; } } } else { for (const p of matchedPaths) { const modified = await isFileModified(p); if (modified) { const confirmThis = await confirmPrompt({ title: "Git Modification Warning", content: `File ${path.basename(p)} has uncommitted changes. Continue anyway?`, defaultValue: false }); if (!confirmThis) { relinka("info", `Operation cancelled for ${p}.`); } } } } relinka("info", "Matched files processed successfully."); }