claudekit-cli 1.1.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.1.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,6 @@
1
1
  import { resolve } from "node:path";
2
2
  import { pathExists, readdir } from "fs-extra";
3
- import ora from "ora";
3
+ import { AuthManager } from "../lib/auth.js";
4
4
  import { DownloadManager } from "../lib/download.js";
5
5
  import { GitHubClient } from "../lib/github.js";
6
6
  import { FileMerger } from "../lib/merge.js";
@@ -8,6 +8,7 @@ import { PromptsManager } from "../lib/prompts.js";
8
8
  import { AVAILABLE_KITS, type NewCommandOptions, NewCommandOptionsSchema } from "../types.js";
9
9
  import { ConfigManager } from "../utils/config.js";
10
10
  import { logger } from "../utils/logger.js";
11
+ import { createSpinner } from "../utils/safe-spinner.js";
11
12
 
12
13
  export async function newCommand(options: NewCommandOptions): Promise<void> {
13
14
  const prompts = new PromptsManager();
@@ -58,7 +59,7 @@ export async function newCommand(options: NewCommandOptions): Promise<void> {
58
59
  const github = new GitHubClient();
59
60
 
60
61
  // Check repository access
61
- const spinner = ora("Checking repository access...").start();
62
+ const spinner = createSpinner("Checking repository access...").start();
62
63
  const hasAccess = await github.checkAccess(kitConfig);
63
64
  if (!hasAccess) {
64
65
  spinner.fail("Access denied to repository");
@@ -81,20 +82,51 @@ export async function newCommand(options: NewCommandOptions): Promise<void> {
81
82
 
82
83
  logger.success(`Found release: ${release.tag_name} - ${release.name}`);
83
84
 
84
- // Find downloadable asset
85
- const asset = release.assets.find(
86
- (a) => a.name.endsWith(".tar.gz") || a.name.endsWith(".tgz") || a.name.endsWith(".zip"),
87
- );
85
+ // Get downloadable asset (custom asset or GitHub tarball)
86
+ const downloadInfo = GitHubClient.getDownloadableAsset(release);
88
87
 
89
- if (!asset) {
90
- logger.error("No downloadable archive found in release");
91
- return;
92
- }
88
+ logger.info(`Download source: ${downloadInfo.type}`);
89
+ logger.debug(`Download URL: ${downloadInfo.url}`);
93
90
 
94
91
  // Download asset
95
92
  const downloadManager = new DownloadManager();
96
93
  const tempDir = await downloadManager.createTempDir();
97
- const archivePath = await downloadManager.downloadAsset(asset, tempDir);
94
+
95
+ // Get authentication token for API requests
96
+ const { token } = await AuthManager.getToken();
97
+
98
+ let archivePath: string;
99
+ try {
100
+ // Try downloading the asset/tarball with authentication
101
+ archivePath = await downloadManager.downloadFile({
102
+ url: downloadInfo.url,
103
+ name: downloadInfo.name,
104
+ size: downloadInfo.size,
105
+ destDir: tempDir,
106
+ token, // Always pass token for private repository access
107
+ });
108
+ } catch (error) {
109
+ // If asset download fails, fallback to GitHub tarball
110
+ if (downloadInfo.type === "asset") {
111
+ logger.warning("Asset download failed, falling back to GitHub tarball...");
112
+ const tarballInfo = {
113
+ type: "github-tarball" as const,
114
+ url: release.tarball_url,
115
+ name: `${kitConfig.repo}-${release.tag_name}.tar.gz`,
116
+ size: 0, // Size unknown for tarball
117
+ };
118
+
119
+ archivePath = await downloadManager.downloadFile({
120
+ url: tarballInfo.url,
121
+ name: tarballInfo.name,
122
+ size: tarballInfo.size,
123
+ destDir: tempDir,
124
+ token,
125
+ });
126
+ } else {
127
+ throw error;
128
+ }
129
+ }
98
130
 
99
131
  // Extract archive
100
132
  const extractDir = `${tempDir}/extracted`;
@@ -1,13 +1,15 @@
1
1
  import { resolve } from "node:path";
2
2
  import { pathExists } from "fs-extra";
3
- import ora from "ora";
3
+ import { AuthManager } from "../lib/auth.js";
4
4
  import { DownloadManager } from "../lib/download.js";
5
5
  import { GitHubClient } from "../lib/github.js";
6
6
  import { FileMerger } from "../lib/merge.js";
7
7
  import { PromptsManager } from "../lib/prompts.js";
8
8
  import { AVAILABLE_KITS, type UpdateCommandOptions, UpdateCommandOptionsSchema } from "../types.js";
9
9
  import { ConfigManager } from "../utils/config.js";
10
+ import { FileScanner } from "../utils/file-scanner.js";
10
11
  import { logger } from "../utils/logger.js";
12
+ import { createSpinner } from "../utils/safe-spinner.js";
11
13
 
12
14
  export async function updateCommand(options: UpdateCommandOptions): Promise<void> {
13
15
  const prompts = new PromptsManager();
@@ -50,7 +52,7 @@ export async function updateCommand(options: UpdateCommandOptions): Promise<void
50
52
  const github = new GitHubClient();
51
53
 
52
54
  // Check repository access
53
- const spinner = ora("Checking repository access...").start();
55
+ const spinner = createSpinner("Checking repository access...").start();
54
56
  const hasAccess = await github.checkAccess(kitConfig);
55
57
  if (!hasAccess) {
56
58
  spinner.fail("Access denied to repository");
@@ -73,36 +75,80 @@ export async function updateCommand(options: UpdateCommandOptions): Promise<void
73
75
 
74
76
  logger.success(`Found release: ${release.tag_name} - ${release.name}`);
75
77
 
76
- // Find downloadable asset
77
- const asset = release.assets.find(
78
- (a) => a.name.endsWith(".tar.gz") || a.name.endsWith(".tgz") || a.name.endsWith(".zip"),
79
- );
78
+ // Get downloadable asset (custom asset or GitHub tarball)
79
+ const downloadInfo = GitHubClient.getDownloadableAsset(release);
80
80
 
81
- if (!asset) {
82
- logger.error("No downloadable archive found in release");
83
- return;
84
- }
81
+ logger.info(`Download source: ${downloadInfo.type}`);
82
+ logger.debug(`Download URL: ${downloadInfo.url}`);
85
83
 
86
84
  // Download asset
87
85
  const downloadManager = new DownloadManager();
88
86
  const tempDir = await downloadManager.createTempDir();
89
- const archivePath = await downloadManager.downloadAsset(asset, tempDir);
87
+
88
+ // Get authentication token for API requests
89
+ const { token } = await AuthManager.getToken();
90
+
91
+ let archivePath: string;
92
+ try {
93
+ // Try downloading the asset/tarball with authentication
94
+ archivePath = await downloadManager.downloadFile({
95
+ url: downloadInfo.url,
96
+ name: downloadInfo.name,
97
+ size: downloadInfo.size,
98
+ destDir: tempDir,
99
+ token, // Always pass token for private repository access
100
+ });
101
+ } catch (error) {
102
+ // If asset download fails, fallback to GitHub tarball
103
+ if (downloadInfo.type === "asset") {
104
+ logger.warning("Asset download failed, falling back to GitHub tarball...");
105
+ const tarballInfo = {
106
+ type: "github-tarball" as const,
107
+ url: release.tarball_url,
108
+ name: `${kitConfig.repo}-${release.tag_name}.tar.gz`,
109
+ size: 0, // Size unknown for tarball
110
+ };
111
+
112
+ archivePath = await downloadManager.downloadFile({
113
+ url: tarballInfo.url,
114
+ name: tarballInfo.name,
115
+ size: tarballInfo.size,
116
+ destDir: tempDir,
117
+ token,
118
+ });
119
+ } else {
120
+ throw error;
121
+ }
122
+ }
90
123
 
91
124
  // Extract archive
92
125
  const extractDir = `${tempDir}/extracted`;
93
126
  await downloadManager.extractArchive(archivePath, extractDir);
94
127
 
128
+ // Identify custom .claude files to preserve
129
+ logger.info("Scanning for custom .claude files...");
130
+ const customClaudeFiles = await FileScanner.findCustomFiles(resolvedDir, extractDir, ".claude");
131
+
95
132
  // Merge files with confirmation
96
133
  const merger = new FileMerger();
134
+
135
+ // Add custom .claude files to ignore patterns
136
+ if (customClaudeFiles.length > 0) {
137
+ merger.addIgnorePatterns(customClaudeFiles);
138
+ logger.success(`Protected ${customClaudeFiles.length} custom .claude file(s)`);
139
+ }
140
+
97
141
  await merger.merge(extractDir, resolvedDir, false); // Show confirmation for updates
98
142
 
99
143
  prompts.outro(`✨ Project updated successfully at ${resolvedDir}`);
100
144
 
101
145
  // Show next steps
102
- prompts.note(
103
- "Your project has been updated with the latest version.\nProtected files (.env, etc.) were not modified.",
104
- "Update complete",
105
- );
146
+ const protectedNote =
147
+ customClaudeFiles.length > 0
148
+ ? "Your project has been updated with the latest version.\nProtected files (.env, .claude custom files, etc.) were not modified."
149
+ : "Your project has been updated with the latest version.\nProtected files (.env, etc.) were not modified.";
150
+
151
+ prompts.note(protectedNote, "Update complete");
106
152
  } catch (error) {
107
153
  if (error instanceof Error && error.message === "Merge cancelled by user") {
108
154
  logger.warning("Update cancelled");
package/src/index.ts CHANGED
@@ -7,6 +7,15 @@ import { cac } from "cac";
7
7
  import { newCommand } from "./commands/new.js";
8
8
  import { updateCommand } from "./commands/update.js";
9
9
  import { versionCommand } from "./commands/version.js";
10
+ import { logger } from "./utils/logger.js";
11
+
12
+ // Set proper output encoding to prevent unicode rendering issues
13
+ if (process.stdout.setEncoding) {
14
+ process.stdout.setEncoding("utf8");
15
+ }
16
+ if (process.stderr.setEncoding) {
17
+ process.stderr.setEncoding("utf8");
18
+ }
10
19
 
11
20
  const __dirname = fileURLToPath(new URL(".", import.meta.url));
12
21
 
@@ -15,6 +24,10 @@ const packageJson = JSON.parse(readFileSync(join(__dirname, "../package.json"),
15
24
 
16
25
  const cli = cac("ck");
17
26
 
27
+ // Global options
28
+ cli.option("--verbose, -v", "Enable verbose logging for debugging");
29
+ cli.option("--log-file <path>", "Write logs to file");
30
+
18
31
  // New command
19
32
  cli
20
33
  .command("new", "Bootstrap a new ClaudeKit project")
@@ -51,5 +64,33 @@ cli.version(packageJson.version);
51
64
  // Help
52
65
  cli.help();
53
66
 
54
- // Parse CLI arguments
67
+ // Parse to get global options first
68
+ const parsed = cli.parse(process.argv, { run: false });
69
+
70
+ // Check environment variable
71
+ const envVerbose =
72
+ process.env.CLAUDEKIT_VERBOSE === "1" || process.env.CLAUDEKIT_VERBOSE === "true";
73
+
74
+ // Enable verbose if flag or env var is set
75
+ const isVerbose = parsed.options.verbose || envVerbose;
76
+
77
+ if (isVerbose) {
78
+ logger.setVerbose(true);
79
+ }
80
+
81
+ // Set log file if specified
82
+ if (parsed.options.logFile) {
83
+ logger.setLogFile(parsed.options.logFile);
84
+ }
85
+
86
+ // Log startup info in verbose mode
87
+ logger.verbose("ClaudeKit CLI starting", {
88
+ version: packageJson.version,
89
+ command: parsed.args[0] || "none",
90
+ options: parsed.options,
91
+ cwd: process.cwd(),
92
+ node: process.version,
93
+ });
94
+
95
+ // Parse again to run the command
55
96
  cli.parse();
@@ -5,7 +5,7 @@ import { join } from "node:path";
5
5
  import { pipeline } from "node:stream";
6
6
  import { promisify } from "node:util";
7
7
  import cliProgress from "cli-progress";
8
- import ora from "ora";
8
+ import ignore from "ignore";
9
9
  import * as tar from "tar";
10
10
  import unzipper from "unzipper";
11
11
  import {
@@ -15,10 +15,34 @@ import {
15
15
  type GitHubReleaseAsset,
16
16
  } from "../types.js";
17
17
  import { logger } from "../utils/logger.js";
18
+ import { createSpinner } from "../utils/safe-spinner.js";
18
19
 
19
20
  const streamPipeline = promisify(pipeline);
20
21
 
21
22
  export class DownloadManager {
23
+ /**
24
+ * Patterns to exclude from extraction
25
+ */
26
+ private static EXCLUDE_PATTERNS = [
27
+ ".git",
28
+ ".git/**",
29
+ ".github",
30
+ ".github/**",
31
+ "node_modules",
32
+ "node_modules/**",
33
+ ".DS_Store",
34
+ "Thumbs.db",
35
+ "*.log",
36
+ ];
37
+
38
+ /**
39
+ * Check if file path should be excluded
40
+ */
41
+ private shouldExclude(filePath: string): boolean {
42
+ const ig = ignore().add(DownloadManager.EXCLUDE_PATTERNS);
43
+ return ig.ignores(filePath);
44
+ }
45
+
22
46
  /**
23
47
  * Download asset from URL with progress tracking
24
48
  */
@@ -31,11 +55,11 @@ export class DownloadManager {
31
55
 
32
56
  logger.info(`Downloading ${asset.name} (${this.formatBytes(asset.size)})...`);
33
57
 
34
- // Create progress bar
58
+ // Create progress bar with simple ASCII characters
35
59
  const progressBar = new cliProgress.SingleBar({
36
60
  format: "Progress |{bar}| {percentage}% | {value}/{total} MB",
37
- barCompleteChar: "\u2588",
38
- barIncompleteChar: "\u2591",
61
+ barCompleteChar: "=",
62
+ barIncompleteChar: "-",
39
63
  hideCursor: true,
40
64
  });
41
65
 
@@ -91,6 +115,92 @@ export class DownloadManager {
91
115
  }
92
116
  }
93
117
 
118
+ /**
119
+ * Download file from URL with progress tracking (supports both assets and API URLs)
120
+ */
121
+ async downloadFile(params: {
122
+ url: string;
123
+ name: string;
124
+ size?: number;
125
+ destDir: string;
126
+ token?: string;
127
+ }): Promise<string> {
128
+ const { url, name, size, destDir, token } = params;
129
+ const destPath = join(destDir, name);
130
+
131
+ await mkdir(destDir, { recursive: true });
132
+
133
+ logger.info(`Downloading ${name}${size ? ` (${this.formatBytes(size)})` : ""}...`);
134
+
135
+ const headers: Record<string, string> = {};
136
+
137
+ // Add authentication for GitHub API URLs
138
+ if (token && url.includes("api.github.com")) {
139
+ headers.Authorization = `Bearer ${token}`;
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";
143
+ } else {
144
+ headers.Accept = "application/octet-stream";
145
+ }
146
+
147
+ const response = await fetch(url, { headers });
148
+
149
+ if (!response.ok) {
150
+ throw new DownloadError(`Failed to download: ${response.statusText}`);
151
+ }
152
+
153
+ const totalSize = size || Number(response.headers.get("content-length")) || 0;
154
+ let downloadedSize = 0;
155
+
156
+ // Create progress bar only if we know the size (using simple ASCII characters)
157
+ const progressBar =
158
+ totalSize > 0
159
+ ? new cliProgress.SingleBar({
160
+ format: "Progress |{bar}| {percentage}% | {value}/{total} MB",
161
+ barCompleteChar: "=",
162
+ barIncompleteChar: "-",
163
+ hideCursor: true,
164
+ })
165
+ : null;
166
+
167
+ if (progressBar) {
168
+ progressBar.start(Math.round(totalSize / 1024 / 1024), 0);
169
+ }
170
+
171
+ const fileStream = createWriteStream(destPath);
172
+ const reader = response.body?.getReader();
173
+
174
+ if (!reader) {
175
+ throw new DownloadError("Failed to get response reader");
176
+ }
177
+
178
+ try {
179
+ while (true) {
180
+ const { done, value } = await reader.read();
181
+
182
+ if (done) break;
183
+
184
+ fileStream.write(value);
185
+ downloadedSize += value.length;
186
+
187
+ if (progressBar) {
188
+ progressBar.update(Math.round(downloadedSize / 1024 / 1024));
189
+ }
190
+ }
191
+
192
+ fileStream.end();
193
+ if (progressBar) progressBar.stop();
194
+
195
+ logger.success(`Downloaded ${name}`);
196
+ return destPath;
197
+ } catch (error) {
198
+ fileStream.close();
199
+ if (progressBar) progressBar.stop();
200
+ throw error;
201
+ }
202
+ }
203
+
94
204
  /**
95
205
  * Extract archive to destination
96
206
  */
@@ -99,7 +209,7 @@ export class DownloadManager {
99
209
  destDir: string,
100
210
  archiveType?: ArchiveType,
101
211
  ): Promise<void> {
102
- const spinner = ora("Extracting files...").start();
212
+ const spinner = createSpinner("Extracting files...").start();
103
213
 
104
214
  try {
105
215
  // Detect archive type from filename if not provided
@@ -133,6 +243,14 @@ export class DownloadManager {
133
243
  file: archivePath,
134
244
  cwd: destDir,
135
245
  strip: 1, // Strip the root directory from the archive
246
+ filter: (path: string) => {
247
+ // Exclude unwanted files
248
+ const shouldInclude = !this.shouldExclude(path);
249
+ if (!shouldInclude) {
250
+ logger.debug(`Excluding: ${path}`);
251
+ }
252
+ return shouldInclude;
253
+ },
136
254
  });
137
255
  }
138
256
 
@@ -140,7 +258,121 @@ export class DownloadManager {
140
258
  * Extract zip archive
141
259
  */
142
260
  private async extractZip(archivePath: string, destDir: string): Promise<void> {
143
- await streamPipeline(createReadStream(archivePath), unzipper.Extract({ path: destDir }));
261
+ const { readdir, stat, mkdir: mkdirPromise, copyFile, rm } = await import("node:fs/promises");
262
+ const { join: pathJoin } = await import("node:path");
263
+
264
+ // Extract to a temporary directory first
265
+ const tempExtractDir = `${destDir}-temp`;
266
+ await mkdirPromise(tempExtractDir, { recursive: true });
267
+
268
+ try {
269
+ // Extract zip to temp directory
270
+ await streamPipeline(
271
+ createReadStream(archivePath),
272
+ unzipper.Extract({ path: tempExtractDir }),
273
+ );
274
+
275
+ // Find the root directory in the zip (if any)
276
+ const entries = await readdir(tempExtractDir);
277
+
278
+ // If there's a single root directory, strip it
279
+ if (entries.length === 1) {
280
+ const rootEntry = entries[0];
281
+ const rootPath = pathJoin(tempExtractDir, rootEntry);
282
+ const rootStat = await stat(rootPath);
283
+
284
+ if (rootStat.isDirectory()) {
285
+ // Move contents from the root directory to the destination
286
+ await this.moveDirectoryContents(rootPath, destDir);
287
+ } else {
288
+ // Single file, just move it
289
+ await mkdirPromise(destDir, { recursive: true });
290
+ await copyFile(rootPath, pathJoin(destDir, rootEntry));
291
+ }
292
+ } else {
293
+ // Multiple entries at root, move them all
294
+ await this.moveDirectoryContents(tempExtractDir, destDir);
295
+ }
296
+
297
+ // Clean up temp directory
298
+ await rm(tempExtractDir, { recursive: true, force: true });
299
+ } catch (error) {
300
+ // Clean up temp directory on error
301
+ try {
302
+ await rm(tempExtractDir, { recursive: true, force: true });
303
+ } catch {
304
+ // Ignore cleanup errors
305
+ }
306
+ throw error;
307
+ }
308
+ }
309
+
310
+ /**
311
+ * Move directory contents from source to destination, applying exclusion filters
312
+ */
313
+ private async moveDirectoryContents(sourceDir: string, destDir: string): Promise<void> {
314
+ const { readdir, stat, mkdir: mkdirPromise, copyFile } = await import("node:fs/promises");
315
+ const { join: pathJoin, relative } = await import("node:path");
316
+
317
+ await mkdirPromise(destDir, { recursive: true });
318
+
319
+ const entries = await readdir(sourceDir);
320
+
321
+ for (const entry of entries) {
322
+ const sourcePath = pathJoin(sourceDir, entry);
323
+ const destPath = pathJoin(destDir, entry);
324
+ const relativePath = relative(sourceDir, sourcePath);
325
+
326
+ // Skip excluded files
327
+ if (this.shouldExclude(relativePath)) {
328
+ logger.debug(`Excluding: ${relativePath}`);
329
+ continue;
330
+ }
331
+
332
+ const entryStat = await stat(sourcePath);
333
+
334
+ if (entryStat.isDirectory()) {
335
+ // Recursively copy directory
336
+ await this.copyDirectory(sourcePath, destPath);
337
+ } else {
338
+ // Copy file
339
+ await copyFile(sourcePath, destPath);
340
+ }
341
+ }
342
+ }
343
+
344
+ /**
345
+ * Recursively copy directory
346
+ */
347
+ private async copyDirectory(sourceDir: string, destDir: string): Promise<void> {
348
+ const { readdir, stat, mkdir: mkdirPromise, copyFile } = await import("node:fs/promises");
349
+ const { join: pathJoin, relative } = await import("node:path");
350
+
351
+ await mkdirPromise(destDir, { recursive: true });
352
+
353
+ const entries = await readdir(sourceDir);
354
+
355
+ for (const entry of entries) {
356
+ const sourcePath = pathJoin(sourceDir, entry);
357
+ const destPath = pathJoin(destDir, entry);
358
+ const relativePath = relative(sourceDir, sourcePath);
359
+
360
+ // Skip excluded files
361
+ if (this.shouldExclude(relativePath)) {
362
+ logger.debug(`Excluding: ${relativePath}`);
363
+ continue;
364
+ }
365
+
366
+ const entryStat = await stat(sourcePath);
367
+
368
+ if (entryStat.isDirectory()) {
369
+ // Recursively copy directory
370
+ await this.copyDirectory(sourcePath, destPath);
371
+ } else {
372
+ // Copy file
373
+ await copyFile(sourcePath, destPath);
374
+ }
375
+ }
144
376
  }
145
377
 
146
378
  /**
package/src/lib/github.ts CHANGED
@@ -154,4 +154,77 @@ export class GitHubClient {
154
154
  );
155
155
  }
156
156
  }
157
+
158
+ /**
159
+ * Get downloadable asset or source code URL from release
160
+ * Priority:
161
+ * 1. "ClaudeKit Engineer Package" or "ClaudeKit Marketing Package" zip file
162
+ * 2. Other custom uploaded assets (.tar.gz, .tgz, .zip) excluding "Source code" archives
163
+ * 3. GitHub's automatic tarball URL
164
+ */
165
+ static getDownloadableAsset(release: GitHubRelease): {
166
+ type: "asset" | "tarball" | "zipball";
167
+ url: string;
168
+ name: string;
169
+ size?: number;
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
+
181
+ // First priority: Look for official ClaudeKit package assets
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
+ });
190
+
191
+ if (packageAsset) {
192
+ logger.debug(`✓ Selected ClaudeKit package asset: ${packageAsset.name}`);
193
+ return {
194
+ type: "asset",
195
+ url: packageAsset.url, // Use API endpoint for authenticated downloads
196
+ name: packageAsset.name,
197
+ size: packageAsset.size,
198
+ };
199
+ }
200
+
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)
204
+ const customAsset = release.assets.find(
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"),
209
+ );
210
+
211
+ if (customAsset) {
212
+ logger.debug(`✓ Selected custom asset: ${customAsset.name}`);
213
+ return {
214
+ type: "asset",
215
+ url: customAsset.url, // Use API endpoint for authenticated downloads
216
+ name: customAsset.name,
217
+ size: customAsset.size,
218
+ };
219
+ }
220
+
221
+ // Fall back to GitHub's automatic tarball
222
+ logger.debug("⚠ No custom assets found, falling back to GitHub automatic tarball");
223
+ return {
224
+ type: "tarball",
225
+ url: release.tarball_url,
226
+ name: `${release.tag_name}.tar.gz`,
227
+ size: undefined, // Size unknown for automatic tarballs
228
+ };
229
+ }
157
230
  }
@@ -1,5 +1,6 @@
1
1
  import * as clack from "@clack/prompts";
2
2
  import { AVAILABLE_KITS, type KitType } from "../types.js";
3
+ import { intro, note, outro } from "../utils/safe-prompts.js";
3
4
 
4
5
  export class PromptsManager {
5
6
  /**
@@ -94,20 +95,20 @@ export class PromptsManager {
94
95
  * Show intro message
95
96
  */
96
97
  intro(message: string): void {
97
- clack.intro(message);
98
+ intro(message);
98
99
  }
99
100
 
100
101
  /**
101
102
  * Show outro message
102
103
  */
103
104
  outro(message: string): void {
104
- clack.outro(message);
105
+ outro(message);
105
106
  }
106
107
 
107
108
  /**
108
109
  * Show note
109
110
  */
110
111
  note(message: string, title?: string): void {
111
- clack.note(message, title);
112
+ note(message, title);
112
113
  }
113
114
  }