@basetisia/skill-manager 0.1.0
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/README.md +242 -0
- package/bin/index.js +119 -0
- package/package.json +27 -0
- package/scripts/publish.sh +37 -0
- package/scripts/setup.sh +50 -0
- package/src/adapters/adapters.test.js +175 -0
- package/src/adapters/claude.js +26 -0
- package/src/adapters/codex.js +31 -0
- package/src/adapters/cursor.js +46 -0
- package/src/adapters/gemini.js +26 -0
- package/src/adapters/index.js +45 -0
- package/src/adapters/shared.js +48 -0
- package/src/adapters/windsurf.js +82 -0
- package/src/commands/detect.js +135 -0
- package/src/commands/init.js +130 -0
- package/src/commands/install.js +105 -0
- package/src/commands/list.js +69 -0
- package/src/commands/login.js +29 -0
- package/src/commands/logout.js +13 -0
- package/src/commands/status.js +56 -0
- package/src/commands/whoami.js +29 -0
- package/src/manifest.js +84 -0
- package/src/manifest.test.js +183 -0
- package/src/utils/api.js +39 -0
- package/src/utils/auth.js +287 -0
- package/src/utils/config.js +35 -0
- package/src/utils/fs.js +39 -0
- package/src/utils/gitlab.js +78 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { join, basename } from "node:path";
|
|
2
|
+
import { homeDir, cwdDir, writeFile } from "../utils/fs.js";
|
|
3
|
+
import { scanInstalledSkills } from "./shared.js";
|
|
4
|
+
|
|
5
|
+
export const toolName = "codex";
|
|
6
|
+
export const displayName = "Codex CLI";
|
|
7
|
+
|
|
8
|
+
function basePath(scope) {
|
|
9
|
+
if (scope === "personal") {
|
|
10
|
+
return join(homeDir(), ".codex");
|
|
11
|
+
}
|
|
12
|
+
return join(cwdDir(), ".agents", "skills");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function getInstallPath(skillPath, scope) {
|
|
16
|
+
if (scope === "personal") {
|
|
17
|
+
const skillName = basename(skillPath);
|
|
18
|
+
return join(basePath(scope), skillName, "SKILL.md");
|
|
19
|
+
}
|
|
20
|
+
return join(basePath(scope), skillPath, "SKILL.md");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function install(skillPath, skillContent, scope) {
|
|
24
|
+
const dest = getInstallPath(skillPath, scope);
|
|
25
|
+
await writeFile(dest, skillContent);
|
|
26
|
+
return dest;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function getInstalledSkills(scope) {
|
|
30
|
+
return scanInstalledSkills(basePath(scope));
|
|
31
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { join, basename } from "node:path";
|
|
2
|
+
import { homeDir, cwdDir, writeFile, readFile } from "../utils/fs.js";
|
|
3
|
+
import { parseFrontmatter, scanDir } from "./shared.js";
|
|
4
|
+
|
|
5
|
+
export const toolName = "cursor";
|
|
6
|
+
export const displayName = "Cursor";
|
|
7
|
+
|
|
8
|
+
function basePath(scope) {
|
|
9
|
+
return scope === "personal"
|
|
10
|
+
? join(homeDir(), ".cursor", "rules")
|
|
11
|
+
: join(cwdDir(), ".cursor", "rules");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function getInstallPath(skillPath, scope) {
|
|
15
|
+
const skillName = basename(skillPath);
|
|
16
|
+
return join(basePath(scope), `${skillName}.mdc`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function toCursorFormat(skillContent) {
|
|
20
|
+
const { meta, body } = parseFrontmatter(skillContent);
|
|
21
|
+
const description = meta.description || meta.name || "Skill";
|
|
22
|
+
return `---\ndescription: ${description}\nalwaysApply: false\n---\n${body}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function install(skillPath, skillContent, scope) {
|
|
26
|
+
const dest = getInstallPath(skillPath, scope);
|
|
27
|
+
const content = toCursorFormat(skillContent);
|
|
28
|
+
await writeFile(dest, content);
|
|
29
|
+
return dest;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function getInstalledSkills(scope) {
|
|
33
|
+
const dir = basePath(scope);
|
|
34
|
+
const files = await scanDir(dir, ".mdc");
|
|
35
|
+
const skills = [];
|
|
36
|
+
for (const file of files) {
|
|
37
|
+
const content = await readFile(join(dir, file));
|
|
38
|
+
if (!content) continue;
|
|
39
|
+
const { meta } = parseFrontmatter(content);
|
|
40
|
+
skills.push({
|
|
41
|
+
path: file.replace(/\.mdc$/, ""),
|
|
42
|
+
version: meta.version || "unknown",
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
return skills;
|
|
46
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { homeDir, cwdDir, writeFile } from "../utils/fs.js";
|
|
3
|
+
import { scanInstalledSkills } from "./shared.js";
|
|
4
|
+
|
|
5
|
+
export const toolName = "gemini";
|
|
6
|
+
export const displayName = "Gemini CLI";
|
|
7
|
+
|
|
8
|
+
function basePath(scope) {
|
|
9
|
+
return scope === "personal"
|
|
10
|
+
? join(homeDir(), ".gemini", "skills")
|
|
11
|
+
: join(cwdDir(), ".agents", "skills");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function getInstallPath(skillPath, scope) {
|
|
15
|
+
return join(basePath(scope), skillPath, "SKILL.md");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function install(skillPath, skillContent, scope) {
|
|
19
|
+
const dest = getInstallPath(skillPath, scope);
|
|
20
|
+
await writeFile(dest, skillContent);
|
|
21
|
+
return dest;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function getInstalledSkills(scope) {
|
|
25
|
+
return scanInstalledSkills(basePath(scope));
|
|
26
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { access } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { homeDir } from "../utils/fs.js";
|
|
4
|
+
|
|
5
|
+
import * as claude from "./claude.js";
|
|
6
|
+
import * as gemini from "./gemini.js";
|
|
7
|
+
import * as codex from "./codex.js";
|
|
8
|
+
import * as cursor from "./cursor.js";
|
|
9
|
+
import * as windsurf from "./windsurf.js";
|
|
10
|
+
|
|
11
|
+
const adapters = [claude, gemini, codex, cursor, windsurf];
|
|
12
|
+
|
|
13
|
+
export function getAdapter(toolName) {
|
|
14
|
+
const adapter = adapters.find((a) => a.toolName === toolName);
|
|
15
|
+
if (!adapter) {
|
|
16
|
+
throw new Error(`Herramienta no soportada: ${toolName}`);
|
|
17
|
+
}
|
|
18
|
+
return adapter;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function getAllAdapters() {
|
|
22
|
+
return adapters;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function detectInstalledTools() {
|
|
26
|
+
const home = homeDir();
|
|
27
|
+
const checks = [
|
|
28
|
+
{ adapter: claude, dir: join(home, ".claude") },
|
|
29
|
+
{ adapter: gemini, dir: join(home, ".gemini") },
|
|
30
|
+
{ adapter: codex, dir: join(home, ".codex") },
|
|
31
|
+
{ adapter: cursor, dir: join(home, ".cursor") },
|
|
32
|
+
{ adapter: windsurf, dir: join(home, ".windsurf") },
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
const results = [];
|
|
36
|
+
for (const { adapter, dir } of checks) {
|
|
37
|
+
try {
|
|
38
|
+
await access(dir);
|
|
39
|
+
results.push(adapter);
|
|
40
|
+
} catch {
|
|
41
|
+
// directory not found, tool not installed
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return results;
|
|
45
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { readdir } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import yaml from "js-yaml";
|
|
4
|
+
import { readFile } from "../utils/fs.js";
|
|
5
|
+
|
|
6
|
+
export function parseFrontmatter(content) {
|
|
7
|
+
const match = content.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
|
|
8
|
+
if (!match) return { meta: {}, body: content };
|
|
9
|
+
try {
|
|
10
|
+
const meta = yaml.load(match[1]) || {};
|
|
11
|
+
return { meta, body: match[2] };
|
|
12
|
+
} catch {
|
|
13
|
+
return { meta: {}, body: content };
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function scanInstalledSkills(baseDir, segments = [], results = []) {
|
|
18
|
+
const entries = await readdir(baseDir, { withFileTypes: true }).catch(() => []);
|
|
19
|
+
for (const entry of entries) {
|
|
20
|
+
if (entry.isDirectory()) {
|
|
21
|
+
await scanInstalledSkills(
|
|
22
|
+
join(baseDir, entry.name),
|
|
23
|
+
[...segments, entry.name],
|
|
24
|
+
results
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
if (entry.name === "SKILL.md") {
|
|
28
|
+
const content = await readFile(join(baseDir, "SKILL.md"));
|
|
29
|
+
if (content) {
|
|
30
|
+
const { meta } = parseFrontmatter(content);
|
|
31
|
+
results.push({
|
|
32
|
+
path: segments.join("/"),
|
|
33
|
+
version: meta.version || "unknown",
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return results;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function scanDir(dirPath, ext) {
|
|
42
|
+
try {
|
|
43
|
+
const entries = await readdir(dirPath);
|
|
44
|
+
return entries.filter((f) => f.endsWith(ext));
|
|
45
|
+
} catch {
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { join, basename } from "node:path";
|
|
2
|
+
import { homeDir, cwdDir, writeFile, readFile } from "../utils/fs.js";
|
|
3
|
+
import { parseFrontmatter } from "./shared.js";
|
|
4
|
+
|
|
5
|
+
export const toolName = "windsurf";
|
|
6
|
+
export const displayName = "Windsurf";
|
|
7
|
+
|
|
8
|
+
const GLOBAL_RULES = () => join(homeDir(), ".windsurf", "global_rules.md");
|
|
9
|
+
|
|
10
|
+
function projectBase() {
|
|
11
|
+
return join(cwdDir(), ".windsurf", "rules");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function getInstallPath(skillPath, scope) {
|
|
15
|
+
const skillName = basename(skillPath);
|
|
16
|
+
if (scope === "personal") {
|
|
17
|
+
return GLOBAL_RULES();
|
|
18
|
+
}
|
|
19
|
+
return join(projectBase(), `${skillName}.md`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function install(skillPath, skillContent, scope) {
|
|
23
|
+
const skillName = basename(skillPath);
|
|
24
|
+
|
|
25
|
+
if (scope === "personal") {
|
|
26
|
+
const dest = GLOBAL_RULES();
|
|
27
|
+
const existing = (await readFile(dest)) || "";
|
|
28
|
+
const header = `\n## ${skillName}\n`;
|
|
29
|
+
// Replace existing section or append
|
|
30
|
+
const sectionRegex = new RegExp(
|
|
31
|
+
`\\n## ${skillName}\\n[\\s\\S]*?(?=\\n## |$)`
|
|
32
|
+
);
|
|
33
|
+
let updated;
|
|
34
|
+
if (sectionRegex.test(existing)) {
|
|
35
|
+
updated = existing.replace(sectionRegex, `${header}${skillContent}\n`);
|
|
36
|
+
} else {
|
|
37
|
+
updated = existing + `${header}${skillContent}\n`;
|
|
38
|
+
}
|
|
39
|
+
await writeFile(dest, updated);
|
|
40
|
+
return dest;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const dest = join(projectBase(), `${skillName}.md`);
|
|
44
|
+
await writeFile(dest, skillContent);
|
|
45
|
+
return dest;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function getInstalledSkills(scope) {
|
|
49
|
+
if (scope === "personal") {
|
|
50
|
+
const content = await readFile(GLOBAL_RULES());
|
|
51
|
+
if (!content) return [];
|
|
52
|
+
const sections = content.split(/\n## /).filter(Boolean);
|
|
53
|
+
return sections.map((section) => {
|
|
54
|
+
const lines = section.split("\n");
|
|
55
|
+
const name = lines[0].trim();
|
|
56
|
+
const body = lines.slice(1).join("\n");
|
|
57
|
+
const { meta } = parseFrontmatter(body);
|
|
58
|
+
return { path: name, version: meta.version || "unknown" };
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// project scope
|
|
63
|
+
const { readdir } = await import("node:fs/promises");
|
|
64
|
+
const dir = projectBase();
|
|
65
|
+
let files;
|
|
66
|
+
try {
|
|
67
|
+
files = (await readdir(dir)).filter((f) => f.endsWith(".md"));
|
|
68
|
+
} catch {
|
|
69
|
+
return [];
|
|
70
|
+
}
|
|
71
|
+
const skills = [];
|
|
72
|
+
for (const file of files) {
|
|
73
|
+
const content = await readFile(join(dir, file));
|
|
74
|
+
if (!content) continue;
|
|
75
|
+
const { meta } = parseFrontmatter(content);
|
|
76
|
+
skills.push({
|
|
77
|
+
path: file.replace(/\.md$/, ""),
|
|
78
|
+
version: meta.version || "unknown",
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
return skills;
|
|
82
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import ora from "ora";
|
|
3
|
+
import inquirer from "inquirer";
|
|
4
|
+
import { readFile } from "../utils/fs.js";
|
|
5
|
+
import { getAllSkills } from "../manifest.js";
|
|
6
|
+
import { install } from "./install.js";
|
|
7
|
+
|
|
8
|
+
const PROJECT_FILES = [
|
|
9
|
+
{ file: "package.json", tags: ["node", "javascript", "typescript"] },
|
|
10
|
+
{ file: "build.gradle", tags: ["kotlin", "jvm", "gradle", "android"] },
|
|
11
|
+
{ file: "build.gradle.kts", tags: ["kotlin", "jvm", "gradle", "android"] },
|
|
12
|
+
{ file: "pom.xml", tags: ["java", "jvm", "maven"] },
|
|
13
|
+
{ file: "composer.json", tags: ["php"] },
|
|
14
|
+
{ file: "requirements.txt", tags: ["python"] },
|
|
15
|
+
{ file: "pyproject.toml", tags: ["python"] },
|
|
16
|
+
{ file: "Cargo.toml", tags: ["rust"] },
|
|
17
|
+
{ file: "go.mod", tags: ["go", "golang"] },
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
async function detectProjectTags() {
|
|
21
|
+
const tags = new Set();
|
|
22
|
+
for (const { file, tags: fileTags } of PROJECT_FILES) {
|
|
23
|
+
const content = await readFile(file);
|
|
24
|
+
if (content) {
|
|
25
|
+
for (const tag of fileTags) tags.add(tag);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return tags;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function detectRemoteOrigin() {
|
|
32
|
+
const gitConfig = await readFile(".git/config");
|
|
33
|
+
if (!gitConfig) return null;
|
|
34
|
+
const match = gitConfig.match(/\[remote "origin"\]\s*\n\s*url\s*=\s*(.+)/);
|
|
35
|
+
if (!match) return null;
|
|
36
|
+
return match[1].trim();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function detect() {
|
|
40
|
+
const spinner = ora("Analizando proyecto...").start();
|
|
41
|
+
|
|
42
|
+
const [tags, remote, skills] = await Promise.all([
|
|
43
|
+
detectProjectTags(),
|
|
44
|
+
detectRemoteOrigin(),
|
|
45
|
+
getAllSkills().catch(() => []),
|
|
46
|
+
]);
|
|
47
|
+
|
|
48
|
+
spinner.succeed("Análisis completo.");
|
|
49
|
+
|
|
50
|
+
if (skills.length === 0) {
|
|
51
|
+
console.log(chalk.yellow("No se pudo cargar el catálogo de skills."));
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const recommendations = [];
|
|
56
|
+
|
|
57
|
+
// Always suggest internal/global if available
|
|
58
|
+
const globalSkill = skills.find((s) => s.path === "internal/global");
|
|
59
|
+
if (globalSkill) {
|
|
60
|
+
recommendations.push({
|
|
61
|
+
skill: globalSkill,
|
|
62
|
+
reason: "Skill base recomendada para todos los proyectos.",
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Match by project tags against user/development skills
|
|
67
|
+
if (tags.size > 0) {
|
|
68
|
+
const devSkills = skills.filter((s) => s.path.startsWith("user/"));
|
|
69
|
+
for (const skill of devSkills) {
|
|
70
|
+
const skillName = skill.path.toLowerCase();
|
|
71
|
+
for (const tag of tags) {
|
|
72
|
+
if (skillName.includes(tag)) {
|
|
73
|
+
recommendations.push({
|
|
74
|
+
skill,
|
|
75
|
+
reason: `Detectado: ${[...tags].join(", ")} en el proyecto.`,
|
|
76
|
+
});
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Match by git remote against external/client or external/project skills
|
|
84
|
+
if (remote) {
|
|
85
|
+
const remoteNorm = remote.toLowerCase();
|
|
86
|
+
const clientSkills = skills.filter(
|
|
87
|
+
(s) =>
|
|
88
|
+
s.path.startsWith("external/client/") ||
|
|
89
|
+
s.path.startsWith("external/project/")
|
|
90
|
+
);
|
|
91
|
+
for (const skill of clientSkills) {
|
|
92
|
+
const name = skill.path.split("/").pop().toLowerCase();
|
|
93
|
+
if (remoteNorm.includes(name)) {
|
|
94
|
+
recommendations.push({
|
|
95
|
+
skill,
|
|
96
|
+
reason: `El remote origin coincide con "${name}".`,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (recommendations.length === 0) {
|
|
103
|
+
console.log(chalk.yellow("\nNo se encontraron skills recomendadas para este proyecto."));
|
|
104
|
+
console.log(chalk.dim("Usa 'skill-manager list' para ver el catálogo completo."));
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Show recommendations
|
|
109
|
+
console.log();
|
|
110
|
+
console.log(chalk.blue.bold(" Skills recomendadas:"));
|
|
111
|
+
console.log();
|
|
112
|
+
for (const rec of recommendations) {
|
|
113
|
+
console.log(` ${chalk.white(rec.skill.path)} ${chalk.gray(`v${rec.skill.version}`)}`);
|
|
114
|
+
console.log(chalk.dim(` → ${rec.reason}`));
|
|
115
|
+
}
|
|
116
|
+
console.log();
|
|
117
|
+
|
|
118
|
+
const { proceed } = await inquirer.prompt([
|
|
119
|
+
{
|
|
120
|
+
type: "confirm",
|
|
121
|
+
name: "proceed",
|
|
122
|
+
message: "¿Instalar las skills recomendadas?",
|
|
123
|
+
default: true,
|
|
124
|
+
},
|
|
125
|
+
]);
|
|
126
|
+
|
|
127
|
+
if (!proceed) {
|
|
128
|
+
console.log(chalk.yellow("Operación cancelada."));
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
for (const rec of recommendations) {
|
|
133
|
+
await install(rec.skill.path);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import inquirer from "inquirer";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import ora from "ora";
|
|
4
|
+
import { loadConfig, saveConfig, getConfigPath } from "../utils/config.js";
|
|
5
|
+
import { verifyConnection } from "../utils/gitlab.js";
|
|
6
|
+
|
|
7
|
+
export async function init() {
|
|
8
|
+
const existing = await loadConfig();
|
|
9
|
+
if (existing) {
|
|
10
|
+
const { overwrite } = await inquirer.prompt([
|
|
11
|
+
{
|
|
12
|
+
type: "confirm",
|
|
13
|
+
name: "overwrite",
|
|
14
|
+
message: "Ya existe una configuración. ¿Deseas sobrescribirla?",
|
|
15
|
+
default: false,
|
|
16
|
+
},
|
|
17
|
+
]);
|
|
18
|
+
if (!overwrite) {
|
|
19
|
+
console.log(chalk.yellow("Operación cancelada."));
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const answers = await inquirer.prompt([
|
|
25
|
+
{
|
|
26
|
+
type: "list",
|
|
27
|
+
name: "type",
|
|
28
|
+
message: "Tipo de repositorio de skills:",
|
|
29
|
+
choices: [
|
|
30
|
+
{ name: "GitLab", value: "gitlab" },
|
|
31
|
+
{ name: "GitHub", value: "github" },
|
|
32
|
+
{ name: "Local (carpeta en disco)", value: "local" },
|
|
33
|
+
],
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
type: "input",
|
|
37
|
+
name: "url",
|
|
38
|
+
message: "URL base del servidor:",
|
|
39
|
+
default: (prev) =>
|
|
40
|
+
prev.type === "github"
|
|
41
|
+
? "https://github.com"
|
|
42
|
+
: "https://git.basetis.com",
|
|
43
|
+
when: (prev) => prev.type !== "local",
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
type: "input",
|
|
47
|
+
name: "projectId",
|
|
48
|
+
message: (prev) =>
|
|
49
|
+
prev.type === "gitlab"
|
|
50
|
+
? "Project ID (numérico) o ruta (grupo/repo):"
|
|
51
|
+
: "Repositorio (owner/repo):",
|
|
52
|
+
default: (prev) =>
|
|
53
|
+
prev.type === "gitlab" ? "okr-ia/basetisskills" : "",
|
|
54
|
+
when: (prev) => prev.type !== "local",
|
|
55
|
+
validate: (val) => (val.trim() ? true : "Este campo es obligatorio."),
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
type: "input",
|
|
59
|
+
name: "localPath",
|
|
60
|
+
message: "Ruta absoluta a la carpeta de skills:",
|
|
61
|
+
when: (prev) => prev.type === "local",
|
|
62
|
+
validate: (val) => (val.trim() ? true : "Este campo es obligatorio."),
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
type: "password",
|
|
66
|
+
name: "token",
|
|
67
|
+
message: "Token de acceso personal (se guardará en ~/.skill-manager/config.json):",
|
|
68
|
+
when: (prev) => prev.type !== "local",
|
|
69
|
+
validate: (val) => (val.trim() ? true : "El token es obligatorio para repos remotos."),
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
type: "input",
|
|
73
|
+
name: "branch",
|
|
74
|
+
message: "Rama del repositorio:",
|
|
75
|
+
default: "main",
|
|
76
|
+
when: (prev) => prev.type !== "local",
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
type: "list",
|
|
80
|
+
name: "defaultTool",
|
|
81
|
+
message: "Herramienta de IA por defecto:",
|
|
82
|
+
choices: [
|
|
83
|
+
{ name: "Claude Code", value: "claude" },
|
|
84
|
+
{ name: "Gemini CLI", value: "gemini" },
|
|
85
|
+
{ name: "Cursor", value: "cursor" },
|
|
86
|
+
{ name: "Windsurf", value: "windsurf" },
|
|
87
|
+
{ name: "Preguntar siempre", value: null },
|
|
88
|
+
],
|
|
89
|
+
},
|
|
90
|
+
]);
|
|
91
|
+
|
|
92
|
+
const config = {
|
|
93
|
+
registry: {
|
|
94
|
+
type: answers.type,
|
|
95
|
+
url: answers.url || null,
|
|
96
|
+
projectId: answers.projectId || answers.localPath || "",
|
|
97
|
+
token: answers.token || "",
|
|
98
|
+
branch: answers.branch || "main",
|
|
99
|
+
},
|
|
100
|
+
defaultTool: answers.defaultTool,
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
// Verify remote connection
|
|
104
|
+
if (config.registry.type !== "local") {
|
|
105
|
+
const spinner = ora("Verificando conexión con el repositorio...").start();
|
|
106
|
+
try {
|
|
107
|
+
await verifyConnection(config.registry);
|
|
108
|
+
spinner.succeed("Conexión verificada correctamente.");
|
|
109
|
+
} catch (err) {
|
|
110
|
+
spinner.fail("No se pudo conectar al repositorio.");
|
|
111
|
+
console.error(chalk.red(` Error: ${err.message}`));
|
|
112
|
+
const { saveAnyway } = await inquirer.prompt([
|
|
113
|
+
{
|
|
114
|
+
type: "confirm",
|
|
115
|
+
name: "saveAnyway",
|
|
116
|
+
message: "¿Guardar la configuración de todas formas?",
|
|
117
|
+
default: false,
|
|
118
|
+
},
|
|
119
|
+
]);
|
|
120
|
+
if (!saveAnyway) {
|
|
121
|
+
console.log(chalk.yellow("Operación cancelada."));
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
await saveConfig(config);
|
|
128
|
+
console.log(chalk.green("✔ Configuración guardada correctamente."));
|
|
129
|
+
console.log(chalk.dim(` → ${getConfigPath()}`));
|
|
130
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import ora from "ora";
|
|
3
|
+
import inquirer from "inquirer";
|
|
4
|
+
import { resolveChain } from "../manifest.js";
|
|
5
|
+
import { fetchFileContent } from "../utils/gitlab.js";
|
|
6
|
+
import { getAdapter, detectInstalledTools } from "../adapters/index.js";
|
|
7
|
+
|
|
8
|
+
export async function install(skillPath, options = {}) {
|
|
9
|
+
// Resolve the full dependency chain
|
|
10
|
+
const spinner = ora("Resolviendo dependencias...").start();
|
|
11
|
+
let chain;
|
|
12
|
+
try {
|
|
13
|
+
chain = await resolveChain(skillPath);
|
|
14
|
+
spinner.succeed(`Cadena resuelta: ${chain.length} skill(s).`);
|
|
15
|
+
} catch (err) {
|
|
16
|
+
spinner.fail("Error resolviendo dependencias.");
|
|
17
|
+
console.error(chalk.red(err.message));
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Show what will be installed
|
|
22
|
+
console.log();
|
|
23
|
+
console.log(chalk.blue.bold(" Skills a instalar (en orden):"));
|
|
24
|
+
for (const [i, skill] of chain.entries()) {
|
|
25
|
+
const arrow = i < chain.length - 1 ? "├─" : "└─";
|
|
26
|
+
console.log(` ${arrow} ${skill.path} ${chalk.gray(`v${skill.version}`)}`);
|
|
27
|
+
}
|
|
28
|
+
console.log();
|
|
29
|
+
|
|
30
|
+
// Determine target tools
|
|
31
|
+
let toolNames = [];
|
|
32
|
+
if (options.tool) {
|
|
33
|
+
toolNames = options.tool.split(",").map((t) => t.trim());
|
|
34
|
+
} else {
|
|
35
|
+
const detected = await detectInstalledTools();
|
|
36
|
+
if (detected.length === 0) {
|
|
37
|
+
console.error(
|
|
38
|
+
chalk.red("No se detectó ninguna herramienta de IA instalada.")
|
|
39
|
+
);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
const { selected } = await inquirer.prompt([
|
|
43
|
+
{
|
|
44
|
+
type: "checkbox",
|
|
45
|
+
name: "selected",
|
|
46
|
+
message: "¿En qué herramientas quieres instalar?",
|
|
47
|
+
choices: detected.map((a) => ({
|
|
48
|
+
name: a.displayName,
|
|
49
|
+
value: a.toolName,
|
|
50
|
+
checked: true,
|
|
51
|
+
})),
|
|
52
|
+
validate: (val) =>
|
|
53
|
+
val.length > 0 ? true : "Selecciona al menos una herramienta.",
|
|
54
|
+
},
|
|
55
|
+
]);
|
|
56
|
+
toolNames = selected;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Determine scope
|
|
60
|
+
let scope = options.scope;
|
|
61
|
+
if (!scope) {
|
|
62
|
+
const { chosen } = await inquirer.prompt([
|
|
63
|
+
{
|
|
64
|
+
type: "list",
|
|
65
|
+
name: "chosen",
|
|
66
|
+
message: "¿Dónde instalar las skills?",
|
|
67
|
+
choices: [
|
|
68
|
+
{ name: "Global (solo para ti, en ~/)", value: "personal" },
|
|
69
|
+
{ name: "Proyecto (commitear al repo)", value: "project" },
|
|
70
|
+
],
|
|
71
|
+
},
|
|
72
|
+
]);
|
|
73
|
+
scope = chosen;
|
|
74
|
+
}
|
|
75
|
+
// Normalize "global" to "personal"
|
|
76
|
+
if (scope === "global") scope = "personal";
|
|
77
|
+
|
|
78
|
+
// Install each skill in each tool
|
|
79
|
+
const adapters = toolNames.map((name) => getAdapter(name));
|
|
80
|
+
let installed = 0;
|
|
81
|
+
|
|
82
|
+
for (const skill of chain) {
|
|
83
|
+
const installSpinner = ora(`Instalando ${skill.path}...`).start();
|
|
84
|
+
try {
|
|
85
|
+
const skillFile = `${skill.path}/SKILL.md`;
|
|
86
|
+
const content = await fetchFileContent(skillFile);
|
|
87
|
+
|
|
88
|
+
for (const adapter of adapters) {
|
|
89
|
+
await adapter.install(skill.path, content, scope);
|
|
90
|
+
}
|
|
91
|
+
installed++;
|
|
92
|
+
installSpinner.succeed(`Instalada ${skill.path}`);
|
|
93
|
+
} catch (err) {
|
|
94
|
+
installSpinner.fail(`Error instalando ${skill.path}`);
|
|
95
|
+
console.error(chalk.red(` ${err.message}`));
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Summary
|
|
100
|
+
console.log();
|
|
101
|
+
const toolList = adapters.map((a) => a.displayName).join(" y ");
|
|
102
|
+
console.log(
|
|
103
|
+
chalk.green(`✔ ${installed} skill(s) instalada(s) en ${toolList} (${scope})`)
|
|
104
|
+
);
|
|
105
|
+
}
|