UNPKG

@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
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)`); } } } } }