diff --git a/app/actions/rules.yaml b/app/actions/rules.yaml index 20c52bd..4d8240b 100644 --- a/app/actions/rules.yaml +++ b/app/actions/rules.yaml @@ -84,6 +84,7 @@ env-secrets: ## Environment Variables - Store secrets in a .env file (never commit it) - A .env.example file should be provided for reference and any new secrets should be added to it + - Any secret that is no longer needed should be removed from the .env.example file - The implementation should use the dotenv (or similar) library to load environment variables from .env files - Variables should also be loaded from the environment diff --git a/app/main.py b/app/main.py index 3e0f3d7..1459e78 100644 --- a/app/main.py +++ b/app/main.py @@ -3,7 +3,7 @@ from fastapi.staticfiles import StaticFiles from fastapi.templating import Jinja2Templates from pathlib import Path -from app.routes import install, actions, recommend, generate +from app.routes import actions, recommend, generate from app.services.actions_loader import actions_loader from api_analytics.fastapi import Analytics from fastapi_mcp import FastApiMCP @@ -43,7 +43,6 @@ app.mount("/static", StaticFiles(directory=static_dir), name="static") # Include routers -app.include_router(install.router) app.include_router(actions.router) app.include_router(recommend.router) app.include_router(generate.router) diff --git a/app/routes/actions.py b/app/routes/actions.py index 8c79d1e..35ab9e0 100644 --- a/app/routes/actions.py +++ b/app/routes/actions.py @@ -1,14 +1,11 @@ -from fastapi import APIRouter, HTTPException, Body, Query -from app.models.actions import ActionsResponse, Agent, Rule, MCP, Action, ActionType, ActionsListResponse +from fastapi import APIRouter, HTTPException, Query +from app.models.actions import Action, ActionType, ActionsListResponse from app.services.actions_loader import actions_loader -from app.services.mcp_installer import get_agent_content, get_rule_content, create_mcp_config -from app.services.search_service import search_service -from typing import List, Dict, Any, Optional -import json +from typing import Optional router = APIRouter(prefix="/api", tags=["actions"]) -@router.get("/v2/actions", response_model=ActionsListResponse, operation_id="get_unified_actions") +@router.get("/actions", response_model=ActionsListResponse, operation_id="get_unified_actions") async def get_unified_actions( action_type: Optional[ActionType] = Query(None, description="Filter by action type"), tags: Optional[str] = Query(None, description="Comma-separated list of tags to filter by"), @@ -44,275 +41,7 @@ async def get_unified_actions( has_more=(offset + limit) < total ) -@router.get("/v2/actions/{action_id}", response_model=Action, operation_id="get_action_by_id") -async def get_action_by_id(action_id: str): - """Get a specific action by ID""" - action = actions_loader.get_action_by_id(action_id) - if not action: - raise HTTPException(status_code=404, detail=f"Action not found: {action_id}") - return action -@router.get("/actions", response_model=ActionsResponse, operation_id="get_all_actions_endpoint") -async def get_all_actions(): - """Get all available actions (agents, rules, MCPs)""" - return ActionsResponse( - agents=actions_loader.get_agents(), - rules=actions_loader.get_rules(), - mcps=actions_loader.get_mcps() - ) - -@router.get("/agents", operation_id="get_agents_endpoint") -async def get_agents( - after: Optional[str] = Query(None, description="Cursor for pagination (use slug of last item from previous page)"), - limit: int = Query(30, ge=1, le=30, description="Maximum number of results (max 30)") -): - """Get available agents with tags only, limited to 30 items""" - agents = actions_loader.get_agents() - - # Find starting position if 'after' is provided - start_idx = 0 - if after: - for idx, agent in enumerate(agents): - if agent.slug == after: - start_idx = idx + 1 - break - - # Apply limit - paginated_agents = agents[start_idx:start_idx + limit] - - return [ - { - "name": agent.name, - "display_name": agent.display_name, - "slug": agent.slug, - "tags": agent.tags, - "filename": agent.filename - } - for agent in paginated_agents - ] - -@router.get("/rules", operation_id="get_rules_endpoint") -async def get_rules( - after: Optional[str] = Query(None, description="Cursor for pagination (use slug of last item from previous page)"), - limit: int = Query(30, ge=1, le=30, description="Maximum number of results (max 30)") -): - """Get available rules with tags only, limited to 30 items""" - rules = actions_loader.get_rules() - - # Find starting position if 'after' is provided - start_idx = 0 - if after: - for idx, rule in enumerate(rules): - if rule.slug == after: - start_idx = idx + 1 - break - - # Apply limit - paginated_rules = rules[start_idx:start_idx + limit] - - return [ - { - "name": rule.name, - "display_name": rule.display_name, - "slug": rule.slug, - "tags": rule.tags, - "filename": rule.filename - } - for rule in paginated_rules - ] - -@router.get("/mcps", operation_id="get_mcps_endpoint") -async def get_mcps( - after: Optional[str] = Query(None, description="Cursor for pagination (use name of last item from previous page)"), - limit: int = Query(30, ge=1, le=30, description="Maximum number of results (max 30)") -): - """Get available MCPs with tags only, limited to 30 items""" - mcps = actions_loader.get_mcps() - - # Find starting position if 'after' is provided - start_idx = 0 - if after: - for idx, mcp in enumerate(mcps): - if mcp.name == after: - start_idx = idx + 1 - break - - # Apply limit - paginated_mcps = mcps[start_idx:start_idx + limit] - - return [ - { - "name": mcp.name, - "tags": mcp.tags if hasattr(mcp, 'tags') else [] - } - for mcp in paginated_mcps - ] - - - - -@router.get("/merged-block", operation_id="get_merged_actions_block_endpoint") -async def get_merged_actions_block(): - """Get all actions merged into a single block with metadata for frontend""" - agents = actions_loader.get_agents() - rules = actions_loader.get_rules() - mcps = actions_loader.get_mcps() - - # Build merged block with all actions and their metadata - merged = { - "agents": [ - { - "display_name": agent.display_name or agent.name, - "slug": agent.slug or agent.filename.replace('.yaml', '').replace('.md', ''), - "content": agent.content or get_agent_content(agent.filename), - "filename": agent.filename - } - for agent in agents - ], - "rules": [ - { - "display_name": rule.display_name or rule.name, - "slug": rule.slug or rule.filename.replace('.yaml', '').replace('.md', ''), - "content": rule.content or get_rule_content(rule.filename), - "filename": rule.filename - } - for rule in rules - ], - "mcps": [ - { - "name": mcp.name, - "config": mcp.config - } - for mcp in mcps - ] - } - - return merged - -@router.get("/search/agents", tags=["mcp"], operation_id="search_agents_endpoint") -async def search_agents( - query: str = Query(..., description="Search query. Supports wildcards: * (any characters) and ? (single character)"), - limit: int = Query(10, ge=1, le=100, description="Maximum number of results") -): - """Search for agents by name, display_name, or content. Supports wildcard patterns with * and ?""" - results = search_service.search_agents(query, limit) - return {"results": results} -@router.get("/search/rules", tags=["mcp"], operation_id="search_rules_endpoint") -async def search_rules( - query: str = Query(..., description="Search query. Supports wildcards: * (any characters) and ? (single character)"), - limit: int = Query(10, ge=1, le=100, description="Maximum number of results") -): - """Search for rules by name, display_name, content, tags, or author. Supports wildcard patterns with * and ?""" - results = search_service.search_rules(query, limit) - return {"results": results} -@router.get("/search/mcps", tags=["mcp"], operation_id="search_mcps_endpoint") -async def search_mcps( - query: str = Query(..., description="Search query. Supports wildcards: * (any characters) and ? (single character)"), - limit: int = Query(10, ge=1, le=100, description="Maximum number of results") -): - """Search for MCPs by name or config content. Supports wildcard patterns with * and ?""" - results = search_service.search_mcps(query, limit) - return {"results": results} -@router.get("/search", tags=["mcp"], operation_id="search_all_endpoint") -async def search_all( - query: str = Query(..., description="Search query. Supports wildcards: * (any characters) and ? (single character)"), - limit: int = Query(10, ge=1, le=100, description="Maximum number of results per category") -): - """Search across all types (agents, rules, MCPs). Supports wildcard patterns with * and ?""" - return search_service.search_all(query, limit) - -@router.get("/rules/{rule_ids}", tags=["mcp"], operation_id="get_multiple_rules_content") -async def get_multiple_rules_content(rule_ids: str): - """Get content for multiple rules by comma-separated IDs/slugs""" - ids = [id.strip() for id in rule_ids.split(',') if id.strip()] - - if not ids: - raise HTTPException(status_code=400, detail="No rule IDs provided") - - rules = actions_loader.get_rules() - results = [] - - for rule_id in ids: - # Match by slug first, fallback to name for backward compat - rule = next((r for r in rules if (r.slug == rule_id or r.name == rule_id)), None) - - if rule: - results.append({ - "id": rule_id, - "slug": rule.slug, - "name": rule.name, - "display_name": rule.display_name, - "content": rule.content, - "filename": rule.filename - }) - else: - results.append({ - "id": rule_id, - "error": f"Rule not found: {rule_id}" - }) - - return {"rules": results} - -@router.get("/agents/{agent_ids}", tags=["mcp"], operation_id="get_multiple_agents_content") -async def get_multiple_agents_content(agent_ids: str): - """Get content for multiple agents by comma-separated IDs/slugs""" - ids = [id.strip() for id in agent_ids.split(',') if id.strip()] - - if not ids: - raise HTTPException(status_code=400, detail="No agent IDs provided") - - agents = actions_loader.get_agents() - results = [] - - for agent_id in ids: - # Match by slug first, fallback to name for backward compat - agent = next((a for a in agents if (a.slug == agent_id or a.name == agent_id)), None) - - if agent: - results.append({ - "id": agent_id, - "slug": agent.slug, - "name": agent.name, - "display_name": agent.display_name, - "content": agent.content, - "filename": agent.filename - }) - else: - results.append({ - "id": agent_id, - "error": f"Agent not found: {agent_id}" - }) - - return {"agents": results} - -@router.get("/mcps/{mcp_ids}", tags=["mcp"], operation_id="get_multiple_mcps_config") -async def get_multiple_mcps_config(mcp_ids: str): - """Get config for multiple MCPs by comma-separated names. These MCPs will need to be installed on the client side (e.g. in `.mcp.json`)""" - ids = [id.strip() for id in mcp_ids.split(',') if id.strip()] - - if not ids: - raise HTTPException(status_code=400, detail="No MCP IDs provided") - - mcps = actions_loader.get_mcps() - results = [] - - for mcp_id in ids: - # Match by name - mcp = next((m for m in mcps if m.name == mcp_id), None) - - if mcp: - results.append({ - "id": mcp_id, - "name": mcp.name, - "config": mcp.config - }) - else: - results.append({ - "id": mcp_id, - "error": f"MCP not found: {mcp_id}" - }) - - return {"mcps": results} \ No newline at end of file diff --git a/app/routes/generate.py b/app/routes/generate.py index 84bee54..0a313d0 100644 --- a/app/routes/generate.py +++ b/app/routes/generate.py @@ -4,7 +4,7 @@ import json from app.services.actions_loader import actions_loader -router = APIRouter(prefix="/api/v2", tags=["generate"]) +router = APIRouter(prefix="/api", tags=["generate"]) class GenerateRequest(BaseModel): action_ids: List[str] @@ -122,31 +122,20 @@ def generate_patch(files: Dict[str, str], source: str = "scratch", repo_url: str # Add a comment header explaining the patch if source == "repo" and repo_url: patch_lines.append(f"# Gitrules configuration patch generated from repository: {repo_url}") - patch_lines.append("# Apply with: git apply ") - use_git_format = True + patch_lines.append("# Apply with: patch -p0 < ") elif source == "template": patch_lines.append("# Gitrules configuration patch generated from template") patch_lines.append("# Apply with: patch -p0 < ") - use_git_format = False else: patch_lines.append("# Gitrules configuration patch generated from scratch") patch_lines.append("# Apply with: patch -p0 < ") - use_git_format = False patch_lines.append("") for filepath, content in files.items(): - if use_git_format: - # Git format - patch_lines.append(f"diff --git a/{filepath} b/{filepath}") - patch_lines.append("new file mode 100644") - patch_lines.append("index 0000000..1234567") - patch_lines.append("--- /dev/null") - patch_lines.append(f"+++ b/{filepath}") - else: - # Standard patch format - patch_lines.append(f"--- /dev/null") - patch_lines.append(f"+++ {filepath}") + # Standard patch format + patch_lines.append(f"--- /dev/null") + patch_lines.append(f"+++ {filepath}") lines = content.split('\n') if lines and lines[-1] == '': diff --git a/app/routes/install.py b/app/routes/install.py deleted file mode 100644 index 566741b..0000000 --- a/app/routes/install.py +++ /dev/null @@ -1,61 +0,0 @@ -from fastapi import APIRouter, HTTPException, Request -from fastapi.responses import PlainTextResponse -from fastapi.templating import Jinja2Templates -from pydantic import BaseModel -import hashlib -import re -from typing import Dict, Set -from datetime import datetime - -router = APIRouter() -templates = Jinja2Templates(directory="app/templates") - -# In-memory storage for installs -installs_store: Dict[str, str] = {} - -class InstallCreate(BaseModel): - files: Dict[str, str] - -def extract_env_vars_from_files(files: Dict[str, str]) -> Set[str]: - """Extract environment variables from file contents""" - env_vars = set() - for content in files.values(): - # Find ${VAR_NAME} patterns - matches = re.findall(r'\$\{([^}]+)\}', content) - env_vars.update(matches) - return env_vars - -@router.post("/api/install", operation_id="create_install_script") -async def create_install(request: Request, install: InstallCreate): - """Generate install script from files and store by hash""" - # Extract unique directories - directories = set() - for path in install.files.keys(): - parts = path.split('/') - if len(parts) > 1: - for i in range(1, len(parts)): - directories.add('/'.join(parts[:i])) - - # Extract environment variables from all files - env_vars = extract_env_vars_from_files(install.files) - - # Generate script using Jinja2 template - script_content = templates.get_template("install.sh.j2").render( - timestamp=datetime.now().isoformat(), - files=install.files, - directories=sorted(directories), - env_vars=sorted(env_vars) if env_vars else None - ) - - # Hash the script content - content_hash = hashlib.sha256(script_content.encode()).hexdigest()[:12] - installs_store[content_hash] = script_content - - return {"hash": content_hash} - -@router.get("/api/install/{hash_id}.sh", response_class=PlainTextResponse, operation_id="get_install_script") -async def get_install(hash_id: str): - """Retrieve install by hash""" - if hash_id not in installs_store: - raise HTTPException(status_code=404, detail="Install not found") - return installs_store[hash_id] \ No newline at end of file diff --git a/app/static/js/auto_share.js b/app/static/js/auto_share.js deleted file mode 100644 index 4d2080f..0000000 --- a/app/static/js/auto_share.js +++ /dev/null @@ -1,283 +0,0 @@ -/** - * AutoShare Manager - Handles automatic sharing with debounced saves - */ -class AutoShareManager { - constructor() { - this.state = 'synced'; // hidden, synced, syncing, error - this.dirty = false; - this.currentShareUrl = null; - this.currentShareId = null; - this.debounceTimer = null; - this.pendingSync = false; - this.lastPayloadHash = null; - - // UI elements - this.panel = null; - this.linkInput = null; - this.copyButton = null; - - this.init(); - } - - init() { - this.panel = document.getElementById('auto-share-panel'); - this.linkInput = document.getElementById('share-link-input'); - this.copyButton = document.getElementById('copy-share-link'); - - if (this.copyButton) { - this.copyButton.addEventListener('click', () => this.copyLink()); - } - - // Listen for workspace changes - this.attachListeners(); - - // Trigger initial sync if there's content - setTimeout(() => { - const data = this.collectWorkspaceData(); - if (Object.keys(data).length > 0) { - this.markDirty(); - } - }, 500); - } - - attachListeners() { - // Listen for file content changes - window.addEventListener('workspace-content-changed', () => { - this.markDirty(); - }); - - // Listen for file additions/deletions - window.addEventListener('workspace-file-added', () => { - this.markDirty(); - }); - - window.addEventListener('workspace-file-deleted', () => { - this.markDirty(); - }); - } - - markDirty() { - this.dirty = true; - - // Immediately gray out the UI to show context has changed - this.setUnsyncedState(); - - // Clear existing timer - if (this.debounceTimer) { - clearTimeout(this.debounceTimer); - } - - // Set new timer for 500ms - this.debounceTimer = setTimeout(() => { - this.sync(); - }, 500); - } - - async sync() { - // Skip if already syncing - if (this.state === 'syncing') { - this.pendingSync = true; - return; - } - - // Collect current workspace data - const payload = this.collectWorkspaceData(); - - // If no files, don't sync - if (Object.keys(payload).length === 0) { - this.dirty = false; - this.setState('synced'); - if (this.linkInput) { - this.linkInput.value = 'Add files to generate install link'; - } - return; - } - - // Check if payload has changed - const payloadHash = this.hashPayload(payload); - if (payloadHash === this.lastPayloadHash) { - this.dirty = false; - return; - } - - // Update UI to syncing state - this.setState('syncing'); - - try { - // Send to API - const response = await fetch('/api/install', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ files: payload }) - }); - - if (!response.ok) { - throw new Error('Failed to sync'); - } - - const data = await response.json(); - - // Update state with new share URL - this.currentShareId = data.hash; - this.currentShareUrl = `sh -c "$(curl -fsSL ${window.location.origin}/api/install/${data.hash}.sh)"`; - this.lastPayloadHash = payloadHash; - this.dirty = false; - - // Update UI to synced state - this.setState('synced'); - - // Check if we need another sync - if (this.pendingSync || this.dirty) { - this.pendingSync = false; - setTimeout(() => this.sync(), 100); - } - - } catch (error) { - console.error('Auto-share sync failed:', error); - this.setState('error'); - - // Retry after a delay if still dirty - if (this.dirty) { - setTimeout(() => this.sync(), 2000); - } - } - } - - collectWorkspaceData() { - const allFiles = {}; - - // Get files directly from workspace manager state - const state = window.workspaceManager?.getState(); - if (state?.files) { - return { ...state.files }; - } - - // Fallback: collect files from tree structure - function collectFilesFromTree(nodes, collected) { - nodes.forEach(node => { - if (node.type === 'file') { - const state = window.workspaceManager?.getState(); - if (state?.files[node.path]) { - collected[node.path] = state.files[node.path]; - } - } else if (node.type === 'folder' && node.children) { - collectFilesFromTree(node.children, collected); - } - }); - } - - // Collect files from dynamic tree if available - if (window.generateFileTreeData) { - const fileTreeData = window.generateFileTreeData(); - collectFilesFromTree(fileTreeData, allFiles); - } - - return allFiles; - } - - hashPayload(payload) { - // Simple hash for change detection - return JSON.stringify(payload); - } - - setUnsyncedState() { - // Gray out only the script input immediately when content changes - if (this.linkInput) { - this.linkInput.classList.add('opacity-50'); - this.linkInput.disabled = true; - if (this.currentShareUrl) { - this.linkInput.value = this.currentShareUrl; - } - } - // Keep copy button enabled but just disable if no URL - if (this.copyButton) { - this.copyButton.disabled = !this.currentShareUrl; - } - } - - setState(newState) { - this.state = newState; - - switch (newState) { - case 'hidden': - if (this.panel) this.panel.style.display = 'none'; - break; - - case 'synced': - if (this.panel) this.panel.style.display = 'flex'; - if (this.linkInput) { - this.linkInput.value = this.currentShareUrl || 'Add files to generate install link'; - this.linkInput.classList.remove('opacity-50'); - this.linkInput.disabled = false; - } - if (this.copyButton) { - this.copyButton.disabled = !this.currentShareUrl; - if (this.currentShareUrl) { - this.copyButton.classList.remove('opacity-50'); - } else { - this.copyButton.classList.add('opacity-50'); - } - } - break; - - case 'syncing': - if (this.panel) this.panel.style.display = 'flex'; - if (this.linkInput) { - this.linkInput.classList.add('opacity-50'); - this.linkInput.disabled = true; - } - if (this.copyButton) { - this.copyButton.disabled = !this.currentShareUrl; - // Don't gray out the copy button - this.copyButton.classList.remove('opacity-50'); - } - break; - - case 'error': - if (this.panel) this.panel.style.display = 'flex'; - // Keep last good link visible but indicate error somehow - if (this.linkInput && this.currentShareUrl) { - this.linkInput.value = this.currentShareUrl; - this.linkInput.classList.remove('opacity-50'); - this.linkInput.disabled = false; - } - if (this.copyButton && this.currentShareUrl) { - this.copyButton.disabled = false; - this.copyButton.classList.remove('opacity-50'); - } - break; - } - } - - async copyLink() { - if (!this.currentShareUrl) return; - - try { - await navigator.clipboard.writeText(this.currentShareUrl); - - // Show feedback - const originalText = this.copyButton.textContent; - this.copyButton.textContent = 'Copied!'; - this.copyButton.classList.remove('bg-cyan-400'); - this.copyButton.classList.add('bg-green-400'); - - setTimeout(() => { - this.copyButton.textContent = originalText; - this.copyButton.classList.remove('bg-green-400'); - this.copyButton.classList.add('bg-cyan-400'); - }, 2000); - } catch (error) { - console.error('Failed to copy:', error); - } - } -} - -// Initialize when DOM is ready -document.addEventListener('DOMContentLoaded', function() { - // Wait a bit for workspace to be ready - setTimeout(() => { - window.autoShareManager = new AutoShareManager(); - }, 200); -}); \ No newline at end of file diff --git a/app/templates/components/final_step_modal.html b/app/templates/components/final_step_modal.html index bda80d3..7883c57 100644 --- a/app/templates/components/final_step_modal.html +++ b/app/templates/components/final_step_modal.html @@ -56,7 +56,7 @@

