skillctl 0.0.4 → 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/Cargo.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "skillctl"
3
- version = "0.0.4"
3
+ version = "0.0.6"
4
4
  edition = "2021"
5
5
  authors = ["Your Name <your.email@example.com>"]
6
6
  description = "Gestor de Skills para Agentes de IA"
@@ -11,12 +11,13 @@ name = "skillctl"
11
11
  path = "src/main.rs"
12
12
 
13
13
  [dependencies]
14
- clap = { version = "4.5", features = ["derive"] }
15
- serde = { version = "1.0", features = ["derive"] }
16
- toml = "0.8"
14
+ clap = { version = "4.4", features = ["derive"] }
15
+ colored = "2.0"
17
16
  anyhow = "1.0"
18
17
  reqwest = { version = "0.11", features = ["blocking", "json"] }
19
- tokio = { version = "1.35", features = ["full"] }
18
+ serde = { version = "1.0", features = ["derive"] }
19
+ serde_json = "1.0"
20
+ dialoguer = "0.11"
20
21
 
21
22
  [profile.release]
22
23
  opt-level = "z" # Optimizar para tamaño
package/bin/run.js CHANGED
@@ -13,8 +13,10 @@ if (!require('fs').existsSync(binPath)) {
13
13
  }
14
14
 
15
15
  // Ejecutar el binario y pasarle todos los argumentos
