claudekit-cli 1.2.0 → 1.2.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claudekit-cli",
3
- "version": "1.2.0",
3
+ "version": "1.2.1",
4
4
  "description": "CLI tool for bootstrapping and updating ClaudeKit projects",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,6 +1,5 @@
1
1
  import { resolve } from "node:path";
2
2
  import { pathExists, readdir } from "fs-extra";
3
- import ora from "ora";
4
3
  import { AuthManager } from "../lib/auth.js";
5
4
  import { DownloadManager } from "../lib/download.js";
6
5
  import { GitHubClient } from "../lib/github.js";
@@ -9,6 +8,7 @@ import { PromptsManager } from "../lib/prompts.js";
9
8
  import { AVAILABLE_KITS, type NewCommandOptions, NewCommandOptionsSchema } from "../types.js";
10
9
  import { ConfigManager } from "../utils/config.js";
11
10
  import { logger } from "../utils/logger.js";
11
+ import { createSpinner } from "../utils/safe-spinner.js";
12
12
 
13
13
  export async function newCommand(options: NewCommandOptions): Promise<void> {
14
14
  const prompts = new PromptsManager();
@@ -59,7 +59,7 @@ export async function newCommand(options: NewCommandOptions): Promise<void> {
59
59
  const github = new GitHubClient();
60
60
 
61
61
  // Check repository access
62
- const spinner = ora("Checking repository access...").start();
62
+ const spinner = createSpinner("Checking repository access...").start();
63
63
  const hasAccess = await github.checkAccess(kitConfig);
64
64
  if (!hasAccess) {
65
65
  spinner.fail("Access denied to repository");
@@ -1,6 +1,5 @@
1
1
  import { resolve } from "node:path";
2
2
  import { pathExists } from "fs-extra";
3
- import ora from "ora";
4
3
  import { AuthManager } from "../lib/auth.js";
5
4
  import { DownloadManager } from "../lib/download.js";
6
5
  import { GitHubClient } from "../lib/github.js";
@@ -10,6 +9,7 @@ import { AVAILABLE_KITS, type UpdateCommandOptions, UpdateCommandOptionsSchema }
10
9
  import { ConfigManager } from "../utils/config.js";
11
10
  import { FileScanner } from "../utils/file-scanner.js";
12
11
  import { logger } from "../utils/logger.js";
12
+ import { createSpinner } from "../utils/safe-spinner.js";
13
13
 
14
14
  export async function updateCommand(options: UpdateCommandOptions): Promise<void> {
15
15
  const prompts = new PromptsManager();
@@ -52,7 +52,7 @@ export async function updateCommand(options: UpdateCommandOptions): Promise<void
52
52
  const github = new GitHubClient();
53
53
 
54
54
  // Check repository access
55
- const spinner = ora("Checking repository access...").start();
55
+ const spinner = createSpinner("Checking repository access...").start();
56
56
  const hasAccess = await github.checkAccess(kitConfig);
57
57
  if (!hasAccess) {
58
58
  spinner.fail("Access denied to repository");
@@ -6,7 +6,6 @@ import { pipeline } from "node:stream";
6
6
  import { promisify } from "node:util";
7
7
  import cliProgress from "cli-progress";
8
8
  import ignore from "ignore";
9
- import ora from "ora";
10
9
  import * as tar from "tar";
11
10
  import unzipper from "unzipper";
12
11
  import {
@@ -16,6 +15,7 @@ import {
16
15
  type GitHubReleaseAsset,
17
16
  } from "../types.js";
18
17
  import { logger } from "../utils/logger.js";
18
+ import { createSpinner } from "../utils/safe-spinner.js";
19
19
 
20
20
  const streamPipeline = promisify(pipeline);
21
21
 
@@ -55,11 +55,11 @@ export class DownloadManager {
55
55
 
56
56
  logger.info(`Downloading ${asset.name} (${this.formatBytes(asset.size)})...`);
57
57
 
58
- // Create progress bar
58
+ // Create progress bar with simple ASCII characters
59
59
  const progressBar = new cliProgress.SingleBar({
60
60
  format: "Progress |{bar}| {percentage}% | {value}/{total} MB",
61
- barCompleteChar: "\u2588",
62
- barIncompleteChar: "\u2591",
61
+ barCompleteChar: "=",
62
+ barIncompleteChar: "-",
63
63
  hideCursor: true,
64
64
  });
65
65
 
@@ -137,7 +137,9 @@ export class DownloadManager {
137
137
  // Add authentication for GitHub API URLs
138
138
  if (token && url.includes("api.github.com")) {
139
139
  headers.Authorization = `Bearer ${token}`;
140
- headers.Accept = "application/vnd.github+json";
140
+ // Use application/octet-stream for asset downloads (not vnd.github+json)
141
+ headers.Accept = "application/octet-stream";
142
+ headers["X-GitHub-Api-Version"] = "2022-11-28";
141
143
  } else {
142
144
  headers.Accept = "application/octet-stream";
143
145
  }
@@ -151,13 +153,13 @@ export class DownloadManager {
151
153
  const totalSize = size || Number(response.headers.get("content-length")) || 0;
152
154
  let downloadedSize = 0;
153
155
 
154
- // Create progress bar only if we know the size
156
+ // Create progress bar only if we know the size (using simple ASCII characters)
155
157
  const progressBar =
156
158
  totalSize > 0
157
159
  ? new cliProgress.SingleBar({
158
160
  format: "Progress |{bar}| {percentage}% | {value}/{total} MB",
159
- barCompleteChar: "\u2588",
160
- barIncompleteChar: "\u2591",
161
+ barCompleteChar: "=",
162
+ barIncompleteChar: "-",
161
163
  hideCursor: true,
162
164
  })
163
165
  : null;
@@ -207,7 +209,7 @@ export class DownloadManager {
207
209
  destDir: string,
208
210
  archiveType?: ArchiveType,
209
211
  ): Promise<void> {
210
- const spinner = ora("Extracting files...").start();
212
+ const spinner = createSpinner("Extracting files...").start();
211
213
 
212
214
  try {
213
215
  // Detect archive type from filename if not provided
package/src/lib/github.ts CHANGED
@@ -159,7 +159,7 @@ export class GitHubClient {
159
159
  * Get downloadable asset or source code URL from release
160
160
  * Priority:
161
161
  * 1. "ClaudeKit Engineer Package" or "ClaudeKit Marketing Package" zip file
162
- * 2. Other custom uploaded assets (.tar.gz, .tgz, .zip)
162
+ * 2. Other custom uploaded assets (.tar.gz, .tgz, .zip) excluding "Source code" archives
163
163
  * 3. GitHub's automatic tarball URL
164
164
  */
165
165
  static getDownloadableAsset(release: GitHubRelease): {
@@ -168,41 +168,58 @@ export class GitHubClient {
168
168
  name: string;
169
169
  size?: number;
170
170
  } {
171
+ // Log all available assets for debugging
172
+ logger.debug(`Available assets for ${release.tag_name}:`);
173
+ if (release.assets.length === 0) {
174
+ logger.debug(" No custom assets found");
175
+ } else {
176
+ release.assets.forEach((asset, index) => {
177
+ logger.debug(` ${index + 1}. ${asset.name} (${(asset.size / 1024 / 1024).toFixed(2)} MB)`);
178
+ });
179
+ }
180
+
171
181
  // First priority: Look for official ClaudeKit package assets
172
- const packageAsset = release.assets.find(
173
- (a) =>
174
- a.name.toLowerCase().includes("claudekit") &&
175
- a.name.toLowerCase().includes("package") &&
176
- a.name.endsWith(".zip"),
177
- );
182
+ const packageAsset = release.assets.find((a) => {
183
+ const nameLower = a.name.toLowerCase();
184
+ return (
185
+ nameLower.includes("claudekit") &&
186
+ nameLower.includes("package") &&
187
+ nameLower.endsWith(".zip")
188
+ );
189
+ });
178
190
 
179
191
  if (packageAsset) {
180
- logger.debug(`Using ClaudeKit package asset: ${packageAsset.name}`);
192
+ logger.debug(`✓ Selected ClaudeKit package asset: ${packageAsset.name}`);
181
193
  return {
182
194
  type: "asset",
183
- url: packageAsset.browser_download_url,
195
+ url: packageAsset.url, // Use API endpoint for authenticated downloads
184
196
  name: packageAsset.name,
185
197
  size: packageAsset.size,
186
198
  };
187
199
  }
188
200
 
189
- // Second priority: Look for any custom uploaded assets
201
+ logger.debug("⚠ No ClaudeKit package asset found, checking for other custom assets...");
202
+
203
+ // Second priority: Look for any custom uploaded assets (excluding GitHub's automatic source code archives)
190
204
  const customAsset = release.assets.find(
191
- (a) => a.name.endsWith(".tar.gz") || a.name.endsWith(".tgz") || a.name.endsWith(".zip"),
205
+ (a) =>
206
+ (a.name.endsWith(".tar.gz") || a.name.endsWith(".tgz") || a.name.endsWith(".zip")) &&
207
+ !a.name.toLowerCase().startsWith("source") &&
208
+ !a.name.toLowerCase().includes("source code"),
192
209
  );
193
210
 
194
211
  if (customAsset) {
195
- logger.debug(`Using custom asset: ${customAsset.name}`);
212
+ logger.debug(`✓ Selected custom asset: ${customAsset.name}`);
196
213
  return {
197
214
  type: "asset",
198
- url: customAsset.browser_download_url,
215
+ url: customAsset.url, // Use API endpoint for authenticated downloads
199
216
  name: customAsset.name,
200
217
  size: customAsset.size,
201
218
  };
202
219
  }
203
220
 
204
221
  // Fall back to GitHub's automatic tarball
205
- logger.debug("Using GitHub automatic tarball");
222
+ logger.debug(" No custom assets found, falling back to GitHub automatic tarball");
206
223
  return {
207
224
  type: "tarball",
208
225
  url: release.tarball_url,
package/src/types.ts CHANGED
@@ -46,7 +46,8 @@ export type Config = z.infer<typeof ConfigSchema>;
46
46
  export const GitHubReleaseAssetSchema = z.object({
47
47
  id: z.number(),
48
48
  name: z.string(),
49
- browser_download_url: z.string().url(),
49
+ url: z.string().url(), // API endpoint for authenticated downloads
50
+ browser_download_url: z.string().url(), // Direct download URL (public only)
50
51
  size: z.number(),
51
52
  content_type: z.string(),
52
53
  });
@@ -4,9 +4,9 @@ import pc from "picocolors";
4
4
  // Use ASCII-safe symbols to avoid unicode rendering issues in certain terminals
5
5
  const symbols = {
6
6
  info: "[i]",
7
- success: "[]",
7
+ success: "[+]",
8
8
  warning: "[!]",
9
- error: "[]",
9
+ error: "[x]",
10
10
  };
11
11
 
12
12
  interface LogContext {
@@ -1,53 +1,43 @@
1
- import * as clack from "@clack/prompts";
1
+ import picocolors from "picocolors";
2
2
 
3
3
  /**
4
- * Safe wrapper around clack prompts that handles unicode rendering issues.
5
- * Sets up proper environment to minimize encoding problems.
4
+ * Safe wrapper around clack prompts that uses simple ASCII characters
5
+ * instead of unicode box drawing to avoid rendering issues.
6
6
  */
7
7
 
8
- // Store original methods
9
- const originalIntro = clack.intro;
10
- const originalOutro = clack.outro;
11
- const originalNote = clack.note;
12
-
13
8
  /**
14
- * Wrapped intro that handles encoding better
9
+ * Simple intro with ASCII characters
15
10
  */
16
11
  export function intro(message: string): void {
17
- try {
18
- originalIntro(message);
19
- } catch {
20
- // Fallback to simple console log if clack fails
21
- console.log(`\n=== ${message} ===\n`);
22
- }
12
+ console.log();
13
+ console.log(picocolors.cyan(`> ${message}`));
14
+ console.log();
23
15
  }
24
16
 
25
17
  /**
26
- * Wrapped outro that handles encoding better
18
+ * Simple outro with ASCII characters
27
19
  */
28
20
  export function outro(message: string): void {
29
- try {
30
- originalOutro(message);
31
- } catch {
32
- // Fallback to simple console log if clack fails
33
- console.log(`\n=== ${message} ===\n`);
34
- }
21
+ console.log();
22
+ console.log(picocolors.green(`[OK] ${message}`));
23
+ console.log();
35
24
  }
36
25
 
37
26
  /**
38
- * Wrapped note that handles encoding better
27
+ * Simple note with ASCII box drawing
39
28
  */
40
29
  export function note(message: string, title?: string): void {
41
- try {
42
- originalNote(message, title);
43
- } catch {
44
- // Fallback to simple console log if clack fails
45
- if (title) {
46
- console.log(`\n--- ${title} ---`);
47
- }
48
- console.log(message);
30
+ console.log();
31
+ if (title) {
32
+ console.log(picocolors.cyan(` ${title}:`));
49
33
  console.log();
50
34
  }
35
+ // Split message into lines and indent each
36
+ const lines = message.split("\n");
37
+ for (const line of lines) {
38
+ console.log(` ${line}`);
39
+ }
40
+ console.log();
51
41
  }
52
42
 
53
43
  // Re-export other clack functions unchanged
@@ -0,0 +1,38 @@
1
+ import ora, { type Ora, type Options } from "ora";
2
+
3
+ /**
4
+ * Create a spinner with simple ASCII characters to avoid unicode rendering issues
5
+ */
6
+ export function createSpinner(options: string | Options): Ora {
7
+ const spinnerOptions: Options = typeof options === "string" ? { text: options } : options;
8
+
9
+ const spinner = ora({
10
+ ...spinnerOptions,
11
+ // Use simple ASCII spinner instead of unicode
12
+ spinner: "dots",
13
+ // Override symbols to use ASCII
14
+ prefixText: "",
15
+ });
16
+
17
+ // Override succeed and fail methods to use ASCII symbols
18
+ spinner.succeed = (text?: string) => {
19
+ spinner.stopAndPersist({
20
+ symbol: "[+]",
21
+ text: text || spinner.text,
22
+ });
23
+ return spinner;
24
+ };
25
+
26
+ spinner.fail = (text?: string) => {
27
+ spinner.stopAndPersist({
28
+ symbol: "[x]",
29
+ text: text || spinner.text,
30
+ });
31
+ return spinner;
32
+ };
33
+
34
+ return spinner;
35
+ }
36
+
37
+ // Re-export Ora type for convenience
38
+ export type { Ora } from "ora";
@@ -17,6 +17,7 @@ describe("GitHubClient - Asset Download Priority", () => {
17
17
  {
18
18
  id: 1,
19
19
  name: "other-file.tar.gz",
20
+ url: "https://api.github.com/repos/test/repo/releases/assets/1",
20
21
  browser_download_url: "https://github.com/test/other-file.tar.gz",
21
22
  size: 1024,
22
23
  content_type: "application/gzip",
@@ -24,6 +25,7 @@ describe("GitHubClient - Asset Download Priority", () => {
24
25
  {
25
26
  id: 2,
26
27
  name: "ClaudeKit-Engineer-Package.zip",
28
+ url: "https://api.github.com/repos/test/repo/releases/assets/2",
27
29
  browser_download_url: "https://github.com/test/claudekit-package.zip",
28
30
  size: 2048,
29
31
  content_type: "application/zip",
@@ -35,7 +37,7 @@ describe("GitHubClient - Asset Download Priority", () => {
35
37
 
36
38
  expect(result.type).toBe("asset");
37
39
  expect(result.name).toBe("ClaudeKit-Engineer-Package.zip");
38
- expect(result.url).toBe("https://github.com/test/claudekit-package.zip");
40
+ expect(result.url).toBe("https://api.github.com/repos/test/repo/releases/assets/2");
39
41
  expect(result.size).toBe(2048);
40
42
  });
41
43
 
@@ -52,6 +54,7 @@ describe("GitHubClient - Asset Download Priority", () => {
52
54
  {
53
55
  id: 1,
54
56
  name: "random.zip",
57
+ url: "https://api.github.com/repos/test/repo/releases/assets/1",
55
58
  browser_download_url: "https://github.com/test/random.zip",
56
59
  size: 512,
57
60
  content_type: "application/zip",
@@ -59,6 +62,7 @@ describe("GitHubClient - Asset Download Priority", () => {
59
62
  {
60
63
  id: 2,
61
64
  name: "ClaudeKit-Marketing-Package.zip",
65
+ url: "https://api.github.com/repos/test/repo/releases/assets/2",
62
66
  browser_download_url: "https://github.com/test/marketing-package.zip",
63
67
  size: 2048,
64
68
  content_type: "application/zip",
@@ -70,7 +74,7 @@ describe("GitHubClient - Asset Download Priority", () => {
70
74
 
71
75
  expect(result.type).toBe("asset");
72
76
  expect(result.name).toBe("ClaudeKit-Marketing-Package.zip");
73
- expect(result.url).toBe("https://github.com/test/marketing-package.zip");
77
+ expect(result.url).toBe("https://api.github.com/repos/test/repo/releases/assets/2");
74
78
  });
75
79
 
76
80
  test("should match ClaudeKit package case-insensitively", () => {
@@ -86,6 +90,7 @@ describe("GitHubClient - Asset Download Priority", () => {
86
90
  {
87
91
  id: 1,
88
92
  name: "claudekit-engineer-package.zip",
93
+ url: "https://api.github.com/repos/test/repo/releases/assets/1",
89
94
  browser_download_url: "https://github.com/test/package.zip",
90
95
  size: 2048,
91
96
  content_type: "application/zip",
@@ -111,8 +116,9 @@ describe("GitHubClient - Asset Download Priority", () => {
111
116
  assets: [
112
117
  {
113
118
  id: 1,
114
- name: "source-code.zip",
115
- browser_download_url: "https://github.com/test/source.zip",
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",
116
122
  size: 1024,
117
123
  content_type: "application/zip",
118
124
  },
@@ -122,7 +128,7 @@ describe("GitHubClient - Asset Download Priority", () => {
122
128
  const result = GitHubClient.getDownloadableAsset(release);
123
129
 
124
130
  expect(result.type).toBe("asset");
125
- expect(result.name).toBe("source-code.zip");
131
+ expect(result.name).toBe("release-package.zip");
126
132
  });
127
133
 
128
134
  test("should fallback to tar.gz files if no zip found", () => {
@@ -138,6 +144,7 @@ describe("GitHubClient - Asset Download Priority", () => {
138
144
  {
139
145
  id: 1,
140
146
  name: "release.tar.gz",
147
+ url: "https://api.github.com/repos/test/repo/releases/assets/1",
141
148
  browser_download_url: "https://github.com/test/release.tar.gz",
142
149
  size: 1024,
143
150
  content_type: "application/gzip",
@@ -164,6 +171,7 @@ describe("GitHubClient - Asset Download Priority", () => {
164
171
  {
165
172
  id: 1,
166
173
  name: "release.tgz",
174
+ url: "https://api.github.com/repos/test/repo/releases/assets/1",
167
175
  browser_download_url: "https://github.com/test/release.tgz",
168
176
  size: 1024,
169
177
  content_type: "application/gzip",
@@ -210,6 +218,7 @@ describe("GitHubClient - Asset Download Priority", () => {
210
218
  {
211
219
  id: 1,
212
220
  name: "README.md",
221
+ url: "https://api.github.com/repos/test/repo/releases/assets/1",
213
222
  browser_download_url: "https://github.com/test/README.md",
214
223
  size: 128,
215
224
  content_type: "text/markdown",
@@ -217,6 +226,7 @@ describe("GitHubClient - Asset Download Priority", () => {
217
226
  {
218
227
  id: 2,
219
228
  name: "checksums.txt",
229
+ url: "https://api.github.com/repos/test/repo/releases/assets/2",
220
230
  browser_download_url: "https://github.com/test/checksums.txt",
221
231
  size: 64,
222
232
  content_type: "text/plain",
@@ -243,6 +253,7 @@ describe("GitHubClient - Asset Download Priority", () => {
243
253
  {
244
254
  id: 1,
245
255
  name: "source.tar.gz",
256
+ url: "https://api.github.com/repos/test/repo/releases/assets/1",
246
257
  browser_download_url: "https://github.com/test/source.tar.gz",
247
258
  size: 5000,
248
259
  content_type: "application/gzip",
@@ -250,6 +261,7 @@ describe("GitHubClient - Asset Download Priority", () => {
250
261
  {
251
262
  id: 2,
252
263
  name: "docs.zip",
264
+ url: "https://api.github.com/repos/test/repo/releases/assets/2",
253
265
  browser_download_url: "https://github.com/test/docs.zip",
254
266
  size: 3000,
255
267
  content_type: "application/zip",
@@ -257,6 +269,7 @@ describe("GitHubClient - Asset Download Priority", () => {
257
269
  {
258
270
  id: 3,
259
271
  name: "ClaudeKit-Engineer-Package.zip",
272
+ url: "https://api.github.com/repos/test/repo/releases/assets/3",
260
273
  browser_download_url: "https://github.com/test/package.zip",
261
274
  size: 2000,
262
275
  content_type: "application/zip",
@@ -285,6 +298,7 @@ describe("GitHubClient - Asset Download Priority", () => {
285
298
  {
286
299
  id: 1,
287
300
  name: "claudekit_marketing_package.zip",
301
+ url: "https://api.github.com/repos/test/repo/releases/assets/1",
288
302
  browser_download_url: "https://github.com/test/package.zip",
289
303
  size: 2000,
290
304
  content_type: "application/zip",
@@ -297,5 +311,122 @@ describe("GitHubClient - Asset Download Priority", () => {
297
311
  expect(result.type).toBe("asset");
298
312
  expect(result.name).toBe("claudekit_marketing_package.zip");
299
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
+ });
300
431
  });
301
432
  });
@@ -108,6 +108,7 @@ describe("Types and Schemas", () => {
108
108
  const asset = {
109
109
  id: 123,
110
110
  name: "release.tar.gz",
111
+ url: "https://api.github.com/repos/test/repo/releases/assets/123",
111
112
  browser_download_url: "https://github.com/test/release.tar.gz",
112
113
  size: 1024,
113
114
  content_type: "application/gzip",
@@ -122,6 +123,7 @@ describe("Types and Schemas", () => {
122
123
  const asset = {
123
124
  id: 123,
124
125
  name: "release.tar.gz",
126
+ url: "not-a-url",
125
127
  browser_download_url: "not-a-url",
126
128
  size: 1024,
127
129
  content_type: "application/gzip",
@@ -150,6 +152,7 @@ describe("Types and Schemas", () => {
150
152
  {
151
153
  id: 123,
152
154
  name: "release.tar.gz",
155
+ url: "https://api.github.com/repos/test/repo/releases/assets/123",
153
156
  browser_download_url: "https://github.com/test/release.tar.gz",
154
157
  size: 1024,
155
158
  content_type: "application/gzip",