👁️ File Preview

🔧 Patch Preview

The following patch file will be generated for easy application to your repository:

- git apply gitrules-config.patch + patch -p0 < gitrules-config.patch
@@ -248,12 +248,9 @@

🤖 Agents (${selectedAgents.length})

// Generate patch for each file Object.entries(state.files).forEach(([path, content]) => { - // Create a proper git diff format - patchContent += `diff --git a/${path} b/${path}\n`; - patchContent += `new file mode 100644\n`; - patchContent += `index 0000000..0000000\n`; + // Create a proper patch format patchContent += `--- /dev/null\n`; - patchContent += `+++ b/${path}\n`; + patchContent += `+++ ${path}\n`; // Add file content with + prefix for each line const lines = content.split('\n'); @@ -323,11 +320,8 @@

🤖 Agents (${selectedAgents.length})

// Generate patch for each file Object.entries(state.files).forEach(([path, content]) => { - patchContent += `diff --git a/${path} b/${path}\n`; - patchContent += `new file mode 100644\n`; - patchContent += `index 0000000..0000000\n`; patchContent += `--- /dev/null\n`; - patchContent += `+++ b/${path}\n`; + patchContent += `+++ ${path}\n`; const lines = content.split('\n'); patchContent += `@@ -0,0 +1,${lines.length} @@\n`; @@ -350,7 +344,7 @@

