claudekit-cli 1.1.0 → 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.
- package/.github/workflows/release.yml +44 -0
- package/CHANGELOG.md +16 -0
- package/CLAUDE.md +3 -9
- package/LICENSE +21 -0
- package/README.md +53 -1
- package/dist/index.js +11473 -10945
- package/package.json +1 -1
- package/src/commands/new.ts +41 -9
- package/src/commands/update.ts +59 -13
- package/src/index.ts +42 -1
- package/src/lib/download.ts +231 -1
- package/src/lib/github.ts +56 -0
- package/src/lib/prompts.ts +4 -3
- package/src/types.ts +4 -2
- package/src/utils/file-scanner.ts +134 -0
- package/src/utils/logger.ts +108 -21
- package/src/utils/safe-prompts.ts +54 -0
- package/tests/commands/version.test.ts +2 -2
- package/tests/lib/github-download-priority.test.ts +301 -0
- package/tests/lib/github.test.ts +2 -2
- package/tests/lib/merge.test.ts +77 -0
- package/tests/types.test.ts +4 -0
- package/tests/utils/file-scanner.test.ts +202 -0
- package/tests/utils/logger.test.ts +115 -0
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { join, relative, resolve } from "node:path";
|
|
2
|
+
import { lstat, pathExists, readdir } from "fs-extra";
|
|
3
|
+
import { logger } from "./logger.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Utility class for scanning directories and comparing file structures
|
|
7
|
+
*/
|
|
8
|
+
export class FileScanner {
|
|
9
|
+
/**
|
|
10
|
+
* Get all files in a directory recursively
|
|
11
|
+
*
|
|
12
|
+
* @param dirPath - Directory path to scan
|
|
13
|
+
* @param relativeTo - Base path for calculating relative paths (defaults to dirPath)
|
|
14
|
+
* @returns Array of relative file paths
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```typescript
|
|
18
|
+
* const files = await FileScanner.getFiles('/path/to/dir');
|
|
19
|
+
* // Returns: ['file1.txt', 'subdir/file2.txt', ...]
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
static async getFiles(dirPath: string, relativeTo?: string): Promise<string[]> {
|
|
23
|
+
const basePath = relativeTo || dirPath;
|
|
24
|
+
const files: string[] = [];
|
|
25
|
+
|
|
26
|
+
// Check if directory exists
|
|
27
|
+
if (!(await pathExists(dirPath))) {
|
|
28
|
+
return files;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
const entries = await readdir(dirPath);
|
|
33
|
+
|
|
34
|
+
for (const entry of entries) {
|
|
35
|
+
const fullPath = join(dirPath, entry);
|
|
36
|
+
|
|
37
|
+
// Security: Validate path to prevent traversal
|
|
38
|
+
if (!FileScanner.isSafePath(basePath, fullPath)) {
|
|
39
|
+
logger.warning(`Skipping potentially unsafe path: ${entry}`);
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const stats = await lstat(fullPath);
|
|
44
|
+
|
|
45
|
+
// Skip symlinks for security
|
|
46
|
+
if (stats.isSymbolicLink()) {
|
|
47
|
+
logger.debug(`Skipping symlink: ${entry}`);
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (stats.isDirectory()) {
|
|
52
|
+
// Recursively scan subdirectories
|
|
53
|
+
const subFiles = await FileScanner.getFiles(fullPath, basePath);
|
|
54
|
+
files.push(...subFiles);
|
|
55
|
+
} else if (stats.isFile()) {
|
|
56
|
+
// Add relative path
|
|
57
|
+
const relativePath = relative(basePath, fullPath);
|
|
58
|
+
files.push(relativePath);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
} catch (error) {
|
|
62
|
+
const errorMessage =
|
|
63
|
+
error instanceof Error
|
|
64
|
+
? `Failed to scan directory: ${dirPath} - ${error.message}`
|
|
65
|
+
: `Failed to scan directory: ${dirPath}`;
|
|
66
|
+
logger.error(errorMessage);
|
|
67
|
+
throw error;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return files;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Find files in destination that don't exist in source
|
|
75
|
+
*
|
|
76
|
+
* @param destDir - Destination directory path
|
|
77
|
+
* @param sourceDir - Source directory path
|
|
78
|
+
* @param subPath - Subdirectory to compare (e.g., '.claude')
|
|
79
|
+
* @returns Array of relative file paths that are custom (exist in dest but not in source)
|
|
80
|
+
*
|
|
81
|
+
* @example
|
|
82
|
+
* ```typescript
|
|
83
|
+
* const customFiles = await FileScanner.findCustomFiles(
|
|
84
|
+
* '/path/to/project',
|
|
85
|
+
* '/path/to/release',
|
|
86
|
+
* '.claude'
|
|
87
|
+
* );
|
|
88
|
+
* // Returns: ['.claude/custom-command.md', '.claude/workflows/my-workflow.md']
|
|
89
|
+
* ```
|
|
90
|
+
*/
|
|
91
|
+
static async findCustomFiles(
|
|
92
|
+
destDir: string,
|
|
93
|
+
sourceDir: string,
|
|
94
|
+
subPath: string,
|
|
95
|
+
): Promise<string[]> {
|
|
96
|
+
const destSubDir = join(destDir, subPath);
|
|
97
|
+
const sourceSubDir = join(sourceDir, subPath);
|
|
98
|
+
|
|
99
|
+
// Get files from both directories
|
|
100
|
+
const destFiles = await FileScanner.getFiles(destSubDir, destDir);
|
|
101
|
+
const sourceFiles = await FileScanner.getFiles(sourceSubDir, sourceDir);
|
|
102
|
+
|
|
103
|
+
// Create a Set of source files for O(1) lookup
|
|
104
|
+
const sourceFileSet = new Set(sourceFiles);
|
|
105
|
+
|
|
106
|
+
// Find files in destination that don't exist in source
|
|
107
|
+
const customFiles = destFiles.filter((file) => !sourceFileSet.has(file));
|
|
108
|
+
|
|
109
|
+
if (customFiles.length > 0) {
|
|
110
|
+
logger.info(`Found ${customFiles.length} custom file(s) in ${subPath}/`);
|
|
111
|
+
customFiles.slice(0, 5).forEach((file) => logger.debug(` - ${file}`));
|
|
112
|
+
if (customFiles.length > 5) {
|
|
113
|
+
logger.debug(` ... and ${customFiles.length - 5} more`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return customFiles;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Validate path to prevent path traversal attacks
|
|
122
|
+
*
|
|
123
|
+
* @param basePath - Base directory path
|
|
124
|
+
* @param targetPath - Target path to validate
|
|
125
|
+
* @returns true if path is safe, false otherwise
|
|
126
|
+
*/
|
|
127
|
+
private static isSafePath(basePath: string, targetPath: string): boolean {
|
|
128
|
+
const resolvedBase = resolve(basePath);
|
|
129
|
+
const resolvedTarget = resolve(targetPath);
|
|
130
|
+
|
|
131
|
+
// Ensure target is within base
|
|
132
|
+
return resolvedTarget.startsWith(resolvedBase);
|
|
133
|
+
}
|
|
134
|
+
}
|
package/src/utils/logger.ts
CHANGED
|
@@ -1,37 +1,124 @@
|
|
|
1
|
+
import { type WriteStream, createWriteStream } from "node:fs";
|
|
1
2
|
import pc from "picocolors";
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
// Use ASCII-safe symbols to avoid unicode rendering issues in certain terminals
|
|
5
|
+
const symbols = {
|
|
6
|
+
info: "[i]",
|
|
7
|
+
success: "[✓]",
|
|
8
|
+
warning: "[!]",
|
|
9
|
+
error: "[✗]",
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
interface LogContext {
|
|
13
|
+
[key: string]: any;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
class Logger {
|
|
17
|
+
private verboseEnabled = false;
|
|
18
|
+
private logFileStream?: WriteStream;
|
|
7
19
|
|
|
8
|
-
|
|
9
|
-
console.log(pc.
|
|
10
|
-
}
|
|
20
|
+
info(message: string): void {
|
|
21
|
+
console.log(pc.blue(symbols.info), message);
|
|
22
|
+
}
|
|
11
23
|
|
|
12
|
-
|
|
13
|
-
console.log(pc.
|
|
14
|
-
}
|
|
24
|
+
success(message: string): void {
|
|
25
|
+
console.log(pc.green(symbols.success), message);
|
|
26
|
+
}
|
|
15
27
|
|
|
16
|
-
|
|
17
|
-
console.
|
|
18
|
-
}
|
|
28
|
+
warning(message: string): void {
|
|
29
|
+
console.log(pc.yellow(symbols.warning), message);
|
|
30
|
+
}
|
|
19
31
|
|
|
20
|
-
|
|
32
|
+
error(message: string): void {
|
|
33
|
+
console.error(pc.red(symbols.error), message);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
debug(message: string): void {
|
|
21
37
|
if (process.env.DEBUG) {
|
|
22
38
|
console.log(pc.gray("[DEBUG]"), message);
|
|
23
39
|
}
|
|
24
|
-
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
verbose(message: string, context?: LogContext): void {
|
|
43
|
+
if (!this.verboseEnabled) return;
|
|
44
|
+
|
|
45
|
+
const timestamp = this.getTimestamp();
|
|
46
|
+
const sanitizedMessage = this.sanitize(message);
|
|
47
|
+
const formattedContext = context ? this.formatContext(context) : "";
|
|
48
|
+
|
|
49
|
+
const logLine = `${timestamp} ${pc.gray("[VERBOSE]")} ${sanitizedMessage}${formattedContext}`;
|
|
50
|
+
|
|
51
|
+
console.error(logLine);
|
|
52
|
+
|
|
53
|
+
if (this.logFileStream) {
|
|
54
|
+
const plainLogLine = `${timestamp} [VERBOSE] ${sanitizedMessage}${formattedContext}`;
|
|
55
|
+
this.logFileStream.write(`${plainLogLine}\n`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
setVerbose(enabled: boolean): void {
|
|
60
|
+
this.verboseEnabled = enabled;
|
|
61
|
+
if (enabled) {
|
|
62
|
+
this.verbose("Verbose logging enabled");
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
isVerbose(): boolean {
|
|
67
|
+
return this.verboseEnabled;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
setLogFile(path?: string): void {
|
|
71
|
+
if (this.logFileStream) {
|
|
72
|
+
this.logFileStream.end();
|
|
73
|
+
this.logFileStream = undefined;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (path) {
|
|
77
|
+
this.logFileStream = createWriteStream(path, {
|
|
78
|
+
flags: "a",
|
|
79
|
+
mode: 0o600,
|
|
80
|
+
});
|
|
81
|
+
this.verbose(`Logging to file: ${path}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
25
84
|
|
|
26
|
-
|
|
27
|
-
sanitize: (text: string): string => {
|
|
28
|
-
// Remove GitHub tokens
|
|
85
|
+
sanitize(text: string): string {
|
|
29
86
|
return text
|
|
30
87
|
.replace(/ghp_[a-zA-Z0-9]{36}/g, "ghp_***")
|
|
31
88
|
.replace(/github_pat_[a-zA-Z0-9_]{82}/g, "github_pat_***")
|
|
32
89
|
.replace(/gho_[a-zA-Z0-9]{36}/g, "gho_***")
|
|
33
90
|
.replace(/ghu_[a-zA-Z0-9]{36}/g, "ghu_***")
|
|
34
91
|
.replace(/ghs_[a-zA-Z0-9]{36}/g, "ghs_***")
|
|
35
|
-
.replace(/ghr_[a-zA-Z0-9]{36}/g, "ghr_***")
|
|
36
|
-
|
|
37
|
-
|
|
92
|
+
.replace(/ghr_[a-zA-Z0-9]{36}/g, "ghr_***")
|
|
93
|
+
.replace(/Bearer [a-zA-Z0-9_-]+/g, "Bearer ***")
|
|
94
|
+
.replace(/token=[a-zA-Z0-9_-]+/g, "token=***");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
private getTimestamp(): string {
|
|
98
|
+
return new Date().toISOString();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
private formatContext(context: LogContext): string {
|
|
102
|
+
const sanitized = Object.entries(context).reduce((acc, [key, value]) => {
|
|
103
|
+
if (typeof value === "string") {
|
|
104
|
+
acc[key] = this.sanitize(value);
|
|
105
|
+
} else if (value && typeof value === "object") {
|
|
106
|
+
// Recursively sanitize nested objects
|
|
107
|
+
try {
|
|
108
|
+
const stringified = JSON.stringify(value);
|
|
109
|
+
const sanitizedStr = this.sanitize(stringified);
|
|
110
|
+
acc[key] = JSON.parse(sanitizedStr);
|
|
111
|
+
} catch {
|
|
112
|
+
acc[key] = "[Object]";
|
|
113
|
+
}
|
|
114
|
+
} else {
|
|
115
|
+
acc[key] = value;
|
|
116
|
+
}
|
|
117
|
+
return acc;
|
|
118
|
+
}, {} as LogContext);
|
|
119
|
+
|
|
120
|
+
return `\n ${JSON.stringify(sanitized, null, 2).split("\n").join("\n ")}`;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export const logger = new Logger();
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import * as clack from "@clack/prompts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Safe wrapper around clack prompts that handles unicode rendering issues.
|
|
5
|
+
* Sets up proper environment to minimize encoding problems.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// Store original methods
|
|
9
|
+
const originalIntro = clack.intro;
|
|
10
|
+
const originalOutro = clack.outro;
|
|
11
|
+
const originalNote = clack.note;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Wrapped intro that handles encoding better
|
|
15
|
+
*/
|
|
16
|
+
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
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Wrapped outro that handles encoding better
|
|
27
|
+
*/
|
|
28
|
+
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
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Wrapped note that handles encoding better
|
|
39
|
+
*/
|
|
40
|
+
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);
|
|
49
|
+
console.log();
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Re-export other clack functions unchanged
|
|
54
|
+
export { select, confirm, text, isCancel } from "@clack/prompts";
|
|
@@ -54,14 +54,14 @@ describe("Version Command", () => {
|
|
|
54
54
|
const engineerKit = AVAILABLE_KITS.engineer;
|
|
55
55
|
expect(engineerKit.name).toBe("ClaudeKit Engineer");
|
|
56
56
|
expect(engineerKit.repo).toBe("claudekit-engineer");
|
|
57
|
-
expect(engineerKit.owner).toBe("
|
|
57
|
+
expect(engineerKit.owner).toBe("claudekit");
|
|
58
58
|
});
|
|
59
59
|
|
|
60
60
|
test("should have marketing kit configured", () => {
|
|
61
61
|
const marketingKit = AVAILABLE_KITS.marketing;
|
|
62
62
|
expect(marketingKit.name).toBe("ClaudeKit Marketing");
|
|
63
63
|
expect(marketingKit.repo).toBe("claudekit-marketing");
|
|
64
|
-
expect(marketingKit.owner).toBe("
|
|
64
|
+
expect(marketingKit.owner).toBe("claudekit");
|
|
65
65
|
});
|
|
66
66
|
});
|
|
67
67
|
|
|
@@ -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
|
+
});
|
package/tests/lib/github.test.ts
CHANGED
|
@@ -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("
|
|
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("
|
|
45
|
+
expect(marketingKit.owner).toBe("claudekit");
|
|
46
46
|
expect(marketingKit.repo).toBe("claudekit-marketing");
|
|
47
47
|
});
|
|
48
48
|
});
|