skillrepo 2.0.0 → 3.1.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.
Files changed (72) hide show
  1. package/README.md +276 -145
  2. package/bin/skillrepo.mjs +224 -36
  3. package/package.json +6 -3
  4. package/src/commands/add.mjs +176 -0
  5. package/src/commands/get.mjs +116 -0
  6. package/src/commands/init.mjs +589 -143
  7. package/src/commands/list.mjs +176 -0
  8. package/src/commands/remove.mjs +162 -0
  9. package/src/commands/search.mjs +188 -0
  10. package/src/commands/session-sync.mjs +152 -0
  11. package/src/commands/uninstall.mjs +484 -0
  12. package/src/commands/update.mjs +184 -0
  13. package/src/lib/artifact-registry.mjs +265 -0
  14. package/src/lib/cli-config.mjs +230 -0
  15. package/src/lib/config.mjs +238 -0
  16. package/src/lib/detect-ides.mjs +0 -19
  17. package/src/lib/errors.mjs +264 -0
  18. package/src/lib/file-write.mjs +705 -0
  19. package/src/lib/fs-utils.mjs +83 -1
  20. package/src/lib/http.mjs +817 -37
  21. package/src/lib/identifier.mjs +153 -0
  22. package/src/lib/mcp-merge.mjs +275 -0
  23. package/src/lib/mergers/gitignore.mjs +73 -18
  24. package/src/lib/mergers/session-hook.mjs +298 -0
  25. package/src/lib/paths.mjs +67 -17
  26. package/src/lib/prompt.mjs +11 -44
  27. package/src/lib/removers/claude-mcp.mjs +67 -0
  28. package/src/lib/removers/cursor-mcp.mjs +60 -0
  29. package/src/lib/removers/env-local.mjs +55 -0
  30. package/src/lib/removers/gitignore.mjs +108 -0
  31. package/src/lib/removers/settings.mjs +183 -0
  32. package/src/lib/removers/vscode-mcp.mjs +87 -0
  33. package/src/lib/removers/windsurf-mcp.mjs +65 -0
  34. package/src/lib/sync.mjs +305 -0
  35. package/src/test/commands/add.test.mjs +285 -0
  36. package/src/test/commands/get.test.mjs +176 -0
  37. package/src/test/commands/init.test.mjs +697 -0
  38. package/src/test/commands/list.test.mjs +172 -0
  39. package/src/test/commands/remove.test.mjs +234 -0
  40. package/src/test/commands/search.test.mjs +204 -0
  41. package/src/test/commands/session-sync.test.mjs +350 -0
  42. package/src/test/commands/uninstall.test.mjs +768 -0
  43. package/src/test/commands/update.test.mjs +322 -0
  44. package/src/test/detect-ides.test.mjs +9 -14
  45. package/src/test/dispatcher.test.mjs +224 -0
  46. package/src/test/e2e/cli-commands.test.mjs +576 -0
  47. package/src/test/e2e/mock-server.mjs +364 -22
  48. package/src/test/helpers/capture-stream.mjs +48 -0
  49. package/src/test/integration/file-write.integration.test.mjs +279 -0
  50. package/src/test/lib/artifact-registry.test.mjs +268 -0
  51. package/src/test/lib/cli-config.test.mjs +407 -0
  52. package/src/test/lib/config.test.mjs +257 -0
  53. package/src/test/lib/errors.test.mjs +359 -0
  54. package/src/test/lib/file-write.test.mjs +784 -0
  55. package/src/test/lib/http.test.mjs +1198 -0
  56. package/src/test/lib/identifier.test.mjs +157 -0
  57. package/src/test/lib/mcp-merge.test.mjs +345 -0
  58. package/src/test/lib/paths.test.mjs +83 -0
  59. package/src/test/lib/sync.test.mjs +514 -0
  60. package/src/test/mergers/gitignore.test.mjs +145 -20
  61. package/src/test/mergers/session-hook.test.mjs +745 -0
  62. package/src/test/mergers/uninstall-claude-mcp.test.mjs +145 -0
  63. package/src/test/mergers/uninstall-cursor-mcp.test.mjs +108 -0
  64. package/src/test/mergers/uninstall-env-local.test.mjs +144 -0
  65. package/src/test/mergers/uninstall-gitignore.test.mjs +209 -0
  66. package/src/test/mergers/uninstall-settings.test.mjs +285 -0
  67. package/src/test/mergers/uninstall-vscode-mcp.test.mjs +215 -0
  68. package/src/test/mergers/uninstall-windsurf-mcp.test.mjs +122 -0
  69. package/src/lib/write-configs.mjs +0 -202
  70. package/src/test/e2e/HANDOFF.md +0 -223
  71. package/src/test/e2e/cli-init.test.mjs +0 -213
  72. package/src/test/e2e/payload-factory.mjs +0 -22