🤖 Agents (${selectedAgents.length})

// Copy patch to clipboard navigator.clipboard.writeText(patchContent).then(() => { - alert('Patch copied to clipboard! You can now paste and apply it using: git apply'); + alert('Patch copied to clipboard! You can now paste and apply it using: patch -p0'); }).catch(err => { console.error('Failed to copy patch:', err); alert('Failed to copy patch to clipboard. Please download the files instead.'); diff --git a/app/templates/components/workspace.html b/app/templates/components/workspace.html deleted file mode 100644 index acfae86..0000000 --- a/app/templates/components/workspace.html +++ /dev/null @@ -1,149 +0,0 @@ - -{% include 'components/ActionButton.html' %} - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/templates/generate.html b/app/templates/generate.html index 8a265d1..8248301 100644 --- a/app/templates/generate.html +++ b/app/templates/generate.html @@ -58,7 +58,7 @@ } -
+
@@ -74,11 +74,11 @@

Generate Your Configuration

-
+
-
+

📋 Selected Tools

@@ -202,7 +202,7 @@

🔧 Configuration Patch

await loadActionDetails(); generateFiles(); - displaySummary(); + await displaySummary(); displayFilePreviews(); displayPatchPreview(); }); @@ -213,7 +213,7 @@

🔧 Configuration Patch

