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,654 +0,0 @@
1
- import { createWriteStream } from "node:fs";
2
- import { mkdir } from "node:fs/promises";
3
- import { tmpdir } from "node:os";
4
- import { join, relative, resolve } from "node:path";
5
- import cliProgress from "cli-progress";
6
- import extractZip from "extract-zip";
7
- import ignore from "ignore";
8
- import * as tar from "tar";
9
- import {
10
- type ArchiveType,
11
- DownloadError,
12
- ExtractionError,
13
- type GitHubReleaseAsset,
14
- } from "../types.js";
15
- import { logger } from "../utils/logger.js";
16
- import { createSpinner } from "../utils/safe-spinner.js";
17
-
18
- export class DownloadManager {
19
- /**
20
- * Maximum extraction size (500MB) to prevent archive bombs
21
- */
22
- private static MAX_EXTRACTION_SIZE = 500 * 1024 * 1024; // 500MB
23
-
24
- /**
25
- * Patterns to exclude from extraction
26
- */
27
- private static EXCLUDE_PATTERNS = [
28
- ".git",
29
- ".git/**",
30
- ".github",
31
- ".github/**",
32
- "node_modules",
33
- "node_modules/**",
34
- ".DS_Store",
35
- "Thumbs.db",
36
- "*.log",
37
- ];
38
-
39
- /**
40
- * Track total extracted size to prevent archive bombs
41
- */
42
- private totalExtractedSize = 0;
43
-
44
- /**
45
- * Instance-level ignore object with combined default and user patterns
46
- */
47
- private ig: ReturnType<typeof ignore>;
48
-
49
- /**
50
- * Store user-defined exclude patterns
51
- */
52
- private userExcludePatterns: string[] = [];
53
-
54
- /**
55
- * Initialize DownloadManager with default exclude patterns
56
- */
57
- constructor() {
58
- // Initialize ignore with default patterns
59
- this.ig = ignore().add(DownloadManager.EXCLUDE_PATTERNS);
60
- }
61
-
62
- /**
63
- * Set additional user-defined exclude patterns
64
- * These are added to (not replace) the default EXCLUDE_PATTERNS
65
- */
66
- setExcludePatterns(patterns: string[]): void {
67
- this.userExcludePatterns = patterns;
68
- // Reinitialize ignore with both default and user patterns
69
- this.ig = ignore().add([...DownloadManager.EXCLUDE_PATTERNS, ...this.userExcludePatterns]);
70
-
71
- if (patterns.length > 0) {
72
- logger.info(`Added ${patterns.length} custom exclude pattern(s)`);
73
- patterns.forEach((p) => logger.debug(` - ${p}`));
74
- }
75
- }
76
-
77
- /**
78
- * Check if file path should be excluded
79
- * Uses instance-level ignore with both default and user patterns
80
- */
81
- private shouldExclude(filePath: string): boolean {
82
- return this.ig.ignores(filePath);
83
- }
84
-
85
- /**
86
- * Validate path to prevent path traversal attacks (zip slip)
87
- */
88
- private isPathSafe(basePath: string, targetPath: string): boolean {
89
- // Resolve both paths to their absolute canonical forms
90
- const resolvedBase = resolve(basePath);
91
- const resolvedTarget = resolve(targetPath);
92
-
93
- // Calculate relative path from base to target
94
- const relativePath = relative(resolvedBase, resolvedTarget);
95
-
96
- // If path starts with .. or is absolute, it's trying to escape
97
- // Also block if relative path is empty but resolved paths differ (edge case)
98
- return (
99
- !relativePath.startsWith("..") &&
100
- !relativePath.startsWith("/") &&
101
- resolvedTarget.startsWith(resolvedBase)
102
- );
103
- }
104
-
105
- /**
106
- * Track extracted file size and check against limit
107
- */
108
- private checkExtractionSize(fileSize: number): void {
109
- this.totalExtractedSize += fileSize;
110
- if (this.totalExtractedSize > DownloadManager.MAX_EXTRACTION_SIZE) {
111
- throw new ExtractionError(
112
- `Archive exceeds maximum extraction size of ${this.formatBytes(DownloadManager.MAX_EXTRACTION_SIZE)}. Possible archive bomb detected.`,
113
- );
114
- }
115
- }
116
-
117
- /**
118
- * Reset extraction size tracker
119
- */
120
- private resetExtractionSize(): void {
121
- this.totalExtractedSize = 0;
122
- }
123
-
124
- /**
125
- * Download asset from URL with progress tracking
126
- */
127
- async downloadAsset(asset: GitHubReleaseAsset, destDir: string): Promise<string> {
128
- try {
129
- const destPath = join(destDir, asset.name);
130
-
131
- // Ensure destination directory exists
132
- await mkdir(destDir, { recursive: true });
133
-
134
- logger.info(`Downloading ${asset.name} (${this.formatBytes(asset.size)})...`);
135
-
136
- // Create progress bar with simple ASCII characters
137
- const progressBar = new cliProgress.SingleBar({
138
- format: "Progress |{bar}| {percentage}% | {value}/{total} MB",
139
- barCompleteChar: "=",
140
- barIncompleteChar: "-",
141
- hideCursor: true,
142
- });
143
-
144
- const response = await fetch(asset.browser_download_url, {
145
- headers: {
146
- Accept: "application/octet-stream",
147
- },
148
- });
149
-
150
- if (!response.ok) {
151
- throw new DownloadError(`Failed to download: ${response.statusText}`);
152
- }
153
-
154
- const totalSize = asset.size;
155
- let downloadedSize = 0;
156
-
157
- progressBar.start(Math.round(totalSize / 1024 / 1024), 0);
158
-
159
- const fileStream = createWriteStream(destPath);
160
- const reader = response.body?.getReader();
161
-
162
- if (!reader) {
163
- throw new DownloadError("Failed to get response reader");
164
- }
165
-
166
- try {
167
- while (true) {
168
- const { done, value } = await reader.read();
169
-
170
- if (done) {
171
- break;
172
- }
173
-
174
- fileStream.write(value);
175
- downloadedSize += value.length;
176
- progressBar.update(Math.round(downloadedSize / 1024 / 1024));
177
- }
178
-
179
- fileStream.end();
180
- progressBar.stop();
181
-
182
- logger.success(`Downloaded ${asset.name}`);
183
- return destPath;
184
- } catch (error) {
185
- fileStream.close();
186
- progressBar.stop();
187
- throw error;
188
- }
189
- } catch (error) {
190
- throw new DownloadError(
191
- `Failed to download ${asset.name}: ${error instanceof Error ? error.message : "Unknown error"}`,
192
- );
193
- }
194
- }
195
-
196
- /**
197
- * Download file from URL with progress tracking (supports both assets and API URLs)
198
- */
199
- async downloadFile(params: {
200
- url: string;
201
- name: string;
202
- size?: number;
203
- destDir: string;
204
- token?: string;
205
- }): Promise<string> {
206
- const { url, name, size, destDir, token } = params;
207
- const destPath = join(destDir, name);
208
-
209
- await mkdir(destDir, { recursive: true });
210
-
211
- logger.info(`Downloading ${name}${size ? ` (${this.formatBytes(size)})` : ""}...`);
212
-
213
- const headers: Record<string, string> = {};
214
-
215
- // Add authentication for GitHub API URLs
216
- if (token && url.includes("api.github.com")) {
217
- headers.Authorization = `Bearer ${token}`;
218
- // Use application/octet-stream for asset downloads (not vnd.github+json)
219
- headers.Accept = "application/octet-stream";
220
- headers["X-GitHub-Api-Version"] = "2022-11-28";
221
- } else {
222
- headers.Accept = "application/octet-stream";
223
- }
224
-
225
- const response = await fetch(url, { headers });
226
-
227
- if (!response.ok) {
228
- throw new DownloadError(`Failed to download: ${response.statusText}`);
229
- }
230
-
231
- const totalSize = size || Number(response.headers.get("content-length")) || 0;
232
- let downloadedSize = 0;
233
-
234
- // Create progress bar only if we know the size (using simple ASCII characters)
235
- const progressBar =
236
- totalSize > 0
237
- ? new cliProgress.SingleBar({
238
- format: "Progress |{bar}| {percentage}% | {value}/{total} MB",
239
- barCompleteChar: "=",
240
- barIncompleteChar: "-",
241
- hideCursor: true,
242
- })
243
- : null;
244
-
245
- if (progressBar) {
246
- progressBar.start(Math.round(totalSize / 1024 / 1024), 0);
247
- }
248
-
249
- const fileStream = createWriteStream(destPath);
250
- const reader = response.body?.getReader();
251
-
252
- if (!reader) {
253
- throw new DownloadError("Failed to get response reader");
254
- }
255
-
256
- try {
257
- while (true) {
258
- const { done, value } = await reader.read();
259
-
260
- if (done) break;
261
-
262
- fileStream.write(value);
263
- downloadedSize += value.length;
264
-
265
- if (progressBar) {
266
- progressBar.update(Math.round(downloadedSize / 1024 / 1024));
267
- }
268
- }
269
-
270
- fileStream.end();
271
- if (progressBar) progressBar.stop();
272
-
273
- logger.success(`Downloaded ${name}`);
274
- return destPath;
275
- } catch (error) {
276
- fileStream.close();
277
- if (progressBar) progressBar.stop();
278
- throw error;
279
- }
280
- }
281
-
282
- /**
283
- * Extract archive to destination
284
- */
285
- async extractArchive(
286
- archivePath: string,
287
- destDir: string,
288
- archiveType?: ArchiveType,
289
- ): Promise<void> {
290
- const spinner = createSpinner("Extracting files...").start();
291
-
292
- try {
293
- // Reset extraction size tracker
294
- this.resetExtractionSize();
295
-
296
- // Detect archive type from filename if not provided
297
- const detectedType = archiveType || this.detectArchiveType(archivePath);
298
-
299
- // Ensure destination directory exists
300
- await mkdir(destDir, { recursive: true });
301
-
302
- if (detectedType === "tar.gz") {
303
- await this.extractTarGz(archivePath, destDir);
304
- } else if (detectedType === "zip") {
305
- await this.extractZip(archivePath, destDir);
306
- } else {
307
- throw new ExtractionError(`Unsupported archive type: ${detectedType}`);
308
- }
309
-
310
- spinner.succeed("Files extracted successfully");
311
- } catch (error) {
312
- spinner.fail("Extraction failed");
313
- throw new ExtractionError(
314
- `Failed to extract archive: ${error instanceof Error ? error.message : "Unknown error"}`,
315
- );
316
- }
317
- }
318
-
319
- /**
320
- * Extract tar.gz archive
321
- */
322
- private async extractTarGz(archivePath: string, destDir: string): Promise<void> {
323
- const { readdir, stat, mkdir: mkdirPromise, copyFile, rm } = await import("node:fs/promises");
324
- const { join: pathJoin } = await import("node:path");
325
-
326
- // Extract to a temporary directory first
327
- const tempExtractDir = `${destDir}-temp`;
328
- await mkdirPromise(tempExtractDir, { recursive: true });
329
-
330
- try {
331
- // Extract without stripping first
332
- await tar.extract({
333
- file: archivePath,
334
- cwd: tempExtractDir,
335
- strip: 0, // Don't strip yet - we'll decide based on wrapper detection
336
- filter: (path: string) => {
337
- // Exclude unwanted files
338
- const shouldInclude = !this.shouldExclude(path);
339
- if (!shouldInclude) {
340
- logger.debug(`Excluding: ${path}`);
341
- }
342
- return shouldInclude;
343
- },
344
- });
345
-
346
- logger.debug(`Extracted TAR.GZ to temp: ${tempExtractDir}`);
347
-
348
- // Apply same wrapper detection logic as zip
349
- const entries = await readdir(tempExtractDir);
350
- logger.debug(`Root entries: ${entries.join(", ")}`);
351
-
352
- if (entries.length === 1) {
353
- const rootEntry = entries[0];
354
- const rootPath = pathJoin(tempExtractDir, rootEntry);
355
- const rootStat = await stat(rootPath);
356
-
357
- if (rootStat.isDirectory()) {
358
- // Check contents of root directory
359
- const rootContents = await readdir(rootPath);
360
- logger.debug(`Root directory '${rootEntry}' contains: ${rootContents.join(", ")}`);
361
-
362
- // Only strip if root is a version/release wrapper
363
- const isWrapper = this.isWrapperDirectory(rootEntry);
364
- logger.debug(`Is wrapper directory: ${isWrapper}`);
365
-
366
- if (isWrapper) {
367
- // Strip wrapper and move contents
368
- logger.debug(`Stripping wrapper directory: ${rootEntry}`);
369
- await this.moveDirectoryContents(rootPath, destDir);
370
- } else {
371
- // Keep root directory - move everything including root
372
- logger.debug("Preserving complete directory structure");
373
- await this.moveDirectoryContents(tempExtractDir, destDir);
374
- }
375
- } else {
376
- // Single file, just move it
377
- await mkdirPromise(destDir, { recursive: true });
378
- await copyFile(rootPath, pathJoin(destDir, rootEntry));
379
- }
380
- } else {
381
- // Multiple entries at root, move them all
382
- logger.debug("Multiple root entries - moving all");
383
- await this.moveDirectoryContents(tempExtractDir, destDir);
384
- }
385
-
386
- logger.debug(`Moved contents to: ${destDir}`);
387
-
388
- // Clean up temp directory
389
- await rm(tempExtractDir, { recursive: true, force: true });
390
- } catch (error) {
391
- // Clean up temp directory on error
392
- try {
393
- await rm(tempExtractDir, { recursive: true, force: true });
394
- } catch {
395
- // Ignore cleanup errors
396
- }
397
- throw error;
398
- }
399
- }
400
-
401
- /**
402
- * Check if directory name is a version/release wrapper
403
- * Examples: claudekit-engineer-v1.0.0, claudekit-engineer-1.0.0, repo-abc1234,
404
- * project-v1.0.0-alpha, project-1.2.3-beta.1, repo-v2.0.0-rc.5
405
- */
406
- private isWrapperDirectory(dirName: string): boolean {
407
- // Match version patterns with optional prerelease: project-v1.0.0, project-1.0.0-alpha, project-v2.0.0-rc.1
408
- const versionPattern = /^[\w-]+-v?\d+\.\d+\.\d+(-[\w.]+)?$/;
409
- // Match commit hash patterns: project-abc1234 (7-40 chars for short/full SHA)
410
- const hashPattern = /^[\w-]+-[a-f0-9]{7,40}$/;
411
-
412
- return versionPattern.test(dirName) || hashPattern.test(dirName);
413
- }
414
-
415
- /**
416
- * Extract zip archive
417
- */
418
- private async extractZip(archivePath: string, destDir: string): Promise<void> {
419
- const { readdir, stat, mkdir: mkdirPromise, copyFile, rm } = await import("node:fs/promises");
420
- const { join: pathJoin } = await import("node:path");
421
-
422
- // Extract to a temporary directory first
423
- const tempExtractDir = `${destDir}-temp`;
424
- await mkdirPromise(tempExtractDir, { recursive: true });
425
-
426
- try {
427
- // Extract zip to temp directory using extract-zip
428
- await extractZip(archivePath, { dir: tempExtractDir });
429
-
430
- logger.debug(`Extracted ZIP to temp: ${tempExtractDir}`);
431
-
432
- // Find the root directory in the zip (if any)
433
- const entries = await readdir(tempExtractDir);
434
- logger.debug(`Root entries: ${entries.join(", ")}`);
435
-
436
- // If there's a single root directory, check if it's a wrapper
437
- if (entries.length === 1) {
438
- const rootEntry = entries[0];
439
- const rootPath = pathJoin(tempExtractDir, rootEntry);
440
- const rootStat = await stat(rootPath);
441
-
442
- if (rootStat.isDirectory()) {
443
- // Check contents of root directory
444
- const rootContents = await readdir(rootPath);
445
- logger.debug(`Root directory '${rootEntry}' contains: ${rootContents.join(", ")}`);
446
-
447
- // Only strip if root is a version/release wrapper
448
- const isWrapper = this.isWrapperDirectory(rootEntry);
449
- logger.debug(`Is wrapper directory: ${isWrapper}`);
450
-
451
- if (isWrapper) {
452
- // Strip wrapper and move contents
453
- logger.debug(`Stripping wrapper directory: ${rootEntry}`);
454
- await this.moveDirectoryContents(rootPath, destDir);
455
- } else {
456
- // Keep root directory - move everything including root
457
- logger.debug("Preserving complete directory structure");
458
- await this.moveDirectoryContents(tempExtractDir, destDir);
459
- }
460
- } else {
461
- // Single file, just move it
462
- await mkdirPromise(destDir, { recursive: true });
463
- await copyFile(rootPath, pathJoin(destDir, rootEntry));
464
- }
465
- } else {
466
- // Multiple entries at root, move them all
467
- logger.debug("Multiple root entries - moving all");
468
- await this.moveDirectoryContents(tempExtractDir, destDir);
469
- }
470
-
471
- logger.debug(`Moved contents to: ${destDir}`);
472
-
473
- // Clean up temp directory
474
- await rm(tempExtractDir, { recursive: true, force: true });
475
- } catch (error) {
476
- // Clean up temp directory on error
477
- try {
478
- await rm(tempExtractDir, { recursive: true, force: true });
479
- } catch {
480
- // Ignore cleanup errors
481
- }
482
- throw error;
483
- }
484
- }
485
-
486
- /**
487
- * Move directory contents from source to destination, applying exclusion filters
488
- */
489
- private async moveDirectoryContents(sourceDir: string, destDir: string): Promise<void> {
490
- const { readdir, stat, mkdir: mkdirPromise, copyFile } = await import("node:fs/promises");
491
- const { join: pathJoin, relative } = await import("node:path");
492
-
493
- await mkdirPromise(destDir, { recursive: true });
494
-
495
- const entries = await readdir(sourceDir);
496
-
497
- for (const entry of entries) {
498
- const sourcePath = pathJoin(sourceDir, entry);
499
- const destPath = pathJoin(destDir, entry);
500
- const relativePath = relative(sourceDir, sourcePath);
501
-
502
- // Validate path safety (prevent path traversal)
503
- if (!this.isPathSafe(destDir, destPath)) {
504
- logger.warning(`Skipping unsafe path: ${relativePath}`);
505
- throw new ExtractionError(`Path traversal attempt detected: ${relativePath}`);
506
- }
507
-
508
- // Skip excluded files
509
- if (this.shouldExclude(relativePath)) {
510
- logger.debug(`Excluding: ${relativePath}`);
511
- continue;
512
- }
513
-
514
- const entryStat = await stat(sourcePath);
515
-
516
- if (entryStat.isDirectory()) {
517
- // Recursively copy directory
518
- await this.copyDirectory(sourcePath, destPath);
519
- } else {
520
- // Track file size and check limit
521
- this.checkExtractionSize(entryStat.size);
522
- // Copy file
523
- await copyFile(sourcePath, destPath);
524
- }
525
- }
526
- }
527
-
528
- /**
529
- * Recursively copy directory
530
- */
531
- private async copyDirectory(sourceDir: string, destDir: string): Promise<void> {
532
- const { readdir, stat, mkdir: mkdirPromise, copyFile } = await import("node:fs/promises");
533
- const { join: pathJoin, relative } = await import("node:path");
534
-
535
- await mkdirPromise(destDir, { recursive: true });
536
-
537
- const entries = await readdir(sourceDir);
538
-
539
- for (const entry of entries) {
540
- const sourcePath = pathJoin(sourceDir, entry);
541
- const destPath = pathJoin(destDir, entry);
542
- const relativePath = relative(sourceDir, sourcePath);
543
-
544
- // Validate path safety (prevent path traversal)
545
- if (!this.isPathSafe(destDir, destPath)) {
546
- logger.warning(`Skipping unsafe path: ${relativePath}`);
547
- throw new ExtractionError(`Path traversal attempt detected: ${relativePath}`);
548
- }
549
-
550
- // Skip excluded files
551
- if (this.shouldExclude(relativePath)) {
552
- logger.debug(`Excluding: ${relativePath}`);
553
- continue;
554
- }
555
-
556
- const entryStat = await stat(sourcePath);
557
-
558
- if (entryStat.isDirectory()) {
559
- // Recursively copy directory
560
- await this.copyDirectory(sourcePath, destPath);
561
- } else {
562
- // Track file size and check limit
563
- this.checkExtractionSize(entryStat.size);
564
- // Copy file
565
- await copyFile(sourcePath, destPath);
566
- }
567
- }
568
- }
569
-
570
- /**
571
- * Detect archive type from filename
572
- */
573
- private detectArchiveType(filename: string): ArchiveType {
574
- if (filename.endsWith(".tar.gz") || filename.endsWith(".tgz")) {
575
- return "tar.gz";
576
- }
577
- if (filename.endsWith(".zip")) {
578
- return "zip";
579
- }
580
- throw new ExtractionError(`Cannot detect archive type from filename: ${filename}`);
581
- }
582
-
583
- /**
584
- * Validate extraction results
585
- * @throws {ExtractionError} If validation fails
586
- */
587
- async validateExtraction(extractDir: string): Promise<void> {
588
- const { readdir, access } = await import("node:fs/promises");
589
- const { join: pathJoin } = await import("node:path");
590
- const { constants } = await import("node:fs");
591
-
592
- try {
593
- // Check if extract directory exists and is not empty
594
- const entries = await readdir(extractDir);
595
- logger.debug(`Extracted files: ${entries.join(", ")}`);
596
-
597
- if (entries.length === 0) {
598
- throw new ExtractionError("Extraction resulted in no files");
599
- }
600
-
601
- // Verify critical paths exist
602
- const criticalPaths = [".claude", "CLAUDE.md"];
603
- const missingPaths: string[] = [];
604
-
605
- for (const path of criticalPaths) {
606
- try {
607
- await access(pathJoin(extractDir, path), constants.F_OK);
608
- logger.debug(`✓ Found: ${path}`);
609
- } catch {
610
- logger.warning(`Expected path not found: ${path}`);
611
- missingPaths.push(path);
612
- }
613
- }
614
-
615
- // Warn if critical paths are missing but don't fail validation
616
- if (missingPaths.length > 0) {
617
- logger.warning(
618
- `Some expected paths are missing: ${missingPaths.join(", ")}. This may not be a ClaudeKit project.`,
619
- );
620
- }
621
-
622
- logger.debug("Extraction validation passed");
623
- } catch (error) {
624
- if (error instanceof ExtractionError) {
625
- throw error;
626
- }
627
- throw new ExtractionError(
628
- `Validation failed: ${error instanceof Error ? error.message : "Unknown error"}`,
629
- );
630
- }
631
- }
632
-
633
- /**
634
- * Create temporary download directory
635
- */
636
- async createTempDir(): Promise<string> {
637
- const tempDir = join(tmpdir(), `claudekit-${Date.now()}`);
638
- await mkdir(tempDir, { recursive: true });
639
- return tempDir;
640
- }
641
-
642
- /**
643
- * Format bytes to human readable string
644
- */
645
- private formatBytes(bytes: number): string {
646
- if (bytes === 0) return "0 Bytes";
647
-
648
- const k = 1024;
649
- const sizes = ["Bytes", "KB", "MB", "GB"];
650
- const i = Math.floor(Math.log(bytes) / Math.log(k));
651
-
652
- return `${Math.round((bytes / k ** i) * 100) / 100} ${sizes[i]}`;
653
- }
654
- }