@@ -1,9 +1,29 @@
1
+ /**
2
+ * Unit tests for src/lib/mergers/gitignore.mjs (PR4 of #646).
3
+ *
4
+ * The v2.0.0 merger added a single `.claude/rules/skillrepo-*.md`
5
+ * pattern for the now-deleted rules delivery flow. PR4 rewrote it to
6
+ * add the three init-required paths: .env.local, .claude/skills/,
7
+ * and .claude/settings.local.json. These tests cover:
8
+ *
9
+ * - fresh .gitignore creation (all three entries in one block)
10
+ * - append when .gitignore exists but is missing entries
11
+ * - skip when every entry is already present
12
+ * - partial skip when some entries are present and some aren't
13
+ * - idempotent second run
14
+ * - line-ending preservation (CRLF vs LF)
15
+ * - line-exact matching (a comment containing ".env.local" does NOT
16
+ * satisfy the check)
17
+ */
18
+
1
19
  import { describe, it, beforeEach, afterEach } from "node:test";
2
20
  import assert from "node:assert/strict";
3
- import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, rmSync } from "node:fs";
21
+ import { mkdtempSync, writeFileSync, readFileSync, rmSync } from "node:fs";
4
22
  import { join } from "node:path";
5
23
  import { tmpdir } from "node:os";
6
24
 
25
+ import { mergeGitignore } from "../../lib/mergers/gitignore.mjs";
26
+
7
27
  let originalCwd;
8
28
  let tempDir;
9
29
 
@@ -18,46 +38,151 @@ afterEach(() => {
18
38
  rmSync(tempDir, { recursive: true, force: true });
19
39
  });
20
40
 
