feat: Add analyze and Make My Version buttons
- Analyze shows skill description and preview - Make My Version creates custom skill without installing from ClawHub
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { exec } from "child_process";
|
||||
import { promisify } from "util";
|
||||
import { existsSync } from "fs";
|
||||
import { existsSync, readFileSync } from "fs";
|
||||
import { readFile, writeFile } from "fs/promises";
|
||||
import path from "path";
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
@@ -82,6 +83,85 @@ export async function GET(req: NextRequest) {
|
||||
return NextResponse.json({ info: stdout });
|
||||
}
|
||||
|
||||
if (action === "analyze") {
|
||||
try {
|
||||
// Install to temp for analysis
|
||||
const tempPath = `/tmp/clawhub-analyze-${slug}`;
|
||||
await execAsync(`rm -rf ${tempPath} 2>/dev/null; ${CLAWHUB_BIN} install ${slug} --dir ${tempPath} 2>&1`);
|
||||
|
||||
// Read SKILL.md
|
||||
const skillMdPath = path.join(tempPath, slug, "SKILL.md");
|
||||
let content = "";
|
||||
if (existsSync(skillMdPath)) {
|
||||
content = await readFile(skillMdPath, "utf-8");
|
||||
} else {
|
||||
// Try without slug subfolder
|
||||
const altPath = path.join(tempPath, "SKILL.md");
|
||||
if (existsSync(altPath)) {
|
||||
content = await readFile(altPath, "utf-8");
|
||||
}
|
||||
}
|
||||
|
||||
// Extract description
|
||||
const descMatch = content.match(/description:\s*([^\n]+)/i);
|
||||
const desc = descMatch ? descMatch[1].trim() : "No description available";
|
||||
|
||||
// Extract triggers
|
||||
const triggerMatch = content.match(/triggers?:\s*([^\n]+)/i);
|
||||
const triggers = triggerMatch ? triggerMatch[1].trim() : "General use";
|
||||
|
||||
// Clean up
|
||||
await execAsync(`rm -rf ${tempPath} 2>/dev/null`);
|
||||
|
||||
return NextResponse.json({
|
||||
description: desc,
|
||||
triggers: triggers,
|
||||
raw: content.substring(0, 2000)
|
||||
});
|
||||
} catch (e: any) {
|
||||
return NextResponse.json({ error: e.message, description: "Could not analyze skill" });
|
||||
}
|
||||
}
|
||||
|
||||
if (action === "clone") {
|
||||
try {
|
||||
// Install to temp
|
||||
const tempPath = `/tmp/clawhub-clone-${slug}`;
|
||||
await execAsync(`rm -rf ${tempPath} 2>/dev/null; ${CLAWHUB_BIN} install ${slug} --dir ${tempPath} 2>&1`);
|
||||
|
||||
// Find the skill folder
|
||||
const entries = await readdir(tempPath);
|
||||
const skillFolder = entries.find(e => e.includes(slug.replace(/-/g, '')));
|
||||
const sourcePath = path.join(tempPath, skillFolder || slug);
|
||||
|
||||
// Create custom version
|
||||
const customSlug = `${slug}-custom`;
|
||||
const destPath = path.join(SKILLS_BASE, "horus", customSlug);
|
||||
|
||||
// Copy files
|
||||
await execAsync(`cp -r ${sourcePath} ${destPath} 2>&1`);
|
||||
|
||||
// Modify SKILL.md
|
||||
const skillMdPath = path.join(destPath, "SKILL.md");
|
||||
if (existsSync(skillMdPath)) {
|
||||
let content = await readFile(skillMdPath, "utf-8");
|
||||
content = content
|
||||
.replace(/name:\s*([^\n]+)/i, `name: $1 (Custom)`)
|
||||
.replace(/description:\s*([^\n]+)/i, `description: Custom version for Haitham's workflow`);
|
||||
await writeFile(skillMdPath, content);
|
||||
}
|
||||
|
||||
// Clean up
|
||||
await execAsync(`rm -rf ${tempPath} 2>/dev/null`);
|
||||
|
||||
return NextResponse.json({
|
||||
output: `Created custom skill: ${customSlug}\nPath: ${destPath}\n\nEdit SKILL.md to customize further!`
|
||||
});
|
||||
} catch (e: any) {
|
||||
return NextResponse.json({ error: e.message });
|
||||
}
|
||||
}
|
||||
|
||||
if (action === "sync") {
|
||||
try {
|
||||
const { stdout, stderr } = await execAsync(`cd ${SKILLS_BASE} && git pull origin main 2>&1`);
|
||||
|
||||
@@ -8,6 +8,8 @@ interface Skill {
|
||||
name: string;
|
||||
score: number;
|
||||
category?: string;
|
||||
description?: string;
|
||||
triggers?: string;
|
||||
}
|
||||
|
||||
const AGENTS = ["horus", "amun", "cleo"] as const;
|
||||
@@ -43,6 +45,7 @@ export default function ClawHubMarketplace() {
|
||||
const [search, setSearch] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [selectedSkill, setSelectedSkill] = useState<Skill | null>(null);
|
||||
const [skillDetails, setSkillDetails] = useState<{description?: string; triggers?: string} | null>(null);
|
||||
const [selectedAgents, setSelectedAgents] = useState<string[]>(["horus"]);
|
||||
const [view, setView] = useState<"popular" | "categories" | "search">("popular");
|
||||
const [action, setAction] = useState<"install" | "delete" | "sync" | "push" | null>(null);
|
||||
@@ -200,6 +203,46 @@ export default function ClawHubMarketplace() {
|
||||
setSyncing(false);
|
||||
};
|
||||
|
||||
const handleAnalyze = async () => {
|
||||
if (!selectedSkill) return;
|
||||
setAction("analyze");
|
||||
setOutput("Analyzing skill...\n");
|
||||
try {
|
||||
const res = await fetch(`/api/clawhub?action=analyze&slug=${encodeURIComponent(selectedSkill.slug)}`);
|
||||
const data = await res.json();
|
||||
setSkillDetails({
|
||||
description: data.description,
|
||||
triggers: data.triggers
|
||||
});
|
||||
setOutput(`📋 ${selectedSkill.name}\n\n` +
|
||||
`Description: ${data.description || "N/A"}\n\n` +
|
||||
`Triggers: ${data.triggers || "N/A"}\n\n` +
|
||||
(data.raw ? `📄 SKILL.md Preview:\n${data.raw.substring(0, 500)}...` : ""));
|
||||
} catch (e) {
|
||||
setOutput("Error: " + String(e));
|
||||
}
|
||||
setAction(null);
|
||||
};
|
||||
|
||||
const handleClone = async () => {
|
||||
if (!selectedSkill) return;
|
||||
setAction("clone");
|
||||
setOutput("Creating custom version...\n");
|
||||
try {
|
||||
const res = await fetch("/api/clawhub", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ action: "clone", slug: selectedSkill.slug }),
|
||||
});
|
||||
const data = await res.json();
|
||||
setOutput(data.output || data.error || "Clone complete!");
|
||||
fetchInstalled();
|
||||
} catch (e) {
|
||||
setOutput("Error: " + String(e));
|
||||
}
|
||||
setAction(null);
|
||||
};
|
||||
|
||||
const handlePush = async () => {
|
||||
setAction("push");
|
||||
setOutput("Pushing to GitHub...\n");
|
||||
@@ -453,21 +496,37 @@ export default function ClawHubMarketplace() {
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex flex-col gap-2 mb-4">
|
||||
<button
|
||||
onClick={handleAnalyze}
|
||||
disabled={action !== null}
|
||||
className="bg-blue-600 hover:bg-blue-700 disabled:bg-blue-800 px-4 py-2 rounded-lg font-medium transition flex items-center justify-center gap-2"
|
||||
>
|
||||
{action === "analyze" ? "⏳ Analyzing..." : "🔍 Analyze Skill"}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleClone}
|
||||
disabled={action !== null}
|
||||
className="bg-purple-600 hover:bg-purple-700 disabled:bg-purple-800 px-4 py-2 rounded-lg font-medium transition flex items-center justify-center gap-2"
|
||||
>
|
||||
{action === "clone" ? "⏳ Creating..." : "🧬 Make My Version"}
|
||||
</button>
|
||||
<div className="border-t border-slate-600 pt-2 mt-2">
|
||||
<button
|
||||
onClick={handleInstall}
|
||||
disabled={action !== null || selectedAgents.length === 0}
|
||||
className="bg-green-600 hover:bg-green-700 disabled:bg-green-800 disabled:cursor-not-allowed px-4 py-2 rounded-lg font-medium transition flex items-center justify-center gap-2"
|
||||
className="w-full bg-green-600 hover:bg-green-700 disabled:bg-green-800 disabled:cursor-not-allowed px-4 py-2 rounded-lg font-medium transition flex items-center justify-center gap-2"
|
||||
>
|
||||
{action === "install" ? "⏳ Installing..." : `✅ Install on ${selectedAgents.length} agent(s)`}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleDelete}
|
||||
disabled={action !== null || selectedAgents.length === 0 || !isInstalled(selectedSkill.slug)}
|
||||
className="bg-red-600 hover:bg-red-700 disabled:bg-red-800 disabled:cursor-not-allowed px-4 py-2 rounded-lg font-medium transition flex items-center justify-center gap-2"
|
||||
className="w-full bg-red-600 hover:bg-red-700 disabled:bg-red-800 disabled:cursor-not-allowed px-4 py-2 rounded-lg font-medium transition flex items-center justify-center gap-2 mt-2"
|
||||
>
|
||||
{action === "delete" ? "⏳ Removing..." : `🗑️ Remove from ${selectedAgents.length} agent(s)`}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Output */}
|
||||
{output && (
|
||||
|
||||
Reference in New Issue
Block a user