skillrepo 4.0.0 → 4.2.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 +49 -2
- package/bin/skillrepo.mjs +8 -0
- package/package.json +10 -4
- package/src/commands/init-cohort-hooks.mjs +127 -0
- package/src/commands/init.mjs +45 -6
- package/src/commands/push.mjs +187 -0
- package/src/commands/uninstall.mjs +12 -1
- package/src/commands/update.mjs +97 -16
- package/src/lib/agent-hook-merge.mjs +203 -0
- package/src/lib/agent-registry.mjs +186 -2
- package/src/lib/artifact-registry.mjs +111 -2
- package/src/lib/fs-utils.mjs +16 -1
- package/src/lib/http.mjs +169 -11
- package/src/lib/mergers/agent-hook-claude-shape.mjs +519 -0
- package/src/lib/mergers/agent-hook-cursor-shape.mjs +318 -0
- package/src/lib/removers/agent-hooks.mjs +83 -0
- package/src/lib/skill-walk.mjs +97 -0
- package/src/test/commands/init.test.mjs +281 -0
- package/src/test/commands/push.test.mjs +289 -0
- package/src/test/commands/update.test.mjs +135 -0
- package/src/test/dispatcher.test.mjs +10 -2
- package/src/test/e2e/cli-cohort-hooks.test.mjs +393 -0
- package/src/test/e2e/mock-server.mjs +92 -10
- package/src/test/integration/agent-hooks.integration.test.mjs +340 -0
- package/src/test/lib/agent-hook-merge.test.mjs +172 -0
- package/src/test/lib/artifact-registry.test.mjs +39 -0
- package/src/test/lib/http.test.mjs +242 -1
- package/src/test/lib/skill-walk.test.mjs +127 -0
- package/src/test/mergers/agent-hook-claude-shape.test.mjs +518 -0
- package/src/test/mergers/agent-hook-cursor-shape.test.mjs +306 -0
- package/src/test/removers/agent-hooks.test.mjs +206 -0
|
@@ -81,6 +81,27 @@ const MERGER_EXPECTED = Object.freeze({
|
|
|
81
81
|
// know to look in the right place. Round-trip contracts are
|
|
82
82
|
// verified in src/test/mergers/session-hook.test.mjs.
|
|
83
83
|
"session-hook": ["settings-session-hook", "settings-session-hook-global"],
|
|
84
|
+
// Cohort SessionStart-hook installers added by #1240. Two installer
|
|
85
|
+
// modules — one per schema variant — both writing entries whose
|
|
86
|
+
// `command` field equals AGENT_HOOK_COMMAND so the shared
|
|
87
|
+
// `removers/agent-hooks.mjs` batch remover finds them via
|
|
88
|
+
// AGENT_HOOK_FINGERPRINT.
|
|
89
|
+
//
|
|
90
|
+
// - claude-shape: nested `hooks.<Event>[i].hooks[j]` entries.
|
|
91
|
+
// Used by Gemini CLI, Codex CLI, and VS Code + Copilot.
|
|
92
|
+
// - cursor-shape: flat `hooks.<event>[i]` entries with a
|
|
93
|
+
// root-level `version: 1`. Used by Cursor.
|
|
94
|
+
//
|
|
95
|
+
// Each writes one descriptor id per vendor it installs for; the
|
|
96
|
+
// dispatcher (`agent-hook-merge.mjs`) routes one vendor key per
|
|
97
|
+
// call but a single shape merger may be invoked for multiple
|
|
98
|
+
// vendors over the course of an init run.
|
|
99
|
+
"agent-hook-claude-shape": [
|
|
100
|
+
"agent-hook-gemini",
|
|
101
|
+
"agent-hook-codex",
|
|
102
|
+
"agent-hook-copilot",
|
|
103
|
+
],
|
|
104
|
+
"agent-hook-cursor-shape": ["agent-hook-cursor"],
|
|
84
105
|
});
|
|
85
106
|
|
|
86
107
|
/**
|
|
@@ -100,6 +121,17 @@ const REMOVER_EXPECTED = Object.freeze({
|
|
|
100
121
|
// local and global variants go through the same walk with the
|
|
101
122
|
// settings remover's `{ global }` option.
|
|
102
123
|
settings: ["settings-session-hook", "settings-session-hook-global"],
|
|
124
|
+
// Cohort SessionStart-hook batch remover added by #1240. One file
|
|
125
|
+
// covers all four cohort vendors — the dispatcher routes by
|
|
126
|
+
// `vendorKey` to the right shape-specific walker. The four
|
|
127
|
+
// descriptor ids are derived from AGENT_REGISTRY (one per vendor
|
|
128
|
+
// with non-null `agentHook`).
|
|
129
|
+
"agent-hooks": [
|
|
130
|
+
"agent-hook-cursor",
|
|
131
|
+
"agent-hook-gemini",
|
|
132
|
+
"agent-hook-codex",
|
|
133
|
+
"agent-hook-copilot",
|
|
134
|
+
],
|
|
103
135
|
});
|
|
104
136
|
|
|
105
137
|
/**
|
|
@@ -250,12 +282,19 @@ describe("artifact-registry: drift enforcement", () => {
|
|
|
250
282
|
// Catches a descriptor that uses a `kind` the dispatch table
|
|
251
283
|
// doesn't understand — would produce a runtime "no remover
|
|
252
284
|
// bound" error at uninstall time rather than a clean test failure.
|
|
285
|
+
//
|
|
286
|
+
// `agent-hook` was added in #1240 alongside the cohort
|
|
287
|
+
// SessionStart hook framework. Its dispatch lives in
|
|
288
|
+
// `commands/uninstall.mjs:runForDescriptor` (kind branch above
|
|
289
|
+
// the FILE_REMOVERS lookup) and routes to
|
|
290
|
+
// `removers/agent-hooks.mjs:removeAgentHookArtifact`.
|
|
253
291
|
const VALID_KINDS = new Set([
|
|
254
292
|
"json-key",
|
|
255
293
|
"json-input",
|
|
256
294
|
"line",
|
|
257
295
|
"section",
|
|
258
296
|
"directory",
|
|
297
|
+
"agent-hook",
|
|
259
298
|
]);
|
|
260
299
|
for (const d of ARTIFACT_REGISTRY) {
|
|
261
300
|
assert.ok(
|
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
searchSkills,
|
|
40
40
|
addSkillToLibrary,
|
|
41
41
|
removeSkillFromLibrary,
|
|
42
|
+
pushSkill,
|
|
42
43
|
isCliSource,
|
|
43
44
|
CLI_SOURCE_VALUES,
|
|
44
45
|
} from "../../lib/http.mjs";
|
|
@@ -795,7 +796,7 @@ describe("addSkillToLibrary", () => {
|
|
|
795
796
|
it("returns { status: 'added' } on 201", async () => {
|
|
796
797
|
const srv = await startServer((req, res) => {
|
|
797
798
|
assert.equal(req.method, "POST");
|
|
798
|
-
assert.match(req.url, /^\/api\/v1\/library$/);
|
|
799
|
+
assert.match(req.url, /^\/api\/v1\/library\/refs$/);
|
|
799
800
|
assert.equal(req.headers["content-type"], "application/json");
|
|
800
801
|
jsonRes(res, 201, {
|
|
801
802
|
added: {
|
|
@@ -988,6 +989,246 @@ describe("addSkillToLibrary", () => {
|
|
|
988
989
|
});
|
|
989
990
|
});
|
|
990
991
|
|
|
992
|
+
// ── pushSkill (#1455 multipart upsert) ─────────────────────────────────
|
|
993
|
+
|
|
994
|
+
describe("pushSkill", () => {
|
|
995
|
+
const SKILL_MD = "---\nname: my-skill\ndescription: t\n---\n\nbody\n";
|
|
996
|
+
|
|
997
|
+
/**
|
|
998
|
+
* Helper: read the request's Content-Type and verify it's multipart;
|
|
999
|
+
* accept the body without parsing. Tests assert on the response shape
|
|
1000
|
+
* the helper returns and on headers / URL.
|
|
1001
|
+
*/
|
|
1002
|
+
function makePushServer(handler) {
|
|
1003
|
+
return startServer(handler);
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
it("hits POST /api/v1/library with multipart Content-Type", async () => {
|
|
1007
|
+
const srv = await makePushServer((req, res) => {
|
|
1008
|
+
assert.equal(req.method, "POST");
|
|
1009
|
+
assert.match(req.url, /^\/api\/v1\/library$/);
|
|
1010
|
+
assert.match(req.headers["content-type"] ?? "", /multipart\/form-data/);
|
|
1011
|
+
jsonRes(res, 201, {
|
|
1012
|
+
action: "created",
|
|
1013
|
+
bump: null,
|
|
1014
|
+
skill: { owner: "mock", name: "my-skill", version: "1.0" },
|
|
1015
|
+
});
|
|
1016
|
+
});
|
|
1017
|
+
try {
|
|
1018
|
+
await pushSkill(srv.url, VALID_KEY, {
|
|
1019
|
+
files: [{ relativePath: "SKILL.md", content: SKILL_MD }],
|
|
1020
|
+
});
|
|
1021
|
+
} finally {
|
|
1022
|
+
await srv.close();
|
|
1023
|
+
}
|
|
1024
|
+
});
|
|
1025
|
+
|
|
1026
|
+
it("sends an Idempotency-Key header on every request (auto-generated)", async () => {
|
|
1027
|
+
let observedKey;
|
|
1028
|
+
const srv = await makePushServer((req, res) => {
|
|
1029
|
+
observedKey = req.headers["idempotency-key"];
|
|
1030
|
+
jsonRes(res, 201, {
|
|
1031
|
+
action: "created",
|
|
1032
|
+
bump: null,
|
|
1033
|
+
skill: { owner: "mock", name: "my-skill", version: "1.0" },
|
|
1034
|
+
});
|
|
1035
|
+
});
|
|
1036
|
+
try {
|
|
1037
|
+
await pushSkill(srv.url, VALID_KEY, {
|
|
1038
|
+
files: [{ relativePath: "SKILL.md", content: SKILL_MD }],
|
|
1039
|
+
});
|
|
1040
|
+
assert.ok(observedKey, "Idempotency-Key header should be present");
|
|
1041
|
+
assert.ok(observedKey.length >= 16, "Auto-generated key should be reasonably long");
|
|
1042
|
+
} finally {
|
|
1043
|
+
await srv.close();
|
|
1044
|
+
}
|
|
1045
|
+
});
|
|
1046
|
+
|
|
1047
|
+
it("threads an explicit idempotencyKey through to the header", async () => {
|
|
1048
|
+
let observedKey;
|
|
1049
|
+
const srv = await makePushServer((req, res) => {
|
|
1050
|
+
observedKey = req.headers["idempotency-key"];
|
|
1051
|
+
jsonRes(res, 201, {
|
|
1052
|
+
action: "created",
|
|
1053
|
+
bump: null,
|
|
1054
|
+
skill: { owner: "mock", name: "my-skill", version: "1.0" },
|
|
1055
|
+
});
|
|
1056
|
+
});
|
|
1057
|
+
try {
|
|
1058
|
+
await pushSkill(srv.url, VALID_KEY, {
|
|
1059
|
+
files: [{ relativePath: "SKILL.md", content: SKILL_MD }],
|
|
1060
|
+
idempotencyKey: "custom-key-abc",
|
|
1061
|
+
});
|
|
1062
|
+
assert.equal(observedKey, "custom-key-abc");
|
|
1063
|
+
} finally {
|
|
1064
|
+
await srv.close();
|
|
1065
|
+
}
|
|
1066
|
+
});
|
|
1067
|
+
|
|
1068
|
+
it("returns { action: 'created', bump: null, skill } on 201", async () => {
|
|
1069
|
+
const srv = await makePushServer((req, res) => {
|
|
1070
|
+
jsonRes(res, 201, {
|
|
1071
|
+
action: "created",
|
|
1072
|
+
bump: null,
|
|
1073
|
+
skill: { owner: "mock", name: "my-skill", version: "1.0" },
|
|
1074
|
+
});
|
|
1075
|
+
});
|
|
1076
|
+
try {
|
|
1077
|
+
const result = await pushSkill(srv.url, VALID_KEY, {
|
|
1078
|
+
files: [{ relativePath: "SKILL.md", content: SKILL_MD }],
|
|
1079
|
+
});
|
|
1080
|
+
assert.equal(result.action, "created");
|
|
1081
|
+
assert.equal(result.bump, null);
|
|
1082
|
+
assert.equal(result.skill.version, "1.0");
|
|
1083
|
+
} finally {
|
|
1084
|
+
await srv.close();
|
|
1085
|
+
}
|
|
1086
|
+
});
|
|
1087
|
+
|
|
1088
|
+
it("returns { action: 'updated', bump: 'minor' | 'major' } on 200 update", async () => {
|
|
1089
|
+
const srv = await makePushServer((req, res) => {
|
|
1090
|
+
jsonRes(res, 200, {
|
|
1091
|
+
action: "updated",
|
|
1092
|
+
bump: "minor",
|
|
1093
|
+
skill: { owner: "mock", name: "my-skill", version: "1.1" },
|
|
1094
|
+
});
|
|
1095
|
+
});
|
|
1096
|
+
try {
|
|
1097
|
+
const result = await pushSkill(srv.url, VALID_KEY, {
|
|
1098
|
+
files: [{ relativePath: "SKILL.md", content: SKILL_MD }],
|
|
1099
|
+
});
|
|
1100
|
+
assert.equal(result.action, "updated");
|
|
1101
|
+
assert.equal(result.bump, "minor");
|
|
1102
|
+
assert.equal(result.skill.version, "1.1");
|
|
1103
|
+
} finally {
|
|
1104
|
+
await srv.close();
|
|
1105
|
+
}
|
|
1106
|
+
});
|
|
1107
|
+
|
|
1108
|
+
it("returns { action: 'unchanged', bump: null } on 200 no-op", async () => {
|
|
1109
|
+
const srv = await makePushServer((req, res) => {
|
|
1110
|
+
jsonRes(res, 200, {
|
|
1111
|
+
action: "unchanged",
|
|
1112
|
+
bump: null,
|
|
1113
|
+
skill: { owner: "mock", name: "my-skill", version: "1.0" },
|
|
1114
|
+
});
|
|
1115
|
+
});
|
|
1116
|
+
try {
|
|
1117
|
+
const result = await pushSkill(srv.url, VALID_KEY, {
|
|
1118
|
+
files: [{ relativePath: "SKILL.md", content: SKILL_MD }],
|
|
1119
|
+
});
|
|
1120
|
+
assert.equal(result.action, "unchanged");
|
|
1121
|
+
assert.equal(result.bump, null);
|
|
1122
|
+
} finally {
|
|
1123
|
+
await srv.close();
|
|
1124
|
+
}
|
|
1125
|
+
});
|
|
1126
|
+
|
|
1127
|
+
it("throws networkError on 2xx with unknown `action` (server contract violation)", async () => {
|
|
1128
|
+
const srv = await makePushServer((req, res) => {
|
|
1129
|
+
jsonRes(res, 200, {
|
|
1130
|
+
action: "weird",
|
|
1131
|
+
skill: { owner: "x", name: "x", version: "1.0" },
|
|
1132
|
+
});
|
|
1133
|
+
});
|
|
1134
|
+
try {
|
|
1135
|
+
await assert.rejects(
|
|
1136
|
+
() =>
|
|
1137
|
+
pushSkill(srv.url, VALID_KEY, {
|
|
1138
|
+
files: [{ relativePath: "SKILL.md", content: SKILL_MD }],
|
|
1139
|
+
}),
|
|
1140
|
+
(err) =>
|
|
1141
|
+
err instanceof CliError && err.exitCode === EXIT_NETWORK,
|
|
1142
|
+
);
|
|
1143
|
+
} finally {
|
|
1144
|
+
await srv.close();
|
|
1145
|
+
}
|
|
1146
|
+
});
|
|
1147
|
+
|
|
1148
|
+
it("maps 403 plan_limit through mapErrorResponse to validationError", async () => {
|
|
1149
|
+
const srv = await makePushServer((req, res) => {
|
|
1150
|
+
jsonRes(res, 403, {
|
|
1151
|
+
error: "Plan limit reached",
|
|
1152
|
+
code: "plan_limit",
|
|
1153
|
+
});
|
|
1154
|
+
});
|
|
1155
|
+
try {
|
|
1156
|
+
await assert.rejects(
|
|
1157
|
+
() =>
|
|
1158
|
+
pushSkill(srv.url, VALID_KEY, {
|
|
1159
|
+
files: [{ relativePath: "SKILL.md", content: SKILL_MD }],
|
|
1160
|
+
}),
|
|
1161
|
+
(err) =>
|
|
1162
|
+
err instanceof CliError && err.exitCode === EXIT_VALIDATION,
|
|
1163
|
+
);
|
|
1164
|
+
} finally {
|
|
1165
|
+
await srv.close();
|
|
1166
|
+
}
|
|
1167
|
+
});
|
|
1168
|
+
|
|
1169
|
+
it("maps 413 payload_too_large through mapErrorResponse", async () => {
|
|
1170
|
+
const srv = await makePushServer((req, res) => {
|
|
1171
|
+
jsonRes(res, 413, {
|
|
1172
|
+
error: "Payload exceeds limit",
|
|
1173
|
+
code: "payload_too_large",
|
|
1174
|
+
});
|
|
1175
|
+
});
|
|
1176
|
+
try {
|
|
1177
|
+
await assert.rejects(
|
|
1178
|
+
() =>
|
|
1179
|
+
pushSkill(srv.url, VALID_KEY, {
|
|
1180
|
+
files: [{ relativePath: "SKILL.md", content: SKILL_MD }],
|
|
1181
|
+
}),
|
|
1182
|
+
(err) => err instanceof CliError,
|
|
1183
|
+
);
|
|
1184
|
+
} finally {
|
|
1185
|
+
await srv.close();
|
|
1186
|
+
}
|
|
1187
|
+
});
|
|
1188
|
+
|
|
1189
|
+
it("retries on transient 5xx (Idempotency-Key makes retry safe)", async () => {
|
|
1190
|
+
let calls = 0;
|
|
1191
|
+
const srv = await makePushServer((req, res) => {
|
|
1192
|
+
calls++;
|
|
1193
|
+
if (calls === 1) {
|
|
1194
|
+
jsonRes(res, 503, { error: "transient" });
|
|
1195
|
+
return;
|
|
1196
|
+
}
|
|
1197
|
+
jsonRes(res, 201, {
|
|
1198
|
+
action: "created",
|
|
1199
|
+
bump: null,
|
|
1200
|
+
skill: { owner: "mock", name: "my-skill", version: "1.0" },
|
|
1201
|
+
});
|
|
1202
|
+
});
|
|
1203
|
+
try {
|
|
1204
|
+
const result = await pushSkill(srv.url, VALID_KEY, {
|
|
1205
|
+
files: [{ relativePath: "SKILL.md", content: SKILL_MD }],
|
|
1206
|
+
});
|
|
1207
|
+
assert.equal(result.action, "created");
|
|
1208
|
+
assert.ok(calls >= 2, "Should have retried at least once");
|
|
1209
|
+
} finally {
|
|
1210
|
+
await srv.close();
|
|
1211
|
+
}
|
|
1212
|
+
});
|
|
1213
|
+
|
|
1214
|
+
it("guards against empty serverUrl / apiKey via the cross-cutting check", async () => {
|
|
1215
|
+
await assert.rejects(
|
|
1216
|
+
() =>
|
|
1217
|
+
pushSkill("", VALID_KEY, {
|
|
1218
|
+
files: [{ relativePath: "SKILL.md", content: SKILL_MD }],
|
|
1219
|
+
}),
|
|
1220
|
+
(err) => err instanceof CliError && err.exitCode === EXIT_VALIDATION,
|
|
1221
|
+
);
|
|
1222
|
+
await assert.rejects(
|
|
1223
|
+
() =>
|
|
1224
|
+
pushSkill("http://x", "", {
|
|
1225
|
+
files: [{ relativePath: "SKILL.md", content: SKILL_MD }],
|
|
1226
|
+
}),
|
|
1227
|
+
(err) => err instanceof CliError && err.exitCode === EXIT_AUTH,
|
|
1228
|
+
);
|
|
1229
|
+
});
|
|
1230
|
+
});
|
|
1231
|
+
|
|
991
1232
|
// ── removeSkillFromLibrary (PR3a) ──────────────────────────────────────
|
|
992
1233
|
|
|
993
1234
|
describe("removeSkillFromLibrary", () => {
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for `walkSkillFiles` (#1455).
|
|
3
|
+
*
|
|
4
|
+
* Verifies the walker's exclusion + path-normalization rules:
|
|
5
|
+
* - Hidden files / dirs (anything starting with `.`) are excluded
|
|
6
|
+
* - `node_modules` directories are excluded
|
|
7
|
+
* - Root `SKILL.md` is INCLUDED (uploaded uniformly with other files
|
|
8
|
+
* per the agentskills.io spec)
|
|
9
|
+
* - Returned paths are POSIX-style + sorted
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { describe, it, beforeEach, afterEach } from "node:test";
|
|
13
|
+
import assert from "node:assert/strict";
|
|
14
|
+
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
import { tmpdir } from "node:os";
|
|
17
|
+
|
|
18
|
+
import { walkSkillFiles } from "../../lib/skill-walk.mjs";
|
|
19
|
+
|
|
20
|
+
let sandbox;
|
|
21
|
+
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
sandbox = mkdtempSync(join(tmpdir(), "skillrepo-walk-"));
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
afterEach(() => {
|
|
27
|
+
rmSync(sandbox, { recursive: true, force: true });
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
function file(rel, content = "x") {
|
|
31
|
+
const abs = join(sandbox, rel);
|
|
32
|
+
mkdirSync(join(abs, ".."), { recursive: true });
|
|
33
|
+
writeFileSync(abs, content);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
describe("walkSkillFiles", () => {
|
|
37
|
+
it("returns all skill files in deterministic (sorted) order", async () => {
|
|
38
|
+
file("SKILL.md", "---\nname: x\n---\n");
|
|
39
|
+
file("references/intro.md");
|
|
40
|
+
file("scripts/run.sh");
|
|
41
|
+
file("assets/logo.png");
|
|
42
|
+
|
|
43
|
+
const walked = await walkSkillFiles(sandbox);
|
|
44
|
+
const paths = walked.map((f) => f.relativePath);
|
|
45
|
+
// `localeCompare` sorts uppercase after lowercase, so SKILL.md
|
|
46
|
+
// lands last among entries with no subdir prefix.
|
|
47
|
+
assert.deepEqual(paths, [
|
|
48
|
+
"assets/logo.png",
|
|
49
|
+
"references/intro.md",
|
|
50
|
+
"scripts/run.sh",
|
|
51
|
+
"SKILL.md",
|
|
52
|
+
]);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("includes root SKILL.md as a regular file (agentskills.io spec)", async () => {
|
|
56
|
+
file("SKILL.md", "---\nname: x\n---\n");
|
|
57
|
+
file("references/a.md");
|
|
58
|
+
|
|
59
|
+
const walked = await walkSkillFiles(sandbox);
|
|
60
|
+
const paths = walked.map((f) => f.relativePath);
|
|
61
|
+
assert.ok(paths.includes("SKILL.md"));
|
|
62
|
+
assert.deepEqual(paths, ["references/a.md", "SKILL.md"]);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("excludes hidden files and dirs at every depth", async () => {
|
|
66
|
+
file("SKILL.md");
|
|
67
|
+
file(".env");
|
|
68
|
+
file(".git/HEAD");
|
|
69
|
+
file(".vscode/settings.json");
|
|
70
|
+
file("references/.DS_Store");
|
|
71
|
+
file("references/.hidden/secret");
|
|
72
|
+
file("references/foo.md");
|
|
73
|
+
|
|
74
|
+
const walked = await walkSkillFiles(sandbox);
|
|
75
|
+
const paths = walked.map((f) => f.relativePath);
|
|
76
|
+
assert.deepEqual(paths, ["references/foo.md", "SKILL.md"]);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("excludes node_modules directories", async () => {
|
|
80
|
+
file("SKILL.md");
|
|
81
|
+
file("node_modules/foo/index.js");
|
|
82
|
+
file("scripts/node_modules/bar/index.js");
|
|
83
|
+
file("references/intro.md");
|
|
84
|
+
|
|
85
|
+
const walked = await walkSkillFiles(sandbox);
|
|
86
|
+
const paths = walked.map((f) => f.relativePath);
|
|
87
|
+
assert.deepEqual(paths, ["references/intro.md", "SKILL.md"]);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("uses POSIX forward-slash paths even on Windows-style separators", async () => {
|
|
91
|
+
file("SKILL.md");
|
|
92
|
+
file("references/nested/deep.md");
|
|
93
|
+
|
|
94
|
+
const walked = await walkSkillFiles(sandbox);
|
|
95
|
+
const paths = walked.map((f) => f.relativePath);
|
|
96
|
+
for (const p of paths) {
|
|
97
|
+
assert.ok(!p.includes("\\"), `Path ${p} contains backslash`);
|
|
98
|
+
}
|
|
99
|
+
assert.ok(paths.includes("SKILL.md"));
|
|
100
|
+
assert.ok(paths.includes("references/nested/deep.md"));
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("returns just SKILL.md when the directory has only SKILL.md + hidden files", async () => {
|
|
104
|
+
file("SKILL.md");
|
|
105
|
+
file(".env");
|
|
106
|
+
file(".git/HEAD");
|
|
107
|
+
|
|
108
|
+
const walked = await walkSkillFiles(sandbox);
|
|
109
|
+
const paths = walked.map((f) => f.relativePath);
|
|
110
|
+
assert.deepEqual(paths, ["SKILL.md"]);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("returns absolute paths + sizes for each file", async () => {
|
|
114
|
+
file("SKILL.md", "ab");
|
|
115
|
+
file("references/a.md", "abc");
|
|
116
|
+
|
|
117
|
+
const walked = await walkSkillFiles(sandbox);
|
|
118
|
+
assert.equal(walked.length, 2);
|
|
119
|
+
const refEntry = walked.find((f) => f.relativePath === "references/a.md");
|
|
120
|
+
assert.ok(refEntry);
|
|
121
|
+
assert.ok(refEntry.absolutePath.startsWith(sandbox));
|
|
122
|
+
assert.equal(refEntry.size, 3);
|
|
123
|
+
const skillEntry = walked.find((f) => f.relativePath === "SKILL.md");
|
|
124
|
+
assert.ok(skillEntry);
|
|
125
|
+
assert.equal(skillEntry.size, 2);
|
|
126
|
+
});
|
|
127
|
+
});
|