const selectedFormats = getSelectedFormats(); // Fetch details for all selected actions and formats - const response = await fetch('/api/v2/generate', { + const response = await fetch('/api/generate', { method: 'POST', headers: { 'Content-Type': 'application/json' @@ -257,91 +257,79 @@

🔧 Configuration Patch

// This function could be used for client-side generation if needed } -function displaySummary() { +async function displaySummary() { const container = document.getElementById('selectedSummary'); - // Group actions by type - const grouped = { - agents: [], - rules: [], - rulesets: [], - mcps: [], - packs: [] - }; - - // Parse the generated files to extract action types - if (generatedFiles['CLAUDE.md']) { - // Count rules and rulesets - const claudeContent = generatedFiles['CLAUDE.md']; - const ruleMatches = claudeContent.match(/## .+/g) || []; - grouped.rules = ruleMatches; - } - - if (generatedFiles['AGENTS.md']) { - // Count agents from AGENTS.md content - const agentsContent = generatedFiles['AGENTS.md']; - const agentMatches = agentsContent.match(/^## .+$/gm) || []; - grouped.agents = agentMatches; - } else if (generatedFiles['.claude/agents']) { - // Fallback: Count individual agent files - grouped.agents = Object.keys(generatedFiles).filter(f => f.startsWith('.claude/agents/')); - } - - if (generatedFiles['.mcp.json']) { - // Count MCPs - try { - const mcpConfig = JSON.parse(generatedFiles['.mcp.json']); - grouped.mcps = Object.keys(mcpConfig.mcpServers || {}); - } catch (e) { - console.error('Error parsing MCP config:', e); - } - } - - let html = ''; - - if (grouped.agents.length > 0) { - html += ` -
-

