claudekit-cli 1.0.1 → 1.2.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 (69) hide show
  1. package/.github/workflows/ci.yml +2 -0
  2. package/.github/workflows/release.yml +44 -0
  3. package/CHANGELOG.md +28 -0
  4. package/CLAUDE.md +3 -2
  5. package/LICENSE +21 -0
  6. package/README.md +73 -3
  7. package/dist/index.js +11556 -10926
  8. package/package.json +1 -1
  9. package/src/commands/new.ts +41 -9
  10. package/src/commands/update.ts +59 -13
  11. package/src/commands/version.ts +135 -0
  12. package/src/index.ts +53 -1
  13. package/src/lib/download.ts +231 -1
  14. package/src/lib/github.ts +56 -0
  15. package/src/lib/prompts.ts +4 -3
  16. package/src/types.ts +11 -2
  17. package/src/utils/file-scanner.ts +134 -0
  18. package/src/utils/logger.ts +108 -21
  19. package/src/utils/safe-prompts.ts +54 -0
  20. package/tests/commands/version.test.ts +297 -0
  21. package/tests/lib/github-download-priority.test.ts +301 -0
  22. package/tests/lib/github.test.ts +2 -2
  23. package/tests/lib/merge.test.ts +77 -0
  24. package/tests/types.test.ts +4 -0
  25. package/tests/utils/file-scanner.test.ts +202 -0
  26. package/tests/utils/logger.test.ts +115 -0
  27. package/.opencode/agent/code-reviewer.md +0 -141
  28. package/.opencode/agent/debugger.md +0 -74
  29. package/.opencode/agent/docs-manager.md +0 -119
  30. package/.opencode/agent/git-manager.md +0 -60
  31. package/.opencode/agent/planner-researcher.md +0 -100
  32. package/.opencode/agent/planner.md +0 -87
  33. package/.opencode/agent/project-manager.md +0 -113
  34. package/.opencode/agent/researcher.md +0 -173
  35. package/.opencode/agent/solution-brainstormer.md +0 -89
  36. package/.opencode/agent/system-architecture.md +0 -192
  37. package/.opencode/agent/tester.md +0 -96
  38. package/.opencode/agent/ui-ux-designer.md +0 -203
  39. package/.opencode/agent/ui-ux-developer.md +0 -97
  40. package/.opencode/command/cook.md +0 -7
  41. package/.opencode/command/debug.md +0 -10
  42. package/.opencode/command/design/3d.md +0 -65
  43. package/.opencode/command/design/fast.md +0 -18
  44. package/.opencode/command/design/good.md +0 -21
  45. package/.opencode/command/design/screenshot.md +0 -22
  46. package/.opencode/command/design/video.md +0 -22
  47. package/.opencode/command/fix/ci.md +0 -8
  48. package/.opencode/command/fix/fast.md +0 -11
  49. package/.opencode/command/fix/hard.md +0 -15
  50. package/.opencode/command/fix/logs.md +0 -16
  51. package/.opencode/command/fix/test.md +0 -18
  52. package/.opencode/command/fix/types.md +0 -10
  53. package/.opencode/command/git/cm.md +0 -5
  54. package/.opencode/command/git/cp.md +0 -4
  55. package/.opencode/command/plan/ci.md +0 -12
  56. package/.opencode/command/plan/two.md +0 -13
  57. package/.opencode/command/plan.md +0 -10
  58. package/.opencode/command/test.md +0 -7
  59. package/.opencode/command/watzup.md +0 -8
  60. package/plans/251008-claudekit-cli-implementation-plan.md +0 -1469
  61. package/plans/reports/251008-from-code-reviewer-to-developer-review-report.md +0 -864
  62. package/plans/reports/251008-from-tester-to-developer-test-summary-report.md +0 -409
  63. package/plans/reports/251008-researcher-download-extraction-report.md +0 -1377
  64. package/plans/reports/251008-researcher-github-api-report.md +0 -1339
  65. package/plans/research/251008-cli-frameworks-bun-research.md +0 -1051
  66. package/plans/templates/bug-fix-template.md +0 -69
  67. package/plans/templates/feature-implementation-template.md +0 -84
  68. package/plans/templates/refactor-template.md +0 -82
  69. package/plans/templates/template-usage-guide.md +0 -58
