claudekit-cli 1.4.0 → 1.5.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 (50) hide show
  1. package/bin/ck-darwin-arm64 +0 -0
  2. package/bin/ck-darwin-x64 +0 -0
  3. package/bin/ck-linux-x64 +0 -0
  4. package/bin/ck-win32-x64.exe +0 -0
  5. package/package.json +8 -2
  6. package/scripts/postinstall.js +74 -0
  7. package/.github/workflows/ci.yml +0 -45
  8. package/.github/workflows/claude-code-review.yml +0 -57
  9. package/.github/workflows/claude.yml +0 -50
  10. package/.github/workflows/release.yml +0 -102
  11. package/.releaserc.json +0 -17
  12. package/.repomixignore +0 -15
  13. package/AGENTS.md +0 -217
  14. package/CHANGELOG.md +0 -88
  15. package/CLAUDE.md +0 -34
  16. package/biome.json +0 -28
  17. package/bun.lock +0 -863
  18. package/dist/index.js +0 -22489
  19. package/src/commands/new.ts +0 -185
  20. package/src/commands/update.ts +0 -174
  21. package/src/commands/version.ts +0 -135
  22. package/src/index.ts +0 -102
  23. package/src/lib/auth.ts +0 -157
  24. package/src/lib/download.ts +0 -654
  25. package/src/lib/github.ts +0 -230
  26. package/src/lib/merge.ts +0 -116
  27. package/src/lib/prompts.ts +0 -114
  28. package/src/types.ts +0 -171
  29. package/src/utils/config.ts +0 -87
  30. package/src/utils/file-scanner.ts +0 -134
  31. package/src/utils/logger.ts +0 -124
  32. package/src/utils/safe-prompts.ts +0 -44
  33. package/src/utils/safe-spinner.ts +0 -38
  34. package/src/version.json +0 -3
  35. package/test-integration/demo/.mcp.json +0 -13
  36. package/test-integration/demo/.repomixignore +0 -15
  37. package/test-integration/demo/CLAUDE.md +0 -34
  38. package/tests/commands/version.test.ts +0 -297
  39. package/tests/integration/cli.test.ts +0 -252
  40. package/tests/lib/auth.test.ts +0 -116
  41. package/tests/lib/download.test.ts +0 -292
  42. package/tests/lib/github-download-priority.test.ts +0 -432
  43. package/tests/lib/github.test.ts +0 -52
  44. package/tests/lib/merge.test.ts +0 -215
  45. package/tests/lib/prompts.test.ts +0 -66
  46. package/tests/types.test.ts +0 -337
  47. package/tests/utils/config.test.ts +0 -263
  48. package/tests/utils/file-scanner.test.ts +0 -202
  49. package/tests/utils/logger.test.ts +0 -239
  50. package/tsconfig.json +0 -30
