skillrepo 2.0.0 → 3.0.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 +215 -150
- package/bin/skillrepo.mjs +210 -36
- package/package.json +6 -3
- package/src/commands/add.mjs +176 -0
- package/src/commands/get.mjs +116 -0
- package/src/commands/init.mjs +471 -143
- package/src/commands/list.mjs +176 -0
- package/src/commands/remove.mjs +167 -0
- package/src/commands/search.mjs +188 -0
- package/src/commands/update.mjs +67 -0
- package/src/lib/cli-config.mjs +230 -0
- package/src/lib/config.mjs +238 -0
- package/src/lib/detect-ides.mjs +0 -19
- package/src/lib/errors.mjs +264 -0
- package/src/lib/file-write.mjs +705 -0
- package/src/lib/http.mjs +817 -37
- package/src/lib/identifier.mjs +153 -0
- package/src/lib/mcp-merge.mjs +275 -0
- package/src/lib/mergers/gitignore.mjs +73 -18
- package/src/lib/paths.mjs +46 -17
- package/src/lib/prompt.mjs +11 -44
- package/src/lib/sync.mjs +305 -0
- package/src/test/commands/add.test.mjs +285 -0
- package/src/test/commands/get.test.mjs +176 -0
- package/src/test/commands/init.test.mjs +486 -0
- package/src/test/commands/list.test.mjs +172 -0
- package/src/test/commands/remove.test.mjs +234 -0
- package/src/test/commands/search.test.mjs +204 -0
- package/src/test/commands/update.test.mjs +164 -0
- package/src/test/detect-ides.test.mjs +9 -14
- package/src/test/dispatcher.test.mjs +224 -0
- package/src/test/e2e/cli-commands.test.mjs +576 -0
- package/src/test/e2e/mock-server.mjs +364 -22
- package/src/test/helpers/capture-stream.mjs +48 -0
- package/src/test/integration/file-write.integration.test.mjs +279 -0
- package/src/test/lib/cli-config.test.mjs +407 -0
- package/src/test/lib/config.test.mjs +257 -0
- package/src/test/lib/errors.test.mjs +359 -0
- package/src/test/lib/file-write.test.mjs +784 -0
- package/src/test/lib/http.test.mjs +1198 -0
- package/src/test/lib/identifier.test.mjs +157 -0
- package/src/test/lib/mcp-merge.test.mjs +345 -0
- package/src/test/lib/paths.test.mjs +83 -0
- package/src/test/lib/sync.test.mjs +514 -0
- package/src/test/mergers/gitignore.test.mjs +145 -20
- package/src/lib/write-configs.mjs +0 -202
- package/src/test/e2e/HANDOFF.md +0 -223
- package/src/test/e2e/cli-init.test.mjs +0 -213
- package/src/test/e2e/payload-factory.mjs +0 -22
|
@@ -1,213 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* CLI E2E tests for `skillrepo init`.
|
|
3
|
-
*
|
|
4
|
-
* Spins up a mock HTTP server, runs the real CLI binary as a subprocess,
|
|
5
|
-
* and verifies the generated setup actually works — not just that files exist.
|
|
6
|
-
*
|
|
7
|
-
* Uses node:test + node:assert — zero external dependencies.
|
|
8
|
-
*
|
|
9
|
-
* IMPORTANT: The CLI is run via async `execFile` (not `execFileSync`).
|
|
10
|
-
* execFileSync would block the Node event loop and starve the mock HTTP
|
|
11
|
-
* server running in the same process, causing the CLI's fetch to hang.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import { describe, it, before, after, beforeEach, afterEach } from "node:test";
|
|
15
|
-
import assert from "node:assert/strict";
|
|
16
|
-
import {
|
|
17
|
-
mkdtempSync, mkdirSync, rmSync, readFileSync, writeFileSync,
|
|
18
|
-
existsSync,
|
|
19
|
-
} from "node:fs";
|
|
20
|
-
import { join, resolve, dirname } from "node:path";
|
|
21
|
-
import { tmpdir } from "node:os";
|
|
22
|
-
import { execFile } from "node:child_process";
|
|
23
|
-
import { fileURLToPath } from "node:url";
|
|
24
|
-
|
|
25
|
-
import { createMockServer } from "./mock-server.mjs";
|
|
26
|
-
import { buildPayload } from "./payload-factory.mjs";
|
|
27
|
-
|
|
28
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
29
|
-
const CLI_BIN = resolve(__dirname, "../../../bin/skillrepo.mjs");
|
|
30
|
-
const API_KEY = "sk_live_test123";
|
|
31
|
-
|
|
32
|
-
// ---------------------------------------------------------------------------
|
|
33
|
-
// Helpers
|
|
34
|
-
// ---------------------------------------------------------------------------
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Run the CLI init command against a mock server in a given cwd.
|
|
38
|
-
* Returns a promise that resolves with stdout on success.
|
|
39
|
-
*/
|
|
40
|
-
function runInit(cwd, port, { key = API_KEY, extraArgs = [] } = {}) {
|
|
41
|
-
return new Promise((resolve, reject) => {
|
|
42
|
-
execFile(
|
|
43
|
-
process.execPath,
|
|
44
|
-
[CLI_BIN, "init", "--url", `http://127.0.0.1:${port}`, "--key", key, "--yes", ...extraArgs],
|
|
45
|
-
{ cwd, encoding: "utf-8", timeout: 15_000, env: { ...process.env, NODE_NO_WARNINGS: "1" } },
|
|
46
|
-
(err, stdout, stderr) => {
|
|
47
|
-
if (err) return reject(Object.assign(err, { stdout, stderr }));
|
|
48
|
-
resolve(stdout);
|
|
49
|
-
},
|
|
50
|
-
);
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/** Run the CLI and expect it to fail. Returns { stdout, stderr, status }. */
|
|
55
|
-
function runInitExpectFail(cwd, port, { key = API_KEY } = {}) {
|
|
56
|
-
return new Promise((resolve) => {
|
|
57
|
-
execFile(
|
|
58
|
-
process.execPath,
|
|
59
|
-
[CLI_BIN, "init", "--url", `http://127.0.0.1:${port}`, "--key", key, "--yes"],
|
|
60
|
-
{ cwd, encoding: "utf-8", timeout: 15_000, env: { ...process.env, NODE_NO_WARNINGS: "1" } },
|
|
61
|
-
(err, stdout, stderr) => {
|
|
62
|
-
if (err) {
|
|
63
|
-
resolve({ stdout: stdout ?? "", stderr: stderr ?? "", status: err.code ?? 1 });
|
|
64
|
-
} else {
|
|
65
|
-
resolve({ stdout, stderr: "", status: 0 });
|
|
66
|
-
}
|
|
67
|
-
},
|
|
68
|
-
);
|
|
69
|
-
});
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/** Read a file from the temp dir. */
|
|
73
|
-
function readFile(dir, relPath) {
|
|
74
|
-
return readFileSync(join(dir, relPath), "utf-8");
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/** Check if a file exists in the temp dir. */
|
|
78
|
-
function fileExists(dir, relPath) {
|
|
79
|
-
return existsSync(join(dir, relPath));
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/** Parse JSON from a file in the temp dir. */
|
|
83
|
-
function readJSON(dir, relPath) {
|
|
84
|
-
return JSON.parse(readFile(dir, relPath));
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/** Create a fresh temp dir with .claude/ and .cursor/ markers. */
|
|
88
|
-
function makeTempDir() {
|
|
89
|
-
const dir = mkdtempSync(join(tmpdir(), "cli-e2e-"));
|
|
90
|
-
mkdirSync(join(dir, ".claude"), { recursive: true });
|
|
91
|
-
mkdirSync(join(dir, ".cursor"), { recursive: true });
|
|
92
|
-
return dir;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// ---------------------------------------------------------------------------
|
|
96
|
-
// Test suite
|
|
97
|
-
// ---------------------------------------------------------------------------
|
|
98
|
-
|
|
99
|
-
describe("CLI E2E: skillrepo init", () => {
|
|
100
|
-
let server;
|
|
101
|
-
let port;
|
|
102
|
-
let tempDir;
|
|
103
|
-
|
|
104
|
-
before(async () => {
|
|
105
|
-
server = createMockServer({});
|
|
106
|
-
port = await server.start();
|
|
107
|
-
const payload = buildPayload({ baseUrl: `http://127.0.0.1:${port}` });
|
|
108
|
-
server.setPayload(payload);
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
after(async () => {
|
|
112
|
-
if (server) await server.stop();
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
beforeEach(() => {
|
|
116
|
-
tempDir = makeTempDir();
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
afterEach(() => {
|
|
120
|
-
if (tempDir) rmSync(tempDir, { recursive: true, force: true });
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
// =========================================================================
|
|
124
|
-
// 1. MCP Config
|
|
125
|
-
// =========================================================================
|
|
126
|
-
|
|
127
|
-
it(".mcp.json is created with MCP server config", async () => {
|
|
128
|
-
await runInit(tempDir, port);
|
|
129
|
-
|
|
130
|
-
const mcp = readJSON(tempDir, ".mcp.json");
|
|
131
|
-
assert.ok(mcp.mcpServers?.skillrepo, "Should have skillrepo MCP server entry");
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
it(".env.local contains the API key", async () => {
|
|
135
|
-
await runInit(tempDir, port);
|
|
136
|
-
|
|
137
|
-
const envContent = readFile(tempDir, ".env.local");
|
|
138
|
-
assert.ok(envContent.includes(`SKILLREPO_ACCESS_KEY=${API_KEY}`));
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
// =========================================================================
|
|
142
|
-
// 2. Error Handling
|
|
143
|
-
// =========================================================================
|
|
144
|
-
|
|
145
|
-
it("invalid API key (401) exits with error", async () => {
|
|
146
|
-
const srv = createMockServer({}, { validKey: "sk_live_onlythisone" });
|
|
147
|
-
const p = await srv.start();
|
|
148
|
-
const payload = buildPayload({ baseUrl: `http://127.0.0.1:${p}` });
|
|
149
|
-
srv.setPayload(payload);
|
|
150
|
-
|
|
151
|
-
try {
|
|
152
|
-
const result = await runInitExpectFail(tempDir, p, { key: "sk_live_wrongkey" });
|
|
153
|
-
assert.notEqual(result.status, 0);
|
|
154
|
-
const output = result.stdout + result.stderr;
|
|
155
|
-
assert.ok(output.toLowerCase().includes("invalid") || output.toLowerCase().includes("error"));
|
|
156
|
-
} finally {
|
|
157
|
-
await srv.stop();
|
|
158
|
-
}
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
it("unreachable server exits with error", async () => {
|
|
162
|
-
const result = await runInitExpectFail(tempDir, 19999);
|
|
163
|
-
assert.notEqual(result.status, 0);
|
|
164
|
-
const output = result.stdout + result.stderr;
|
|
165
|
-
assert.ok(
|
|
166
|
-
output.toLowerCase().includes("cannot reach") ||
|
|
167
|
-
output.toLowerCase().includes("error") ||
|
|
168
|
-
output.toLowerCase().includes("econnrefused"),
|
|
169
|
-
);
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
// =========================================================================
|
|
173
|
-
// 3. Generated Files Are Structurally Valid
|
|
174
|
-
// =========================================================================
|
|
175
|
-
|
|
176
|
-
it("all JSON files parse without error", async () => {
|
|
177
|
-
await runInit(tempDir, port);
|
|
178
|
-
|
|
179
|
-
for (const f of [".mcp.json"]) {
|
|
180
|
-
assert.doesNotThrow(() => readJSON(tempDir, f), `Invalid JSON: ${f}`);
|
|
181
|
-
}
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
// =========================================================================
|
|
185
|
-
// 4. .gitignore and old format cleanup
|
|
186
|
-
// =========================================================================
|
|
187
|
-
|
|
188
|
-
it("old format files are cleaned up during migration", async () => {
|
|
189
|
-
// Create old format files before init
|
|
190
|
-
writeFileSync(join(tempDir, ".claude/skillrepo.md"), "# Old skillrepo md");
|
|
191
|
-
writeFileSync(join(tempDir, ".claude/skillrepo-index.json"), '{"version":1}');
|
|
192
|
-
writeFileSync(join(tempDir, ".claude/skillrepo-config.json"), '{"version":2}');
|
|
193
|
-
|
|
194
|
-
await runInit(tempDir, port);
|
|
195
|
-
|
|
196
|
-
// Old format files should be removed
|
|
197
|
-
assert.ok(!fileExists(tempDir, ".claude/skillrepo.md"), "Old .claude/skillrepo.md should be removed");
|
|
198
|
-
assert.ok(!fileExists(tempDir, ".claude/skillrepo-index.json"), "Old .claude/skillrepo-index.json should be removed");
|
|
199
|
-
assert.ok(!fileExists(tempDir, ".claude/skillrepo-config.json"), "Old .claude/skillrepo-config.json should be removed");
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
// =========================================================================
|
|
203
|
-
// 5. No hook files generated (hooks removed in v1)
|
|
204
|
-
// =========================================================================
|
|
205
|
-
|
|
206
|
-
it("does not generate hook files", async () => {
|
|
207
|
-
await runInit(tempDir, port);
|
|
208
|
-
|
|
209
|
-
assert.ok(!fileExists(tempDir, ".claude/hooks/skillrepo-sync.mjs"), "Sync hook should not exist");
|
|
210
|
-
assert.ok(!fileExists(tempDir, ".claude/hooks/skillrepo-prompt-match.mjs"), "Prompt-match hook should not exist");
|
|
211
|
-
assert.ok(!fileExists(tempDir, ".claude/hooks/skillrepo-pretool-activation.mjs"), "PreToolUse hook should not exist");
|
|
212
|
-
});
|
|
213
|
-
});
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Factory for SetupPayload objects used in CLI E2E tests.
|
|
3
|
-
*
|
|
4
|
-
* Phase D (#534): The setup endpoint now returns only `skillCount` and
|
|
5
|
-
* `mcpUrl`. All hook/rule/config generation was removed — standalone
|
|
6
|
-
* scripts bundled with the CLI handle everything.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Build a SetupPayload for E2E tests.
|
|
11
|
-
*
|
|
12
|
-
* @param {object} [options]
|
|
13
|
-
* @param {string} [options.baseUrl] - The base URL for the mock server
|
|
14
|
-
* @returns {object} A SetupPayload
|
|
15
|
-
*/
|
|
16
|
-
export function buildPayload(options = {}) {
|
|
17
|
-
const baseUrl = options.baseUrl ?? "http://localhost:9999";
|
|
18
|
-
return {
|
|
19
|
-
skillCount: 5,
|
|
20
|
-
mcpUrl: `${baseUrl}/api/mcp`,
|
|
21
|
-
};
|
|
22
|
-
}
|