@@ -0,0 +1,297 @@
1
+ import { beforeEach, describe, expect, mock, test } from "bun:test";
2
+ import type { GitHubRelease } from "../../src/types.js";
3
+ import { AVAILABLE_KITS, VersionCommandOptionsSchema } from "../../src/types.js";
4
+
5
+ describe("Version Command", () => {
6
+ beforeEach(() => {
7
+ // Set environment variable to avoid auth prompts during tests
8
+ process.env.GITHUB_TOKEN = "ghp_test_token_for_testing";
9
+ });
10
+
11
+ describe("VersionCommandOptionsSchema", () => {
12
+ test("should accept valid options with kit filter", () => {
13
+ const options = { kit: "engineer" as const };
14
+ const result = VersionCommandOptionsSchema.parse(options);
15
+ expect(result.kit).toBe("engineer");
16
+ });
17
+
18
+ test("should accept valid options with limit", () => {
19
+ const options = { limit: 10 };
20
+ const result = VersionCommandOptionsSchema.parse(options);
21
+ expect(result.limit).toBe(10);
22
+ });
23
+
24
+ test("should accept valid options with all flag", () => {
25
+ const options = { all: true };
26
+ const result = VersionCommandOptionsSchema.parse(options);
27
+ expect(result.all).toBe(true);
28
+ });
29
+
30
+ test("should accept all options combined", () => {
31
+ const options = { kit: "marketing" as const, limit: 20, all: true };
32
+ const result = VersionCommandOptionsSchema.parse(options);
33
+ expect(result.kit).toBe("marketing");
34
+ expect(result.limit).toBe(20);
35
+ expect(result.all).toBe(true);
36
+ });
37
+
38
+ test("should accept empty options", () => {
39
+ const options = {};
40
+ const result = VersionCommandOptionsSchema.parse(options);
41
+ expect(result.kit).toBeUndefined();
42
+ expect(result.limit).toBeUndefined();
43
+ expect(result.all).toBeUndefined();
44
+ });
45
+
46
+ test("should reject invalid kit type", () => {
47
+ const options = { kit: "invalid" };
48
+ expect(() => VersionCommandOptionsSchema.parse(options)).toThrow();
49
+ });
50
+ });
51
+
52
+ describe("Kit Configuration", () => {
53
+ test("should have engineer kit configured", () => {
54
+ const engineerKit = AVAILABLE_KITS.engineer;
55
+ expect(engineerKit.name).toBe("ClaudeKit Engineer");
56
+ expect(engineerKit.repo).toBe("claudekit-engineer");
57
+ expect(engineerKit.owner).toBe("claudekit");
58
+ });
59
+
60
+ test("should have marketing kit configured", () => {
61
+ const marketingKit = AVAILABLE_KITS.marketing;
62
+ expect(marketingKit.name).toBe("ClaudeKit Marketing");
63
+ expect(marketingKit.repo).toBe("claudekit-marketing");
64
+ expect(marketingKit.owner).toBe("claudekit");
65
+ });
66
+ });
67
+
68
+ describe("Release Data Handling", () => {
69
+ test("should handle release with all fields", () => {
70
+ const release: GitHubRelease = {
71
+ id: 1,
72
+ tag_name: "v1.0.0",
73
+ name: "Release 1.0.0",
74
+ draft: false,
75
+ prerelease: false,
76
+ assets: [],
77
+ published_at: "2024-01-01T00:00:00Z",
78
+ };
79
+
80
+ expect(release.tag_name).toBe("v1.0.0");
81
+ expect(release.name).toBe("Release 1.0.0");
82
+ expect(release.draft).toBe(false);
83
+ expect(release.prerelease).toBe(false);
84
+ });
85
+
86
+ test("should handle release without published_at", () => {
87
+ const release: GitHubRelease = {
88
+ id: 1,
89
+ tag_name: "v1.0.0",
90
+ name: "Release 1.0.0",
91
+ draft: false,
92
+ prerelease: false,
93
+ assets: [],
94
+ };
95
+
96
+ expect(release.published_at).toBeUndefined();
97
+ });
98
+
99
+ test("should handle draft release", () => {
100
+ const release: GitHubRelease = {
101
+ id: 1,
102
+ tag_name: "v1.0.0-draft",
103
+ name: "Draft Release",
104
+ draft: true,
105
+ prerelease: false,
106
+ assets: [],
107
+ };
108
+
109
+ expect(release.draft).toBe(true);
110
+ });
111
+
112
+ test("should handle prerelease", () => {
113
+ const release: GitHubRelease = {
114
+ id: 1,
115
+ tag_name: "v1.0.0-beta.1",
116
+ name: "Beta Release",
117
+ draft: false,
118
+ prerelease: true,
119
+ assets: [],
120
+ };
121
+
122
+ expect(release.prerelease).toBe(true);
123
+ });
124
+ });
125
+
126
+ describe("Date Formatting", () => {
127
+ test("should format recent dates correctly", () => {
128
+ const now = new Date();
129
+ const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000);
130
+ const dateString = yesterday.toISOString();
131
+
132
+ // The actual formatting logic is in the command file
133
+ // We just verify the date string is valid
134
+ expect(dateString).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/);
135
+ });
136
+
137
+ test("should handle undefined date", () => {
138
+ const dateString = undefined;
139
+ expect(dateString).toBeUndefined();
140
+ });
141
+ });
142
+
143
+ describe("Release Filtering", () => {
144
+ const releases: GitHubRelease[] = [
145
+ {
146
+ id: 1,
147
+ tag_name: "v1.0.0",
148
+ name: "Stable Release",
149
+ draft: false,
150
+ prerelease: false,
151
+ assets: [],
152
+ published_at: "2024-01-01T00:00:00Z",
153
+ },
154
+ {
155
+ id: 2,
156
+ tag_name: "v1.1.0-beta.1",
157
+ name: "Beta Release",
158
+ draft: false,
159
+ prerelease: true,
160
+ assets: [],
161
+ published_at: "2024-01-02T00:00:00Z",
162
+ },
163
+ {
164
+ id: 3,
165
+ tag_name: "v1.2.0-draft",
166
+ name: "Draft Release",
167
+ draft: true,
168
+ prerelease: false,
169
+ assets: [],
170
+ published_at: "2024-01-03T00:00:00Z",
171
+ },
172
+ ];
173
+
174
+ test("should filter out drafts by default", () => {
175
+ const stable = releases.filter((r) => !r.draft && !r.prerelease);
176
+ expect(stable).toHaveLength(1);
177
+ expect(stable[0].tag_name).toBe("v1.0.0");
178
+ });
179
+
180
+ test("should filter out prereleases by default", () => {
181
+ const stable = releases.filter((r) => !r.draft && !r.prerelease);
182
+ expect(stable.every((r) => !r.prerelease)).toBe(true);
183
+ });
184
+
185
+ test("should include all when --all flag is used", () => {
186
+ const all = releases; // No filtering when --all is true
187
+ expect(all).toHaveLength(3);
188
+ });
189
+
190
+ test("should handle empty release list", () => {
191
+ const empty: GitHubRelease[] = [];
192
+ expect(empty).toHaveLength(0);
193
+ });
194
+ });
195
+
196
+ describe("Command Options Validation", () => {
197
+ test("should validate limit as number", () => {
198
+ const validLimit = { limit: 50 };
199
+ const result = VersionCommandOptionsSchema.parse(validLimit);
200
+ expect(result.limit).toBe(50);
201
+ });
202
+
203
+ test("should validate all as boolean", () => {
204
+ const validAll = { all: false };
205
+ const result = VersionCommandOptionsSchema.parse(validAll);
206
+ expect(result.all).toBe(false);
207
+ });
208
+
209
+ test("should handle optional fields", () => {
210
+ const minimal = {};
211
+ const result = VersionCommandOptionsSchema.parse(minimal);
212
+ expect(result).toBeDefined();
213
+ });
214
+ });
215
+
216
+ describe("Error Scenarios", () => {
217
+ test("should handle invalid option types", () => {
218
+ const invalidLimit = { limit: "not-a-number" };
219
+ expect(() => VersionCommandOptionsSchema.parse(invalidLimit)).toThrow();
220
+ });
221
+
222
+ test("should handle invalid all flag type", () => {
223
+ const invalidAll = { all: "not-a-boolean" };
224
+ expect(() => VersionCommandOptionsSchema.parse(invalidAll)).toThrow();
225
+ });
226
+ });
227
+
228
+ describe("Assets Handling", () => {
229
+ test("should handle release with multiple assets", () => {
230
+ const release: GitHubRelease = {
231
+ id: 1,
232
+ tag_name: "v1.0.0",
233
+ name: "Release",
234
+ draft: false,
235
+ prerelease: false,
236
+ assets: [
237
+ {
238
+ id: 1,
239
+ name: "package.tar.gz",
240
+ browser_download_url: "https://example.com/package.tar.gz",
241
+ size: 1024,
242
+ content_type: "application/gzip",
243
+ },
244
+ {
245
+ id: 2,
246
+ name: "package.zip",
247
+ browser_download_url: "https://example.com/package.zip",
248
+ size: 2048,
249
+ content_type: "application/zip",
250
+ },
251
+ ],
252
+ };
253
+
254
+ expect(release.assets).toHaveLength(2);
255
+ });
256
+
257
+ test("should handle release with no assets", () => {
258
+ const release: GitHubRelease = {
259
+ id: 1,
260
+ tag_name: "v1.0.0",
261
+ name: "Release",
262
+ draft: false,
263
+ prerelease: false,
264
+ assets: [],
265
+ };
266
+
267
+ expect(release.assets).toHaveLength(0);
268
+ });
269
+ });
270
+
271
+ describe("Integration Scenarios", () => {
272
+ test("should handle both kits in parallel", () => {
273
+ const kits = Object.keys(AVAILABLE_KITS);
274
+ expect(kits).toContain("engineer");
275
+ expect(kits).toContain("marketing");
276
+ expect(kits).toHaveLength(2);
277
+ });
278
+
279
+ test("should support filtering by engineer kit", () => {
280
+ const options = { kit: "engineer" as const };
281
+ const result = VersionCommandOptionsSchema.parse(options);
282
+ expect(result.kit).toBe("engineer");
283
+
284
+ const kitConfig = AVAILABLE_KITS[result.kit];
285
+ expect(kitConfig.repo).toBe("claudekit-engineer");
286
+ });
287
+
288
+ test("should support filtering by marketing kit", () => {
289
+ const options = { kit: "marketing" as const };
290
+ const result = VersionCommandOptionsSchema.parse(options);
291
+ expect(result.kit).toBe("marketing");
292
+
293
+ const kitConfig = AVAILABLE_KITS[result.kit];
294
+ expect(kitConfig.repo).toBe("claudekit-marketing");
295
+ });
296
+ });
297
+ });
@@ -0,0 +1,301 @@
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
+ browser_download_url: "https://github.com/test/other-file.tar.gz",
21
+ size: 1024,
22
+ content_type: "application/gzip",
23
+ },
24
+ {
25
+ id: 2,
26
+ name: "ClaudeKit-Engineer-Package.zip",
27
+ browser_download_url: "https://github.com/test/claudekit-package.zip",
28
+ size: 2048,
29
+ content_type: "application/zip",
30
+ },
31
+ ],
32
+ };
33
+
34
+ const result = GitHubClient.getDownloadableAsset(release);
35
+
36
+ expect(result.type).toBe("asset");
37
+ expect(result.name).toBe("ClaudeKit-Engineer-Package.zip");
38
+ expect(result.url).toBe("https://github.com/test/claudekit-package.zip");
39
+ expect(result.size).toBe(2048);
40
+ });
41
+
42
+ test("should prioritize ClaudeKit Marketing Package zip file", () => {
43
+ const release: GitHubRelease = {
44
+ id: 1,
45
+ tag_name: "v1.0.0",
46
+ name: "Release 1.0.0",
47
+ draft: false,
48
+ prerelease: false,
49
+ tarball_url: "https://api.github.com/repos/test/repo/tarball/v1.0.0",
50
+ zipball_url: "https://api.github.com/repos/test/repo/zipball/v1.0.0",
51
+ assets: [
52
+ {
53
+ id: 1,
54
+ name: "random.zip",
55
+ browser_download_url: "https://github.com/test/random.zip",
56
+ size: 512,
57
+ content_type: "application/zip",
58
+ },
59
+ {
60
+ id: 2,
61
+ name: "ClaudeKit-Marketing-Package.zip",
62
+ browser_download_url: "https://github.com/test/marketing-package.zip",
63
+ size: 2048,
64
+ content_type: "application/zip",
65
+ },
66
+ ],
67
+ };
68
+
69
+ const result = GitHubClient.getDownloadableAsset(release);
70
+
71
+ expect(result.type).toBe("asset");
72
+ expect(result.name).toBe("ClaudeKit-Marketing-Package.zip");
73
+ expect(result.url).toBe("https://github.com/test/marketing-package.zip");
74
+ });
75
+
76
+ test("should match ClaudeKit package case-insensitively", () => {
77
+ const release: GitHubRelease = {
78
+ id: 1,
79
+ tag_name: "v1.0.0",
80
+ name: "Release 1.0.0",
81
+ draft: false,
82
+ prerelease: false,
83
+ tarball_url: "https://api.github.com/repos/test/repo/tarball/v1.0.0",
84
+ zipball_url: "https://api.github.com/repos/test/repo/zipball/v1.0.0",
85
+ assets: [
86
+ {
87
+ id: 1,
88
+ name: "claudekit-engineer-package.zip",
89
+ browser_download_url: "https://github.com/test/package.zip",
90
+ size: 2048,
91
+ content_type: "application/zip",
92
+ },
93
+ ],
94
+ };
95
+
96
+ const result = GitHubClient.getDownloadableAsset(release);
97
+
98
+ expect(result.type).toBe("asset");
99
+ expect(result.name).toBe("claudekit-engineer-package.zip");
100
+ });
101
+
102
+ test("should fallback to other zip files if no ClaudeKit package found", () => {
103
+ const release: GitHubRelease = {
104
+ id: 1,
105
+ tag_name: "v1.0.0",
106
+ name: "Release 1.0.0",
107
+ draft: false,
108
+ prerelease: false,
109
+ tarball_url: "https://api.github.com/repos/test/repo/tarball/v1.0.0",
110
+ zipball_url: "https://api.github.com/repos/test/repo/zipball/v1.0.0",
111
+ assets: [
112
+ {
113
+ id: 1,
114
+ name: "source-code.zip",
115
+ browser_download_url: "https://github.com/test/source.zip",
116
+ size: 1024,
117
+ content_type: "application/zip",
118
+ },
119
+ ],
120
+ };
121
+
122
+ const result = GitHubClient.getDownloadableAsset(release);
123
+
124
+ expect(result.type).toBe("asset");
125
+ expect(result.name).toBe("source-code.zip");
126
+ });
127
+
128
+ test("should fallback to tar.gz files if no zip found", () => {
129
+ const release: GitHubRelease = {
130
+ id: 1,
131
+ tag_name: "v1.0.0",
132
+ name: "Release 1.0.0",
133
+ draft: false,
134
+ prerelease: false,
135
+ tarball_url: "https://api.github.com/repos/test/repo/tarball/v1.0.0",
136
+ zipball_url: "https://api.github.com/repos/test/repo/zipball/v1.0.0",
137
+ assets: [
138
+ {
139
+ id: 1,
140
+ name: "release.tar.gz",
141
+ browser_download_url: "https://github.com/test/release.tar.gz",
142
+ size: 1024,
143
+ content_type: "application/gzip",
144
+ },
145
+ ],
146
+ };
147
+
148
+ const result = GitHubClient.getDownloadableAsset(release);
149
+
150
+ expect(result.type).toBe("asset");
151
+ expect(result.name).toBe("release.tar.gz");
152
+ });
153
+
154
+ test("should fallback to tgz files", () => {
155
+ const release: GitHubRelease = {
156
+ id: 1,
157
+ tag_name: "v1.0.0",
158
+ name: "Release 1.0.0",
159
+ draft: false,
160
+ prerelease: false,
161
+ tarball_url: "https://api.github.com/repos/test/repo/tarball/v1.0.0",
162
+ zipball_url: "https://api.github.com/repos/test/repo/zipball/v1.0.0",
163
+ assets: [
164
+ {
165
+ id: 1,
166
+ name: "release.tgz",
167
+ browser_download_url: "https://github.com/test/release.tgz",
168
+ size: 1024,
169
+ content_type: "application/gzip",
170
+ },
171
+ ],
172
+ };
173
+
174
+ const result = GitHubClient.getDownloadableAsset(release);
175
+
176
+ expect(result.type).toBe("asset");
177
+ expect(result.name).toBe("release.tgz");
178
+ });
179
+
180
+ test("should fallback to GitHub automatic tarball if no assets", () => {
181
+ const release: GitHubRelease = {
182
+ id: 1,
183
+ tag_name: "v1.0.0",
184
+ name: "Release 1.0.0",
185
+ draft: false,
186
+ prerelease: false,
187
+ tarball_url: "https://api.github.com/repos/test/repo/tarball/v1.0.0",
188
+ zipball_url: "https://api.github.com/repos/test/repo/zipball/v1.0.0",
189
+ assets: [],
190
+ };
191
+
192
+ const result = GitHubClient.getDownloadableAsset(release);
193
+
194
+ expect(result.type).toBe("tarball");
195
+ expect(result.url).toBe("https://api.github.com/repos/test/repo/tarball/v1.0.0");
196
+ expect(result.name).toBe("v1.0.0.tar.gz");
197
+ expect(result.size).toBeUndefined();
198
+ });
199
+
200
+ test("should fallback to tarball if assets have no archive files", () => {
201
+ const release: GitHubRelease = {
202
+ id: 1,
203
+ tag_name: "v1.0.0",
204
+ name: "Release 1.0.0",
205
+ draft: false,
206
+ prerelease: false,
207
+ tarball_url: "https://api.github.com/repos/test/repo/tarball/v1.0.0",
208
+ zipball_url: "https://api.github.com/repos/test/repo/zipball/v1.0.0",
209
+ assets: [
210
+ {
211
+ id: 1,
212
+ name: "README.md",
213
+ browser_download_url: "https://github.com/test/README.md",
214
+ size: 128,
215
+ content_type: "text/markdown",
216
+ },
217
+ {
218
+ id: 2,
219
+ name: "checksums.txt",
220
+ browser_download_url: "https://github.com/test/checksums.txt",
221
+ size: 64,
222
+ content_type: "text/plain",
223
+ },
224
+ ],
225
+ };
226
+
227
+ const result = GitHubClient.getDownloadableAsset(release);
228
+
229
+ expect(result.type).toBe("tarball");
230
+ expect(result.url).toBe("https://api.github.com/repos/test/repo/tarball/v1.0.0");
231
+ });
232
+
233
+ test("should prioritize ClaudeKit package over other archives", () => {
234
+ const release: GitHubRelease = {
235
+ id: 1,
236
+ tag_name: "v1.0.0",
237
+ name: "Release 1.0.0",
238
+ draft: false,
239
+ prerelease: false,
240
+ tarball_url: "https://api.github.com/repos/test/repo/tarball/v1.0.0",
241
+ zipball_url: "https://api.github.com/repos/test/repo/zipball/v1.0.0",
242
+ assets: [
243
+ {
244
+ id: 1,
245
+ name: "source.tar.gz",
246
+ browser_download_url: "https://github.com/test/source.tar.gz",
247
+ size: 5000,
248
+ content_type: "application/gzip",
249
+ },
250
+ {
251
+ id: 2,
252
+ name: "docs.zip",
253
+ browser_download_url: "https://github.com/test/docs.zip",
254
+ size: 3000,
255
+ content_type: "application/zip",
256
+ },
257
+ {
258
+ id: 3,
259
+ name: "ClaudeKit-Engineer-Package.zip",
260
+ browser_download_url: "https://github.com/test/package.zip",
261
+ size: 2000,
262
+ content_type: "application/zip",
263
+ },
264
+ ],
265
+ };
266
+
267
+ const result = GitHubClient.getDownloadableAsset(release);
268
+
269
+ // Should pick the ClaudeKit package even though it's listed last
270
+ expect(result.type).toBe("asset");
271
+ expect(result.name).toBe("ClaudeKit-Engineer-Package.zip");
272
+ expect(result.size).toBe(2000);
273
+ });
274
+
275
+ test("should handle assets with variations in naming", () => {
276
+ const release: GitHubRelease = {
277
+ id: 1,
278
+ tag_name: "v1.0.0",
279
+ name: "Release 1.0.0",
280
+ draft: false,
281
+ prerelease: false,
282
+ tarball_url: "https://api.github.com/repos/test/repo/tarball/v1.0.0",
283
+ zipball_url: "https://api.github.com/repos/test/repo/zipball/v1.0.0",
284
+ assets: [
285
+ {
286
+ id: 1,
287
+ name: "claudekit_marketing_package.zip",
288
+ browser_download_url: "https://github.com/test/package.zip",
289
+ size: 2000,
290
+ content_type: "application/zip",
291
+ },
292
+ ],
293
+ };
294
+
295
+ const result = GitHubClient.getDownloadableAsset(release);
296
+
297
+ expect(result.type).toBe("asset");
298
+ expect(result.name).toBe("claudekit_marketing_package.zip");
299
+ });
300
+ });
301
+ });
@@ -36,13 +36,13 @@ describe("GitHubClient", () => {
36
36
  describe("integration scenarios", () => {
37
37
  test("should handle kit configuration correctly", () => {
38
38
  const engineerKit = AVAILABLE_KITS.engineer;
39
- expect(engineerKit.owner).toBe("mrgoonie");
39
+ expect(engineerKit.owner).toBe("claudekit");
40
40
  expect(engineerKit.repo).toBe("claudekit-engineer");
41
41
  });
42
42
 
43
43
  test("should handle marketing kit configuration", () => {
44
44
  const marketingKit = AVAILABLE_KITS.marketing;
45
- expect(marketingKit.owner).toBe("mrgoonie");
45
+ expect(marketingKit.owner).toBe("claudekit");
46
46
  expect(marketingKit.repo).toBe("claudekit-marketing");
47
47
  });
48
48
  });
@@ -135,4 +135,81 @@ describe("FileMerger", () => {
135
135
  expect(existsSync(join(testDestDir, "custom-ignore.txt"))).toBe(false);
136
136
  });
137
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
+ });
138
215
  });