🤖 Agents (${grouped.agents.length})

-
    - ${grouped.agents.map(a => { - if (typeof a === 'string') { - // If it's a header match from AGENTS.md (like "## Agent Name") - return `
  • • ${a.replace('## ', '')}
  • `; - } else { - // If it's a file path (fallback for individual files) - return `
  • • ${a.split('/').pop().replace('.yaml', '').replace('.md', '')}
  • `; - } - }).join('')} -
-
- `; - } - - if (grouped.rules.length > 0) { - html += ` -
-

📋 Rules (${grouped.rules.length})

-
    - ${grouped.rules.slice(0, 5).map(r => `
  • • ${r.replace('## ', '')}
  • `).join('')} - ${grouped.rules.length > 5 ? `
  • ... and ${grouped.rules.length - 5} more
  • ` : ''} -
-
- `; + if (!selectedActions || selectedActions.length === 0) { + container.innerHTML = '

No tools selected

'; + return; } - if (grouped.mcps.length > 0) { - html += ` -
-

🔌 MCPs (${grouped.mcps.length})

-
    - ${grouped.mcps.map(m => `
  • • ${m}
  • `).join('')} -
-
- `; + try { + // Fetch details for all selected actions + const response = await fetch('/api/actions?limit=100'); + const data = await response.json(); + const allActions = data.actions || []; + + // Filter to get only selected actions + const selectedActionDetails = allActions.filter(action => + selectedActions.includes(action.id) + ); + + // Group actions by type + const grouped = { + agents: [], + rules: [], + rulesets: [], + mcps: [], + packs: [] + }; + + selectedActionDetails.forEach(action => { + if (grouped[action.action_type + 's']) { + grouped[action.action_type + 's'].push(action); + } else if (action.action_type === 'ruleset') { + grouped.rulesets.push(action); + } else if (action.action_type === 'pack') { + grouped.packs.push(action); + } + }); + + let html = ''; + + // Display each category + Object.entries(grouped).forEach(([type, items]) => { + if (items.length === 0) return; + + const typeColors = { + agents: { bg: 'bg-cyan-50', icon: '🤖' }, + rules: { bg: 'bg-pink-50', icon: '📋' }, + rulesets: { bg: 'bg-pink-50', icon: '📚' }, + mcps: { bg: 'bg-yellow-50', icon: '🔌' }, + packs: { bg: 'bg-purple-50', icon: '📦' } + }; + + const config = typeColors[type] || { bg: 'bg-gray-50', icon: '⚙️' }; + const displayName = type.charAt(0).toUpperCase() + type.slice(1); + + html += ` +
+

${config.icon} ${displayName} (${items.length})

+
    + ${items.slice(0, 8).map(item => + `
  • • ${item.display_name || item.name}
  • ` + ).join('')} + ${items.length > 8 ? `
  • ... and ${items.length - 8} more
  • ` : ''} +
+
+ `; + }); + + container.innerHTML = html || '

No tools selected

'; + } catch (error) { + console.error('Error loading action details:', error); + container.innerHTML = '

Error loading selected tools

'; } - - container.innerHTML = html || '

No tools selected

'; } function displayFilePreviews() { @@ -415,11 +403,7 @@

🔌 MCPs (${grouped.mcps.length})

const commandElement = document.getElementById('patchCommand'); // Update command based on source - if (sourceInfo.source === 'repo') { - commandElement.innerHTML = 'git apply << \'EOF\'
[patch content]
EOF'; - } else { - commandElement.innerHTML = 'patch -p0 << \'EOF\'
[patch content]
EOF'; - } + commandElement.innerHTML = 'patch -p0 << \'EOF\'
[patch content]
EOF'; if (!patchContent) { container.innerHTML = '
No patch generated
'; @@ -485,11 +469,6 @@

🔌 MCPs (${grouped.mcps.length})

zip.file(path, content); }); - // Also add the patch file if it exists - if (patchContent) { - zip.file('gitrules-config.patch', patchContent); - } - // Generate the ZIP file const zipBlob = await zip.generateAsync({ type: 'blob' }); @@ -545,19 +524,11 @@

🔌 MCPs (${grouped.mcps.length})

let contentToCopy; let message; - if (isFromRepo) { - // For repo source: git apply with inline patch - contentToCopy = `git apply << 'EOF' + // Use patch for all sources + contentToCopy = `patch -p0 << 'EOF' ${patchContent} EOF`; - message = 'Git patch command copied to clipboard! Paste and run directly in your terminal'; - } else { - // For template/scratch: patch with inline content - contentToCopy = `patch -p0 << 'EOF' -${patchContent} -EOF`; - message = 'Patch command copied to clipboard! Paste and run directly in your terminal'; - } + message = 'Patch command copied to clipboard! Paste and run directly in your terminal'; navigator.clipboard.writeText(contentToCopy).then(() => { showNotification(message); @@ -589,17 +560,15 @@

🔌 MCPs (${grouped.mcps.length})

function applyConfiguration() { // Show instructions for applying - const isFromRepo = sourceInfo.source === 'repo'; - const command = isFromRepo ? 'git apply gitrules-config.patch' : 'patch -p0 < gitrules-config.patch'; - const reviewStep = isFromRepo ? 'git status' : 'ls -la'; - const commitStep = isFromRepo ? 'git commit -am "Add Gitrules configuration"' : ''; + const command = 'patch -p0 < gitrules-config.patch'; + const reviewStep = 'ls -la'; const instructions = ` To apply this configuration to your repository: 1. Save the patch file as 'gitrules-config.patch' 2. Run: ${command} -3. Review the changes with: ${reviewStep}${commitStep ? '\n4. Commit when ready: ' + commitStep : ''} +3. Review the changes with: ${reviewStep} `; alert(instructions); diff --git a/app/templates/index.html b/app/templates/index.html deleted file mode 100644 index 64c3a29..0000000 --- a/app/templates/index.html +++ /dev/null @@ -1,20 +0,0 @@ -{% extends "base.html" %} - -{% block content %} - {% include 'components/hero.html' %} - - - {% include 'components/quick_actions.html' %} - - - {% include 'components/workspace.html' %} - - - {% include 'components/file_modal.html' %} - - - {% include 'components/context_modal.html' %} - - - {% include 'components/final_step_modal.html' %} -{% endblock %} \ No newline at end of file diff --git a/app/templates/install.sh.j2 b/app/templates/install.sh.j2 deleted file mode 100644 index cc06fe3..0000000 --- a/app/templates/install.sh.j2 +++ /dev/null @@ -1,59 +0,0 @@ -#!/bin/bash -# Gitrules Installation Script -# Generated: {{ timestamp }} - -echo " ____ _ _ ____ _ " -echo " / ___(_) |_| _ \\ _ _| | ___ ___ " -echo "| | _| | __| |_) | | | | |/ _ \\/ __|" -echo "| |_| | | |_| _ <| |_| | | __/\\__ \\" -echo " \\____|_|\\__|_| \\_\\\\__,_|_|\\___||___/ " - -echo "" - -echo "This script will create/modify the following files:" -echo "" -{% for path in files -%} -echo " - {{ path }}" -{% endfor -%} -echo "" -echo "Total files: {{ files|length }}" -echo "" -if [ -t 0 ]; then - printf "Do you want to proceed? (Y/n): " - read REPLY - if [ "$REPLY" != "Y" ] && [ "$REPLY" != "y" ] && [ -n "$REPLY" ]; then - echo "Installation cancelled." - exit 1 - fi -else - echo "Running in non-interactive mode, proceeding automatically..." - echo "" -fi - -echo "" -echo "Creating files..." -echo "" - -# Create necessary directories -{% for dir in directories -%} -mkdir -p "{{ dir }}" -{% endfor -%} - -{% for path, content in files.items() -%} -# Creating {{ path }} -cat > "{{ path }}" << 'GITRULES_EOF' -{{ content|safe }}{% if not content.endswith('\n') %} -{% endif %}GITRULES_EOF - -{% endfor -%} -echo "Installation complete!" -echo "" -echo "Files have been created in your current directory." -{% if env_vars %} -echo "" -echo "⚠️ Environment Variables Required:" -{% for env_var in env_vars %} -echo " - {{ env_var }}" -{% endfor %} -{% endif %} -echo "" \ No newline at end of file diff --git a/app/templates/select.html b/app/templates/select.html index 84ebf9f..60df61d 100644 --- a/app/templates/select.html +++ b/app/templates/select.html @@ -192,7 +192,7 @@

async function loadActions() { try { - const response = await fetch('/api/v2/actions?limit=100'); + const response = await fetch('/api/actions?limit=100'); const data = await response.json(); allActions = data.actions; filteredActions = [...allActions];