@basetisia/skill-manager 0.2.0 → 0.3.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/.claude/skills/habilidades/typescript/SKILL.md +16 -0
- package/bin/index.js +70 -0
- package/package.json +1 -1
- package/src/commands/install-bulk.js +170 -0
- package/src/commands/install.js +5 -0
- package/src/commands/list.js +11 -2
- package/src/commands/push.js +104 -0
- package/src/commands/update.js +127 -0
- package/src/utils/config.js +12 -1
- package/src/utils/installed.js +76 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# TypeScript — Contexto v1.1.0
|
|
2
|
+
|
|
3
|
+
## Reglas
|
|
4
|
+
- Usa TypeScript strict mode siempre
|
|
5
|
+
- ES Modules (import/export), nunca CommonJS
|
|
6
|
+
- Usa const por defecto, let solo cuando sea necesario
|
|
7
|
+
- Nombres de archivos en kebab-case
|
|
8
|
+
- Tipos/interfaces en PascalCase
|
|
9
|
+
- Funciones en camelCase
|
|
10
|
+
- No uses any — tipalo todo
|
|
11
|
+
- Prefiere type sobre interface salvo que necesites extends
|
|
12
|
+
|
|
13
|
+
## NUEVO: Imports
|
|
14
|
+
- Ordena imports: builtins → externos → internos
|
|
15
|
+
- Nunca uses import *
|
|
16
|
+
- Usa import type para tipos
|
package/bin/index.js
CHANGED
|
@@ -4,12 +4,15 @@ import { Command } from "commander";
|
|
|
4
4
|
import { init } from "../src/commands/init.js";
|
|
5
5
|
import { list } from "../src/commands/list.js";
|
|
6
6
|
import { install } from "../src/commands/install.js";
|
|
7
|
+
import { installBulk } from "../src/commands/install-bulk.js";
|
|
7
8
|
import { status } from "../src/commands/status.js";
|
|
8
9
|
import { detect } from "../src/commands/detect.js";
|
|
9
10
|
import { login } from "../src/commands/login.js";
|
|
10
11
|
import { logout } from "../src/commands/logout.js";
|
|
11
12
|
import { whoami } from "../src/commands/whoami.js";
|
|
12
13
|
import { sync } from "../src/commands/sync.js";
|
|
14
|
+
import { push } from "../src/commands/push.js";
|
|
15
|
+
import { update } from "../src/commands/update.js";
|
|
13
16
|
|
|
14
17
|
const program = new Command();
|
|
15
18
|
|
|
@@ -57,6 +60,49 @@ program
|
|
|
57
60
|
}
|
|
58
61
|
});
|
|
59
62
|
|
|
63
|
+
// Bulk install commands: install-project / install-area / install-team / install-sector
|
|
64
|
+
const bulkOpts = (cmd) =>
|
|
65
|
+
cmd
|
|
66
|
+
.option("-t, --tool <herramienta>", "Herramienta(s) destino, separadas por coma")
|
|
67
|
+
.option("-s, --scope <ámbito>", "Ámbito de instalación: global o project")
|
|
68
|
+
.option("-y, --yes", "Asume sí en las confirmaciones");
|
|
69
|
+
|
|
70
|
+
bulkOpts(
|
|
71
|
+
program
|
|
72
|
+
.command("install-project <slug>")
|
|
73
|
+
.description("Instala todas las skills publicadas de un proyecto")
|
|
74
|
+
).action(async (slug, options) => {
|
|
75
|
+
try { await installBulk("project", slug, options); }
|
|
76
|
+
catch (err) { console.error(err.message); process.exit(1); }
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
bulkOpts(
|
|
80
|
+
program
|
|
81
|
+
.command("install-area <slug>")
|
|
82
|
+
.description("Instala todas las skills publicadas de un área")
|
|
83
|
+
).action(async (slug, options) => {
|
|
84
|
+
try { await installBulk("area", slug, options); }
|
|
85
|
+
catch (err) { console.error(err.message); process.exit(1); }
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
bulkOpts(
|
|
89
|
+
program
|
|
90
|
+
.command("install-team <slug>")
|
|
91
|
+
.description("Instala todas las skills publicadas de un team")
|
|
92
|
+
).action(async (slug, options) => {
|
|
93
|
+
try { await installBulk("team", slug, options); }
|
|
94
|
+
catch (err) { console.error(err.message); process.exit(1); }
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
bulkOpts(
|
|
98
|
+
program
|
|
99
|
+
.command("install-sector <slug>")
|
|
100
|
+
.description("Instala todas las skills publicadas de un sector")
|
|
101
|
+
).action(async (slug, options) => {
|
|
102
|
+
try { await installBulk("sector", slug, options); }
|
|
103
|
+
catch (err) { console.error(err.message); process.exit(1); }
|
|
104
|
+
});
|
|
105
|
+
|
|
60
106
|
program
|
|
61
107
|
.command("status")
|
|
62
108
|
.description("Muestra el estado de las skills instaladas vs el catálogo")
|
|
@@ -93,6 +139,30 @@ program
|
|
|
93
139
|
}
|
|
94
140
|
});
|
|
95
141
|
|
|
142
|
+
program
|
|
143
|
+
.command("push <ruta>")
|
|
144
|
+
.description("Publica los cambios locales de un skill al servidor")
|
|
145
|
+
.action(async (ruta) => {
|
|
146
|
+
try {
|
|
147
|
+
await push(ruta);
|
|
148
|
+
} catch (err) {
|
|
149
|
+
console.error(err.message);
|
|
150
|
+
process.exit(1);
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
program
|
|
155
|
+
.command("update")
|
|
156
|
+
.description("Comprueba y aplica actualizaciones de skills instalados")
|
|
157
|
+
.action(async () => {
|
|
158
|
+
try {
|
|
159
|
+
await update();
|
|
160
|
+
} catch (err) {
|
|
161
|
+
console.error(err.message);
|
|
162
|
+
process.exit(1);
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
|
|
96
166
|
program
|
|
97
167
|
.command("login")
|
|
98
168
|
.description("Inicia sesión con tu cuenta de Basetis (Google)")
|
package/package.json
CHANGED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import ora from "ora";
|
|
3
|
+
import inquirer from "inquirer";
|
|
4
|
+
import { requireConfig } from "../utils/config.js";
|
|
5
|
+
import { apiRequest } from "../utils/api.js";
|
|
6
|
+
import { getAdapter, detectInstalledTools } from "../adapters/index.js";
|
|
7
|
+
import { trackInstall } from "../utils/installed.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Bulk install: instala todas las skills publicadas de un proyecto/area/team/sector.
|
|
11
|
+
* @param {string} kind - 'project' | 'area' | 'team' | 'sector'
|
|
12
|
+
* @param {string} slug - Slug de la entidad o proyecto
|
|
13
|
+
*/
|
|
14
|
+
export async function installBulk(kind, slug, options = {}) {
|
|
15
|
+
const config = await requireConfig();
|
|
16
|
+
const validKinds = ["project", "area", "team", "sector"];
|
|
17
|
+
if (!validKinds.includes(kind)) {
|
|
18
|
+
console.error(chalk.red(`Tipo no válido: ${kind}. Debe ser uno de: ${validKinds.join(", ")}`));
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const spinner = ora(`Buscando skills del ${kind} '${slug}'...`).start();
|
|
23
|
+
|
|
24
|
+
// Fetch all data needed
|
|
25
|
+
let skills, target;
|
|
26
|
+
try {
|
|
27
|
+
skills = await apiRequest(`/api/companies/${config.companyId}/skills`);
|
|
28
|
+
skills = skills.filter((s) => s.status === "published" && !s.path.endsWith("/_index"));
|
|
29
|
+
|
|
30
|
+
if (kind === "project") {
|
|
31
|
+
const projects = await apiRequest(`/api/companies/${config.companyId}/projects`);
|
|
32
|
+
target = projects.find((p) => p.slug === slug);
|
|
33
|
+
if (!target) {
|
|
34
|
+
spinner.fail(`Proyecto '${slug}' no encontrado.`);
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
skills = skills.filter((s) => s.project_id === target.id);
|
|
38
|
+
} else {
|
|
39
|
+
const endpoint = { area: "areas", team: "teams", sector: "sectors" }[kind];
|
|
40
|
+
const entities = await apiRequest(`/api/companies/${config.companyId}/${endpoint}`);
|
|
41
|
+
target = entities.find((e) => e.slug === slug);
|
|
42
|
+
if (!target) {
|
|
43
|
+
spinner.fail(`${kind.charAt(0).toUpperCase() + kind.slice(1)} '${slug}' no encontrado.`);
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
skills = skills.filter((s) => s.entity_id === target.id);
|
|
47
|
+
}
|
|
48
|
+
} catch (err) {
|
|
49
|
+
spinner.fail("Error al cargar datos.");
|
|
50
|
+
throw err;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
spinner.stop();
|
|
54
|
+
|
|
55
|
+
if (skills.length === 0) {
|
|
56
|
+
console.log(chalk.yellow(`\n No hay skills publicadas en ${kind} '${target.name}'.\n`));
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Show what will be installed
|
|
61
|
+
console.log(
|
|
62
|
+
chalk.blue.bold(
|
|
63
|
+
`\n ${target.name} (${kind}) tiene ${skills.length} skill(s) publicada(s):\n`
|
|
64
|
+
)
|
|
65
|
+
);
|
|
66
|
+
for (const skill of skills) {
|
|
67
|
+
const desc = skill.description ? chalk.dim(` — ${skill.description}`) : "";
|
|
68
|
+
console.log(` ${chalk.cyan(skill.path)} ${chalk.gray(`v${skill.version}`)}${desc}`);
|
|
69
|
+
}
|
|
70
|
+
console.log();
|
|
71
|
+
|
|
72
|
+
// Confirm
|
|
73
|
+
if (!options.yes) {
|
|
74
|
+
const { confirm } = await inquirer.prompt([
|
|
75
|
+
{
|
|
76
|
+
type: "confirm",
|
|
77
|
+
name: "confirm",
|
|
78
|
+
message: `¿Instalar las ${skills.length} skill(s)?`,
|
|
79
|
+
default: true,
|
|
80
|
+
},
|
|
81
|
+
]);
|
|
82
|
+
if (!confirm) {
|
|
83
|
+
console.log(chalk.dim("Cancelado."));
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Determine target tools
|
|
89
|
+
let toolNames = [];
|
|
90
|
+
if (options.tool) {
|
|
91
|
+
toolNames = options.tool.split(",").map((t) => t.trim());
|
|
92
|
+
} else if (config.defaultTool) {
|
|
93
|
+
toolNames = [config.defaultTool];
|
|
94
|
+
} else {
|
|
95
|
+
const detected = await detectInstalledTools();
|
|
96
|
+
if (detected.length === 0) {
|
|
97
|
+
console.error(chalk.red("No se detectó ninguna herramienta de IA instalada."));
|
|
98
|
+
process.exit(1);
|
|
99
|
+
}
|
|
100
|
+
const { selected } = await inquirer.prompt([
|
|
101
|
+
{
|
|
102
|
+
type: "checkbox",
|
|
103
|
+
name: "selected",
|
|
104
|
+
message: "¿En qué herramientas quieres instalar?",
|
|
105
|
+
choices: detected.map((a) => ({
|
|
106
|
+
name: a.displayName,
|
|
107
|
+
value: a.toolName,
|
|
108
|
+
checked: true,
|
|
109
|
+
})),
|
|
110
|
+
validate: (val) => (val.length > 0 ? true : "Selecciona al menos una herramienta."),
|
|
111
|
+
},
|
|
112
|
+
]);
|
|
113
|
+
toolNames = selected;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Determine scope
|
|
117
|
+
let scope = options.scope;
|
|
118
|
+
if (!scope) {
|
|
119
|
+
const { chosen } = await inquirer.prompt([
|
|
120
|
+
{
|
|
121
|
+
type: "list",
|
|
122
|
+
name: "chosen",
|
|
123
|
+
message: "¿Dónde instalar las skills?",
|
|
124
|
+
choices: [
|
|
125
|
+
{ name: "Global (solo para ti, en ~/)", value: "personal" },
|
|
126
|
+
{ name: "Proyecto (en el repo actual)", value: "project" },
|
|
127
|
+
],
|
|
128
|
+
},
|
|
129
|
+
]);
|
|
130
|
+
scope = chosen;
|
|
131
|
+
}
|
|
132
|
+
if (scope === "global") scope = "personal";
|
|
133
|
+
|
|
134
|
+
// Install all
|
|
135
|
+
const adapters = toolNames.map((name) => getAdapter(name));
|
|
136
|
+
let installed = 0;
|
|
137
|
+
let failed = 0;
|
|
138
|
+
|
|
139
|
+
for (const skill of skills) {
|
|
140
|
+
const installSpinner = ora(`Instalando ${skill.path}...`).start();
|
|
141
|
+
try {
|
|
142
|
+
const content = skill.content || "";
|
|
143
|
+
for (const adapter of adapters) {
|
|
144
|
+
await adapter.install(skill.path, content, scope);
|
|
145
|
+
}
|
|
146
|
+
await trackInstall(skill, toolNames[0], scope);
|
|
147
|
+
installed++;
|
|
148
|
+
installSpinner.succeed(`Instalada ${skill.path}`);
|
|
149
|
+
} catch (err) {
|
|
150
|
+
installSpinner.fail(`Error instalando ${skill.path}`);
|
|
151
|
+
console.error(chalk.red(` ${err.message}`));
|
|
152
|
+
failed++;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Summary
|
|
157
|
+
console.log();
|
|
158
|
+
const toolList = adapters.map((a) => a.displayName).join(" y ");
|
|
159
|
+
if (failed > 0) {
|
|
160
|
+
console.log(
|
|
161
|
+
chalk.yellow(
|
|
162
|
+
`⚠ ${installed} skill(s) instalada(s), ${failed} fallaron en ${toolList} (${scope})`
|
|
163
|
+
)
|
|
164
|
+
);
|
|
165
|
+
} else {
|
|
166
|
+
console.log(
|
|
167
|
+
chalk.green(`✔ ${installed} skill(s) instalada(s) en ${toolList} (${scope})`)
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
}
|
package/src/commands/install.js
CHANGED
|
@@ -4,6 +4,7 @@ import inquirer from "inquirer";
|
|
|
4
4
|
import { requireConfig } from "../utils/config.js";
|
|
5
5
|
import { apiRequest } from "../utils/api.js";
|
|
6
6
|
import { getAdapter, detectInstalledTools } from "../adapters/index.js";
|
|
7
|
+
import { trackInstall } from "../utils/installed.js";
|
|
7
8
|
|
|
8
9
|
export async function install(skillPath, options = {}) {
|
|
9
10
|
const config = await requireConfig();
|
|
@@ -19,6 +20,9 @@ export async function install(skillPath, options = {}) {
|
|
|
19
20
|
throw err;
|
|
20
21
|
}
|
|
21
22
|
|
|
23
|
+
// Filter out _index skills (auto-generated, not installable)
|
|
24
|
+
skills = skills.filter((s) => !s.path.endsWith("/_index"));
|
|
25
|
+
|
|
22
26
|
// Find the target skill
|
|
23
27
|
const target = skills.find((s) => s.path === skillPath);
|
|
24
28
|
if (!target) {
|
|
@@ -159,6 +163,7 @@ export async function install(skillPath, options = {}) {
|
|
|
159
163
|
for (const adapter of adapters) {
|
|
160
164
|
await adapter.install(skill.path, content, scope);
|
|
161
165
|
}
|
|
166
|
+
await trackInstall(skill, toolNames[0], scope);
|
|
162
167
|
installed++;
|
|
163
168
|
installSpinner.succeed(`Instalada ${skill.path}`);
|
|
164
169
|
} catch (err) {
|
package/src/commands/list.js
CHANGED
|
@@ -2,6 +2,8 @@ import chalk from "chalk";
|
|
|
2
2
|
import ora from "ora";
|
|
3
3
|
import { requireConfig } from "../utils/config.js";
|
|
4
4
|
import { apiRequest } from "../utils/api.js";
|
|
5
|
+
import { shouldCheckUpdates } from "../utils/installed.js";
|
|
6
|
+
import { checkUpdatesPassive } from "./update.js";
|
|
5
7
|
|
|
6
8
|
const LEVEL_ICONS = {
|
|
7
9
|
root: "🏢",
|
|
@@ -22,6 +24,11 @@ const TYPE_LABELS = {
|
|
|
22
24
|
export async function list(options = {}) {
|
|
23
25
|
const config = await requireConfig();
|
|
24
26
|
|
|
27
|
+
// Passive update check (every 5 min)
|
|
28
|
+
if (await shouldCheckUpdates()) {
|
|
29
|
+
await checkUpdatesPassive(config);
|
|
30
|
+
}
|
|
31
|
+
|
|
25
32
|
const spinner = ora("Cargando skills...").start();
|
|
26
33
|
let skills;
|
|
27
34
|
try {
|
|
@@ -32,8 +39,10 @@ export async function list(options = {}) {
|
|
|
32
39
|
throw err;
|
|
33
40
|
}
|
|
34
41
|
|
|
35
|
-
// Filter published only
|
|
36
|
-
skills = skills.filter(
|
|
42
|
+
// Filter published only and exclude auto-generated _index skills
|
|
43
|
+
skills = skills.filter(
|
|
44
|
+
(s) => s.status === "published" && !s.path.endsWith("/_index")
|
|
45
|
+
);
|
|
37
46
|
|
|
38
47
|
// Apply text filter if provided
|
|
39
48
|
if (options.filter) {
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import ora from "ora";
|
|
3
|
+
import inquirer from "inquirer";
|
|
4
|
+
import { readFile } from "node:fs/promises";
|
|
5
|
+
import { requireConfig } from "../utils/config.js";
|
|
6
|
+
import { apiRequest } from "../utils/api.js";
|
|
7
|
+
import { getInstalledSkills } from "../utils/installed.js";
|
|
8
|
+
import { getAdapter } from "../adapters/index.js";
|
|
9
|
+
|
|
10
|
+
export async function push(skillPath) {
|
|
11
|
+
const config = await requireConfig();
|
|
12
|
+
|
|
13
|
+
// Find the installed skill
|
|
14
|
+
const installed = await getInstalledSkills();
|
|
15
|
+
const entry = installed.find((s) => s.path === skillPath);
|
|
16
|
+
|
|
17
|
+
if (!entry) {
|
|
18
|
+
console.log(chalk.red(`Skill no instalado localmente: ${skillPath}`));
|
|
19
|
+
console.log(chalk.dim(" Solo puedes hacer push de skills que tengas instalados."));
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Read local content
|
|
24
|
+
const adapter = getAdapter(entry.tool || config.defaultTool);
|
|
25
|
+
let localContent;
|
|
26
|
+
try {
|
|
27
|
+
const localPath = adapter.getInstallPath(skillPath, entry.scope);
|
|
28
|
+
localContent = await readFile(localPath, "utf-8");
|
|
29
|
+
} catch (err) {
|
|
30
|
+
console.log(chalk.red(`No se pudo leer el skill local: ${skillPath}`));
|
|
31
|
+
console.log(chalk.dim(` ${err.message}`));
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Fetch remote to compare
|
|
36
|
+
const spinner = ora("Comparando con la versión del servidor...").start();
|
|
37
|
+
let remoteSkill;
|
|
38
|
+
try {
|
|
39
|
+
const skills = await apiRequest(`/api/companies/${config.companyId}/skills`);
|
|
40
|
+
remoteSkill = skills.find((s) => s.path === skillPath);
|
|
41
|
+
} catch (err) {
|
|
42
|
+
spinner.fail("No se pudo conectar con la API.");
|
|
43
|
+
throw err;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (!remoteSkill) {
|
|
47
|
+
spinner.fail("Skill no encontrado en el servidor.");
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (localContent.trim() === (remoteSkill.content || "").trim()) {
|
|
52
|
+
spinner.succeed("El skill local es idéntico al del servidor. No hay cambios.");
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
spinner.stop();
|
|
57
|
+
|
|
58
|
+
// Show diff summary
|
|
59
|
+
const localLines = localContent.split("\n").length;
|
|
60
|
+
const remoteLines = (remoteSkill.content || "").split("\n").length;
|
|
61
|
+
console.log(
|
|
62
|
+
chalk.dim(`\n Local: ${localLines} líneas | Servidor: ${remoteLines} líneas`)
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
// Ask for commit message
|
|
66
|
+
const { message } = await inquirer.prompt([
|
|
67
|
+
{
|
|
68
|
+
type: "input",
|
|
69
|
+
name: "message",
|
|
70
|
+
message: "Describe los cambios:",
|
|
71
|
+
validate: (val) => (val.trim() ? true : "El mensaje es obligatorio."),
|
|
72
|
+
},
|
|
73
|
+
]);
|
|
74
|
+
|
|
75
|
+
// Push
|
|
76
|
+
const pushSpinner = ora("Enviando cambios...").start();
|
|
77
|
+
try {
|
|
78
|
+
const result = await apiRequest(
|
|
79
|
+
`/api/companies/${config.companyId}/skills/${remoteSkill.id}/push`,
|
|
80
|
+
{
|
|
81
|
+
method: "POST",
|
|
82
|
+
body: JSON.stringify({ content: localContent, message }),
|
|
83
|
+
}
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
if (result.action === "pushed") {
|
|
87
|
+
pushSpinner.succeed(
|
|
88
|
+
chalk.green(
|
|
89
|
+
`Cambios publicados directamente (v${result.skill.version})`
|
|
90
|
+
)
|
|
91
|
+
);
|
|
92
|
+
} else if (result.action === "proposed") {
|
|
93
|
+
pushSpinner.succeed(
|
|
94
|
+
chalk.yellow(
|
|
95
|
+
"Propuesta de cambio creada. Un admin o lead debe aprobarla."
|
|
96
|
+
)
|
|
97
|
+
);
|
|
98
|
+
console.log(chalk.dim(` ID de propuesta: ${result.proposal.id}`));
|
|
99
|
+
}
|
|
100
|
+
} catch (err) {
|
|
101
|
+
pushSpinner.fail("Error enviando cambios.");
|
|
102
|
+
throw err;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import ora from "ora";
|
|
3
|
+
import inquirer from "inquirer";
|
|
4
|
+
import { requireConfig } from "../utils/config.js";
|
|
5
|
+
import { apiRequest } from "../utils/api.js";
|
|
6
|
+
import { getInstalledSkills, updateInstalledVersion, markChecked } from "../utils/installed.js";
|
|
7
|
+
import { getAdapter } from "../adapters/index.js";
|
|
8
|
+
|
|
9
|
+
export async function update() {
|
|
10
|
+
const config = await requireConfig();
|
|
11
|
+
const installed = await getInstalledSkills();
|
|
12
|
+
|
|
13
|
+
if (installed.length === 0) {
|
|
14
|
+
console.log(chalk.dim("No tienes skills instalados."));
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const spinner = ora("Comprobando actualizaciones...").start();
|
|
19
|
+
|
|
20
|
+
let updates;
|
|
21
|
+
try {
|
|
22
|
+
const body = installed.map((s) => ({ path: s.path, version: s.version }));
|
|
23
|
+
const result = await apiRequest(
|
|
24
|
+
`/api/companies/${config.companyId}/skills/check-updates`,
|
|
25
|
+
{
|
|
26
|
+
method: "POST",
|
|
27
|
+
body: JSON.stringify({ installed: body }),
|
|
28
|
+
}
|
|
29
|
+
);
|
|
30
|
+
updates = (result.updates || []).filter((u) => !u.path.endsWith("/_index"));
|
|
31
|
+
await markChecked();
|
|
32
|
+
} catch (err) {
|
|
33
|
+
spinner.fail("No se pudo comprobar actualizaciones.");
|
|
34
|
+
throw err;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (updates.length === 0) {
|
|
38
|
+
spinner.succeed("Todo al día — no hay actualizaciones.");
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
spinner.succeed(`${updates.length} actualización(es) disponible(s):\n`);
|
|
43
|
+
|
|
44
|
+
let updated = 0;
|
|
45
|
+
for (const upd of updates) {
|
|
46
|
+
console.log(
|
|
47
|
+
` ${chalk.cyan(upd.path)} ${chalk.gray(upd.currentVersion)} → ${chalk.green(upd.latestVersion)}`
|
|
48
|
+
);
|
|
49
|
+
if (upd.author) console.log(chalk.dim(` Autor: ${upd.author}`));
|
|
50
|
+
if (upd.description) console.log(chalk.dim(` ${upd.description}`));
|
|
51
|
+
|
|
52
|
+
const { doUpdate } = await inquirer.prompt([
|
|
53
|
+
{
|
|
54
|
+
type: "confirm",
|
|
55
|
+
name: "doUpdate",
|
|
56
|
+
message: `¿Actualizar ${upd.path}?`,
|
|
57
|
+
default: true,
|
|
58
|
+
},
|
|
59
|
+
]);
|
|
60
|
+
|
|
61
|
+
if (!doUpdate) {
|
|
62
|
+
console.log(chalk.dim(" Omitido.\n"));
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const installSpinner = ora(`Actualizando ${upd.path}...`).start();
|
|
67
|
+
try {
|
|
68
|
+
// Fetch full skill content
|
|
69
|
+
const skill = await apiRequest(
|
|
70
|
+
`/api/companies/${config.companyId}/skills/${upd.id}`
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
// Find installed entry to know tool and scope
|
|
74
|
+
const entry = installed.find((s) => s.path === upd.path);
|
|
75
|
+
const tool = entry?.tool || config.defaultTool;
|
|
76
|
+
const scope = entry?.scope || "project";
|
|
77
|
+
|
|
78
|
+
const adapter = getAdapter(tool);
|
|
79
|
+
await adapter.install(skill.path, skill.content || "", scope);
|
|
80
|
+
await updateInstalledVersion(upd.path, upd.latestVersion);
|
|
81
|
+
|
|
82
|
+
updated++;
|
|
83
|
+
installSpinner.succeed(`Actualizada ${upd.path} a v${upd.latestVersion}`);
|
|
84
|
+
} catch (err) {
|
|
85
|
+
installSpinner.fail(`Error actualizando ${upd.path}`);
|
|
86
|
+
console.error(chalk.red(` ${err.message}`));
|
|
87
|
+
}
|
|
88
|
+
console.log();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
console.log(chalk.green(`✔ ${updated} skill(s) actualizado(s)`));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Passive check — called from other commands
|
|
95
|
+
export async function checkUpdatesPassive(config) {
|
|
96
|
+
try {
|
|
97
|
+
const installed = await getInstalledSkills();
|
|
98
|
+
if (installed.length === 0) return;
|
|
99
|
+
|
|
100
|
+
const body = installed.map((s) => ({ path: s.path, version: s.version }));
|
|
101
|
+
const result = await apiRequest(
|
|
102
|
+
`/api/companies/${config.companyId}/skills/check-updates`,
|
|
103
|
+
{
|
|
104
|
+
method: "POST",
|
|
105
|
+
body: JSON.stringify({ installed: body }),
|
|
106
|
+
}
|
|
107
|
+
);
|
|
108
|
+
await markChecked();
|
|
109
|
+
|
|
110
|
+
const updates = (result.updates || []).filter((u) => !u.path.endsWith("/_index"));
|
|
111
|
+
if (updates.length > 0) {
|
|
112
|
+
console.log(
|
|
113
|
+
chalk.yellow(
|
|
114
|
+
`\n ⚠ ${updates.length} actualización(es) disponible(s):`
|
|
115
|
+
)
|
|
116
|
+
);
|
|
117
|
+
for (const u of updates) {
|
|
118
|
+
console.log(
|
|
119
|
+
chalk.dim(` ${u.path} ${u.currentVersion} → ${u.latestVersion}`)
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
console.log(chalk.dim(" Ejecuta: skill-manager update\n"));
|
|
123
|
+
}
|
|
124
|
+
} catch {
|
|
125
|
+
// Silent fail — don't break the main command
|
|
126
|
+
}
|
|
127
|
+
}
|
package/src/utils/config.js
CHANGED
|
@@ -25,8 +25,19 @@ export async function saveConfig(config) {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
export async function requireConfig() {
|
|
28
|
+
// Check credentials first
|
|
29
|
+
const { homedir } = await import("node:os");
|
|
30
|
+
const credsPath = join(homedir(), ".skill-manager", "credentials.json");
|
|
31
|
+
try {
|
|
32
|
+
await readFile(credsPath, "utf-8");
|
|
33
|
+
} catch {
|
|
34
|
+
throw new Error(
|
|
35
|
+
"No estás autenticado. Ejecuta primero: skill-manager login"
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
28
39
|
const config = await loadConfig();
|
|
29
|
-
if (!config) {
|
|
40
|
+
if (!config || !config.companyId) {
|
|
30
41
|
throw new Error(
|
|
31
42
|
"No se encontró configuración. Ejecuta primero: skill-manager init"
|
|
32
43
|
);
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
|
4
|
+
|
|
5
|
+
const CONFIG_DIR = join(homedir(), ".skill-manager");
|
|
6
|
+
const INSTALLED_FILE = join(CONFIG_DIR, "installed.json");
|
|
7
|
+
const CHECK_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes
|
|
8
|
+
|
|
9
|
+
async function loadInstalled() {
|
|
10
|
+
try {
|
|
11
|
+
const raw = await readFile(INSTALLED_FILE, "utf-8");
|
|
12
|
+
return JSON.parse(raw);
|
|
13
|
+
} catch (err) {
|
|
14
|
+
if (err.code === "ENOENT") return { lastCheckAt: null, skills: [] };
|
|
15
|
+
throw err;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function saveInstalled(data) {
|
|
20
|
+
await mkdir(CONFIG_DIR, { recursive: true });
|
|
21
|
+
await writeFile(INSTALLED_FILE, JSON.stringify(data, null, 2) + "\n");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function trackInstall(skill, tool, scope) {
|
|
25
|
+
// Never track auto-generated _index skills
|
|
26
|
+
if (skill.path.endsWith("/_index")) return;
|
|
27
|
+
|
|
28
|
+
const data = await loadInstalled();
|
|
29
|
+
|
|
30
|
+
// Remove existing entry for same path + projectDir
|
|
31
|
+
const projectDir = scope === "project" ? process.cwd() : null;
|
|
32
|
+
data.skills = data.skills.filter(
|
|
33
|
+
(s) => !(s.path === skill.path && s.projectDir === projectDir)
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
data.skills.push({
|
|
37
|
+
path: skill.path,
|
|
38
|
+
version: skill.version,
|
|
39
|
+
skillId: skill.id,
|
|
40
|
+
companyId: skill.company_id,
|
|
41
|
+
installedAt: new Date().toISOString(),
|
|
42
|
+
tool,
|
|
43
|
+
scope,
|
|
44
|
+
projectDir,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
await saveInstalled(data);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function getInstalledSkills() {
|
|
51
|
+
const data = await loadInstalled();
|
|
52
|
+
return data.skills || [];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function updateInstalledVersion(path, newVersion) {
|
|
56
|
+
const data = await loadInstalled();
|
|
57
|
+
for (const s of data.skills) {
|
|
58
|
+
if (s.path === path) {
|
|
59
|
+
s.version = newVersion;
|
|
60
|
+
s.installedAt = new Date().toISOString();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
await saveInstalled(data);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function shouldCheckUpdates() {
|
|
67
|
+
const data = await loadInstalled();
|
|
68
|
+
if (!data.lastCheckAt) return true;
|
|
69
|
+
return Date.now() - new Date(data.lastCheckAt).getTime() > CHECK_INTERVAL_MS;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export async function markChecked() {
|
|
73
|
+
const data = await loadInstalled();
|
|
74
|
+
data.lastCheckAt = new Date().toISOString();
|
|
75
|
+
await saveInstalled(data);
|
|
76
|
+
}
|