21
- describe("mergeGitignore", () => {
22
- it("creates .gitignore when it does not exist", async () => {
23
- const { mergeGitignore } = await import("../../lib/mergers/gitignore.mjs");
41
+ describe("mergeGitignore — creation", () => {
42
+ it("creates .gitignore with all three entries when file does not exist", () => {
24
43
  const result = mergeGitignore();
25
44
 
26
45
  assert.equal(result.action, "created");
46
+ assert.deepEqual(result.added.sort(), [
47
+ ".claude/settings.local.json",
48
+ ".claude/skills/",
49
+ ".env.local",
50
+ ]);
51
+
27
52
  const content = readFileSync(join(tempDir, ".gitignore"), "utf-8");
28
- assert.ok(content.includes(".claude/rules/skillrepo-*.md"));
29
- assert.ok(content.includes("# SkillRepo"));
53
+ assert.match(content, /^# SkillRepo CLI/m);
54
+ assert.match(content, /^\.env\.local$/m);
55
+ assert.match(content, /^\.claude\/skills\/$/m);
56
+ assert.match(content, /^\.claude\/settings\.local\.json$/m);
30
57
  });
58
+ });
31
59
 
32
- it("appends to existing .gitignore", async () => {
33
- const { mergeGitignore } = await import("../../lib/mergers/gitignore.mjs");
34
-
60
+ describe("mergeGitignore — append to existing file", () => {
61
+ it("appends all three entries when none are present", () => {
35
62
  writeFileSync(join(tempDir, ".gitignore"), "node_modules/\n.env\n");
36
63
  const result = mergeGitignore();
37
64
 
38
65
  assert.equal(result.action, "updated");
66
+ assert.equal(result.added.length, 3);
67
+
39
68
  const content = readFileSync(join(tempDir, ".gitignore"), "utf-8");
69
+ // Original content preserved at the start
40
70
  assert.ok(content.startsWith("node_modules/\n.env\n"));
41
- assert.ok(content.includes(".claude/rules/skillrepo-*.md"));
71
+ // All three entries present
72
+ assert.match(content, /^\.env\.local$/m);
73
+ assert.match(content, /^\.claude\/skills\/$/m);
74
+ assert.match(content, /^\.claude\/settings\.local\.json$/m);
42
75
  });
43
76
 
44
- it("skips when entry already exists", async () => {
45
- const { mergeGitignore } = await import("../../lib/mergers/gitignore.mjs");
46
-
47
- writeFileSync(join(tempDir, ".gitignore"), "node_modules/\n.claude/rules/skillrepo-*.md\n");
77
+ it("appends only missing entries when some are already present", () => {
78
+ // .env.local already exists only the other two should be added
79
+ writeFileSync(
80
+ join(tempDir, ".gitignore"),
81
+ "node_modules/\n.env.local\n",
82
+ );
48
83
  const result = mergeGitignore();
49
84
 
50
- assert.equal(result.action, "skipped");
51
- });
85
+ assert.equal(result.action, "updated");
86
+ assert.deepEqual(result.added.sort(), [
87
+ ".claude/settings.local.json",
88
+ ".claude/skills/",
89
+ ]);
52
90
 
53
- it("appends newline before section when existing file lacks trailing newline", async () => {
54
- const { mergeGitignore } = await import("../../lib/mergers/gitignore.mjs");
91
+ const content = readFileSync(join(tempDir, ".gitignore"), "utf-8");
92
+ // .env.local should NOT appear twice
93
+ const envLocalMatches = content.match(/^\.env\.local$/gm) ?? [];
94
+ assert.equal(envLocalMatches.length, 1, ".env.local must not be duplicated");
95
+ });
55
96
 
97
+ it("appends a newline before the section if the existing file lacks trailing newline", () => {
56
98
  writeFileSync(join(tempDir, ".gitignore"), "node_modules/");
57
99
  mergeGitignore();
58
100
 
59
101
  const content = readFileSync(join(tempDir, ".gitignore"), "utf-8");
60
- // Should have a newline before the SkillRepo section
61
- assert.ok(content.includes("node_modules/\n\n# SkillRepo"));
102
+ // Separator newline between existing content and the SkillRepo section
103
+ assert.match(content, /node_modules\/\n\n# SkillRepo/);
104
+ });
105
+
106
+ it("preserves CRLF line endings on existing files", () => {
107
+ writeFileSync(join(tempDir, ".gitignore"), "node_modules/\r\n.env\r\n");
108
+ mergeGitignore();
109
+
110
+ const content = readFileSync(join(tempDir, ".gitignore"), "utf-8");
111
+ // The SkillRepo section should use CRLF to match
112
+ assert.match(content, /# SkillRepo CLI.*\r\n\.env\.local\r\n/);
113
+ });
114
+ });
115
+
116
+ describe("mergeGitignore — idempotency", () => {
117
+ it("returns skipped when every entry is already present", () => {
118
+ writeFileSync(
119
+ join(tempDir, ".gitignore"),
120
+ [
121
+ "node_modules/",
122
+ ".env.local",
123
+ ".claude/skills/",
124
+ ".claude/settings.local.json",
125
+ ].join("\n") + "\n",
126
+ );
127
+ const result = mergeGitignore();
128
+
129
+ assert.equal(result.action, "skipped");
130
+ assert.equal(result.added.length, 0);
131
+ });
132
+
133
+ it("second run after creation is a no-op skip", () => {
134
+ const first = mergeGitignore();
135
+ assert.equal(first.action, "created");
136
+
137
+ const second = mergeGitignore();
138
+ assert.equal(second.action, "skipped");
139
+ assert.equal(second.added.length, 0);
140
+ });
141
+
142
+ it("third run after an append is also a no-op skip", () => {
143
+ writeFileSync(join(tempDir, ".gitignore"), "node_modules/\n");
144
+ const first = mergeGitignore();
145
+ assert.equal(first.action, "updated");
146
+
147
+ const second = mergeGitignore();
148
+ assert.equal(second.action, "skipped");
149
+ });
150
+ });
151
+
152
+ describe("mergeGitignore — line-exact matching", () => {
153
+ it("does NOT count a comment containing .env.local as satisfied", () => {
154
+ writeFileSync(
155
+ join(tempDir, ".gitignore"),
156
+ "# mention of .env.local in a comment\n",
157
+ );
158
+ const result = mergeGitignore();
159
+
160
+ // The comment line is not a literal .env.local line, so the
161
+ // merger should still add it.
162
+ assert.equal(result.action, "updated");
163
+ assert.ok(result.added.includes(".env.local"));
164
+ });
165
+
166
+ it("does NOT count .env.local.backup as satisfying .env.local", () => {
167
+ writeFileSync(
168
+ join(tempDir, ".gitignore"),
169
+ ".env.local.backup\n",
170
+ );
171
+ const result = mergeGitignore();
172
+
173
+ assert.equal(result.action, "updated");
174
+ assert.ok(result.added.includes(".env.local"));
175
+ });
176
+
177
+ it("tolerates leading/trailing whitespace on existing lines", () => {
178
+ // Some editors add trailing whitespace. The merger trims on
179
+ // compare so these lines should still count as matches.
180
+ writeFileSync(
181
+ join(tempDir, ".gitignore"),
182
+ " .env.local \n .claude/skills/\n.claude/settings.local.json\n",
183
+ );
184
+ const result = mergeGitignore();
185
+
186
+ assert.equal(result.action, "skipped");
62
187
  });
63
188
  });