@reliverse/rse-sdk
Version:
@reliverse/rse-sdk allows you to create new plugins for @reliverse/rse CLI, interact with reliverse.org, and even extend your own CLI functionality (you may also try @reliverse/dler-sdk for this case).
314 lines (313 loc) • 9.96 kB
JavaScript
import path from "@reliverse/pathkit";
import { ensuredir } from "@reliverse/relifso";
import fs from "@reliverse/relifso";
import { relinka } from "@reliverse/relinka";
import {
selectPrompt,
multiselectPrompt,
confirmPrompt
} from "@reliverse/rempts";
import { ofetch } from "ofetch";
import {
DEFAULT_BRANCH,
getRepoCacheDir,
RULE_FILE_EXTENSION,
RULES_REPOS
} from "./add-rule-const.js";
import {
convertTsToMdc,
downloadRules,
handleRuleUpdates,
hasInstalledRules,
installRules
} from "./add-rule-utils.js";
export async function handleDirectRules(opts) {
const { cwd, source, ruleNames } = opts;
let repoId = "";
if (source === "official") {
repoId = "blefnk/awesome-cursor-rules";
} else if (source === "community") {
repoId = "pontusab/directories";
} else {
throw new Error(
"Cannot use source='prompt' together with --get. Please specify `--source official` or `--source community`."
);
}
const allAvailable = await fetchRepoFiles(repoId);
if (allAvailable.length === 0) {
relinka("error", "No rule files found in repository");
return;
}
const userWantsAll = ruleNames.length === 1 && ruleNames[0] === "all";
let targetNames = [];
if (userWantsAll) {
targetNames = allAvailable.map((f) => f.baseNameNoExt);
} else {
targetNames = ruleNames.map((name) => removeExtension(name));
}
const selected = allAvailable.filter(
(item) => targetNames.includes(item.baseNameNoExt)
);
if (selected.length === 0) {
relinka("error", "No matching rule files found for your --get arguments");
return;
}
const downloadedFiles = await downloadSpecificFiles(repoId, selected);
if (downloadedFiles.length === 0) {
relinka("error", "No rule files downloaded");
return;
}
const [owner] = repoId.split("/");
if (!owner) {
relinka("error", `Invalid repository: ${repoId}`);
return;
}
await installRules(downloadedFiles, owner, cwd);
relinka("verbose", "Rules installation completed (no prompt).");
}
async function fetchRepoFiles(repoId) {
const repo = RULES_REPOS.find((r) => r.id === repoId);
if (!repo) {
relinka("error", `Repository not found in config: ${repoId}`);
return [];
}
const [owner, repoName] = repoId.split("/");
const branch = repo.branch || DEFAULT_BRANCH;
const apiUrl = `https://api.github.com/repos/${owner}/${repoName}/git/trees/${branch}?recursive=1`;
try {
const repoData = await ofetch(apiUrl, { retry: 3, retryDelay: 1e3 });
let matches = [];
if (repo.isCommunity) {
const comPath = repo.communityPath || "";
matches = repoData.tree.filter(
(item) => item.type === "blob" && item.path.endsWith(".ts") && item.path.startsWith(comPath) && !item.path.split("/").some((part) => part.startsWith("."))
);
} else {
matches = repoData.tree.filter(
(item) => item.type === "blob" && item.path.endsWith(".md") && item.path.toLowerCase() !== "readme.md" && !item.path.split("/").some((part) => part.startsWith("."))
);
}
return matches.map((file) => {
const bn = path.basename(file.path);
let baseNoExt = "";
if (repo.isCommunity) {
baseNoExt = path.basename(bn, ".ts");
} else {
baseNoExt = path.basename(bn, ".md");
}
return {
path: file.path,
baseName: bn,
baseNameNoExt: baseNoExt,
isCommunity: !!repo.isCommunity
};
});
} catch (error) {
relinka("error", `Failed to fetch repo files from ${repoId}: ${error}`);
return [];
}
}
function removeExtension(name) {
const exts = [".ts", ".md", ".mdc"];
let result = name;
for (const ext of exts) {
if (result.toLowerCase().endsWith(ext)) {
result = result.slice(0, -ext.length);
break;
}
}
return result;
}
async function downloadSpecificFiles(repoId, files) {
const [owner, repoName] = repoId.split("/");
if (!owner || !repoName) {
relinka("error", "Invalid repository ID format");
return [];
}
const repo = RULES_REPOS.find((r) => r.id === repoId);
if (!repo) {
relinka("error", "Repository not found in configuration");
return [];
}
const branch = repo.branch || DEFAULT_BRANCH;
const repoCacheDir = getRepoCacheDir(owner);
await ensuredir(repoCacheDir);
const results = [];
for (const fileInfo of files) {
let expectedCacheFile = "";
if (fileInfo.isCommunity) {
expectedCacheFile = `${fileInfo.baseNameNoExt}${RULE_FILE_EXTENSION}`;
} else {
expectedCacheFile = `${fileInfo.baseNameNoExt}${RULE_FILE_EXTENSION}`;
}
const cacheFilePath = path.join(repoCacheDir, expectedCacheFile);
if (await fs.pathExists(cacheFilePath)) {
relinka("verbose", `[Cached] ${fileInfo.path} is already in cache.`);
results.push(expectedCacheFile);
continue;
}
try {
const contentUrl = `https://raw.githubusercontent.com/${owner}/${repoName}/${branch}/${fileInfo.path}`;
const content = await ofetch(contentUrl, {
responseType: "json",
retry: 2,
retryDelay: 500
});
if (fileInfo.isCommunity) {
const mdContent = convertTsToMdc(content, fileInfo.path);
await fs.writeFile(cacheFilePath, mdContent);
} else {
await fs.writeFile(cacheFilePath, content);
}
results.push(expectedCacheFile);
relinka("verbose", `Downloaded & processed ${fileInfo.path}`);
} catch (error) {
relinka("error", `Failed to download ${fileInfo.path}: ${error}`);
}
}
if (results.length > 0) {
const metadata = {
repoId,
branch,
downloadedAt: (/* @__PURE__ */ new Date()).toISOString(),
totalFiles: results.length,
tags: repo.tags || [],
isOfficial: repo.isOfficial || false,
isCommunity: repo.isCommunity || false
};
await fs.writeFile(
path.join(repoCacheDir, ".metadata.json"),
JSON.stringify(metadata, null, 2)
);
await fs.writeFile(
path.join(repoCacheDir, ".last_updated"),
(/* @__PURE__ */ new Date()).toISOString()
);
}
return results;
}
export async function showRulesMenu({
cwd,
isDev,
source
}) {
if (source === "official" || source === "community") {
const repoId = source === "official" ? "blefnk/awesome-cursor-rules" : "pontusab/directories";
const [owner] = repoId.split("/");
if (!owner) {
relinka("error", "Invalid repository selection");
return;
}
const filesToInstall = await downloadRules(repoId, isDev);
if (filesToInstall.length === 0) {
relinka("error", "No rule files found");
return;
}
await installRules(filesToInstall, owner, cwd);
relinka("verbose", "Rules installation completed");
return;
}
const hasInstalledMdc = await hasInstalledRules(cwd);
const mainOptions = [
"download-official",
"download-community"
];
if (hasInstalledMdc) {
mainOptions.push("update", "manage");
}
const mainOption = await selectPrompt({
title: "AI IDE Rules",
options: [
{
value: "download-official",
label: "Download official",
hint: "blefnk/awesome-cursor-rules"
},
{
value: "download-community",
label: "Download community",
hint: "pontusab/directories"
},
...hasInstalledMdc ? [
{
value: "update",
label: "Update rules",
hint: "Check and update installed rules"
},
{
value: "manage",
label: "Manage installed rules",
hint: "View or remove installed rules"
}
] : []
]
});
if (mainOption === "download-official" || mainOption === "download-community") {
const repoId = mainOption === "download-official" ? "blefnk/awesome-cursor-rules" : "pontusab/directories";
const [owner] = repoId.split("/");
if (!owner) {
relinka("error", "Invalid repository selection");
return;
}
const filesToInstall = await downloadRules(repoId, isDev);
if (filesToInstall.length === 0) {
relinka("error", "No rule files found");
return;
}
await installRules(filesToInstall, owner, cwd);
relinka("verbose", "Rules installation completed");
} else if (mainOption === "update") {
await handleRuleUpdates(cwd, isDev);
} else if (mainOption === "manage") {
const rulesDir = path.join(cwd, ".cursor", "rules");
const files = await fs.readdir(rulesDir);
const ruleFiles = files.filter(
(file) => file.endsWith(".md") || file.endsWith(RULE_FILE_EXTENSION)
);
if (ruleFiles.length === 0) {
relinka("info", "No rules installed");
return;
}
const managementOption = await selectPrompt({
title: "Rule Management",
options: [
{
value: "view",
label: "View installed rules",
hint: `${ruleFiles.length} rules installed`
},
{
value: "delete",
label: "Delete rules",
hint: "Remove installed rules"
}
]
});
if (managementOption === "view") {
relinka("success", "Installed rules:");
for (const file of ruleFiles) {
relinka("info", ` - ${path.basename(file)}`);
}
} else if (managementOption === "delete") {
const filesToDelete = await multiselectPrompt({
title: "Select rules to delete",
displayInstructions: true,
options: ruleFiles.map((file) => ({
value: file,
label: path.basename(file)
}))
});
if (filesToDelete.length > 0) {
const confirmed = await confirmPrompt({
title: `Delete ${filesToDelete.length} rule(s)?`
});
if (confirmed) {
for (const file of filesToDelete) {
await fs.remove(path.join(rulesDir, file));
}
relinka("success", `Deleted ${filesToDelete.length} rule(s)`);
}
}
}
}
}