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 +6 -5
- package/bin/run.js +4 -2
- package/package.json +1 -1
- package/scripts/install.js +1 -1
- package/src/main.rs +162 -172
- package/bin/skillctl +0 -0
package/Cargo.toml
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "skillctl"
|
|
3
|
-
version = "0.0.
|
|
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.
|
|
15
|
-
|
|
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
|
-
|
|
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), {
|
|
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
package/scripts/install.js
CHANGED
|
@@ -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.
|
|
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
|
|
4
|
-
use
|
|
2
|
+
use colored::*;
|
|
3
|
+
use anyhow::{Context, Result};
|
|
5
4
|
use std::fs;
|
|
6
5
|
use std::path::Path;
|
|
7
|
-
use
|
|
6
|
+
use serde::{Deserialize, Serialize};
|
|
7
|
+
use dialoguer::{Select, theme::ColorfulTheme};
|
|
8
|
+
use std::collections::HashMap;
|
|
8
9
|
|
|
9
|
-
// --- MODELO DE DATOS (
|
|
10
|
+
// --- MODELO DE DATOS (JSON) ---
|
|
10
11
|
#[derive(Serialize, Deserialize, Debug)]
|
|
11
|
-
struct
|
|
12
|
-
|
|
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,
|
|
19
|
-
branch: String, // main, master, etc.
|
|
19
|
+
url: String,
|
|
20
20
|
local_path: String,
|
|
21
|
-
last_updated: String,
|
|
22
21
|
}
|
|
23
22
|
|
|
24
|
-
|
|
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(
|
|
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
|
|
44
|
+
/// Inicializa el proyecto y elige editor
|
|
36
45
|
Init,
|
|
37
|
-
/// Añade una
|
|
38
|
-
Add {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
///
|
|
47
|
-
|
|
53
|
+
/// Instala todas las skills definidas en skills.json
|
|
54
|
+
Install,
|
|
48
55
|
}
|
|
49
56
|
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
// ---
|
|
82
|
-
fn
|
|
83
|
-
let
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
111
|
-
println!("✅ .cursorrules actualizado.");
|
|
104
|
+
println!("✅ Configuración guardada en 'skills.json'. Editor: {}", editor_key.green());
|
|
112
105
|
Ok(())
|
|
113
106
|
}
|
|
114
107
|
|
|
115
|
-
//
|
|
116
|
-
fn
|
|
117
|
-
//
|
|
118
|
-
|
|
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
|
-
|
|
130
|
-
println!("✅ .antigravity actualizado.");
|
|
131
|
-
Ok(())
|
|
132
|
-
}
|
|
114
|
+
println!("{} {}...", "📦 Añadiendo skill:".blue(), skill_name);
|
|
133
115
|
|
|
134
|
-
//
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
-
|
|
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
|
-
|
|
158
|
-
|
|
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
|
-
|
|
161
|
-
|
|
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
|
-
// ---
|
|
176
|
-
fn
|
|
177
|
-
let
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|
-
// ---
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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
|