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.
- package/bin/ck-darwin-arm64 +0 -0
- package/bin/ck-darwin-x64 +0 -0
- package/bin/ck-linux-x64 +0 -0
- package/bin/ck-win32-x64.exe +0 -0
- package/package.json +8 -2
- package/scripts/postinstall.js +74 -0
- package/.github/workflows/ci.yml +0 -45
- package/.github/workflows/claude-code-review.yml +0 -57
- package/.github/workflows/claude.yml +0 -50
- package/.github/workflows/release.yml +0 -102
- package/.releaserc.json +0 -17
- package/.repomixignore +0 -15
- package/AGENTS.md +0 -217
- package/CHANGELOG.md +0 -88
- package/CLAUDE.md +0 -34
- package/biome.json +0 -28
- package/bun.lock +0 -863
- package/dist/index.js +0 -22489
- package/src/commands/new.ts +0 -185
- package/src/commands/update.ts +0 -174
- package/src/commands/version.ts +0 -135
- package/src/index.ts +0 -102
- package/src/lib/auth.ts +0 -157
- package/src/lib/download.ts +0 -654
- package/src/lib/github.ts +0 -230
- package/src/lib/merge.ts +0 -116
- package/src/lib/prompts.ts +0 -114
- package/src/types.ts +0 -171
- package/src/utils/config.ts +0 -87
- package/src/utils/file-scanner.ts +0 -134
- package/src/utils/logger.ts +0 -124
- package/src/utils/safe-prompts.ts +0 -44
- package/src/utils/safe-spinner.ts +0 -38
- package/src/version.json +0 -3
- package/test-integration/demo/.mcp.json +0 -13
- package/test-integration/demo/.repomixignore +0 -15
- package/test-integration/demo/CLAUDE.md +0 -34
- package/tests/commands/version.test.ts +0 -297
- package/tests/integration/cli.test.ts +0 -252
- package/tests/lib/auth.test.ts +0 -116
- package/tests/lib/download.test.ts +0 -292
- package/tests/lib/github-download-priority.test.ts +0 -432
- package/tests/lib/github.test.ts +0 -52
- package/tests/lib/merge.test.ts +0 -215
- package/tests/lib/prompts.test.ts +0 -66
- package/tests/types.test.ts +0 -337
- package/tests/utils/config.test.ts +0 -263
- package/tests/utils/file-scanner.test.ts +0 -202
- package/tests/utils/logger.test.ts +0 -239
- package/tsconfig.json +0 -30
|
@@ -1,134 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
import { type WriteStream, createWriteStream } from "node:fs";
|
|
2
|
-
import pc from "picocolors";
|
|
3
|
-
|
|
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: "[x]",
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
interface LogContext {
|
|
13
|
-
[key: string]: any;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
class Logger {
|
|
17
|
-
private verboseEnabled = false;
|
|
18
|
-
private logFileStream?: WriteStream;
|
|
19
|
-
|
|
20
|
-
info(message: string): void {
|
|
21
|
-
console.log(pc.blue(symbols.info), message);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
success(message: string): void {
|
|
25
|
-
console.log(pc.green(symbols.success), message);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
warning(message: string): void {
|
|
29
|
-
console.log(pc.yellow(symbols.warning), message);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
error(message: string): void {
|
|
33
|
-
console.error(pc.red(symbols.error), message);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
debug(message: string): void {
|
|
37
|
-
if (process.env.DEBUG) {
|
|
38
|
-
console.log(pc.gray("[DEBUG]"), message);
|
|
39
|
-
}
|
|
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
|
-
}
|
|
84
|
-
|
|
85
|
-
sanitize(text: string): string {
|
|
86
|
-
return text
|
|
87
|
-
.replace(/ghp_[a-zA-Z0-9]{36}/g, "ghp_***")
|
|
88
|
-
.replace(/github_pat_[a-zA-Z0-9_]{82}/g, "github_pat_***")
|
|
89
|
-
.replace(/gho_[a-zA-Z0-9]{36}/g, "gho_***")
|
|
90
|
-
.replace(/ghu_[a-zA-Z0-9]{36}/g, "ghu_***")
|
|
91
|
-
.replace(/ghs_[a-zA-Z0-9]{36}/g, "ghs_***")
|
|
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();
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import picocolors from "picocolors";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Safe wrapper around clack prompts that uses simple ASCII characters
|
|
5
|
-
* instead of unicode box drawing to avoid rendering issues.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Simple intro with ASCII characters
|
|
10
|
-
*/
|
|
11
|
-
export function intro(message: string): void {
|
|
12
|
-
console.log();
|
|
13
|
-
console.log(picocolors.cyan(`> ${message}`));
|
|
14
|
-
console.log();
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Simple outro with ASCII characters
|
|
19
|
-
*/
|
|
20
|
-
export function outro(message: string): void {
|
|
21
|
-
console.log();
|
|
22
|
-
console.log(picocolors.green(`[OK] ${message}`));
|
|
23
|
-
console.log();
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Simple note with ASCII box drawing
|
|
28
|
-
*/
|
|
29
|
-
export function note(message: string, title?: string): void {
|
|
30
|
-
console.log();
|
|
31
|
-
if (title) {
|
|
32
|
-
console.log(picocolors.cyan(` ${title}:`));
|
|
33
|
-
console.log();
|
|
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();
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// Re-export other clack functions unchanged
|
|
44
|
-
export { select, confirm, text, isCancel } from "@clack/prompts";
|
|
@@ -1,38 +0,0 @@
|
|
|
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";
|
package/src/version.json
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
# CLAUDE.md
|
|
2
|
-
|
|
3
|
-
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
-
|
|
5
|
-
## Role & Responsibilities
|
|
6
|
-
|
|
7
|
-
Your role is to analyze user requirements, delegate tasks to appropriate sub-agents, and ensure cohesive delivery of features that meet specifications and architectural standards.
|
|
8
|
-
|
|
9
|
-
## Workflows
|
|
10
|
-
|
|
11
|
-
- Primary workflow: `./.claude/workflows/primary-workflow.md`
|
|
12
|
-
- Development rules: `./.claude/workflows/development-rules.md`
|
|
13
|
-
- Orchestration protocols: `./.claude/workflows/orchestration-protocol.md`
|
|
14
|
-
- Documentation management: `./.claude/workflows/documentation-management.md`
|
|
15
|
-
|
|
16
|
-
**IMPORTANT:** You must follow strictly the development rules in `./.claude/workflows/development-rules.md` file.
|
|
17
|
-
**IMPORTANT:** Before you plan or proceed any implementation, always read the `./README.md` file first to get context.
|
|
18
|
-
**IMPORTANT:** Sacrifice grammar for the sake of concision when writing reports.
|
|
19
|
-
**IMPORTANT:** In reports, list any unresolved questions at the end, if any.
|
|
20
|
-
|
|
21
|
-
## Documentation Management
|
|
22
|
-
|
|
23
|
-
We keep all important docs in `./docs` folder and keep updating them, structure like below:
|
|
24
|
-
|
|
25
|
-
```
|
|
26
|
-
./docs
|
|
27
|
-
├── project-overview-pdr.md
|
|
28
|
-
├── code-standards.md
|
|
29
|
-
├── codebase-summary.md
|
|
30
|
-
├── design-guidelines.md
|
|
31
|
-
├── deployment-guide.md
|
|
32
|
-
├── system-architecture.md
|
|
33
|
-
└── project-roadmap.md
|
|
34
|
-
```
|
|
@@ -1,297 +0,0 @@
|
|
|
1
|
-
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
2
|
-
import type { GitHubRelease } from "../../src/types.js";
|
|
3
|
-
import { AVAILABLE_KITS, VersionCommandOptionsSchema } from "../../src/types.js";
|
|
4
|
-
|
|
5
|
-
describe("Version Command", () => {
|
|
6
|
-
beforeEach(() => {
|
|
7
|
-
// Set environment variable to avoid auth prompts during tests
|
|
8
|
-
process.env.GITHUB_TOKEN = "ghp_test_token_for_testing";
|
|
9
|
-
});
|
|
10
|
-
|
|
11
|
-
describe("VersionCommandOptionsSchema", () => {
|
|
12
|
-
test("should accept valid options with kit filter", () => {
|
|
13
|
-
const options = { kit: "engineer" as const };
|
|
14
|
-
const result = VersionCommandOptionsSchema.parse(options);
|
|
15
|
-
expect(result.kit).toBe("engineer");
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
test("should accept valid options with limit", () => {
|
|
19
|
-
const options = { limit: 10 };
|
|
20
|
-
const result = VersionCommandOptionsSchema.parse(options);
|
|
21
|
-
expect(result.limit).toBe(10);
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
test("should accept valid options with all flag", () => {
|
|
25
|
-
const options = { all: true };
|
|
26
|
-
const result = VersionCommandOptionsSchema.parse(options);
|
|
27
|
-
expect(result.all).toBe(true);
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
test("should accept all options combined", () => {
|
|
31
|
-
const options = { kit: "marketing" as const, limit: 20, all: true };
|
|
32
|
-
const result = VersionCommandOptionsSchema.parse(options);
|
|
33
|
-
expect(result.kit).toBe("marketing");
|
|
34
|
-
expect(result.limit).toBe(20);
|
|
35
|
-
expect(result.all).toBe(true);
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
test("should accept empty options", () => {
|
|
39
|
-
const options = {};
|
|
40
|
-
const result = VersionCommandOptionsSchema.parse(options);
|
|
41
|
-
expect(result.kit).toBeUndefined();
|
|
42
|
-
expect(result.limit).toBeUndefined();
|
|
43
|
-
expect(result.all).toBeUndefined();
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
test("should reject invalid kit type", () => {
|
|
47
|
-
const options = { kit: "invalid" };
|
|
48
|
-
expect(() => VersionCommandOptionsSchema.parse(options)).toThrow();
|
|
49
|
-
});
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
describe("Kit Configuration", () => {
|
|
53
|
-
test("should have engineer kit configured", () => {
|
|
54
|
-
const engineerKit = AVAILABLE_KITS.engineer;
|
|
55
|
-
expect(engineerKit.name).toBe("ClaudeKit Engineer");
|
|
56
|
-
expect(engineerKit.repo).toBe("claudekit-engineer");
|
|
57
|
-
expect(engineerKit.owner).toBe("claudekit");
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
test("should have marketing kit configured", () => {
|
|
61
|
-
const marketingKit = AVAILABLE_KITS.marketing;
|
|
62
|
-
expect(marketingKit.name).toBe("ClaudeKit Marketing");
|
|
63
|
-
expect(marketingKit.repo).toBe("claudekit-marketing");
|
|
64
|
-
expect(marketingKit.owner).toBe("claudekit");
|
|
65
|
-
});
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
describe("Release Data Handling", () => {
|
|
69
|
-
test("should handle release with all fields", () => {
|
|
70
|
-
const release: GitHubRelease = {
|
|
71
|
-
id: 1,
|
|
72
|
-
tag_name: "v1.0.0",
|
|
73
|
-
name: "Release 1.0.0",
|
|
74
|
-
draft: false,
|
|
75
|
-
prerelease: false,
|
|
76
|
-
assets: [],
|
|
77
|
-
published_at: "2024-01-01T00:00:00Z",
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
expect(release.tag_name).toBe("v1.0.0");
|
|
81
|
-
expect(release.name).toBe("Release 1.0.0");
|
|
82
|
-
expect(release.draft).toBe(false);
|
|
83
|
-
expect(release.prerelease).toBe(false);
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
test("should handle release without published_at", () => {
|
|
87
|
-
const release: GitHubRelease = {
|
|
88
|
-
id: 1,
|
|
89
|
-
tag_name: "v1.0.0",
|
|
90
|
-
name: "Release 1.0.0",
|
|
91
|
-
draft: false,
|
|
92
|
-
prerelease: false,
|
|
93
|
-
assets: [],
|
|
94
|
-
};
|
|
95
|
-
|
|
96
|
-
expect(release.published_at).toBeUndefined();
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
test("should handle draft release", () => {
|
|
100
|
-
const release: GitHubRelease = {
|
|
101
|
-
id: 1,
|
|
102
|
-
tag_name: "v1.0.0-draft",
|
|
103
|
-
name: "Draft Release",
|
|
104
|
-
draft: true,
|
|
105
|
-
prerelease: false,
|
|
106
|
-
assets: [],
|
|
107
|
-
};
|
|
108
|
-
|
|
109
|
-
expect(release.draft).toBe(true);
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
test("should handle prerelease", () => {
|
|
113
|
-
const release: GitHubRelease = {
|
|
114
|
-
id: 1,
|
|
115
|
-
tag_name: "v1.0.0-beta.1",
|
|
116
|
-
name: "Beta Release",
|
|
117
|
-
draft: false,
|
|
118
|
-
prerelease: true,
|
|
119
|
-
assets: [],
|
|
120
|
-
};
|
|
121
|
-
|
|
122
|
-
expect(release.prerelease).toBe(true);
|
|
123
|
-
});
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
describe("Date Formatting", () => {
|
|
127
|
-
test("should format recent dates correctly", () => {
|
|
128
|
-
const now = new Date();
|
|
129
|
-
const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000);
|
|
130
|
-
const dateString = yesterday.toISOString();
|
|
131
|
-
|
|
132
|
-
// The actual formatting logic is in the command file
|
|
133
|
-
// We just verify the date string is valid
|
|
134
|
-
expect(dateString).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/);
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
test("should handle undefined date", () => {
|
|
138
|
-
const dateString = undefined;
|
|
139
|
-
expect(dateString).toBeUndefined();
|
|
140
|
-
});
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
describe("Release Filtering", () => {
|
|
144
|
-
const releases: GitHubRelease[] = [
|
|
145
|
-
{
|
|
146
|
-
id: 1,
|
|
147
|
-
tag_name: "v1.0.0",
|
|
148
|
-
name: "Stable Release",
|
|
149
|
-
draft: false,
|
|
150
|
-
prerelease: false,
|
|
151
|
-
assets: [],
|
|
152
|
-
published_at: "2024-01-01T00:00:00Z",
|
|
153
|
-
},
|
|
154
|
-
{
|
|
155
|
-
id: 2,
|
|
156
|
-
tag_name: "v1.1.0-beta.1",
|
|
157
|
-
name: "Beta Release",
|
|
158
|
-
draft: false,
|
|
159
|
-
prerelease: true,
|
|
160
|
-
assets: [],
|
|
161
|
-
published_at: "2024-01-02T00:00:00Z",
|
|
162
|
-
},
|
|
163
|
-
{
|
|
164
|
-
id: 3,
|
|
165
|
-
tag_name: "v1.2.0-draft",
|
|
166
|
-
name: "Draft Release",
|
|
167
|
-
draft: true,
|
|
168
|
-
prerelease: false,
|
|
169
|
-
assets: [],
|
|
170
|
-
published_at: "2024-01-03T00:00:00Z",
|
|
171
|
-
},
|
|
172
|
-
];
|
|
173
|
-
|
|
174
|
-
test("should filter out drafts by default", () => {
|
|
175
|
-
const stable = releases.filter((r) => !r.draft && !r.prerelease);
|
|
176
|
-
expect(stable).toHaveLength(1);
|
|
177
|
-
expect(stable[0].tag_name).toBe("v1.0.0");
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
test("should filter out prereleases by default", () => {
|
|
181
|
-
const stable = releases.filter((r) => !r.draft && !r.prerelease);
|
|
182
|
-
expect(stable.every((r) => !r.prerelease)).toBe(true);
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
test("should include all when --all flag is used", () => {
|
|
186
|
-
const all = releases; // No filtering when --all is true
|
|
187
|
-
expect(all).toHaveLength(3);
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
test("should handle empty release list", () => {
|
|
191
|
-
const empty: GitHubRelease[] = [];
|
|
192
|
-
expect(empty).toHaveLength(0);
|
|
193
|
-
});
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
describe("Command Options Validation", () => {
|
|
197
|
-
test("should validate limit as number", () => {
|
|
198
|
-
const validLimit = { limit: 50 };
|
|
199
|
-
const result = VersionCommandOptionsSchema.parse(validLimit);
|
|
200
|
-
expect(result.limit).toBe(50);
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
test("should validate all as boolean", () => {
|
|
204
|
-
const validAll = { all: false };
|
|
205
|
-
const result = VersionCommandOptionsSchema.parse(validAll);
|
|
206
|
-
expect(result.all).toBe(false);
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
test("should handle optional fields", () => {
|
|
210
|
-
const minimal = {};
|
|
211
|
-
const result = VersionCommandOptionsSchema.parse(minimal);
|
|
212
|
-
expect(result).toBeDefined();
|
|
213
|
-
});
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
describe("Error Scenarios", () => {
|
|
217
|
-
test("should handle invalid option types", () => {
|
|
218
|
-
const invalidLimit = { limit: "not-a-number" };
|
|
219
|
-
expect(() => VersionCommandOptionsSchema.parse(invalidLimit)).toThrow();
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
test("should handle invalid all flag type", () => {
|
|
223
|
-
const invalidAll = { all: "not-a-boolean" };
|
|
224
|
-
expect(() => VersionCommandOptionsSchema.parse(invalidAll)).toThrow();
|
|
225
|
-
});
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
describe("Assets Handling", () => {
|
|
229
|
-
test("should handle release with multiple assets", () => {
|
|
230
|
-
const release: GitHubRelease = {
|
|
231
|
-
id: 1,
|
|
232
|
-
tag_name: "v1.0.0",
|
|
233
|
-
name: "Release",
|
|
234
|
-
draft: false,
|
|
235
|
-
prerelease: false,
|
|
236
|
-
assets: [
|
|
237
|
-
{
|
|
238
|
-
id: 1,
|
|
239
|
-
name: "package.tar.gz",
|
|
240
|
-
browser_download_url: "https://example.com/package.tar.gz",
|
|
241
|
-
size: 1024,
|
|
242
|
-
content_type: "application/gzip",
|
|
243
|
-
},
|
|
244
|
-
{
|
|
245
|
-
id: 2,
|
|
246
|
-
name: "package.zip",
|
|
247
|
-
browser_download_url: "https://example.com/package.zip",
|
|
248
|
-
size: 2048,
|
|
249
|
-
content_type: "application/zip",
|
|
250
|
-
},
|
|
251
|
-
],
|
|
252
|
-
};
|
|
253
|
-
|
|
254
|
-
expect(release.assets).toHaveLength(2);
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
test("should handle release with no assets", () => {
|
|
258
|
-
const release: GitHubRelease = {
|
|
259
|
-
id: 1,
|
|
260
|
-
tag_name: "v1.0.0",
|
|
261
|
-
name: "Release",
|
|
262
|
-
draft: false,
|
|
263
|
-
prerelease: false,
|
|
264
|
-
assets: [],
|
|
265
|
-
};
|
|
266
|
-
|
|
267
|
-
expect(release.assets).toHaveLength(0);
|
|
268
|
-
});
|
|
269
|
-
});
|
|
270
|
-
|
|
271
|
-
describe("Integration Scenarios", () => {
|
|
272
|
-
test("should handle both kits in parallel", () => {
|
|
273
|
-
const kits = Object.keys(AVAILABLE_KITS);
|
|
274
|
-
expect(kits).toContain("engineer");
|
|
275
|
-
expect(kits).toContain("marketing");
|
|
276
|
-
expect(kits).toHaveLength(2);
|
|
277
|
-
});
|
|
278
|
-
|
|
279
|
-
test("should support filtering by engineer kit", () => {
|
|
280
|
-
const options = { kit: "engineer" as const };
|
|
281
|
-
const result = VersionCommandOptionsSchema.parse(options);
|
|
282
|
-
expect(result.kit).toBe("engineer");
|
|
283
|
-
|
|
284
|
-
const kitConfig = AVAILABLE_KITS[result.kit];
|
|
285
|
-
expect(kitConfig.repo).toBe("claudekit-engineer");
|
|
286
|
-
});
|
|
287
|
-
|
|
288
|
-
test("should support filtering by marketing kit", () => {
|
|
289
|
-
const options = { kit: "marketing" as const };
|
|
290
|
-
const result = VersionCommandOptionsSchema.parse(options);
|
|
291
|
-
expect(result.kit).toBe("marketing");
|
|
292
|
-
|
|
293
|
-
const kitConfig = AVAILABLE_KITS[result.kit];
|
|
294
|
-
expect(kitConfig.repo).toBe("claudekit-marketing");
|
|
295
|
-
});
|
|
296
|
-
});
|
|
297
|
-
});
|