@@ -1,432 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import { GitHubClient } from "../../src/lib/github.js";
3
- import type { GitHubRelease } from "../../src/types.js";
4
-
5
- describe("GitHubClient - Asset Download Priority", () => {
6
- describe("getDownloadableAsset", () => {
7
- test("should prioritize ClaudeKit Engineer Package zip file", () => {
8
- const release: GitHubRelease = {
9
- id: 1,
10
- tag_name: "v1.0.0",
11
- name: "Release 1.0.0",
12
- draft: false,
13
- prerelease: false,
14
- tarball_url: "https://api.github.com/repos/test/repo/tarball/v1.0.0",
15
- zipball_url: "https://api.github.com/repos/test/repo/zipball/v1.0.0",
16
- assets: [
17
- {
18
- id: 1,
19
- name: "other-file.tar.gz",
20
- url: "https://api.github.com/repos/test/repo/releases/assets/1",
21
- browser_download_url: "https://github.com/test/other-file.tar.gz",
22
- size: 1024,
23
- content_type: "application/gzip",
24
- },
25
- {
26
- id: 2,
27
- name: "ClaudeKit-Engineer-Package.zip",
28
- url: "https://api.github.com/repos/test/repo/releases/assets/2",
29
- browser_download_url: "https://github.com/test/claudekit-package.zip",
30
- size: 2048,
31
- content_type: "application/zip",
32
- },
33
- ],
34
- };
35
-
36
- const result = GitHubClient.getDownloadableAsset(release);
37
-
38
- expect(result.type).toBe("asset");
39
- expect(result.name).toBe("ClaudeKit-Engineer-Package.zip");
40
- expect(result.url).toBe("https://api.github.com/repos/test/repo/releases/assets/2");
41
- expect(result.size).toBe(2048);
42
- });
43
-
44
- test("should prioritize ClaudeKit Marketing Package zip file", () => {
45
- const release: GitHubRelease = {
46
- id: 1,
47
- tag_name: "v1.0.0",
48
- name: "Release 1.0.0",
49
- draft: false,
50
- prerelease: false,
51
- tarball_url: "https://api.github.com/repos/test/repo/tarball/v1.0.0",
52
- zipball_url: "https://api.github.com/repos/test/repo/zipball/v1.0.0",
53
- assets: [
54
- {
55
- id: 1,
56
- name: "random.zip",
57
- url: "https://api.github.com/repos/test/repo/releases/assets/1",
58
- browser_download_url: "https://github.com/test/random.zip",
59
- size: 512,
60
- content_type: "application/zip",
61
- },
62
- {
63
- id: 2,
64
- name: "ClaudeKit-Marketing-Package.zip",
65
- url: "https://api.github.com/repos/test/repo/releases/assets/2",
66
- browser_download_url: "https://github.com/test/marketing-package.zip",
67
- size: 2048,
68
- content_type: "application/zip",
69
- },
70
- ],
71
- };
72
-
73
- const result = GitHubClient.getDownloadableAsset(release);
74
-
75
- expect(result.type).toBe("asset");
76
- expect(result.name).toBe("ClaudeKit-Marketing-Package.zip");
77
- expect(result.url).toBe("https://api.github.com/repos/test/repo/releases/assets/2");
78
- });
79
-
80
- test("should match ClaudeKit package case-insensitively", () => {
81
- const release: GitHubRelease = {
82
- id: 1,
83
- tag_name: "v1.0.0",
84
- name: "Release 1.0.0",
85
- draft: false,
86
- prerelease: false,
87
- tarball_url: "https://api.github.com/repos/test/repo/tarball/v1.0.0",
88
- zipball_url: "https://api.github.com/repos/test/repo/zipball/v1.0.0",
89
- assets: [
90
- {
91
- id: 1,
92
- name: "claudekit-engineer-package.zip",
93
- url: "https://api.github.com/repos/test/repo/releases/assets/1",
94
- browser_download_url: "https://github.com/test/package.zip",
95
- size: 2048,
96
- content_type: "application/zip",
97
- },
98
- ],
99
- };
100
-
101
- const result = GitHubClient.getDownloadableAsset(release);
102
-
103
- expect(result.type).toBe("asset");
104
- expect(result.name).toBe("claudekit-engineer-package.zip");
105
- });
106
-
107
- test("should fallback to other zip files if no ClaudeKit package found", () => {
108
- const release: GitHubRelease = {
109
- id: 1,
110
- tag_name: "v1.0.0",
111
- name: "Release 1.0.0",
112
- draft: false,
113
- prerelease: false,
114
- tarball_url: "https://api.github.com/repos/test/repo/tarball/v1.0.0",
115
- zipball_url: "https://api.github.com/repos/test/repo/zipball/v1.0.0",
116
- assets: [
117
- {
118
- id: 1,
119
- name: "release-package.zip",
120
- url: "https://api.github.com/repos/test/repo/releases/assets/1",
121
- browser_download_url: "https://github.com/test/release.zip",
122
- size: 1024,
123
- content_type: "application/zip",
124
- },
125
- ],
126
- };
127
-
128
- const result = GitHubClient.getDownloadableAsset(release);
129
-
130
- expect(result.type).toBe("asset");
131
- expect(result.name).toBe("release-package.zip");
132
- });
133
-
134
- test("should fallback to tar.gz files if no zip found", () => {
135
- const release: GitHubRelease = {
136
- id: 1,
137
- tag_name: "v1.0.0",
138
- name: "Release 1.0.0",
139
- draft: false,
140
- prerelease: false,
141
- tarball_url: "https://api.github.com/repos/test/repo/tarball/v1.0.0",
142
- zipball_url: "https://api.github.com/repos/test/repo/zipball/v1.0.0",
143
- assets: [
144
- {
145
- id: 1,
146
- name: "release.tar.gz",
147
- url: "https://api.github.com/repos/test/repo/releases/assets/1",
148
- browser_download_url: "https://github.com/test/release.tar.gz",
149
- size: 1024,
150
- content_type: "application/gzip",
151
- },
152
- ],
153
- };
154
-
155
- const result = GitHubClient.getDownloadableAsset(release);
156
-
157
- expect(result.type).toBe("asset");
158
- expect(result.name).toBe("release.tar.gz");
159
- });
160
-
161
- test("should fallback to tgz files", () => {
162
- const release: GitHubRelease = {
163
- id: 1,
164
- tag_name: "v1.0.0",
165
- name: "Release 1.0.0",
166
- draft: false,
167
- prerelease: false,
168
- tarball_url: "https://api.github.com/repos/test/repo/tarball/v1.0.0",
169
- zipball_url: "https://api.github.com/repos/test/repo/zipball/v1.0.0",
170
- assets: [
171
- {
172
- id: 1,
173
- name: "release.tgz",
174
- url: "https://api.github.com/repos/test/repo/releases/assets/1",
175
- browser_download_url: "https://github.com/test/release.tgz",
176
- size: 1024,
177
- content_type: "application/gzip",
178
- },
179
- ],
180
- };
181
-
182
- const result = GitHubClient.getDownloadableAsset(release);
183
-
184
- expect(result.type).toBe("asset");
185
- expect(result.name).toBe("release.tgz");
186
- });
187
-
188
- test("should fallback to GitHub automatic tarball if no assets", () => {
189
- const release: GitHubRelease = {
190
- id: 1,
191
- tag_name: "v1.0.0",
192
- name: "Release 1.0.0",
193
- draft: false,
194
- prerelease: false,
195
- tarball_url: "https://api.github.com/repos/test/repo/tarball/v1.0.0",
196
- zipball_url: "https://api.github.com/repos/test/repo/zipball/v1.0.0",
197
- assets: [],
198
- };
199
-
200
- const result = GitHubClient.getDownloadableAsset(release);
201
-
202
- expect(result.type).toBe("tarball");
203
- expect(result.url).toBe("https://api.github.com/repos/test/repo/tarball/v1.0.0");
204
- expect(result.name).toBe("v1.0.0.tar.gz");
205
- expect(result.size).toBeUndefined();
206
- });
207
-
208
- test("should fallback to tarball if assets have no archive files", () => {
209
- const release: GitHubRelease = {
210
- id: 1,
211
- tag_name: "v1.0.0",
212
- name: "Release 1.0.0",
213
- draft: false,
214
- prerelease: false,
215
- tarball_url: "https://api.github.com/repos/test/repo/tarball/v1.0.0",
216
- zipball_url: "https://api.github.com/repos/test/repo/zipball/v1.0.0",
217
- assets: [
218
- {
219
- id: 1,
220
- name: "README.md",
221
- url: "https://api.github.com/repos/test/repo/releases/assets/1",
222
- browser_download_url: "https://github.com/test/README.md",
223
- size: 128,
224
- content_type: "text/markdown",
225
- },
226
- {
227
- id: 2,
228
- name: "checksums.txt",
229
- url: "https://api.github.com/repos/test/repo/releases/assets/2",
230
- browser_download_url: "https://github.com/test/checksums.txt",
231
- size: 64,
232
- content_type: "text/plain",
233
- },
234
- ],
235
- };
236
-
237
- const result = GitHubClient.getDownloadableAsset(release);
238
-
239
- expect(result.type).toBe("tarball");
240
- expect(result.url).toBe("https://api.github.com/repos/test/repo/tarball/v1.0.0");
241
- });
242
-
243
- test("should prioritize ClaudeKit package over other archives", () => {
244
- const release: GitHubRelease = {
245
- id: 1,
246
- tag_name: "v1.0.0",
247
- name: "Release 1.0.0",
248
- draft: false,
249
- prerelease: false,
250
- tarball_url: "https://api.github.com/repos/test/repo/tarball/v1.0.0",
251
- zipball_url: "https://api.github.com/repos/test/repo/zipball/v1.0.0",
252
- assets: [
253
- {
254
- id: 1,
255
- name: "source.tar.gz",
256
- url: "https://api.github.com/repos/test/repo/releases/assets/1",
257
- browser_download_url: "https://github.com/test/source.tar.gz",
258
- size: 5000,
259
- content_type: "application/gzip",
260
- },
261
- {
262
- id: 2,
263
- name: "docs.zip",
264
- url: "https://api.github.com/repos/test/repo/releases/assets/2",
265
- browser_download_url: "https://github.com/test/docs.zip",
266
- size: 3000,
267
- content_type: "application/zip",
268
- },
269
- {
270
- id: 3,
271
- name: "ClaudeKit-Engineer-Package.zip",
272
- url: "https://api.github.com/repos/test/repo/releases/assets/3",
273
- browser_download_url: "https://github.com/test/package.zip",
274
- size: 2000,
275
- content_type: "application/zip",
276
- },
277
- ],
278
- };
279
-
280
- const result = GitHubClient.getDownloadableAsset(release);
281
-
282
- // Should pick the ClaudeKit package even though it's listed last
283
- expect(result.type).toBe("asset");
284
- expect(result.name).toBe("ClaudeKit-Engineer-Package.zip");
285
- expect(result.size).toBe(2000);
286
- });
287
-
288
- test("should handle assets with variations in naming", () => {
289
- const release: GitHubRelease = {
290
- id: 1,
291
- tag_name: "v1.0.0",
292
- name: "Release 1.0.0",
293
- draft: false,
294
- prerelease: false,
295
- tarball_url: "https://api.github.com/repos/test/repo/tarball/v1.0.0",
296
- zipball_url: "https://api.github.com/repos/test/repo/zipball/v1.0.0",
297
- assets: [
298
- {
299
- id: 1,
300
- name: "claudekit_marketing_package.zip",
301
- url: "https://api.github.com/repos/test/repo/releases/assets/1",
302
- browser_download_url: "https://github.com/test/package.zip",
303
- size: 2000,
304
- content_type: "application/zip",
305
- },
306
- ],
307
- };
308
-
309
- const result = GitHubClient.getDownloadableAsset(release);
310
-
311
- expect(result.type).toBe("asset");
312
- expect(result.name).toBe("claudekit_marketing_package.zip");
313
- });
314
-
315
- test("should handle assets with spaces in name", () => {
316
- const release: GitHubRelease = {
317
- id: 1,
318
- tag_name: "v1.4.0",
319
- name: "Release 1.4.0",
320
- draft: false,
321
- prerelease: false,
322
- tarball_url: "https://api.github.com/repos/test/repo/tarball/v1.4.0",
323
- zipball_url: "https://api.github.com/repos/test/repo/zipball/v1.4.0",
324
- assets: [
325
- {
326
- id: 1,
327
- name: "Changelog",
328
- url: "https://api.github.com/repos/test/repo/releases/assets/1",
329
- browser_download_url: "https://github.com/test/changelog",
330
- size: 7979,
331
- content_type: "text/plain",
332
- },
333
- {
334
- id: 2,
335
- name: "ClaudeKit Engineer Package.zip",
336
- url: "https://api.github.com/repos/test/repo/releases/assets/2",
337
- browser_download_url: "https://github.com/test/package.zip",
338
- size: 3334963,
339
- content_type: "application/zip",
340
- },
341
- ],
342
- };
343
-
344
- const result = GitHubClient.getDownloadableAsset(release);
345
-
346
- expect(result.type).toBe("asset");
347
- expect(result.name).toBe("ClaudeKit Engineer Package.zip");
348
- expect(result.size).toBe(3334963);
349
- });
350
-
351
- test("should exclude assets named 'Source code' or starting with 'source'", () => {
352
- const release: GitHubRelease = {
353
- id: 1,
354
- tag_name: "v1.0.0",
355
- name: "Release 1.0.0",
356
- draft: false,
357
- prerelease: false,
358
- tarball_url: "https://api.github.com/repos/test/repo/tarball/v1.0.0",
359
- zipball_url: "https://api.github.com/repos/test/repo/zipball/v1.0.0",
360
- assets: [
361
- {
362
- id: 1,
363
- name: "Source code.zip",
364
- url: "https://api.github.com/repos/test/repo/releases/assets/1",
365
- browser_download_url: "https://github.com/test/source.zip",
366
- size: 5000,
367
- content_type: "application/zip",
368
- },
369
- {
370
- id: 2,
371
- name: "source-archive.tar.gz",
372
- url: "https://api.github.com/repos/test/repo/releases/assets/2",
373
- browser_download_url: "https://github.com/test/source.tar.gz",
374
- size: 4500,
375
- content_type: "application/gzip",
376
- },
377
- ],
378
- };
379
-
380
- const result = GitHubClient.getDownloadableAsset(release);
381
-
382
- // Should fall back to tarball instead of using "Source code" assets
383
- expect(result.type).toBe("tarball");
384
- expect(result.url).toBe("https://api.github.com/repos/test/repo/tarball/v1.0.0");
385
- });
386
-
387
- test("should prioritize ClaudeKit package over source code archives", () => {
388
- const release: GitHubRelease = {
389
- id: 1,
390
- tag_name: "v1.4.0",
391
- name: "Release 1.4.0",
392
- draft: false,
393
- prerelease: false,
394
- tarball_url: "https://api.github.com/repos/test/repo/tarball/v1.4.0",
395
- zipball_url: "https://api.github.com/repos/test/repo/zipball/v1.4.0",
396
- assets: [
397
- {
398
- id: 1,
399
- name: "Source code.zip",
400
- url: "https://api.github.com/repos/test/repo/releases/assets/1",
401
- browser_download_url: "https://github.com/test/source.zip",
402
- size: 5000,
403
- content_type: "application/zip",
404
- },
405
- {
406
- id: 2,
407
- name: "ClaudeKit Engineer Package.zip",
408
- url: "https://api.github.com/repos/test/repo/releases/assets/2",
409
- browser_download_url: "https://github.com/test/package.zip",
410
- size: 3334963,
411
- content_type: "application/zip",
412
- },
413
- {
414
- id: 3,
415
- name: "Source code.tar.gz",
416
- url: "https://api.github.com/repos/test/repo/releases/assets/3",
417
- browser_download_url: "https://github.com/test/source.tar.gz",
418
- size: 4500,
419
- content_type: "application/gzip",
420
- },
421
- ],
422
- };
423
-
424
- const result = GitHubClient.getDownloadableAsset(release);
425
-
426
- // Should pick the ClaudeKit package and ignore source code archives
427
- expect(result.type).toBe("asset");
428
- expect(result.name).toBe("ClaudeKit Engineer Package.zip");
429
- expect(result.size).toBe(3334963);
430
- });
431
- });
432
- });
@@ -1,52 +0,0 @@
1
- import { beforeEach, describe, expect, mock, test } from "bun:test";
2
- import { GitHubClient } from "../../src/lib/github.js";
3
- import { AVAILABLE_KITS, GitHubError } from "../../src/types.js";
4
-
5
- describe("GitHubClient", () => {
6
- let client: GitHubClient;
7
-
8
- beforeEach(() => {
9
- client = new GitHubClient();
10
- // Set environment variable to avoid auth prompts during tests
11
- process.env.GITHUB_TOKEN = "ghp_test_token_for_testing";
12
- });
13
-
14
- describe("constructor", () => {
15
- test("should create GitHubClient instance", () => {
16
- expect(client).toBeInstanceOf(GitHubClient);
17
- });
18
- });
19
-
20
- describe("error handling", () => {
21
- test("GitHubError should contain message and status code", () => {
22
- const error = new GitHubError("Test error", 404);
23
- expect(error.message).toBe("Test error");
24
- expect(error.statusCode).toBe(404);
25
- expect(error.code).toBe("GITHUB_ERROR");
26
- expect(error.name).toBe("GitHubError");
27
- });
28
-
29
- test("GitHubError should work without status code", () => {
30
- const error = new GitHubError("Test error");
31
- expect(error.message).toBe("Test error");
32
- expect(error.statusCode).toBeUndefined();
33
- });
34
- });
35
-
36
- describe("integration scenarios", () => {
37
- test("should handle kit configuration correctly", () => {
38
- const engineerKit = AVAILABLE_KITS.engineer;
39
- expect(engineerKit.owner).toBe("claudekit");
40
- expect(engineerKit.repo).toBe("claudekit-engineer");
41
- });
42
-
43
- test("should handle marketing kit configuration", () => {
44
- const marketingKit = AVAILABLE_KITS.marketing;
45
- expect(marketingKit.owner).toBe("claudekit");
46
- expect(marketingKit.repo).toBe("claudekit-marketing");
47
- });
48
- });
49
-
50
- // Note: Actual API tests would require mocking Octokit or using a test fixture
51
- // We're keeping these tests simple to avoid external dependencies
52
- });
@@ -1,215 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
- import { existsSync } from "node:fs";
3
- import { mkdir, rm, writeFile } from "node:fs/promises";
4
- import { tmpdir } from "node:os";
5
- import { join } from "node:path";
6
- import { FileMerger } from "../../src/lib/merge.js";
7
-
8
- describe("FileMerger", () => {
9
- let merger: FileMerger;
10
- let testSourceDir: string;
11
- let testDestDir: string;
12
-
13
- beforeEach(async () => {
14
- merger = new FileMerger();
15
-
16
- // Create temporary test directories
17
- const timestamp = Date.now();
18
- testSourceDir = join(tmpdir(), `test-source-${timestamp}`);
19
- testDestDir = join(tmpdir(), `test-dest-${timestamp}`);
20
-
21
- await mkdir(testSourceDir, { recursive: true });
22
- await mkdir(testDestDir, { recursive: true });
23
- });
24
-
25
- afterEach(async () => {
26
- // Cleanup test directories
27
- if (existsSync(testSourceDir)) {
28
- await rm(testSourceDir, { recursive: true, force: true });
29
- }
30
- if (existsSync(testDestDir)) {
31
- await rm(testDestDir, { recursive: true, force: true });
32
- }
33
- });
34
-
35
- describe("constructor", () => {
36
- test("should create FileMerger instance", () => {
37
- expect(merger).toBeInstanceOf(FileMerger);
38
- });
39
- });
40
-
41
- describe("addIgnorePatterns", () => {
42
- test("should add custom ignore patterns", () => {
43
- const patterns = ["*.log", "temp/**"];
44
- expect(() => merger.addIgnorePatterns(patterns)).not.toThrow();
45
- });
46
-
47
- test("should accept empty array", () => {
48
- expect(() => merger.addIgnorePatterns([])).not.toThrow();
49
- });
50
- });
51
-
52
- describe("merge with skipConfirmation", () => {
53
- test("should copy files from source to destination", async () => {
54
- // Create test files
55
- await writeFile(join(testSourceDir, "test.txt"), "test content");
56
- await writeFile(join(testSourceDir, "readme.md"), "# README");
57
-
58
- await merger.merge(testSourceDir, testDestDir, true);
59
-
60
- // Verify files were copied
61
- expect(existsSync(join(testDestDir, "test.txt"))).toBe(true);
62
- expect(existsSync(join(testDestDir, "readme.md"))).toBe(true);
63
- });
64
-
65
- test("should skip protected files like .env", async () => {
66
- // Create test files including protected ones
67
- await writeFile(join(testSourceDir, "normal.txt"), "normal");
68
- await writeFile(join(testSourceDir, ".env"), "SECRET=value");
69
-
70
- await merger.merge(testSourceDir, testDestDir, true);
71
-
72
- // Verify normal file was copied but .env was not
73
- expect(existsSync(join(testDestDir, "normal.txt"))).toBe(true);
74
- expect(existsSync(join(testDestDir, ".env"))).toBe(false);
75
- });
76
-
77
- test("should skip protected patterns like *.key", async () => {
78
- await writeFile(join(testSourceDir, "normal.txt"), "normal");
79
- await writeFile(join(testSourceDir, "private.key"), "key data");
80
-
81
- await merger.merge(testSourceDir, testDestDir, true);
82
-
83
- expect(existsSync(join(testDestDir, "normal.txt"))).toBe(true);
84
- expect(existsSync(join(testDestDir, "private.key"))).toBe(false);
85
- });
86
-
87
- test("should handle nested directories", async () => {
88
- const nestedDir = join(testSourceDir, "nested", "deep");
89
- await mkdir(nestedDir, { recursive: true });
90
- await writeFile(join(nestedDir, "file.txt"), "nested content");
91
-
92
- await merger.merge(testSourceDir, testDestDir, true);
93
-
94
- expect(existsSync(join(testDestDir, "nested", "deep", "file.txt"))).toBe(true);
95
- });
96
-
97
- test("should overwrite existing files", async () => {
98
- // Create files in both directories
99
- await writeFile(join(testSourceDir, "file.txt"), "new content");
100
- await writeFile(join(testDestDir, "file.txt"), "old content");
101
-
102
- await merger.merge(testSourceDir, testDestDir, true);
103
-
104
- const content = await Bun.file(join(testDestDir, "file.txt")).text();
105
- expect(content).toBe("new content");
106
- });
107
-
108
- test("should handle empty source directory", async () => {
109
- // Empty directory should complete without errors
110
- await merger.merge(testSourceDir, testDestDir, true);
111
- // If we get here, the test passed
112
- expect(true).toBe(true);
113
- });
114
- });
115
-
116
- describe("edge cases", () => {
117
- test("should handle files with special characters in names", async () => {
118
- const specialFileName = "file with spaces.txt";
119
- await writeFile(join(testSourceDir, specialFileName), "content");
120
-
121
- await merger.merge(testSourceDir, testDestDir, true);
122
-
123
- expect(existsSync(join(testDestDir, specialFileName))).toBe(true);
124
- });
125
-
126
- test("should skip custom ignore patterns", async () => {
127
- merger.addIgnorePatterns(["custom-*"]);
128
-
129
- await writeFile(join(testSourceDir, "normal.txt"), "normal");
130
- await writeFile(join(testSourceDir, "custom-ignore.txt"), "ignore me");
131
-
132
- await merger.merge(testSourceDir, testDestDir, true);
133
-
134
- expect(existsSync(join(testDestDir, "normal.txt"))).toBe(true);
135
- expect(existsSync(join(testDestDir, "custom-ignore.txt"))).toBe(false);
136
- });
137
- });
138
-
139
- describe("custom .claude file preservation", () => {
140
- test("should preserve custom .claude files when patterns are added", async () => {
141
- // Create .claude directories
142
- const sourceClaudeDir = join(testSourceDir, ".claude");
143
- const destClaudeDir = join(testDestDir, ".claude");
144
- await mkdir(sourceClaudeDir, { recursive: true });
145
- await mkdir(destClaudeDir, { recursive: true });
146
-
147
- // Create files in source (from release package)
148
- await writeFile(join(sourceClaudeDir, "standard.md"), "standard content");
149
-
150
- // Create files in destination (existing + custom)
151
- await writeFile(join(destClaudeDir, "standard.md"), "old standard content");
152
- await writeFile(join(destClaudeDir, "custom.md"), "custom content");
153
-
154
- // Add custom file to ignore patterns (this would be done by update.ts)
155
- merger.addIgnorePatterns([".claude/custom.md"]);
156
-
157
- await merger.merge(testSourceDir, testDestDir, true);
158
-
159
- // Standard file should be overwritten
160
- const standardContent = await Bun.file(join(destClaudeDir, "standard.md")).text();
161
- expect(standardContent).toBe("standard content");
162
-
163
- // Custom file should be preserved
164
- expect(existsSync(join(destClaudeDir, "custom.md"))).toBe(true);
165
- const customContent = await Bun.file(join(destClaudeDir, "custom.md")).text();
166
- expect(customContent).toBe("custom content");
167
- });
168
-
169
- test("should preserve nested custom .claude files", async () => {
170
- // Create nested .claude structure
171
- const sourceCommandsDir = join(testSourceDir, ".claude", "commands");
172
- const destCommandsDir = join(testDestDir, ".claude", "commands");
173
- await mkdir(sourceCommandsDir, { recursive: true });
174
- await mkdir(destCommandsDir, { recursive: true });
175
-
176
- // Create standard file in source
177
- await writeFile(join(sourceCommandsDir, "standard-cmd.md"), "standard command");
178
-
179
- // Create custom file in destination
180
- await writeFile(join(destCommandsDir, "custom-cmd.md"), "custom command");
181
-
182
- // Add custom file to ignore patterns
183
- merger.addIgnorePatterns([".claude/commands/custom-cmd.md"]);
184
-
185
- await merger.merge(testSourceDir, testDestDir, true);
186
-
187
- // Custom file should be preserved
188
- expect(existsSync(join(destCommandsDir, "custom-cmd.md"))).toBe(true);
189
- const customContent = await Bun.file(join(destCommandsDir, "custom-cmd.md")).text();
190
- expect(customContent).toBe("custom command");
191
- });
192
-
193
- test("should preserve multiple custom .claude files", async () => {
194
- const sourceClaudeDir = join(testSourceDir, ".claude");
195
- const destClaudeDir = join(testDestDir, ".claude");
196
- await mkdir(sourceClaudeDir, { recursive: true });
197
- await mkdir(destClaudeDir, { recursive: true });
198
-
199
- // Create multiple custom files in destination
200
- await writeFile(join(destClaudeDir, "custom1.md"), "custom1");
201
- await writeFile(join(destClaudeDir, "custom2.md"), "custom2");
202
- await writeFile(join(destClaudeDir, "custom3.md"), "custom3");
203
-
204
- // Add all custom files to ignore patterns
205
- merger.addIgnorePatterns([".claude/custom1.md", ".claude/custom2.md", ".claude/custom3.md"]);
206
-
207
- await merger.merge(testSourceDir, testDestDir, true);
208
-
209
- // All custom files should be preserved
210
- expect(existsSync(join(destClaudeDir, "custom1.md"))).toBe(true);
211
- expect(existsSync(join(destClaudeDir, "custom2.md"))).toBe(true);
212
- expect(existsSync(join(destClaudeDir, "custom3.md"))).toBe(true);
213
- });
214
- });
215
- });