@skilltap/core 0.4.4 → 0.5.2
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/package.json +1 -1
- package/src/config.ts +5 -1
- package/src/frontmatter.test.ts +85 -0
- package/src/frontmatter.ts +26 -7
- package/src/install.npm.test.ts +5 -2
- package/src/schemas/config.ts +2 -0
- package/src/self-update.ts +1 -1
- package/src/taps.test.ts +63 -1
- package/src/taps.ts +134 -3
- package/src/update.ts +9 -4
package/package.json
CHANGED
package/src/config.ts
CHANGED
|
@@ -92,7 +92,11 @@ enabled = ["skills.sh"]
|
|
|
92
92
|
# name = "my-org"
|
|
93
93
|
# url = "https://skills.example.com"
|
|
94
94
|
|
|
95
|
-
#
|
|
95
|
+
# Built-in tap: the official skilltap-skills collection.
|
|
96
|
+
# Set to false to opt out of the built-in tap entirely.
|
|
97
|
+
builtin_tap = true
|
|
98
|
+
|
|
99
|
+
# Additional tap definitions (repeatable section)
|
|
96
100
|
# [[taps]]
|
|
97
101
|
# name = "home"
|
|
98
102
|
# url = "https://gitea.example.com/nathan/my-skills-tap"
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import { parseSkillFrontmatter } from "./frontmatter";
|
|
3
|
+
|
|
4
|
+
describe("parseSkillFrontmatter", () => {
|
|
5
|
+
test("parses simple single-line values", () => {
|
|
6
|
+
const content = `---
|
|
7
|
+
name: my-skill
|
|
8
|
+
description: A short description
|
|
9
|
+
license: MIT
|
|
10
|
+
---
|
|
11
|
+
`;
|
|
12
|
+
const result = parseSkillFrontmatter(content);
|
|
13
|
+
expect(result).toEqual({
|
|
14
|
+
name: "my-skill",
|
|
15
|
+
description: "A short description",
|
|
16
|
+
license: "MIT",
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test("returns null when no frontmatter", () => {
|
|
21
|
+
expect(parseSkillFrontmatter("No frontmatter here")).toBeNull();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("coerces boolean values", () => {
|
|
25
|
+
const result = parseSkillFrontmatter("---\nfoo: true\nbar: false\n---\n");
|
|
26
|
+
expect(result?.foo).toBe(true);
|
|
27
|
+
expect(result?.bar).toBe(false);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("coerces numeric values", () => {
|
|
31
|
+
const result = parseSkillFrontmatter("---\ncount: 42\n---\n");
|
|
32
|
+
expect(result?.count).toBe(42);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("parses folded block scalar (>)", () => {
|
|
36
|
+
const content = `---
|
|
37
|
+
name: my-skill
|
|
38
|
+
description: >
|
|
39
|
+
This is a long description
|
|
40
|
+
that spans multiple lines.
|
|
41
|
+
license: MIT
|
|
42
|
+
---
|
|
43
|
+
`;
|
|
44
|
+
const result = parseSkillFrontmatter(content);
|
|
45
|
+
expect(result?.description).toBe("This is a long description that spans multiple lines.");
|
|
46
|
+
expect(result?.license).toBe("MIT");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("parses literal block scalar (|)", () => {
|
|
50
|
+
const content = `---
|
|
51
|
+
name: my-skill
|
|
52
|
+
description: |
|
|
53
|
+
Line one.
|
|
54
|
+
Line two.
|
|
55
|
+
---
|
|
56
|
+
`;
|
|
57
|
+
const result = parseSkillFrontmatter(content);
|
|
58
|
+
expect(result?.description).toBe("Line one.\nLine two.");
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("folded block scalar with single line", () => {
|
|
62
|
+
const content = `---
|
|
63
|
+
description: >
|
|
64
|
+
Single line text.
|
|
65
|
+
---
|
|
66
|
+
`;
|
|
67
|
+
const result = parseSkillFrontmatter(content);
|
|
68
|
+
expect(result?.description).toBe("Single line text.");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("block scalar followed by another key", () => {
|
|
72
|
+
const content = `---
|
|
73
|
+
name: my-skill
|
|
74
|
+
description: >
|
|
75
|
+
Multi-line
|
|
76
|
+
description here.
|
|
77
|
+
license: Apache-2.0
|
|
78
|
+
---
|
|
79
|
+
`;
|
|
80
|
+
const result = parseSkillFrontmatter(content);
|
|
81
|
+
expect(result?.description).toBe("Multi-line description here.");
|
|
82
|
+
expect(result?.license).toBe("Apache-2.0");
|
|
83
|
+
expect(result?.name).toBe("my-skill");
|
|
84
|
+
});
|
|
85
|
+
});
|
package/src/frontmatter.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Parse YAML-style frontmatter between leading --- delimiters.
|
|
3
3
|
* Returns null if no frontmatter found.
|
|
4
|
+
* Supports block scalars: > (folded) and | (literal).
|
|
4
5
|
*/
|
|
5
6
|
export function parseSkillFrontmatter(
|
|
6
7
|
content: string,
|
|
@@ -10,16 +11,34 @@ export function parseSkillFrontmatter(
|
|
|
10
11
|
// biome-ignore lint/style/noNonNullAssertion: match[1] is defined because the regex has a capturing group
|
|
11
12
|
const block = match[1]!;
|
|
12
13
|
const data: Record<string, unknown> = {};
|
|
13
|
-
|
|
14
|
+
const lines = block.split("\n");
|
|
15
|
+
let i = 0;
|
|
16
|
+
while (i < lines.length) {
|
|
17
|
+
const line = lines[i]!;
|
|
14
18
|
const sep = line.indexOf(":");
|
|
15
|
-
if (sep === -1) continue;
|
|
19
|
+
if (sep === -1) { i++; continue; }
|
|
16
20
|
const key = line.slice(0, sep).trim();
|
|
17
|
-
if (!key) continue;
|
|
21
|
+
if (!key) { i++; continue; }
|
|
18
22
|
const raw = line.slice(sep + 1).trim();
|
|
19
|
-
if (raw === "
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
+
if (raw === ">" || raw === "|") {
|
|
24
|
+
const style = raw;
|
|
25
|
+
const parts: string[] = [];
|
|
26
|
+
i++;
|
|
27
|
+
while (i < lines.length) {
|
|
28
|
+
const next = lines[i]!;
|
|
29
|
+
if (next.length > 0 && (next[0] === " " || next[0] === "\t")) {
|
|
30
|
+
parts.push(next.trimStart());
|
|
31
|
+
i++;
|
|
32
|
+
} else break;
|
|
33
|
+
}
|
|
34
|
+
data[key] = style === ">" ? parts.join(" ").trim() : parts.join("\n").trimEnd();
|
|
35
|
+
} else {
|
|
36
|
+
if (raw === "true") data[key] = true;
|
|
37
|
+
else if (raw === "false") data[key] = false;
|
|
38
|
+
else if (raw !== "" && !Number.isNaN(Number(raw))) data[key] = Number(raw);
|
|
39
|
+
else data[key] = raw;
|
|
40
|
+
i++;
|
|
41
|
+
}
|
|
23
42
|
}
|
|
24
43
|
return data;
|
|
25
44
|
}
|
package/src/install.npm.test.ts
CHANGED
|
@@ -439,7 +439,10 @@ describe("updateSkill — npm", () => {
|
|
|
439
439
|
integrity: v2.integrity,
|
|
440
440
|
});
|
|
441
441
|
|
|
442
|
-
const result = await updateSkill(
|
|
442
|
+
const result = await updateSkill(
|
|
443
|
+
{ yes: true },
|
|
444
|
+
async () => ({ tier: "unverified" as const }),
|
|
445
|
+
);
|
|
443
446
|
expect(result.ok).toBe(true);
|
|
444
447
|
if (!result.ok) return;
|
|
445
448
|
|
|
@@ -464,5 +467,5 @@ describe("updateSkill — npm", () => {
|
|
|
464
467
|
await removeTmpDir(tgzDir1);
|
|
465
468
|
await removeTmpDir(tgzDir2);
|
|
466
469
|
}
|
|
467
|
-
});
|
|
470
|
+
}, 30_000);
|
|
468
471
|
});
|
package/src/schemas/config.ts
CHANGED
|
@@ -53,6 +53,8 @@ export const ConfigSchema = z.object({
|
|
|
53
53
|
security: SecurityConfigSchema.prefault({}),
|
|
54
54
|
"agent-mode": AgentModeSchema.prefault({}),
|
|
55
55
|
registry: RegistryConfigSchema,
|
|
56
|
+
/** Whether the built-in skilltap-skills tap is enabled. Set to false to opt out. */
|
|
57
|
+
builtin_tap: z.boolean().default(true),
|
|
56
58
|
taps: z
|
|
57
59
|
.array(
|
|
58
60
|
z.object({
|
package/src/self-update.ts
CHANGED
|
@@ -60,7 +60,7 @@ async function writeCache(configDir: string, latest: string): Promise<void> {
|
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
async function fetchLatestVersion(): Promise<string | null> {
|
|
63
|
+
export async function fetchLatestVersion(): Promise<string | null> {
|
|
64
64
|
try {
|
|
65
65
|
const response = await fetch(
|
|
66
66
|
`https://api.github.com/repos/${GITHUB_OWNER}/${GITHUB_REPO}/releases/latest`,
|
package/src/taps.test.ts
CHANGED
|
@@ -9,7 +9,14 @@ import {
|
|
|
9
9
|
import { loadConfig } from "./config";
|
|
10
10
|
import { installSkill } from "./install";
|
|
11
11
|
import type { TapEntry } from "./taps";
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
addTap,
|
|
14
|
+
loadTaps,
|
|
15
|
+
parseGitHubTapShorthand,
|
|
16
|
+
removeTap,
|
|
17
|
+
searchTaps,
|
|
18
|
+
updateTap,
|
|
19
|
+
} from "./taps";
|
|
13
20
|
|
|
14
21
|
type Env = {
|
|
15
22
|
SKILLTAP_HOME?: string;
|
|
@@ -74,6 +81,61 @@ async function createLocalSkillRepo(
|
|
|
74
81
|
return { path: repoDir, cleanup: () => removeTmpDir(repoDir) };
|
|
75
82
|
}
|
|
76
83
|
|
|
84
|
+
// ─── Unit tests: parseGitHubTapShorthand ───────────────────────────────────
|
|
85
|
+
|
|
86
|
+
describe("parseGitHubTapShorthand", () => {
|
|
87
|
+
test("parses owner/repo", () => {
|
|
88
|
+
expect(parseGitHubTapShorthand("user/my-tap")).toEqual({
|
|
89
|
+
name: "my-tap",
|
|
90
|
+
url: "https://github.com/user/my-tap.git",
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("parses github:owner/repo", () => {
|
|
95
|
+
expect(parseGitHubTapShorthand("github:acme/skills")).toEqual({
|
|
96
|
+
name: "skills",
|
|
97
|
+
url: "https://github.com/acme/skills.git",
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("strips @ref suffix", () => {
|
|
102
|
+
expect(parseGitHubTapShorthand("user/tap@main")).toEqual({
|
|
103
|
+
name: "tap",
|
|
104
|
+
url: "https://github.com/user/tap.git",
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("returns null for bare names", () => {
|
|
109
|
+
expect(parseGitHubTapShorthand("my-tap")).toBeNull();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("returns null for full URLs", () => {
|
|
113
|
+
expect(
|
|
114
|
+
parseGitHubTapShorthand("https://github.com/user/repo.git"),
|
|
115
|
+
).toBeNull();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("returns null for npm: prefix", () => {
|
|
119
|
+
expect(parseGitHubTapShorthand("npm:my-package")).toBeNull();
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("returns null for local paths", () => {
|
|
123
|
+
expect(parseGitHubTapShorthand("./local")).toBeNull();
|
|
124
|
+
expect(parseGitHubTapShorthand("/abs/path")).toBeNull();
|
|
125
|
+
expect(parseGitHubTapShorthand("~/home")).toBeNull();
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("returns null for three-part paths", () => {
|
|
129
|
+
expect(parseGitHubTapShorthand("a/b/c")).toBeNull();
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test("returns null for git@ URLs", () => {
|
|
133
|
+
expect(
|
|
134
|
+
parseGitHubTapShorthand("git@github.com:user/repo.git"),
|
|
135
|
+
).toBeNull();
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
77
139
|
// ─── Unit tests: searchTaps ────────────────────────────────────────────────
|
|
78
140
|
|
|
79
141
|
describe("searchTaps", () => {
|
package/src/taps.ts
CHANGED
|
@@ -11,6 +11,12 @@ import type { Tap, TapSkill } from "./schemas/tap";
|
|
|
11
11
|
import { TapSchema } from "./schemas/tap";
|
|
12
12
|
import { err, type GitError, ok, type Result, UserError } from "./types";
|
|
13
13
|
|
|
14
|
+
/** The built-in tap — always available unless explicitly opted out via config. */
|
|
15
|
+
export const BUILTIN_TAP = {
|
|
16
|
+
name: "skilltap-skills",
|
|
17
|
+
url: "https://github.com/nklisch/skilltap-skills.git",
|
|
18
|
+
} as const;
|
|
19
|
+
|
|
14
20
|
export type TapEntry = { tapName: string; skill: TapSkill };
|
|
15
21
|
|
|
16
22
|
export type UpdateTapResult = {
|
|
@@ -63,6 +69,57 @@ function registrySourceToRepo(source: RegistrySource): string {
|
|
|
63
69
|
}
|
|
64
70
|
}
|
|
65
71
|
|
|
72
|
+
export type GitHubTapShorthand = { name: string; url: string };
|
|
73
|
+
|
|
74
|
+
const GH_LOCAL_PREFIXES = ["./", "/", "~/"];
|
|
75
|
+
const GH_URL_PROTOCOLS = ["https://", "http://", "git@", "ssh://", "npm:"];
|
|
76
|
+
|
|
77
|
+
/** Parse GitHub shorthand (owner/repo) into a tap name + clone URL. Returns null if not shorthand. */
|
|
78
|
+
export function parseGitHubTapShorthand(
|
|
79
|
+
source: string,
|
|
80
|
+
): GitHubTapShorthand | null {
|
|
81
|
+
let s = source;
|
|
82
|
+
if (s.startsWith("github:")) s = s.slice("github:".length);
|
|
83
|
+
else if (!s.includes("/")) return null;
|
|
84
|
+
|
|
85
|
+
if (GH_URL_PROTOCOLS.some((p) => s.startsWith(p))) return null;
|
|
86
|
+
if (GH_LOCAL_PREFIXES.some((p) => s.startsWith(p))) return null;
|
|
87
|
+
|
|
88
|
+
// Strip @ref suffix (taps always clone HEAD)
|
|
89
|
+
const atIdx = s.lastIndexOf("@");
|
|
90
|
+
if (atIdx !== -1) s = s.slice(0, atIdx);
|
|
91
|
+
|
|
92
|
+
const parts = s.split("/").filter(Boolean);
|
|
93
|
+
if (parts.length !== 2) return null;
|
|
94
|
+
|
|
95
|
+
const [owner, repo] = parts;
|
|
96
|
+
return {
|
|
97
|
+
name: repo!,
|
|
98
|
+
url: `https://github.com/${owner}/${repo}.git`,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Returns true if the built-in tap is already cloned locally. */
|
|
103
|
+
export async function isBuiltinTapCloned(): Promise<boolean> {
|
|
104
|
+
return Bun.file(join(tapDir(BUILTIN_TAP.name), "tap.json")).exists();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Ensure the built-in tap is cloned locally. Idempotent — no-op if already present.
|
|
109
|
+
* Returns ok(undefined) whether freshly cloned or already present.
|
|
110
|
+
*/
|
|
111
|
+
export async function ensureBuiltinTap(): Promise<Result<void, UserError | GitError>> {
|
|
112
|
+
const dir = tapDir(BUILTIN_TAP.name);
|
|
113
|
+
const exists = await Bun.file(join(dir, "tap.json")).exists();
|
|
114
|
+
if (exists) return ok(undefined);
|
|
115
|
+
|
|
116
|
+
const gitCheck = await checkGitInstalled();
|
|
117
|
+
if (!gitCheck.ok) return gitCheck;
|
|
118
|
+
|
|
119
|
+
const cloneResult = await clone(BUILTIN_TAP.url, dir, { depth: 1 });
|
|
120
|
+
return cloneResult;
|
|
121
|
+
}
|
|
122
|
+
|
|
66
123
|
export async function addTap(
|
|
67
124
|
name: string,
|
|
68
125
|
url: string,
|
|
@@ -72,6 +129,15 @@ export async function addTap(
|
|
|
72
129
|
if (!configResult.ok) return configResult;
|
|
73
130
|
const config = configResult.value;
|
|
74
131
|
|
|
132
|
+
if (name === BUILTIN_TAP.name && config.builtin_tap !== false) {
|
|
133
|
+
return err(
|
|
134
|
+
new UserError(
|
|
135
|
+
`'${BUILTIN_TAP.name}' is the built-in tap and is already included.`,
|
|
136
|
+
"To disable it, set 'builtin_tap = false' in your config.toml.",
|
|
137
|
+
),
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
75
141
|
if (config.taps.some((t) => t.name === name)) {
|
|
76
142
|
return err(
|
|
77
143
|
new UserError(
|
|
@@ -123,6 +189,23 @@ export async function removeTap(
|
|
|
123
189
|
if (!configResult.ok) return configResult;
|
|
124
190
|
const config = configResult.value;
|
|
125
191
|
|
|
192
|
+
// Special case: disable the built-in tap
|
|
193
|
+
if (name === BUILTIN_TAP.name) {
|
|
194
|
+
if (config.builtin_tap === false) {
|
|
195
|
+
return err(
|
|
196
|
+
new UserError(
|
|
197
|
+
`Built-in tap '${BUILTIN_TAP.name}' is already disabled.`,
|
|
198
|
+
"Set 'builtin_tap = true' in config.toml to re-enable it.",
|
|
199
|
+
),
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
config.builtin_tap = false;
|
|
203
|
+
const saveResult = await saveConfig(config);
|
|
204
|
+
if (!saveResult.ok) return saveResult;
|
|
205
|
+
await rm(tapDir(name), { recursive: true, force: true });
|
|
206
|
+
return ok(undefined);
|
|
207
|
+
}
|
|
208
|
+
|
|
126
209
|
const idx = config.taps.findIndex((t) => t.name === name);
|
|
127
210
|
if (idx === -1) {
|
|
128
211
|
return err(
|
|
@@ -152,6 +235,46 @@ export async function updateTap(
|
|
|
152
235
|
if (!configResult.ok) return configResult;
|
|
153
236
|
const config = configResult.value;
|
|
154
237
|
|
|
238
|
+
const updated: Record<string, number> = {};
|
|
239
|
+
const http: string[] = [];
|
|
240
|
+
|
|
241
|
+
// Handle built-in tap: update if enabled and requested
|
|
242
|
+
if (name === BUILTIN_TAP.name) {
|
|
243
|
+
if (config.builtin_tap === false) {
|
|
244
|
+
return err(
|
|
245
|
+
new UserError(
|
|
246
|
+
`Tap '${BUILTIN_TAP.name}' is not configured.`,
|
|
247
|
+
"Set 'builtin_tap = true' in config.toml to re-enable it.",
|
|
248
|
+
),
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
const dir = tapDir(BUILTIN_TAP.name);
|
|
252
|
+
if (!(await Bun.file(join(dir, "tap.json")).exists())) {
|
|
253
|
+
return err(
|
|
254
|
+
new UserError(
|
|
255
|
+
`Built-in tap '${BUILTIN_TAP.name}' is not yet cloned.`,
|
|
256
|
+
"Run 'skilltap tap install' to set it up.",
|
|
257
|
+
),
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
const pullResult = await pull(dir);
|
|
261
|
+
if (!pullResult.ok) return pullResult;
|
|
262
|
+
const tapResult = await loadTapJson(dir, BUILTIN_TAP.name);
|
|
263
|
+
updated[BUILTIN_TAP.name] = tapResult.ok ? tapResult.value.skills.length : 0;
|
|
264
|
+
return ok({ updated, http });
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Update all: include built-in tap if enabled and cloned
|
|
268
|
+
if (!name && config.builtin_tap !== false) {
|
|
269
|
+
const dir = tapDir(BUILTIN_TAP.name);
|
|
270
|
+
if (await Bun.file(join(dir, "tap.json")).exists()) {
|
|
271
|
+
const pullResult = await pull(dir);
|
|
272
|
+
if (!pullResult.ok) return pullResult;
|
|
273
|
+
const tapResult = await loadTapJson(dir, BUILTIN_TAP.name);
|
|
274
|
+
updated[BUILTIN_TAP.name] = tapResult.ok ? tapResult.value.skills.length : 0;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
155
278
|
const targets = name
|
|
156
279
|
? config.taps.filter((t) => t.name === name)
|
|
157
280
|
: config.taps;
|
|
@@ -165,9 +288,6 @@ export async function updateTap(
|
|
|
165
288
|
);
|
|
166
289
|
}
|
|
167
290
|
|
|
168
|
-
const updated: Record<string, number> = {};
|
|
169
|
-
const http: string[] = [];
|
|
170
|
-
|
|
171
291
|
for (const tap of targets) {
|
|
172
292
|
if (tap.type === "http") {
|
|
173
293
|
http.push(tap.name);
|
|
@@ -193,6 +313,17 @@ export async function loadTaps(): Promise<Result<TapEntry[], UserError>> {
|
|
|
193
313
|
|
|
194
314
|
const entries: TapEntry[] = [];
|
|
195
315
|
|
|
316
|
+
// Load built-in tap first (if enabled and already cloned)
|
|
317
|
+
if (config.builtin_tap !== false) {
|
|
318
|
+
const dir = tapDir(BUILTIN_TAP.name);
|
|
319
|
+
const tapResult = await loadTapJson(dir, BUILTIN_TAP.name);
|
|
320
|
+
if (tapResult.ok) {
|
|
321
|
+
for (const skill of tapResult.value.skills) {
|
|
322
|
+
entries.push({ tapName: BUILTIN_TAP.name, skill });
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
196
327
|
for (const tap of config.taps) {
|
|
197
328
|
if (tap.type === "http") {
|
|
198
329
|
// HTTP registry: fetch skills from API
|
package/src/update.ts
CHANGED
|
@@ -22,6 +22,8 @@ import { scanSemantic } from "./security/semantic";
|
|
|
22
22
|
import { wrapShell } from "./shell";
|
|
23
23
|
import { createAgentSymlinks, removeAgentSymlinks } from "./symlink";
|
|
24
24
|
import { parseGitHubRepo, resolveTrust } from "./trust";
|
|
25
|
+
|
|
26
|
+
type ResolveTrustFn = typeof resolveTrust;
|
|
25
27
|
import type { Result } from "./types";
|
|
26
28
|
import {
|
|
27
29
|
err,
|
|
@@ -172,6 +174,7 @@ async function updateNpmSkill(
|
|
|
172
174
|
installed: { skills: InstalledSkill[] },
|
|
173
175
|
options: UpdateOptions,
|
|
174
176
|
result: UpdateResult,
|
|
177
|
+
_resolveTrust: ResolveTrustFn,
|
|
175
178
|
): Promise<Result<void, UserError | NetworkError | ScanError>> {
|
|
176
179
|
// biome-ignore lint/style/noNonNullAssertion: caller checks record.repo?.startsWith("npm:")
|
|
177
180
|
const { name: packageName } = parseNpmSource(record.repo!);
|
|
@@ -262,7 +265,7 @@ async function updateNpmSkill(
|
|
|
262
265
|
await refreshAgentSymlinks(record, options.projectRoot);
|
|
263
266
|
|
|
264
267
|
// Re-verify trust for the new version
|
|
265
|
-
const newTrust = await
|
|
268
|
+
const newTrust = await _resolveTrust({
|
|
266
269
|
adapter: "npm",
|
|
267
270
|
url: record.repo!,
|
|
268
271
|
tap: record.tap,
|
|
@@ -294,6 +297,7 @@ async function updateGitSkill(
|
|
|
294
297
|
installed: { skills: InstalledSkill[] },
|
|
295
298
|
options: UpdateOptions,
|
|
296
299
|
result: UpdateResult,
|
|
300
|
+
_resolveTrust: ResolveTrustFn,
|
|
297
301
|
): Promise<Result<void, UserError | GitError | ScanError>> {
|
|
298
302
|
// Standalone: work dir is the install path. Multi-skill: work dir is the cache.
|
|
299
303
|
const isMulti = record.path !== null;
|
|
@@ -388,7 +392,7 @@ async function updateGitSkill(
|
|
|
388
392
|
if (!newShaResult.ok) return newShaResult;
|
|
389
393
|
|
|
390
394
|
// Re-verify trust for the updated skill
|
|
391
|
-
const newTrust = await
|
|
395
|
+
const newTrust = await _resolveTrust({
|
|
392
396
|
adapter: "git",
|
|
393
397
|
url: record.repo ?? "",
|
|
394
398
|
tap: record.tap,
|
|
@@ -412,6 +416,7 @@ async function updateGitSkill(
|
|
|
412
416
|
|
|
413
417
|
export async function updateSkill(
|
|
414
418
|
options: UpdateOptions = {},
|
|
419
|
+
_resolveTrust: ResolveTrustFn = resolveTrust,
|
|
415
420
|
): Promise<Result<UpdateResult, UserError | GitError | ScanError | NetworkError>> {
|
|
416
421
|
debug("updateSkill", { name: options.name ?? "all" });
|
|
417
422
|
const installedResult = await loadInstalled();
|
|
@@ -444,10 +449,10 @@ export async function updateSkill(
|
|
|
444
449
|
|
|
445
450
|
// Dispatch to adapter-specific update handler
|
|
446
451
|
if (record.repo?.startsWith("npm:")) {
|
|
447
|
-
const npmResult = await updateNpmSkill(record, installed, options, result);
|
|
452
|
+
const npmResult = await updateNpmSkill(record, installed, options, result, _resolveTrust);
|
|
448
453
|
if (!npmResult.ok) return npmResult;
|
|
449
454
|
} else {
|
|
450
|
-
const gitResult = await updateGitSkill(record, installed, options, result);
|
|
455
|
+
const gitResult = await updateGitSkill(record, installed, options, result, _resolveTrust);
|
|
451
456
|
if (!gitResult.ok) return gitResult;
|
|
452
457
|
}
|
|
453
458
|
}
|