16
- const child = spawn(binPath, process.argv.slice(2), { stdio: 'inherit' });
17
-
16
+ const child = spawn(binPath, process.argv.slice(2), {
17
+ stdio: 'inherit',
18
+ cwd: process.cwd()
19
+ });
18
20
  child.on('close', (code) => {
19
21
  process.exit(code);
20
22
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillctl",
3
- "version": "0.0.4",
3
+ "version": "0.0.6",
4
4
  "description": "Gestor de Skills para Agentes de IA",
5
5
  "bin": {
6
6
  "skillctl": "bin/run.js"
@@ -5,7 +5,7 @@ const os = require('os');
5
5
 
6
6
  // Configuración
7
7
  const REPO = "joeldevz/agent-skill";
8
- const VERSION = "v0.0.4"; // ¡CAMBIA ESTO PARA QUE COINCIDA CON TU TAG DE GITHUB!
8
+ const VERSION = "v0.0.6"; // ¡CAMBIA ESTO PARA QUE COINCIDA CON TU TAG DE GITHUB!
9
9
  const BIN_NAME = "skillctl";
10
10
 
11
11
  // Detectar plataforma
package/src/main.rs CHANGED
@@ -1,30 +1,39 @@
1
- // src/main.rs (Fragmentos clave)
2
1
  use clap::{Parser, Subcommand};
3
- use serde::{Deserialize, Serialize};
4
- use std::collections::HashMap;
2
+ use colored::*;
3
+ use anyhow::{Context, Result};
5
4
  use std::fs;
6
5
  use std::path::Path;
7
- use anyhow::Result;
6
+ use serde::{Deserialize, Serialize};
7
+ use dialoguer::{Select, theme::ColorfulTheme};
8
+ use std::collections::HashMap;
8
9
 
9
- // --- MODELO DE DATOS (El Manifiesto) ---
10
+ // --- MODELO DE DATOS (JSON) ---
10
11
  #[derive(Serialize, Deserialize, Debug)]
11
- struct SkillManifest {
12
- version: String,
12
+ struct SkillConfig {
13
+ editor: String, // "cursor", "antigravity", "vscode"
13
14
  skills: HashMap<String, SkillEntry>,
14
15
  }
15
16
 
16
17
  #[derive(Serialize, Deserialize, Debug)]
17
18
  struct SkillEntry {
18
- url: String, // URL original del repo
19
- branch: String, // main, master, etc.
19
+ url: String,
20
20
  local_path: String,
21
- last_updated: String,
22
21
  }
23
22
 
24
- // --- COMANDOS CLI ---
23
+ impl Default for SkillConfig {
24
+ fn default() -> Self {
25
+ Self {
26
+ editor: "cursor".to_string(),
27
+ skills: HashMap::new(),
28
+ }
29
+ }
30
+ }
31
+
32
+ // --- CLI ARGUMENTS ---
25
33
  #[derive(Parser)]
26
34
  #[command(name = "skillctl")]
27
- #[command(about = "Gestor de Skills para Agentes de IA", long_about = None)]
35
+ #[command(version = "1.0.0")]
36
+ #[command(about = "Gestor de Skills tipo Vercel", long_about = None)]
28
37
  struct Cli {
29
38
  #[command(subcommand)]
30
39
  command: Commands,
@@ -32,206 +41,187 @@ struct Cli {
32
41
 
33
42
  #[derive(Subcommand)]
34
43
  enum Commands {
35
- /// Inicializa un nuevo proyecto de skills
44
+ /// Inicializa el proyecto y elige editor
36
45
  Init,
37
- /// Añade una nueva skill al proyecto
38
- Add { url: String, skill: String },
39
- /// Re-descarga todas las skills desde sus URLs originales
40
- Update,
41
- /// Genera los archivos de configuración para los editores detectados
42
- Sync {
43
- #[arg(short, long)]
44
- editors: Vec<String>, // ej: --editors cursor,antigravity
46
+ /// Añade una skill. Uso: skillctl add <URL> --skill <NOMBRE>
47
+ Add {
48
+ url: String,
49
+ /// Nombre de la skill a extraer
50
+ #[arg(long)] // Esto hace que sea --skill <nombre>
51
+ skill: String,
45
52
  },
46
- /// Lista todas las skills instaladas
47
- List,
53
+ /// Instala todas las skills definidas en skills.json
54
+ Install,
48
55
  }
49
56
 
50
- // --- LÓGICA DE UPDATE ---
51
- fn update_skills() -> Result<()> {
52
- let manifest_path = Path::new("skills.toml");
53
- if !manifest_path.exists() {
54
- println!("❌ No se encontró skills.toml. Usa 'add' primero.");
55
- return Ok(());
56
- }
57
-
58
- let content = fs::read_to_string(manifest_path)?;
59
- let mut manifest: SkillManifest = toml::from_str(&content)?;
60
-
61
- println!("🔄 Buscando actualizaciones para {} skills...", manifest.skills.len());
57
+ fn main() -> Result<()> {
58
+ let cli = Cli::parse();
62
59
 
63
- for (name, entry) in &manifest.skills {
64
- println!(" ⬇️ Actualizando {}...", name);
65
- // Reutilizamos la lógica de descarga (download_file)
66
- // Aquí podrías comprobar hashes git si quisieras ser muy preciso,
67
- // pero re-descargar el RAW file es rápido y efectivo.
68
- download_skill_file(&entry.url, &entry.branch, name)?;
60
+ match &cli.command {
61
+ Commands::Init => init_project()?,
62
+ Commands::Add { url, skill } => add_skill(url, skill)?,
63
+ Commands::Install => install_all()?,
69
64
  }
70
-
71
- // Actualizamos timestamp en el toml (opcional)
72
- fs::write(manifest_path, toml::to_string(&manifest)?)?;
73
-
74
- println!("✅ Todas las skills están al día.");
75
- // Auto-ejecutamos sync para reflejar cambios
76
- sync_editors(vec!["cursor".to_string(), "antigravity".to_string()])?;
77
-
78
65
  Ok(())
79
66
  }
80
67
 
81
- // --- LÓGICA DE SYNC MULTI-EDITOR ---
82
- fn sync_editors(targets: Vec<String>) -> Result<()> {
83
- let manifest = load_manifest()?; // Función helper que lee skills.toml
84
-
85
- for editor in targets {
86
- match editor.as_str() {
87
- "cursor" => generate_cursor_config(&manifest)?,
88
- "antigravity" => generate_antigravity_config(&manifest)?,
89
- "vscode" => generate_vscode_config(&manifest)?, // Copilot instructions
90
- _ => println!("⚠️ Editor desconocido: {}", editor),
91
- }
68
+ // --- COMANDO: INIT (Interactivo) ---
69
+ fn init_project() -> Result<()> {
70
+ let config_path = Path::new("skills.json");
71
+ if config_path.exists() {
72
+ println!("⚠️ Ya existe 'skills.json'.");
73
+ return Ok(());
92
74
  }
93
- Ok(())
94
- }
95
75
 
96
- // Generador para Cursor (.cursorrules)
97
- fn generate_cursor_config(manifest: &SkillManifest) -> Result<()> {
98
- let mut instructions = String::from("# Rules generadas por Skill-CLI\n\n");
76
+ println!("{}", "🚀 Inicializando Skill Controller...".bold().cyan());
77
+
78
+ // Menú interactivo
79
+ let editors = vec!["Cursor (.cursor/skills)", "Antigravity (.antigravity)", "VSCode (.vscode)"];
80
+ let selection = Select::with_theme(&ColorfulTheme::default())
81
+ .with_prompt("¿Qué editor vas a usar?")
82
+ .default(0)
83
+ .items(&editors)
84
+ .interact()
85
+ .unwrap();
86
+
87
+ let editor_key = match selection {
88
+ 0 => "cursor",
89
+ 1 => "antigravity",
90
+ _ => "vscode",
91
+ };
92
+
93
+ let config = SkillConfig {
94
+ editor: editor_key.to_string(),
95
+ skills: HashMap::new(),
96
+ };
97
+
98
+ save_config(&config)?;
99
99
 
100
- for (name, entry) in &manifest.skills {
101
- // Opción A: Referencia al archivo (si el editor sabe leer paths)
102
- instructions.push_str(&format!("## Skill: {}\n", name));
103
- instructions.push_str(&format!("Reference: .cursor/skills/{}/SKILL.md\n\n", name));
104
-
105
- // Opción B (Más robusta): Leer el contenido e inyectarlo si es pequeño
106
- // let content = fs::read_to_string(&entry.local_path)?;
107
- // instructions.push_str(&content);
108
- }
100
+ // Crear carpeta base según editor
101
+ let base_dir = get_skills_dir(editor_key);
102
+ fs::create_dir_all(&base_dir)?;
109
103
 
110
- fs::write(".cursorrules", instructions)?;
111
- println!("✅ .cursorrules actualizado.");
104
+ println!("✅ Configuración guardada en 'skills.json'. Editor: {}", editor_key.green());
112
105
  Ok(())
113
106
  }
114
107
 
115
- // Generador para Antigravity (.antigravity)
116
- fn generate_antigravity_config(manifest: &SkillManifest) -> Result<()> {
117
- // Supongamos que Antigravity usa JSON o un formato diferente
118
- // O tal vez soporta "Symlinks" virtuales.
119
-
120
- let mut config_lines = Vec::new();
121
- config_lines.push("PROJECT_CONTEXT:".to_string());
122
-
123
- for (name, entry) in &manifest.skills {
124
- // Imaginemos que Antigravity necesita path absoluto
125
- let abs_path = fs::canonicalize(&entry.local_path)?;
126
- config_lines.push(format!(" - IMPORT_SKILL: {}", abs_path.display()));
127
- }
108
+ // --- COMANDO: ADD ---
109
+ fn add_skill(repo_url: &str, skill_name: &str) -> Result<()> {
110
+ // 1. Cargar config para saber dónde guardar
111
+ let mut config = load_config()?;
112
+ let skills_dir = get_skills_dir(&config.editor);
128
113
 
129
- fs::write(".antigravity", config_lines.join("\n"))?;
130
- println!("✅ .antigravity actualizado.");
131
- Ok(())
132
- }
114
+ println!("{} {}...", "📦 Añadiendo skill:".blue(), skill_name);
133
115
 
134
- // Generador para VSCode (.github/copilot-instructions.md)
135
- fn generate_vscode_config(manifest: &SkillManifest) -> Result<()> {
136
- let mut instructions = String::from("# GitHub Copilot Instructions\n\n");
116
+ // 2. Lógica de descarga (GitHub Raw)
117
+ let raw_base = repo_url
118
+ .replace("github.com", "raw.githubusercontent.com")
119
+ .trim_end_matches('/')
120
+ .to_string();
137
121
 
138
- for (name, entry) in &manifest.skills {
139
- instructions.push_str(&format!("## Skill: {}\n", name));
140
- instructions.push_str(&format!("Path: {}\n\n", entry.local_path));
141
- }
122
+ // URL: .../main/skills/{nombre}/SKILL.md (Ajustar según estructura real del repo)
123
+ let target_url = format!("{}/main/skills/{}/SKILL.md", raw_base, skill_name);
124
+
125
+ // 3. Descargar
126
+ let content = download_file(&target_url)?;
142
127
 
143
- fs::create_dir_all(".github")?;
144
- fs::write(".github/copilot-instructions.md", instructions)?;
145
- println!("✅ .github/copilot-instructions.md actualizado.");
146
- Ok(())
147
- }
128
+ // 4. Guardar archivo
129
+ let skill_folder = skills_dir.join(skill_name);
130
+ fs::create_dir_all(&skill_folder)?;
131
+ let file_path = skill_folder.join("SKILL.md");
132
+ fs::write(&file_path, &content)?;
148
133
 
149
- // --- LÓGICA DE INIT ---
150
- fn init_project() -> Result<()> {
151
- let manifest_path = Path::new("skills.toml");
152
- if manifest_path.exists() {
153
- println!("⚠️ El archivo skills.toml ya existe.");
154
- return Ok(());
155
- }
134
+ println!("✅ Skill guardada en: {:?}", file_path);
156
135
 
157
- let default_content = r#"# Manifiesto de Skills
158
- version = "1.0"
136
+ // 5. Actualizar JSON
137
+ config.skills.insert(skill_name.to_string(), SkillEntry {
138
+ url: repo_url.to_string(),
139
+ local_path: file_path.to_string_lossy().to_string(),
140
+ });
141
+ save_config(&config)?;
159
142
 
160
- [skills]
161
- # Las skills se añadirán aquí automáticamente al usar 'add'
162
- "#;
143
+ // 6. Actualizar configuración del editor (Integración)
144
+ update_editor_config(&config.editor, skill_name, &file_path)?;
163
145
 
164
- fs::write(manifest_path, default_content)?;
165
-
166
- // Crear carpetas necesarias
167
- fs::create_dir_all(".cursor/skills")?;
168
-
169
- println!("✅ Proyecto inicializado. Se ha creado 'skills.toml'.");
170
- println!("🚀 Prueba ahora: npx skillctl add <url> --skill <nombre>");
171
-
172
146
  Ok(())
173
147
  }
174
148
 
175
- // --- LÓGICA DE LIST ---
176
- fn list_skills() -> Result<()> {
177
- let manifest_path = Path::new("skills.toml");
178
- if !manifest_path.exists() {
179
- println!(" No se encontró skills.toml. Ejecuta 'skillctl init' primero.");
180
- return Ok(());
181
- }
149
+ // --- COMANDO: INSTALL ---
150
+ fn install_all() -> Result<()> {
151
+ let config = load_config().context("No se encontró skills.json. Ejecuta 'init' primero.")?;
152
+
153
+ println!("🔄 Restaurando {} skills para {}...", config.skills.len(), config.editor);
182
154
 
183
- let content = fs::read_to_string(manifest_path)?;
184
- let manifest: SkillManifest = toml::from_str(&content)?;
185
-
186
- if manifest.skills.is_empty() {
187
- println!("📦 No hay skills instaladas.");
188
- println!("💡 Usa 'skillctl add <url> --skill <nombre>' para añadir una.");
189
- } else {
190
- println!("📦 Skills instaladas ({}):", manifest.skills.len());
191
- for (name, entry) in &manifest.skills {
192
- println!(" • {} ({})", name, entry.url);
193
- println!(" └─ Branch: {} | Path: {}", entry.branch, entry.local_path);
155
+ for (name, entry) in &config.skills {
156
+ // Re-usamos la lógica de add pero sin duplicar entradas en el json
157
+ // (Aquí simplificado: solo descargamos el archivo de nuevo)
158
+
159
+ let raw_base = entry.url
160
+ .replace("github.com", "raw.githubusercontent.com")
161
+ .trim_end_matches('/')
162
+ .to_string();
163
+ let target_url = format!("{}/main/skills/{}/SKILL.md", raw_base, name);
164
+
165
+ match download_file(&target_url) {
166
+ Ok(content) => {
167
+ let path = Path::new(&entry.local_path);
168
+ if let Some(parent) = path.parent() {
169
+ fs::create_dir_all(parent)?;
170
+ }
171
+ fs::write(path, content)?;
172
+ println!(" - ✅ {}", name);
173
+ },
174
+ Err(_) => println!(" - ❌ Error descargando {}", name),
194
175
  }
195
176
  }
196
-
177
+ println!("✨ Instalación completada.");
197
178
  Ok(())
198
179
  }
199
180
 
200
- // --- HELPER: Cargar manifiesto ---
201
- fn load_manifest() -> Result<SkillManifest> {
202
- let manifest_path = Path::new("skills.toml");
203
- if !manifest_path.exists() {
204
- anyhow::bail!("No se encontró skills.toml. Ejecuta 'skillctl init' primero.");
181
+ // --- HELPERS ---
182
+
183
+ fn get_skills_dir(editor: &str) -> std::path::PathBuf {
184
+ match editor {
185
+ "antigravity" => Path::new(".antigravity/skills").to_path_buf(),
186
+ "vscode" => Path::new(".vscode/skills").to_path_buf(),
187
+ _ => Path::new(".cursor/skills").to_path_buf(), // Default
205
188
  }
206
-
207
- let content = fs::read_to_string(manifest_path)?;
208
- let manifest: SkillManifest = toml::from_str(&content)?;
209
- Ok(manifest)
210
189
  }
211
190
 
212
- // --- HELPER: Descargar archivo de skill ---
213
- fn download_skill_file(url: &str, branch: &str, name: &str) -> Result<()> {
214
- // Implementación simplificada - aquí iría la lógica real de descarga
215
- println!(" ⬇️ Descargando {} desde {} (branch: {})", name, url, branch);
216
- // TODO: Implementar descarga real usando reqwest o similar
217
- Ok(())
191
+ fn load_config() -> Result<SkillConfig> {
192
+ let content = fs::read_to_string("skills.json")?;
193
+ let config: SkillConfig = serde_json::from_str(&content)?;
194
+ Ok(config)
218
195
  }
219
196
 
220
- // --- FUNCIÓN PRINCIPAL ---
221
- fn main() -> Result<()> {
222
- let cli = Cli::parse();
197
+ fn save_config(config: &SkillConfig) -> Result<()> {
198
+ let content = serde_json::to_string_pretty(config)?;
199
+ fs::write("skills.json", content)?;
200
+ Ok(())
201
+ }
223
202
 
224
- match &cli.command {
225
- Commands::Init => init_project()?,
226
- Commands::Add { url, skill } => {
227
- println!("🔧 Añadiendo skill '{}' desde {}...", skill, url);
228
- // Aquí iría la lógica de add_skill(url, skill)
229
- println!("⚠️ Comando 'add' aún no implementado completamente.");
230
- }
231
- Commands::Update => update_skills()?,
232
- Commands::Sync { editors } => sync_editors(editors.clone())?,
233
- Commands::List => list_skills()?,
203
+ fn download_file(url: &str) -> Result<String> {
204
+ let resp = reqwest::blocking::get(url)?;
205
+ if !resp.status().is_success() {
206
+ anyhow::bail!("404 Not Found");
234
207
  }
208
+ Ok(resp.text()?)
209
+ }
235
210
 
211
+ fn update_editor_config(editor: &str, skill_name: &str, path: &Path) -> Result<()> {
212
+ // Aquí implementas la lógica específica para inyectar en .cursorrules o .antigravity
213
+ // Ejemplo simple para cursor:
214
+ if editor == "cursor" {
215
+ let rule_file = Path::new(".cursorrules");
216
+ let line = format!("\n# Skill: {}\nReference: {}\n", skill_name, path.display());
217
+
218
+ // Append
219
+ let mut file = fs::OpenOptions::new()
220
+ .create(true)
221
+ .append(true)
222
+ .open(rule_file)?;
223
+ use std::io::Write;
224
+ write!(file, "{}", line)?;
225
+ }
236
226
  Ok(())
237
227
  }
package/bin/skillctl DELETED
Binary file