skillrepo 4.8.0 → 4.8.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/bin/skillrepo.mjs +6 -3
- package/package.json +1 -1
- package/src/commands/add.mjs +2 -1
- package/src/commands/init.mjs +15 -5
- package/src/lib/http.mjs +25 -11
- package/src/test/commands/add.test.mjs +79 -2
- package/src/test/commands/get.test.mjs +131 -2
- package/src/test/commands/init-session-sync.test.mjs +724 -0
- package/src/test/commands/init.test.mjs +159 -2
- package/src/test/commands/list.test.mjs +573 -1
- package/src/test/commands/publish.test.mjs +136 -3
- package/src/test/commands/push.test.mjs +280 -1
- package/src/test/commands/remove.test.mjs +221 -2
- package/src/test/commands/search.test.mjs +203 -1
- package/src/test/commands/session-sync.test.mjs +227 -1
- package/src/test/commands/uninstall.test.mjs +216 -0
- package/src/test/commands/update.test.mjs +218 -0
- package/src/test/dispatcher.test.mjs +103 -2
- package/src/test/e2e/advertised-surface.test.mjs +207 -0
- package/src/test/e2e/cli-commands.test.mjs +1 -1
- package/src/test/e2e/uninstall-interactive.test.mjs +93 -0
- package/src/test/e2e/update-check-suppression.test.mjs +135 -0
- package/src/test/integration/update-list-contract.integration.test.mjs +66 -0
- package/src/test/lib/browser-open.test.mjs +43 -0
- package/src/test/lib/config.test.mjs +87 -0
- package/src/test/lib/crypto-shas.test.mjs +17 -0
- package/src/test/lib/file-write.test.mjs +244 -0
- package/src/test/lib/fs-utils.test.mjs +259 -0
- package/src/test/lib/global-install.test.mjs +134 -0
- package/src/test/lib/http-timeout.test.mjs +114 -0
- package/src/test/lib/http.test.mjs +616 -1
- package/src/test/lib/mcp-merge.test.mjs +157 -0
- package/src/test/lib/npm-update-check.test.mjs +180 -0
- package/src/test/lib/placement-walk.test.mjs +132 -0
- package/src/test/lib/skill-walk.test.mjs +39 -1
- package/src/test/lib/sync.test.mjs +139 -5
- package/src/test/lib/telemetry.test.mjs +34 -0
- package/src/test/mergers/claude-mcp.test.mjs +30 -0
- package/src/test/mergers/cursor-mcp.test.mjs +115 -0
- package/src/test/mergers/env-local.test.mjs +126 -0
- package/src/test/mergers/vscode-mcp.test.mjs +177 -0
- package/src/test/mergers/windsurf-mcp.test.mjs +144 -0
- package/src/test/resolve-key.test.mjs +33 -0
|
@@ -15,8 +15,8 @@ import {
|
|
|
15
15
|
import { join } from "node:path";
|
|
16
16
|
import { tmpdir } from "node:os";
|
|
17
17
|
|
|
18
|
-
import { runInit } from "../../commands/init.mjs";
|
|
19
|
-
import { readConfig } from "../../lib/config.mjs";
|
|
18
|
+
import { runInit, directoryHasContent } from "../../commands/init.mjs";
|
|
19
|
+
import { readConfig, writeConfig } from "../../lib/config.mjs";
|
|
20
20
|
import { CliError, EXIT_AUTH, EXIT_VALIDATION } from "../../lib/errors.mjs";
|
|
21
21
|
import { AGENT_REGISTRY } from "../../lib/agent-registry.mjs";
|
|
22
22
|
import { createMockServer } from "../e2e/mock-server.mjs";
|
|
@@ -2154,3 +2154,160 @@ describe("runInit — v3.1.2 step 6 auto-install (npx)", () => {
|
|
|
2154
2154
|
assert.equal(settings.hooks.SessionStart[0].hooks.length, 1);
|
|
2155
2155
|
});
|
|
2156
2156
|
});
|
|
2157
|
+
|
|
2158
|
+
// ── Non-fatal write-failure warnings (#646) ──────────────────────────
|
|
2159
|
+
//
|
|
2160
|
+
// init writes .env.local and .gitignore as best-effort steps AFTER the
|
|
2161
|
+
// config is already saved. A failure on either must produce a warning
|
|
2162
|
+
// and let init continue — never abort setup. These catch/warn branches
|
|
2163
|
+
// had no coverage. Triggered cross-platform by occupying the target
|
|
2164
|
+
// path with a directory so the write throws (no chmod needed).
|
|
2165
|
+
describe("runInit — non-fatal write-failure warnings", () => {
|
|
2166
|
+
beforeEach(setup);
|
|
2167
|
+
afterEach(teardown);
|
|
2168
|
+
|
|
2169
|
+
it("warns but completes when .env.local cannot be written", async () => {
|
|
2170
|
+
mkdirSync(join(process.cwd(), ".env.local"), { recursive: true });
|
|
2171
|
+
await assert.doesNotReject(
|
|
2172
|
+
() =>
|
|
2173
|
+
runInit(
|
|
2174
|
+
["--key", VALID_KEY, "--url", serverUrl, "--yes", "--agent", "claude", "--no-session-sync"],
|
|
2175
|
+
{ stdout, stderr },
|
|
2176
|
+
),
|
|
2177
|
+
"a .env.local write failure must NOT abort init (config is already saved)",
|
|
2178
|
+
);
|
|
2179
|
+
assert.match(stdout.text() + stderr.text(), /Could not write \.env\.local/);
|
|
2180
|
+
});
|
|
2181
|
+
|
|
2182
|
+
it("warns but completes when .gitignore cannot be written", async () => {
|
|
2183
|
+
mkdirSync(join(process.cwd(), ".gitignore"), { recursive: true });
|
|
2184
|
+
await assert.doesNotReject(
|
|
2185
|
+
() =>
|
|
2186
|
+
runInit(
|
|
2187
|
+
["--key", VALID_KEY, "--url", serverUrl, "--yes", "--agent", "claude", "--no-session-sync"],
|
|
2188
|
+
{ stdout, stderr },
|
|
2189
|
+
),
|
|
2190
|
+
"a .gitignore write failure must NOT abort init",
|
|
2191
|
+
);
|
|
2192
|
+
assert.match(stdout.text() + stderr.text(), /Could not update \.gitignore/);
|
|
2193
|
+
});
|
|
2194
|
+
});
|
|
2195
|
+
|
|
2196
|
+
// ── Credential prompt + stale-key recovery (#1923, deps.promptFn seam) ──
|
|
2197
|
+
//
|
|
2198
|
+
// Exercises the step-1 interactive credential prompt and the step-2
|
|
2199
|
+
// stale-key re-prompt recovery (including BOTH reportInitFailure telemetry
|
|
2200
|
+
// call sites) via the deps.promptFn seam — no real TTY/stdin. A server
|
|
2201
|
+
// with a fixed validKey makes any other key 401, so the re-prompt can fail
|
|
2202
|
+
// twice. Every path here THROWS at step 2 (validate), so it never reaches
|
|
2203
|
+
// the later interactive steps (5 MCP / 6 session-sync).
|
|
2204
|
+
describe("runInit — credential prompt + stale-key recovery", () => {
|
|
2205
|
+
let sb, srv, srvUrl, origCwd, origHome, origDetect, out, err;
|
|
2206
|
+
const RIGHT = "sk_live_init_right_key";
|
|
2207
|
+
|
|
2208
|
+
beforeEach(async () => {
|
|
2209
|
+
sb = mkdtempSync(join(tmpdir(), "cli-init-cred-"));
|
|
2210
|
+
mkdirSync(join(sb, "project"), { recursive: true });
|
|
2211
|
+
mkdirSync(join(sb, "home"), { recursive: true });
|
|
2212
|
+
origCwd = process.cwd();
|
|
2213
|
+
origHome = captureHome();
|
|
2214
|
+
origDetect = {};
|
|
2215
|
+
for (const n of DETECTION_ENV_VARS) {
|
|
2216
|
+
origDetect[n] = process.env[n];
|
|
2217
|
+
delete process.env[n];
|
|
2218
|
+
}
|
|
2219
|
+
process.chdir(join(sb, "project"));
|
|
2220
|
+
setSandboxHome(join(sb, "home"));
|
|
2221
|
+
delete process.env.SKILLREPO_ACCESS_KEY;
|
|
2222
|
+
delete process.env.SKILLREPO_URL;
|
|
2223
|
+
// Only RIGHT validates; any other sk_live_ key → 401 on every call.
|
|
2224
|
+
srv = createMockServer({}, { validKey: RIGHT });
|
|
2225
|
+
const port = await srv.start();
|
|
2226
|
+
srvUrl = `http://127.0.0.1:${port}`;
|
|
2227
|
+
out = createCaptureStream();
|
|
2228
|
+
err = createCaptureStream();
|
|
2229
|
+
});
|
|
2230
|
+
|
|
2231
|
+
afterEach(async () => {
|
|
2232
|
+
if (srv) await srv.stop();
|
|
2233
|
+
process.chdir(origCwd);
|
|
2234
|
+
restoreHome(origHome);
|
|
2235
|
+
for (const n of DETECTION_ENV_VARS) {
|
|
2236
|
+
if (origDetect[n] === undefined) delete process.env[n];
|
|
2237
|
+
else process.env[n] = origDetect[n];
|
|
2238
|
+
}
|
|
2239
|
+
if (sb) rmSync(sb, { recursive: true, force: true });
|
|
2240
|
+
srv = null;
|
|
2241
|
+
});
|
|
2242
|
+
|
|
2243
|
+
it("step-1 prompt fires when no key/config; a wrong typed key fails validate (else branch + telemetry)", async () => {
|
|
2244
|
+
// No --key, no existing config, not --yes → the interactive
|
|
2245
|
+
// promptForKey branch runs. The injected prompt returns a wrong key →
|
|
2246
|
+
// validate 401 → non-recoverable else branch (no existingConfig) →
|
|
2247
|
+
// reportInitFailure + rethrow. Covers the step-1 prompt branch and the
|
|
2248
|
+
// step-2 else reportInitFailure call site.
|
|
2249
|
+
let prompted = 0;
|
|
2250
|
+
const promptFn = async () => {
|
|
2251
|
+
prompted++;
|
|
2252
|
+
return "sk_live_wrong_typed";
|
|
2253
|
+
};
|
|
2254
|
+
await assert.rejects(
|
|
2255
|
+
() => runInit(["--url", srvUrl], { stdout: out, stderr: err }, { promptFn }),
|
|
2256
|
+
(e) => e instanceof CliError && e.exitCode === EXIT_AUTH,
|
|
2257
|
+
);
|
|
2258
|
+
assert.equal(prompted, 1, "the step-1 credential prompt must have fired exactly once");
|
|
2259
|
+
});
|
|
2260
|
+
|
|
2261
|
+
it("stale config key → 401 → re-prompt → still 401 → reportInitFailure + rethrow", async () => {
|
|
2262
|
+
writeConfig({ apiKey: "sk_live_stale_old_key", serverUrl: srvUrl });
|
|
2263
|
+
let prompted = 0;
|
|
2264
|
+
const promptFn = async () => {
|
|
2265
|
+
prompted++;
|
|
2266
|
+
return "sk_live_still_wrong_key";
|
|
2267
|
+
};
|
|
2268
|
+
await assert.rejects(
|
|
2269
|
+
() => runInit(["--url", srvUrl], { stdout: out, stderr: err }, { promptFn }),
|
|
2270
|
+
(e) => e instanceof CliError && e.exitCode === EXIT_AUTH,
|
|
2271
|
+
);
|
|
2272
|
+
assert.equal(prompted, 1, "the stale-key re-prompt must have fired");
|
|
2273
|
+
assert.match(out.text() + err.text(), /invalid key|re-prompt/i, "must warn before re-prompting");
|
|
2274
|
+
});
|
|
2275
|
+
|
|
2276
|
+
it("stale config key → 401 → re-prompt with an invalid-format key → validation error", async () => {
|
|
2277
|
+
writeConfig({ apiKey: "sk_live_stale_old_key", serverUrl: srvUrl });
|
|
2278
|
+
const promptFn = async () => "not-a-valid-key"; // no sk_live_ prefix
|
|
2279
|
+
await assert.rejects(
|
|
2280
|
+
() => runInit(["--url", srvUrl], { stdout: out, stderr: err }, { promptFn }),
|
|
2281
|
+
(e) => e instanceof CliError && e.exitCode === EXIT_VALIDATION,
|
|
2282
|
+
);
|
|
2283
|
+
});
|
|
2284
|
+
});
|
|
2285
|
+
|
|
2286
|
+
// ── directoryHasContent (exported helper) ─────────────────────────────
|
|
2287
|
+
describe("directoryHasContent", () => {
|
|
2288
|
+
it("swallows a readdirSync error (path is a file, not a dir) → false", () => {
|
|
2289
|
+
// existsSync(file) is true but readdirSync(file) throws ENOTDIR — the
|
|
2290
|
+
// catch must treat it as "no content" (false), not crash. Cross-
|
|
2291
|
+
// platform: ENOTDIR fires identically on POSIX and Windows.
|
|
2292
|
+
const tmp = mkdtempSync(join(tmpdir(), "dhc-"));
|
|
2293
|
+
const filePath = join(tmp, "a-file");
|
|
2294
|
+
writeFileSync(filePath, "x");
|
|
2295
|
+
try {
|
|
2296
|
+
assert.equal(directoryHasContent(filePath), false);
|
|
2297
|
+
} finally {
|
|
2298
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
2299
|
+
}
|
|
2300
|
+
});
|
|
2301
|
+
|
|
2302
|
+
it("true for a non-empty dir; false for empty and absent dirs", () => {
|
|
2303
|
+
const tmp = mkdtempSync(join(tmpdir(), "dhc2-"));
|
|
2304
|
+
try {
|
|
2305
|
+
assert.equal(directoryHasContent(tmp), false, "empty dir");
|
|
2306
|
+
writeFileSync(join(tmp, "x"), "1");
|
|
2307
|
+
assert.equal(directoryHasContent(tmp), true, "non-empty dir");
|
|
2308
|
+
assert.equal(directoryHasContent(join(tmp, "nope")), false, "absent dir");
|
|
2309
|
+
} finally {
|
|
2310
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
2311
|
+
}
|
|
2312
|
+
});
|
|
2313
|
+
});
|