@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
JavaScript
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.");
}