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.
Files changed (43) hide show
  1. package/bin/skillrepo.mjs +6 -3
  2. package/package.json +1 -1
  3. package/src/commands/add.mjs +2 -1
  4. package/src/commands/init.mjs +15 -5
  5. package/src/lib/http.mjs +25 -11
  6. package/src/test/commands/add.test.mjs +79 -2
  7. package/src/test/commands/get.test.mjs +131 -2
  8. package/src/test/commands/init-session-sync.test.mjs +724 -0
  9. package/src/test/commands/init.test.mjs +159 -2
  10. package/src/test/commands/list.test.mjs +573 -1
  11. package/src/test/commands/publish.test.mjs +136 -3
  12. package/src/test/commands/push.test.mjs +280 -1
  13. package/src/test/commands/remove.test.mjs +221 -2
  14. package/src/test/commands/search.test.mjs +203 -1
  15. package/src/test/commands/session-sync.test.mjs +227 -1
  16. package/src/test/commands/uninstall.test.mjs +216 -0
  17. package/src/test/commands/update.test.mjs +218 -0
  18. package/src/test/dispatcher.test.mjs +103 -2
  19. package/src/test/e2e/advertised-surface.test.mjs +207 -0
  20. package/src/test/e2e/cli-commands.test.mjs +1 -1
  21. package/src/test/e2e/uninstall-interactive.test.mjs +93 -0
  22. package/src/test/e2e/update-check-suppression.test.mjs +135 -0
  23. package/src/test/integration/update-list-contract.integration.test.mjs +66 -0
  24. package/src/test/lib/browser-open.test.mjs +43 -0
  25. package/src/test/lib/config.test.mjs +87 -0
  26. package/src/test/lib/crypto-shas.test.mjs +17 -0
  27. package/src/test/lib/file-write.test.mjs +244 -0
  28. package/src/test/lib/fs-utils.test.mjs +259 -0
  29. package/src/test/lib/global-install.test.mjs +134 -0
  30. package/src/test/lib/http-timeout.test.mjs +114 -0
  31. package/src/test/lib/http.test.mjs +616 -1
  32. package/src/test/lib/mcp-merge.test.mjs +157 -0
  33. package/src/test/lib/npm-update-check.test.mjs +180 -0
  34. package/src/test/lib/placement-walk.test.mjs +132 -0
  35. package/src/test/lib/skill-walk.test.mjs +39 -1
  36. package/src/test/lib/sync.test.mjs +139 -5
  37. package/src/test/lib/telemetry.test.mjs +34 -0
  38. package/src/test/mergers/claude-mcp.test.mjs +30 -0
  39. package/src/test/mergers/cursor-mcp.test.mjs +115 -0
  40. package/src/test/mergers/env-local.test.mjs +126 -0
  41. package/src/test/mergers/vscode-mcp.test.mjs +177 -0
  42. package/src/test/mergers/windsurf-mcp.test.mjs +144 -0
  43. 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
+ });