@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,69 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import ora from "ora";
|
|
3
|
+
import { getAllSkills } from "../manifest.js";
|
|
4
|
+
|
|
5
|
+
const SECTIONS = [
|
|
6
|
+
{ prefix: "user/", label: "User Skills" },
|
|
7
|
+
{ prefix: "internal/", label: "Internal Skills" },
|
|
8
|
+
{ prefix: "external/", label: "External Skills" },
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
export async function list(options = {}) {
|
|
12
|
+
const spinner = ora("Cargando catálogo de skills...").start();
|
|
13
|
+
|
|
14
|
+
let skills;
|
|
15
|
+
try {
|
|
16
|
+
skills = await getAllSkills();
|
|
17
|
+
spinner.succeed("Catálogo cargado.");
|
|
18
|
+
} catch (err) {
|
|
19
|
+
spinner.fail("Error cargando el catálogo.");
|
|
20
|
+
console.error(chalk.red(err.message));
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (options.filter) {
|
|
25
|
+
const term = options.filter.toLowerCase();
|
|
26
|
+
skills = skills.filter((s) => s.path.toLowerCase().includes(term));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (skills.length === 0) {
|
|
30
|
+
console.log(chalk.yellow("No se encontraron skills."));
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
for (const section of SECTIONS) {
|
|
35
|
+
const group = skills.filter((s) => s.path.startsWith(section.prefix));
|
|
36
|
+
if (group.length === 0) continue;
|
|
37
|
+
|
|
38
|
+
console.log();
|
|
39
|
+
console.log(chalk.blue.bold(` ${section.label}`));
|
|
40
|
+
console.log(chalk.blue(" " + "─".repeat(40)));
|
|
41
|
+
|
|
42
|
+
for (const skill of group) {
|
|
43
|
+
const parent = skill.parent
|
|
44
|
+
? chalk.dim(` ← ${skill.parent}`)
|
|
45
|
+
: "";
|
|
46
|
+
console.log(
|
|
47
|
+
` ${chalk.white(skill.path)} ${chalk.gray(`v${skill.version}`)}${parent}`
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Skills that don't match any section
|
|
53
|
+
const uncategorized = skills.filter(
|
|
54
|
+
(s) => !SECTIONS.some((sec) => s.path.startsWith(sec.prefix))
|
|
55
|
+
);
|
|
56
|
+
if (uncategorized.length > 0) {
|
|
57
|
+
console.log();
|
|
58
|
+
console.log(chalk.blue.bold(" Other"));
|
|
59
|
+
console.log(chalk.blue(" " + "─".repeat(40)));
|
|
60
|
+
for (const skill of uncategorized) {
|
|
61
|
+
console.log(
|
|
62
|
+
` ${chalk.white(skill.path)} ${chalk.gray(`v${skill.version}`)}`
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
console.log();
|
|
68
|
+
console.log(chalk.dim(` Total: ${skills.length} skills`));
|
|
69
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import ora from "ora";
|
|
3
|
+
import { startLoginFlow, loadCredentials, parseIdToken } from "../utils/auth.js";
|
|
4
|
+
|
|
5
|
+
export async function login() {
|
|
6
|
+
// Check if already logged in
|
|
7
|
+
const creds = await loadCredentials();
|
|
8
|
+
if (creds) {
|
|
9
|
+
const user = parseIdToken(creds.id_token);
|
|
10
|
+
if (user && creds.expires_at > Date.now()) {
|
|
11
|
+
console.log(
|
|
12
|
+
chalk.yellow(`Ya estás autenticado como ${user.email}. Iniciando nuevo login...\n`)
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const spinner = ora("Abriendo navegador para autenticación...").start();
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
const user = await startLoginFlow();
|
|
21
|
+
spinner.succeed(
|
|
22
|
+
chalk.green(`Autenticado como ${chalk.bold(user.email)}`)
|
|
23
|
+
);
|
|
24
|
+
console.log(chalk.dim(" Token guardado en ~/.skill-manager/credentials.json"));
|
|
25
|
+
} catch (err) {
|
|
26
|
+
spinner.fail(chalk.red("Error de autenticación"));
|
|
27
|
+
throw err;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { loadCredentials, clearCredentials } from "../utils/auth.js";
|
|
3
|
+
|
|
4
|
+
export async function logout() {
|
|
5
|
+
const creds = await loadCredentials();
|
|
6
|
+
if (!creds) {
|
|
7
|
+
console.log(chalk.dim("No hay sesión activa."));
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
await clearCredentials();
|
|
12
|
+
console.log(chalk.green("Sesión cerrada correctamente."));
|
|
13
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import ora from "ora";
|
|
3
|
+
import { detectInstalledTools } from "../adapters/index.js";
|
|
4
|
+
import { getSkillInfo } from "../manifest.js";
|
|
5
|
+
|
|
6
|
+
export async function status() {
|
|
7
|
+
const spinner = ora("Detectando herramientas instaladas...").start();
|
|
8
|
+
const tools = await detectInstalledTools();
|
|
9
|
+
|
|
10
|
+
if (tools.length === 0) {
|
|
11
|
+
spinner.fail("No se detectó ninguna herramienta de IA instalada.");
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
spinner.succeed(`${tools.length} herramienta(s) detectada(s).`);
|
|
15
|
+
|
|
16
|
+
for (const adapter of tools) {
|
|
17
|
+
console.log();
|
|
18
|
+
console.log(chalk.blue.bold(` ${adapter.displayName}`));
|
|
19
|
+
console.log(chalk.blue(" " + "─".repeat(50)));
|
|
20
|
+
|
|
21
|
+
for (const scope of ["personal", "project"]) {
|
|
22
|
+
const skills = await adapter.getInstalledSkills(scope);
|
|
23
|
+
if (skills.length === 0) continue;
|
|
24
|
+
|
|
25
|
+
const label = scope === "personal" ? "Global" : "Proyecto";
|
|
26
|
+
console.log(chalk.dim(` [${label}]`));
|
|
27
|
+
|
|
28
|
+
for (const skill of skills) {
|
|
29
|
+
let catalogVersion = null;
|
|
30
|
+
let statusIcon;
|
|
31
|
+
let statusColor;
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
const info = await getSkillInfo(skill.path);
|
|
35
|
+
catalogVersion = info.version;
|
|
36
|
+
if (skill.version === catalogVersion) {
|
|
37
|
+
statusIcon = "✓ al día";
|
|
38
|
+
statusColor = chalk.green;
|
|
39
|
+
} else {
|
|
40
|
+
statusIcon = `⬆ ${catalogVersion} disponible`;
|
|
41
|
+
statusColor = chalk.yellow;
|
|
42
|
+
}
|
|
43
|
+
} catch {
|
|
44
|
+
statusIcon = "? no en catálogo";
|
|
45
|
+
statusColor = chalk.gray;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const installed = chalk.white(skill.version);
|
|
49
|
+
console.log(
|
|
50
|
+
` ${chalk.white(skill.path.padEnd(35))} ${installed.padEnd(10)} ${statusColor(statusIcon)}`
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
console.log();
|
|
56
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { loadCredentials, parseIdToken } from "../utils/auth.js";
|
|
3
|
+
|
|
4
|
+
export async function whoami() {
|
|
5
|
+
const creds = await loadCredentials();
|
|
6
|
+
if (!creds) {
|
|
7
|
+
console.log(chalk.yellow("No estás autenticado."));
|
|
8
|
+
console.log(chalk.dim("Ejecuta: skill-manager login"));
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const user = parseIdToken(creds.id_token);
|
|
13
|
+
if (!user) {
|
|
14
|
+
console.log(chalk.red("Token inválido. Ejecuta: skill-manager login"));
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const expired = creds.expires_at < Date.now();
|
|
19
|
+
const expiresIn = Math.round((creds.expires_at - Date.now()) / 60_000);
|
|
20
|
+
|
|
21
|
+
console.log(chalk.bold(" Nombre:"), user.name);
|
|
22
|
+
console.log(chalk.bold(" Email: "), user.email);
|
|
23
|
+
|
|
24
|
+
if (expired) {
|
|
25
|
+
console.log(chalk.bold(" Token: "), chalk.red("expirado (se renovará automáticamente)"));
|
|
26
|
+
} else {
|
|
27
|
+
console.log(chalk.bold(" Token: "), chalk.green(`válido (${expiresIn} min restantes)`));
|
|
28
|
+
}
|
|
29
|
+
}
|
package/src/manifest.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import yaml from "js-yaml";
|
|
2
|
+
import { requireConfig } from "./utils/config.js";
|
|
3
|
+
import { fetchFileContent } from "./utils/gitlab.js";
|
|
4
|
+
|
|
5
|
+
let cachedManifest = null;
|
|
6
|
+
|
|
7
|
+
export async function fetchManifest(configOverride) {
|
|
8
|
+
if (cachedManifest) return cachedManifest;
|
|
9
|
+
|
|
10
|
+
const config = configOverride || (await requireConfig());
|
|
11
|
+
let raw;
|
|
12
|
+
try {
|
|
13
|
+
raw = await fetchFileContent("manifest.yml", config);
|
|
14
|
+
} catch (err) {
|
|
15
|
+
if (err.message.includes("no existe")) {
|
|
16
|
+
throw new Error(
|
|
17
|
+
"No se puede conectar al repositorio. Verifica tu token y URL."
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
throw err;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
let manifest;
|
|
24
|
+
try {
|
|
25
|
+
manifest = yaml.load(raw);
|
|
26
|
+
} catch {
|
|
27
|
+
throw new Error("El manifest.yml tiene un formato incorrecto.");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (!manifest || !manifest.skills || !Array.isArray(manifest.skills)) {
|
|
31
|
+
throw new Error("El manifest.yml tiene un formato incorrecto.");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
cachedManifest = manifest;
|
|
35
|
+
return manifest;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function getAllSkills(configOverride) {
|
|
39
|
+
const manifest = await fetchManifest(configOverride);
|
|
40
|
+
return manifest.skills.map((s) => ({
|
|
41
|
+
path: s.path,
|
|
42
|
+
version: s.version || "0.0.0",
|
|
43
|
+
parent: s.parent || null,
|
|
44
|
+
upstream: s.upstream || null,
|
|
45
|
+
}));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function getSkillInfo(skillPath, configOverride) {
|
|
49
|
+
const skills = await getAllSkills(configOverride);
|
|
50
|
+
const skill = skills.find((s) => s.path === skillPath);
|
|
51
|
+
if (!skill) {
|
|
52
|
+
throw new Error(`La skill '${skillPath}' no existe en el catálogo.`);
|
|
53
|
+
}
|
|
54
|
+
return skill;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function resolveChain(skillPath, configOverride) {
|
|
58
|
+
const skills = await getAllSkills(configOverride);
|
|
59
|
+
const byPath = new Map(skills.map((s) => [s.path, s]));
|
|
60
|
+
|
|
61
|
+
const skill = byPath.get(skillPath);
|
|
62
|
+
if (!skill) {
|
|
63
|
+
throw new Error(`La skill '${skillPath}' no existe en el catálogo.`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const chain = [skill];
|
|
67
|
+
let current = skill;
|
|
68
|
+
while (current.parent) {
|
|
69
|
+
const parent = byPath.get(current.parent);
|
|
70
|
+
if (!parent) {
|
|
71
|
+
throw new Error(
|
|
72
|
+
`La skill padre '${current.parent}' referenciada por '${current.path}' no existe en el catálogo.`
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
chain.unshift(parent);
|
|
76
|
+
current = parent;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return chain;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function clearCache() {
|
|
83
|
+
cachedManifest = null;
|
|
84
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { describe, it, beforeEach, mock } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import yaml from "js-yaml";
|
|
4
|
+
import { fetchManifest, getAllSkills, getSkillInfo, resolveChain, clearCache } from "./manifest.js";
|
|
5
|
+
|
|
6
|
+
const MOCK_MANIFEST = {
|
|
7
|
+
skills: [
|
|
8
|
+
{ path: "external/sector/pharma", version: "1.0.0", parent: null, upstream: null },
|
|
9
|
+
{ path: "external/client/alexion", version: "1.2.0", parent: "external/sector/pharma", upstream: null },
|
|
10
|
+
{ path: "external/project/alexion-cockpit", version: "2.0.0", parent: "external/client/alexion", upstream: null },
|
|
11
|
+
{ path: "internal/tooling/eslint-config", version: "0.5.0", parent: null, upstream: null },
|
|
12
|
+
],
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const MOCK_CONFIG = {
|
|
16
|
+
registry: {
|
|
17
|
+
type: "gitlab",
|
|
18
|
+
url: "https://git.basetis.com",
|
|
19
|
+
projectId: "okr-ia/basetisskills",
|
|
20
|
+
token: "fake-token",
|
|
21
|
+
branch: "main",
|
|
22
|
+
},
|
|
23
|
+
defaultTool: "claude",
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
function mockFetch() {
|
|
27
|
+
return mock.fn(() =>
|
|
28
|
+
Promise.resolve({
|
|
29
|
+
ok: true,
|
|
30
|
+
text: () => Promise.resolve(yaml.dump(MOCK_MANIFEST)),
|
|
31
|
+
})
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe("manifest", () => {
|
|
36
|
+
beforeEach(() => {
|
|
37
|
+
clearCache();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("fetchManifest parses yaml and returns manifest", async () => {
|
|
41
|
+
const original = globalThis.fetch;
|
|
42
|
+
globalThis.fetch = mockFetch();
|
|
43
|
+
try {
|
|
44
|
+
const manifest = await fetchManifest(MOCK_CONFIG);
|
|
45
|
+
assert.equal(manifest.skills.length, 4);
|
|
46
|
+
assert.equal(manifest.skills[0].path, "external/sector/pharma");
|
|
47
|
+
} finally {
|
|
48
|
+
globalThis.fetch = original;
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("fetchManifest caches result on second call", async () => {
|
|
53
|
+
const original = globalThis.fetch;
|
|
54
|
+
const mockedFetch = mockFetch();
|
|
55
|
+
globalThis.fetch = mockedFetch;
|
|
56
|
+
try {
|
|
57
|
+
await fetchManifest(MOCK_CONFIG);
|
|
58
|
+
await fetchManifest(MOCK_CONFIG);
|
|
59
|
+
assert.equal(mockedFetch.mock.callCount(), 1);
|
|
60
|
+
} finally {
|
|
61
|
+
globalThis.fetch = original;
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("getAllSkills returns normalized skill objects", async () => {
|
|
66
|
+
const original = globalThis.fetch;
|
|
67
|
+
globalThis.fetch = mockFetch();
|
|
68
|
+
try {
|
|
69
|
+
const skills = await getAllSkills(MOCK_CONFIG);
|
|
70
|
+
assert.equal(skills.length, 4);
|
|
71
|
+
assert.deepEqual(skills[0], {
|
|
72
|
+
path: "external/sector/pharma",
|
|
73
|
+
version: "1.0.0",
|
|
74
|
+
parent: null,
|
|
75
|
+
upstream: null,
|
|
76
|
+
});
|
|
77
|
+
} finally {
|
|
78
|
+
globalThis.fetch = original;
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("getSkillInfo returns a specific skill", async () => {
|
|
83
|
+
const original = globalThis.fetch;
|
|
84
|
+
globalThis.fetch = mockFetch();
|
|
85
|
+
try {
|
|
86
|
+
const skill = await getSkillInfo("external/client/alexion", MOCK_CONFIG);
|
|
87
|
+
assert.equal(skill.path, "external/client/alexion");
|
|
88
|
+
assert.equal(skill.parent, "external/sector/pharma");
|
|
89
|
+
} finally {
|
|
90
|
+
globalThis.fetch = original;
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("getSkillInfo throws for unknown skill", async () => {
|
|
95
|
+
const original = globalThis.fetch;
|
|
96
|
+
globalThis.fetch = mockFetch();
|
|
97
|
+
try {
|
|
98
|
+
await assert.rejects(
|
|
99
|
+
() => getSkillInfo("unknown/skill", MOCK_CONFIG),
|
|
100
|
+
{ message: "La skill 'unknown/skill' no existe en el catálogo." }
|
|
101
|
+
);
|
|
102
|
+
} finally {
|
|
103
|
+
globalThis.fetch = original;
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("resolveChain returns full parent chain in order", async () => {
|
|
108
|
+
const original = globalThis.fetch;
|
|
109
|
+
globalThis.fetch = mockFetch();
|
|
110
|
+
try {
|
|
111
|
+
const chain = await resolveChain("external/project/alexion-cockpit", MOCK_CONFIG);
|
|
112
|
+
const paths = chain.map((s) => s.path);
|
|
113
|
+
assert.deepEqual(paths, [
|
|
114
|
+
"external/sector/pharma",
|
|
115
|
+
"external/client/alexion",
|
|
116
|
+
"external/project/alexion-cockpit",
|
|
117
|
+
]);
|
|
118
|
+
} finally {
|
|
119
|
+
globalThis.fetch = original;
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("resolveChain returns single element for skill without parent", async () => {
|
|
124
|
+
const original = globalThis.fetch;
|
|
125
|
+
globalThis.fetch = mockFetch();
|
|
126
|
+
try {
|
|
127
|
+
const chain = await resolveChain("internal/tooling/eslint-config", MOCK_CONFIG);
|
|
128
|
+
assert.equal(chain.length, 1);
|
|
129
|
+
assert.equal(chain[0].path, "internal/tooling/eslint-config");
|
|
130
|
+
} finally {
|
|
131
|
+
globalThis.fetch = original;
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("resolveChain throws for unknown skill", async () => {
|
|
136
|
+
const original = globalThis.fetch;
|
|
137
|
+
globalThis.fetch = mockFetch();
|
|
138
|
+
try {
|
|
139
|
+
await assert.rejects(
|
|
140
|
+
() => resolveChain("nonexistent/skill", MOCK_CONFIG),
|
|
141
|
+
{ message: "La skill 'nonexistent/skill' no existe en el catálogo." }
|
|
142
|
+
);
|
|
143
|
+
} finally {
|
|
144
|
+
globalThis.fetch = original;
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("fetchManifest throws on malformed yaml", async () => {
|
|
149
|
+
const original = globalThis.fetch;
|
|
150
|
+
globalThis.fetch = mock.fn(() =>
|
|
151
|
+
Promise.resolve({
|
|
152
|
+
ok: true,
|
|
153
|
+
text: () => Promise.resolve("{ invalid: [yaml"),
|
|
154
|
+
})
|
|
155
|
+
);
|
|
156
|
+
try {
|
|
157
|
+
await assert.rejects(
|
|
158
|
+
() => fetchManifest(MOCK_CONFIG),
|
|
159
|
+
{ message: "El manifest.yml tiene un formato incorrecto." }
|
|
160
|
+
);
|
|
161
|
+
} finally {
|
|
162
|
+
globalThis.fetch = original;
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("fetchManifest throws on manifest without skills array", async () => {
|
|
167
|
+
const original = globalThis.fetch;
|
|
168
|
+
globalThis.fetch = mock.fn(() =>
|
|
169
|
+
Promise.resolve({
|
|
170
|
+
ok: true,
|
|
171
|
+
text: () => Promise.resolve(yaml.dump({ version: "1.0" })),
|
|
172
|
+
})
|
|
173
|
+
);
|
|
174
|
+
try {
|
|
175
|
+
await assert.rejects(
|
|
176
|
+
() => fetchManifest(MOCK_CONFIG),
|
|
177
|
+
{ message: "El manifest.yml tiene un formato incorrecto." }
|
|
178
|
+
);
|
|
179
|
+
} finally {
|
|
180
|
+
globalThis.fetch = original;
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
});
|
package/src/utils/api.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { getValidToken, clearCredentials } from "./auth.js";
|
|
2
|
+
import { loadConfig } from "./config.js";
|
|
3
|
+
|
|
4
|
+
const DEFAULT_API_URL = "https://skillsmanager.basetis.com";
|
|
5
|
+
|
|
6
|
+
async function getApiUrl() {
|
|
7
|
+
const config = await loadConfig();
|
|
8
|
+
return config?.apiUrl || DEFAULT_API_URL;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export async function apiRequest(path, options = {}) {
|
|
12
|
+
const token = await getValidToken();
|
|
13
|
+
const apiUrl = await getApiUrl();
|
|
14
|
+
|
|
15
|
+
const response = await fetch(`${apiUrl}${path}`, {
|
|
16
|
+
...options,
|
|
17
|
+
headers: {
|
|
18
|
+
Authorization: `Bearer ${token}`,
|
|
19
|
+
"Content-Type": "application/json",
|
|
20
|
+
...options.headers,
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
if (response.status === 401) {
|
|
25
|
+
await clearCredentials();
|
|
26
|
+
throw new Error("Sesión inválida. Ejecuta: skill-manager login");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (!response.ok) {
|
|
30
|
+
const body = await response.text();
|
|
31
|
+
throw new Error(`API error ${response.status}: ${body}`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return response.json();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function requireAuth() {
|
|
38
|
+
await getValidToken();
|
|
39
|
+
}
|