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
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit/integration tests for src/commands/update.mjs (PR2 of #646).
|
|
3
|
+
*
|
|
4
|
+
* Tests run the runUpdate function directly against an in-process mock
|
|
5
|
+
* server. Captures stdout via a stream override.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, it, beforeEach, afterEach } from "node:test";
|
|
9
|
+
import assert from "node:assert/strict";
|
|
10
|
+
import { mkdtempSync, mkdirSync, rmSync, existsSync, writeFileSync } from "node:fs";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
import { tmpdir } from "node:os";
|
|
13
|
+
|
|
14
|
+
import { runUpdate } from "../../commands/update.mjs";
|
|
15
|
+
import { resolvePlacementDir } from "../../lib/file-write.mjs";
|
|
16
|
+
import { CliError, EXIT_AUTH, EXIT_VALIDATION } from "../../lib/errors.mjs";
|
|
17
|
+
import { createMockServer } from "../e2e/mock-server.mjs";
|
|
18
|
+
import { createCaptureStream } from "../helpers/capture-stream.mjs";
|
|
19
|
+
|
|
20
|
+
let sandbox;
|
|
21
|
+
let server;
|
|
22
|
+
let serverUrl;
|
|
23
|
+
let originalCwd;
|
|
24
|
+
let originalHome;
|
|
25
|
+
let stdout;
|
|
26
|
+
const VALID_KEY = "sk_live_test";
|
|
27
|
+
|
|
28
|
+
function makeSkill(name) {
|
|
29
|
+
return {
|
|
30
|
+
owner: "alice",
|
|
31
|
+
name,
|
|
32
|
+
version: "1.0.0",
|
|
33
|
+
description: `${name} description`,
|
|
34
|
+
files: [
|
|
35
|
+
{
|
|
36
|
+
path: "SKILL.md",
|
|
37
|
+
content: `---\nname: ${name}\ndescription: ${name} description\n---\n\nbody\n`,
|
|
38
|
+
sha256: "x",
|
|
39
|
+
size: 50,
|
|
40
|
+
contentType: "text/markdown",
|
|
41
|
+
},
|
|
42
|
+
],
|
|
43
|
+
updatedAt: new Date().toISOString(),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function setup() {
|
|
48
|
+
sandbox = mkdtempSync(join(tmpdir(), "cli-cmd-update-"));
|
|
49
|
+
mkdirSync(join(sandbox, "project"), { recursive: true });
|
|
50
|
+
mkdirSync(join(sandbox, "home"), { recursive: true });
|
|
51
|
+
originalCwd = process.cwd();
|
|
52
|
+
originalHome = process.env.HOME;
|
|
53
|
+
process.chdir(join(sandbox, "project"));
|
|
54
|
+
process.env.HOME = join(sandbox, "home");
|
|
55
|
+
delete process.env.SKILLREPO_ACCESS_KEY;
|
|
56
|
+
delete process.env.SKILLREPO_URL;
|
|
57
|
+
|
|
58
|
+
server = createMockServer({});
|
|
59
|
+
const port = await server.start();
|
|
60
|
+
serverUrl = `http://127.0.0.1:${port}`;
|
|
61
|
+
|
|
62
|
+
stdout = createCaptureStream();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function teardown() {
|
|
66
|
+
if (server) await server.stop();
|
|
67
|
+
process.chdir(originalCwd);
|
|
68
|
+
process.env.HOME = originalHome;
|
|
69
|
+
if (sandbox) rmSync(sandbox, { recursive: true, force: true });
|
|
70
|
+
server = null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
describe("runUpdate — happy path", () => {
|
|
74
|
+
beforeEach(setup);
|
|
75
|
+
afterEach(teardown);
|
|
76
|
+
|
|
77
|
+
it("syncs an empty library and prints up-to-date", async () => {
|
|
78
|
+
server.setLibraryResponse({ skills: [], removals: [], syncedAt: "x" });
|
|
79
|
+
await runUpdate(["--key", VALID_KEY, "--url", serverUrl], { stdout });
|
|
80
|
+
assert.match(stdout.text(), /up to date/);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("syncs a single skill and prints add count", async () => {
|
|
84
|
+
server.setLibraryResponse({
|
|
85
|
+
skills: [makeSkill("first")],
|
|
86
|
+
removals: [],
|
|
87
|
+
syncedAt: "x",
|
|
88
|
+
});
|
|
89
|
+
await runUpdate(["--key", VALID_KEY, "--url", serverUrl], { stdout });
|
|
90
|
+
assert.match(stdout.text(), /1 added/);
|
|
91
|
+
assert.ok(existsSync(resolvePlacementDir("claudeProject", "first")));
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("--json outputs structured summary", async () => {
|
|
95
|
+
server.setLibraryResponse({
|
|
96
|
+
skills: [makeSkill("first"), makeSkill("second")],
|
|
97
|
+
removals: [],
|
|
98
|
+
syncedAt: "x",
|
|
99
|
+
});
|
|
100
|
+
await runUpdate(["--key", VALID_KEY, "--url", serverUrl, "--json"], { stdout });
|
|
101
|
+
const json = JSON.parse(stdout.text());
|
|
102
|
+
assert.equal(json.added, 2);
|
|
103
|
+
assert.equal(json.removed, 0);
|
|
104
|
+
assert.equal(json.notModified, false);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
describe("runUpdate — credential resolution", () => {
|
|
109
|
+
beforeEach(setup);
|
|
110
|
+
afterEach(teardown);
|
|
111
|
+
|
|
112
|
+
it("reads from the global config file when no flags", async () => {
|
|
113
|
+
mkdirSync(join(process.env.HOME, ".claude", "skillrepo"), { recursive: true });
|
|
114
|
+
writeFileSync(
|
|
115
|
+
join(process.env.HOME, ".claude", "skillrepo", "config.json"),
|
|
116
|
+
JSON.stringify({ apiKey: VALID_KEY, serverUrl }),
|
|
117
|
+
);
|
|
118
|
+
server.setLibraryResponse({ skills: [], removals: [], syncedAt: "x" });
|
|
119
|
+
await runUpdate([], { stdout });
|
|
120
|
+
assert.match(stdout.text(), /up to date/);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("throws authError when no key and no config", async () => {
|
|
124
|
+
await assert.rejects(
|
|
125
|
+
() => runUpdate([]),
|
|
126
|
+
(err) => err instanceof CliError && err.exitCode === EXIT_AUTH,
|
|
127
|
+
);
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
describe("runUpdate — flag handling", () => {
|
|
132
|
+
beforeEach(setup);
|
|
133
|
+
afterEach(teardown);
|
|
134
|
+
|
|
135
|
+
it("rejects unknown flag", async () => {
|
|
136
|
+
await assert.rejects(
|
|
137
|
+
() => runUpdate(["--key", VALID_KEY, "--url", serverUrl, "--bogus"]),
|
|
138
|
+
(err) => err instanceof CliError && err.exitCode === EXIT_VALIDATION,
|
|
139
|
+
);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("--global writes to the global skills dir", async () => {
|
|
143
|
+
server.setLibraryResponse({
|
|
144
|
+
skills: [makeSkill("global-test")],
|
|
145
|
+
removals: [],
|
|
146
|
+
syncedAt: "x",
|
|
147
|
+
});
|
|
148
|
+
await runUpdate(["--key", VALID_KEY, "--url", serverUrl, "--global"]);
|
|
149
|
+
const dir = resolvePlacementDir("claudeGlobal", "global-test");
|
|
150
|
+
assert.ok(existsSync(dir));
|
|
151
|
+
assert.ok(dir.startsWith(process.env.HOME));
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("--ide cursor writes to the project /skills/ fallback", async () => {
|
|
155
|
+
server.setLibraryResponse({
|
|
156
|
+
skills: [makeSkill("cursor-test")],
|
|
157
|
+
removals: [],
|
|
158
|
+
syncedAt: "x",
|
|
159
|
+
});
|
|
160
|
+
await runUpdate(["--key", VALID_KEY, "--url", serverUrl, "--ide", "cursor"]);
|
|
161
|
+
const dir = resolvePlacementDir("projectFallback", "cursor-test");
|
|
162
|
+
assert.ok(existsSync(dir));
|
|
163
|
+
});
|
|
164
|
+
});
|
|
@@ -53,18 +53,13 @@ describe("IDE detection", () => {
|
|
|
53
53
|
// windsurf depends on home dir, skip assertion
|
|
54
54
|
});
|
|
55
55
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const { getDetectedIdeKeys } = await import("../lib/detect-ides.mjs");
|
|
66
|
-
|
|
67
|
-
const keys = getDetectedIdeKeys({ claudeCode: true, cursor: false, windsurf: false, vscode: true });
|
|
68
|
-
assert.deepEqual(keys, ["claudeCode", "vscode"]);
|
|
69
|
-
});
|
|
56
|
+
// PR4 cross-review cleanup: the old v2.0.0 `getDetectedIdeKeys`
|
|
57
|
+
// helper with a "default to claudeCode + cursor when nothing
|
|
58
|
+
// detected" fallback was dead code — the plan explicitly
|
|
59
|
+
// removed the silent fallback in PR3b, and no command called
|
|
60
|
+
// this helper. It was exported and had tests asserting the
|
|
61
|
+
// v2.0.0 behavior, which contradicted the actual v3.0.0 code
|
|
62
|
+
// path. Deleted; init now refuses with a --ide hint when
|
|
63
|
+
// nothing is detected (see the "refuses with clear error when
|
|
64
|
+
// no IDE detected and no --ide flag" test in init.test.mjs).
|
|
70
65
|
});
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for bin/skillrepo.mjs (PR1 of #646).
|
|
3
|
+
*
|
|
4
|
+
* Spawns the real binary as a subprocess and asserts on stdout, stderr,
|
|
5
|
+
* and exit code for each documented routing path. Intentionally thin —
|
|
6
|
+
* the dispatcher should be a pure routing layer, so most logic lives in
|
|
7
|
+
* the command modules tested elsewhere.
|
|
8
|
+
*
|
|
9
|
+
* IMPORTANT: keep these tests fast — they run as part of `npm run check`
|
|
10
|
+
* and a slow dispatcher suite slows down every commit.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { describe, it } from "node:test";
|
|
14
|
+
import assert from "node:assert/strict";
|
|
15
|
+
import { execFile } from "node:child_process";
|
|
16
|
+
import { fileURLToPath } from "node:url";
|
|
17
|
+
import { dirname, resolve } from "node:path";
|
|
18
|
+
import { readFileSync } from "node:fs";
|
|
19
|
+
|
|
20
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
21
|
+
const CLI_BIN = resolve(__dirname, "../../bin/skillrepo.mjs");
|
|
22
|
+
const PKG_PATH = resolve(__dirname, "../../package.json");
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Run the CLI dispatcher and capture stdout, stderr, and exit code.
|
|
26
|
+
* Always resolves — never rejects — so tests can assert on failures.
|
|
27
|
+
*
|
|
28
|
+
* @param {string[]} args
|
|
29
|
+
* @param {object} [opts]
|
|
30
|
+
* @param {object} [opts.env] - Extra env vars (merged onto process.env).
|
|
31
|
+
* Useful for tests that need to point HOME at an empty dir to
|
|
32
|
+
* prove "no global config" code paths fire correctly. Pass
|
|
33
|
+
* SKILLREPO_ACCESS_KEY: "" explicitly to override an inherited
|
|
34
|
+
* env var from the developer's shell.
|
|
35
|
+
*/
|
|
36
|
+
function runCli(args = [], opts = {}) {
|
|
37
|
+
return new Promise((resolve) => {
|
|
38
|
+
const baseEnv = {
|
|
39
|
+
...process.env,
|
|
40
|
+
NO_COLOR: "1",
|
|
41
|
+
// Default tests to "no key in env" so the no-config code paths
|
|
42
|
+
// are exercised reliably across developer machines that may
|
|
43
|
+
// have SKILLREPO_ACCESS_KEY set in their shell.
|
|
44
|
+
SKILLREPO_ACCESS_KEY: "",
|
|
45
|
+
// Short timeout so the network-failure case fails fast rather
|
|
46
|
+
// than waiting 30s on the default safeFetch timeout.
|
|
47
|
+
SKILLREPO_TIMEOUT_MS: "2000",
|
|
48
|
+
};
|
|
49
|
+
execFile(
|
|
50
|
+
process.execPath,
|
|
51
|
+
[CLI_BIN, ...args],
|
|
52
|
+
{
|
|
53
|
+
encoding: "utf-8",
|
|
54
|
+
timeout: 10_000,
|
|
55
|
+
env: { ...baseEnv, ...(opts.env || {}) },
|
|
56
|
+
},
|
|
57
|
+
(err, stdout, stderr) => {
|
|
58
|
+
resolve({
|
|
59
|
+
stdout: stdout ?? "",
|
|
60
|
+
stderr: stderr ?? "",
|
|
61
|
+
status: err ? err.code ?? 1 : 0,
|
|
62
|
+
});
|
|
63
|
+
},
|
|
64
|
+
);
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
describe("dispatcher — top-level help", () => {
|
|
69
|
+
it("`skillrepo` with no args prints top-level help and exits 0", async () => {
|
|
70
|
+
const r = await runCli([]);
|
|
71
|
+
assert.equal(r.status, 0);
|
|
72
|
+
assert.match(r.stdout, /SkillRepo CLI/);
|
|
73
|
+
// All 7 commands listed
|
|
74
|
+
for (const cmd of ["init", "update", "get", "add", "remove", "list", "search"]) {
|
|
75
|
+
assert.match(r.stdout, new RegExp(`\\b${cmd}\\b`), `expected to see "${cmd}" in help`);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("`skillrepo --help` is the same as no args", async () => {
|
|
80
|
+
const r = await runCli(["--help"]);
|
|
81
|
+
assert.equal(r.status, 0);
|
|
82
|
+
assert.match(r.stdout, /SkillRepo CLI/);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("`skillrepo -h` is the same as --help", async () => {
|
|
86
|
+
const r = await runCli(["-h"]);
|
|
87
|
+
assert.equal(r.status, 0);
|
|
88
|
+
assert.match(r.stdout, /SkillRepo CLI/);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe("dispatcher — version", () => {
|
|
93
|
+
it("`skillrepo --version` prints the package.json version", async () => {
|
|
94
|
+
const r = await runCli(["--version"]);
|
|
95
|
+
assert.equal(r.status, 0);
|
|
96
|
+
const pkg = JSON.parse(readFileSync(PKG_PATH, "utf-8"));
|
|
97
|
+
assert.equal(r.stdout.trim(), pkg.version);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("`skillrepo -v` is the same as --version", async () => {
|
|
101
|
+
const r = await runCli(["-v"]);
|
|
102
|
+
assert.equal(r.status, 0);
|
|
103
|
+
const pkg = JSON.parse(readFileSync(PKG_PATH, "utf-8"));
|
|
104
|
+
assert.equal(r.stdout.trim(), pkg.version);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
describe("dispatcher — unknown command", () => {
|
|
109
|
+
it("exits with EXIT_VALIDATION (5) and prints a hint", async () => {
|
|
110
|
+
const r = await runCli(["nonsense"]);
|
|
111
|
+
assert.equal(r.status, 5);
|
|
112
|
+
assert.match(r.stderr, /Unknown command/);
|
|
113
|
+
assert.match(r.stderr, /skillrepo --help/);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("rejects unknown flags as commands too", async () => {
|
|
117
|
+
const r = await runCli(["--bogus"]);
|
|
118
|
+
assert.equal(r.status, 5);
|
|
119
|
+
assert.match(r.stderr, /Unknown command/);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
describe("dispatcher — per-command help", () => {
|
|
124
|
+
// PR1 only ships init for real; the other 6 are stubs but still
|
|
125
|
+
// route --help correctly.
|
|
126
|
+
for (const cmd of ["init", "update", "get", "add", "remove", "list", "search"]) {
|
|
127
|
+
it(`\`skillrepo ${cmd} --help\` prints command-specific help`, async () => {
|
|
128
|
+
const r = await runCli([cmd, "--help"]);
|
|
129
|
+
assert.equal(r.status, 0);
|
|
130
|
+
assert.match(r.stdout, new RegExp(`skillrepo ${cmd}`));
|
|
131
|
+
assert.match(r.stdout, /Usage:/);
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// All 7 commands are implemented as of PR3a. No stubs remain.
|
|
137
|
+
// (PR1 shipped init; PR2 shipped update/get/list/search; PR3a ships add/remove.)
|
|
138
|
+
|
|
139
|
+
describe("dispatcher — implemented commands route to their modules", () => {
|
|
140
|
+
// These commands have real implementations as of PR2. The dispatcher
|
|
141
|
+
// routing test verifies the binary doesn't fall through to the stub
|
|
142
|
+
// factory — we expect a "real" failure (validation/auth/network)
|
|
143
|
+
// rather than the "Not yet implemented" stub message.
|
|
144
|
+
//
|
|
145
|
+
// Each command is invoked WITHOUT credentials in a directory where
|
|
146
|
+
// there is no global config, so the expected outcome is an authError
|
|
147
|
+
// (exit 2, "No access key configured") OR a validationError (exit 5)
|
|
148
|
+
// for commands that require a positional argument.
|
|
149
|
+
it("`skillrepo update` is wired to the real module (not a stub)", async () => {
|
|
150
|
+
const r = await runCli(["update"], { env: { HOME: "/tmp/no-skillrepo-config" } });
|
|
151
|
+
assert.doesNotMatch(r.stderr, /Not yet implemented/);
|
|
152
|
+
// Either auth (no key) or network (no server) — both prove routing worked
|
|
153
|
+
assert.ok([1, 2, 5].includes(r.status), `expected 1/2/5, got ${r.status}`);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("`skillrepo get` is wired to the real module (not a stub)", async () => {
|
|
157
|
+
const r = await runCli(["get", "@alice/pdf-helper"], { env: { HOME: "/tmp/no-skillrepo-config" } });
|
|
158
|
+
assert.doesNotMatch(r.stderr, /Not yet implemented/);
|
|
159
|
+
assert.ok([1, 2, 5].includes(r.status));
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("`skillrepo get` without an identifier exits 5 with a usage hint", async () => {
|
|
163
|
+
// Missing positional should fail validation BEFORE attempting
|
|
164
|
+
// credential resolution — proves the dispatcher passes argv
|
|
165
|
+
// through and the command's positional check fires.
|
|
166
|
+
const r = await runCli(["get"], { env: { HOME: "/tmp/no-skillrepo-config" } });
|
|
167
|
+
assert.doesNotMatch(r.stderr, /Not yet implemented/);
|
|
168
|
+
// Either validation (missing positional) or auth (no key, depending
|
|
169
|
+
// on which check fires first) — both prove the stub is gone.
|
|
170
|
+
assert.ok([2, 5].includes(r.status));
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("`skillrepo list` is wired to the real module (not a stub)", async () => {
|
|
174
|
+
const r = await runCli(["list"], { env: { HOME: "/tmp/no-skillrepo-config" } });
|
|
175
|
+
assert.doesNotMatch(r.stderr, /Not yet implemented/);
|
|
176
|
+
assert.ok([1, 2, 5].includes(r.status));
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("`skillrepo search` is wired to the real module (not a stub)", async () => {
|
|
180
|
+
const r = await runCli(["search", "test"], { env: { HOME: "/tmp/no-skillrepo-config" } });
|
|
181
|
+
assert.doesNotMatch(r.stderr, /Not yet implemented/);
|
|
182
|
+
assert.ok([1, 2, 5].includes(r.status));
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it("`skillrepo search` without a query exits with a usage hint", async () => {
|
|
186
|
+
const r = await runCli(["search"], { env: { HOME: "/tmp/no-skillrepo-config" } });
|
|
187
|
+
assert.doesNotMatch(r.stderr, /Not yet implemented/);
|
|
188
|
+
assert.ok([2, 5].includes(r.status));
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it("`skillrepo add` is wired to the real module (not a stub)", async () => {
|
|
192
|
+
const r = await runCli(["add", "@alice/pdf-helper"], { env: { HOME: "/tmp/no-skillrepo-config" } });
|
|
193
|
+
assert.doesNotMatch(r.stderr, /Not yet implemented/);
|
|
194
|
+
assert.ok([1, 2, 5].includes(r.status));
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it("`skillrepo remove` is wired to the real module (not a stub)", async () => {
|
|
198
|
+
const r = await runCli(["remove", "@alice/pdf-helper"], { env: { HOME: "/tmp/no-skillrepo-config" } });
|
|
199
|
+
assert.doesNotMatch(r.stderr, /Not yet implemented/);
|
|
200
|
+
assert.ok([1, 2, 5].includes(r.status));
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it("`skillrepo add` without identifier exits 2 or 5 (usage hint)", async () => {
|
|
204
|
+
const r = await runCli(["add"], { env: { HOME: "/tmp/no-skillrepo-config" } });
|
|
205
|
+
assert.doesNotMatch(r.stderr, /Not yet implemented/);
|
|
206
|
+
assert.ok([2, 5].includes(r.status));
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it("`skillrepo remove` without identifier exits 2 or 5 (usage hint)", async () => {
|
|
210
|
+
const r = await runCli(["remove"], { env: { HOME: "/tmp/no-skillrepo-config" } });
|
|
211
|
+
assert.doesNotMatch(r.stderr, /Not yet implemented/);
|
|
212
|
+
assert.ok([2, 5].includes(r.status));
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
describe("dispatcher — init still works (PR1 keeps existing init untouched)", () => {
|
|
217
|
+
it("`skillrepo init --help` prints init help", async () => {
|
|
218
|
+
const r = await runCli(["init", "--help"]);
|
|
219
|
+
assert.equal(r.status, 0);
|
|
220
|
+
assert.match(r.stdout, /skillrepo init/);
|
|
221
|
+
});
|
|
222
|
+
// Real init flow is exercised by src/test/e2e/cli-init.test.mjs
|
|
223
|
+
// against the mock server. We don't duplicate that here.
|
|
224
|
+
});
|