claudekit-cli 1.0.1 → 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/ci.yml +2 -0
- package/.github/workflows/release.yml +44 -0
- package/CHANGELOG.md +28 -0
- package/CLAUDE.md +3 -2
- package/LICENSE +21 -0
- package/README.md +73 -3
- package/dist/index.js +11556 -10926
- package/package.json +1 -1
- package/src/commands/new.ts +41 -9
- package/src/commands/update.ts +59 -13
- package/src/commands/version.ts +135 -0
- package/src/index.ts +53 -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 +11 -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 +297 -0
- 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
- package/.opencode/agent/code-reviewer.md +0 -141
- package/.opencode/agent/debugger.md +0 -74
- package/.opencode/agent/docs-manager.md +0 -119
- package/.opencode/agent/git-manager.md +0 -60
- package/.opencode/agent/planner-researcher.md +0 -100
- package/.opencode/agent/planner.md +0 -87
- package/.opencode/agent/project-manager.md +0 -113
- package/.opencode/agent/researcher.md +0 -173
- package/.opencode/agent/solution-brainstormer.md +0 -89
- package/.opencode/agent/system-architecture.md +0 -192
- package/.opencode/agent/tester.md +0 -96
- package/.opencode/agent/ui-ux-designer.md +0 -203
- package/.opencode/agent/ui-ux-developer.md +0 -97
- package/.opencode/command/cook.md +0 -7
- package/.opencode/command/debug.md +0 -10
- package/.opencode/command/design/3d.md +0 -65
- package/.opencode/command/design/fast.md +0 -18
- package/.opencode/command/design/good.md +0 -21
- package/.opencode/command/design/screenshot.md +0 -22
- package/.opencode/command/design/video.md +0 -22
- package/.opencode/command/fix/ci.md +0 -8
- package/.opencode/command/fix/fast.md +0 -11
- package/.opencode/command/fix/hard.md +0 -15
- package/.opencode/command/fix/logs.md +0 -16
- package/.opencode/command/fix/test.md +0 -18
- package/.opencode/command/fix/types.md +0 -10
- package/.opencode/command/git/cm.md +0 -5
- package/.opencode/command/git/cp.md +0 -4
- package/.opencode/command/plan/ci.md +0 -12
- package/.opencode/command/plan/two.md +0 -13
- package/.opencode/command/plan.md +0 -10
- package/.opencode/command/test.md +0 -7
- package/.opencode/command/watzup.md +0 -8
- package/plans/251008-claudekit-cli-implementation-plan.md +0 -1469
- package/plans/reports/251008-from-code-reviewer-to-developer-review-report.md +0 -864
- package/plans/reports/251008-from-tester-to-developer-test-summary-report.md +0 -409
- package/plans/reports/251008-researcher-download-extraction-report.md +0 -1377
- package/plans/reports/251008-researcher-github-api-report.md +0 -1339
- package/plans/research/251008-cli-frameworks-bun-research.md +0 -1051
- package/plans/templates/bug-fix-template.md +0 -69
- package/plans/templates/feature-implementation-template.md +0 -84
- package/plans/templates/refactor-template.md +0 -82
- package/plans/templates/template-usage-guide.md +0 -58
package/package.json
CHANGED
package/src/commands/new.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { resolve } from "node:path";
|
|
2
2
|
import { pathExists, readdir } from "fs-extra";
|
|
3
3
|
import ora from "ora";
|
|
4
|
+
import { AuthManager } from "../lib/auth.js";
|
|
4
5
|
import { DownloadManager } from "../lib/download.js";
|
|
5
6
|
import { GitHubClient } from "../lib/github.js";
|
|
6
7
|
import { FileMerger } from "../lib/merge.js";
|
|
@@ -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
|
-
//
|
|
85
|
-
const
|
|
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
|
-
|
|
90
|
-
|
|
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
|
-
|
|
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`;
|
package/src/commands/update.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { resolve } from "node:path";
|
|
2
2
|
import { pathExists } from "fs-extra";
|
|
3
3
|
import ora from "ora";
|
|
4
|
+
import { AuthManager } from "../lib/auth.js";
|
|
4
5
|
import { DownloadManager } from "../lib/download.js";
|
|
5
6
|
import { GitHubClient } from "../lib/github.js";
|
|
6
7
|
import { FileMerger } from "../lib/merge.js";
|
|
7
8
|
import { PromptsManager } from "../lib/prompts.js";
|
|
8
9
|
import { AVAILABLE_KITS, type UpdateCommandOptions, UpdateCommandOptionsSchema } from "../types.js";
|
|
9
10
|
import { ConfigManager } from "../utils/config.js";
|
|
11
|
+
import { FileScanner } from "../utils/file-scanner.js";
|
|
10
12
|
import { logger } from "../utils/logger.js";
|
|
11
13
|
|
|
12
14
|
export async function updateCommand(options: UpdateCommandOptions): Promise<void> {
|
|
@@ -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
|
-
//
|
|
77
|
-
const
|
|
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
|
-
|
|
82
|
-
|
|
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
|
-
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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");
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import pc from "picocolors";
|
|
2
|
+
import { GitHubClient } from "../lib/github.js";
|
|
3
|
+
import { PromptsManager } from "../lib/prompts.js";
|
|
4
|
+
import {
|
|
5
|
+
AVAILABLE_KITS,
|
|
6
|
+
type GitHubRelease,
|
|
7
|
+
type VersionCommandOptions,
|
|
8
|
+
VersionCommandOptionsSchema,
|
|
9
|
+
} from "../types.js";
|
|
10
|
+
import { logger } from "../utils/logger.js";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Format a date as a relative time string
|
|
14
|
+
*/
|
|
15
|
+
function formatRelativeTime(dateString?: string): string {
|
|
16
|
+
if (!dateString) return "Unknown";
|
|
17
|
+
|
|
18
|
+
const date = new Date(dateString);
|
|
19
|
+
const now = new Date();
|
|
20
|
+
const diffMs = now.getTime() - date.getTime();
|
|
21
|
+
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
|
|
22
|
+
|
|
23
|
+
if (diffDays === 0) return "Today";
|
|
24
|
+
if (diffDays === 1) return "Yesterday";
|
|
25
|
+
if (diffDays < 7) return `${diffDays} days ago`;
|
|
26
|
+
if (diffDays < 30) return `${Math.floor(diffDays / 7)} weeks ago`;
|
|
27
|
+
if (diffDays < 365) return `${Math.floor(diffDays / 30)} months ago`;
|
|
28
|
+
return `${Math.floor(diffDays / 365)} years ago`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Display releases for a single kit
|
|
33
|
+
*/
|
|
34
|
+
function displayKitReleases(kitName: string, releases: GitHubRelease[]): void {
|
|
35
|
+
console.log(`\n${pc.bold(pc.cyan(kitName))} - Available Versions:\n`);
|
|
36
|
+
|
|
37
|
+
if (releases.length === 0) {
|
|
38
|
+
console.log(pc.dim(" No releases found"));
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
for (const release of releases) {
|
|
43
|
+
const version = pc.green(release.tag_name);
|
|
44
|
+
const name = release.name || "No title";
|
|
45
|
+
const publishedAt = formatRelativeTime(release.published_at);
|
|
46
|
+
const assetCount = release.assets.length;
|
|
47
|
+
|
|
48
|
+
// Add badges for prerelease and draft
|
|
49
|
+
const badges: string[] = [];
|
|
50
|
+
if (release.prerelease) badges.push(pc.yellow("[prerelease]"));
|
|
51
|
+
if (release.draft) badges.push(pc.gray("[draft]"));
|
|
52
|
+
const badgeStr = badges.length > 0 ? ` ${badges.join(" ")}` : "";
|
|
53
|
+
|
|
54
|
+
// Format: version | name | time | assets
|
|
55
|
+
const versionPart = version.padEnd(20);
|
|
56
|
+
const namePart = name.length > 40 ? `${name.slice(0, 37)}...` : name.padEnd(40);
|
|
57
|
+
const timePart = pc.dim(publishedAt.padEnd(20));
|
|
58
|
+
const assetPart = pc.dim(`(${assetCount} ${assetCount === 1 ? "asset" : "assets"})`);
|
|
59
|
+
|
|
60
|
+
console.log(` ${versionPart} ${namePart} ${timePart} ${assetPart}${badgeStr}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
console.log(
|
|
64
|
+
pc.dim(`\nShowing ${releases.length} ${releases.length === 1 ? "release" : "releases"}`),
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Version command - List available versions of ClaudeKit repositories
|
|
70
|
+
*/
|
|
71
|
+
export async function versionCommand(options: VersionCommandOptions): Promise<void> {
|
|
72
|
+
const prompts = new PromptsManager();
|
|
73
|
+
|
|
74
|
+
prompts.intro("📦 ClaudeKit - Available Versions");
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
// Validate and parse options
|
|
78
|
+
const validOptions = VersionCommandOptionsSchema.parse(options);
|
|
79
|
+
|
|
80
|
+
// Determine which kits to fetch
|
|
81
|
+
const kitsToFetch = validOptions.kit
|
|
82
|
+
? [validOptions.kit]
|
|
83
|
+
: (Object.keys(AVAILABLE_KITS) as Array<keyof typeof AVAILABLE_KITS>);
|
|
84
|
+
|
|
85
|
+
// Initialize GitHub client
|
|
86
|
+
const github = new GitHubClient();
|
|
87
|
+
|
|
88
|
+
// Determine limit (default to 30, similar to GitHub CLI)
|
|
89
|
+
const limit = validOptions.limit || 30;
|
|
90
|
+
|
|
91
|
+
// Fetch releases for all requested kits in parallel
|
|
92
|
+
const releasePromises = kitsToFetch.map(async (kitType) => {
|
|
93
|
+
const kitConfig = AVAILABLE_KITS[kitType];
|
|
94
|
+
try {
|
|
95
|
+
const releases = await github.listReleases(kitConfig, limit);
|
|
96
|
+
|
|
97
|
+
// Filter out drafts and prereleases unless --all flag is set
|
|
98
|
+
const filteredReleases = validOptions.all
|
|
99
|
+
? releases
|
|
100
|
+
: releases.filter((r) => !r.draft && !r.prerelease);
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
kitType,
|
|
104
|
+
kitConfig,
|
|
105
|
+
releases: filteredReleases,
|
|
106
|
+
error: null,
|
|
107
|
+
};
|
|
108
|
+
} catch (error) {
|
|
109
|
+
return {
|
|
110
|
+
kitType,
|
|
111
|
+
kitConfig,
|
|
112
|
+
releases: [],
|
|
113
|
+
error: error instanceof Error ? error.message : "Unknown error",
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const results = await Promise.all(releasePromises);
|
|
119
|
+
|
|
120
|
+
// Display results
|
|
121
|
+
for (const result of results) {
|
|
122
|
+
if (result.error) {
|
|
123
|
+
console.log(`\n${pc.bold(pc.cyan(result.kitConfig.name))} - ${pc.red("Error")}`);
|
|
124
|
+
console.log(pc.dim(` ${result.error}`));
|
|
125
|
+
} else {
|
|
126
|
+
displayKitReleases(result.kitConfig.name, result.releases);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
prompts.outro("✨ Done");
|
|
131
|
+
} catch (error) {
|
|
132
|
+
logger.error(error instanceof Error ? error.message : "Unknown error occurred");
|
|
133
|
+
process.exit(1);
|
|
134
|
+
}
|
|
135
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -6,6 +6,16 @@ import { fileURLToPath } from "node:url";
|
|
|
6
6
|
import { cac } from "cac";
|
|
7
7
|
import { newCommand } from "./commands/new.js";
|
|
8
8
|
import { updateCommand } from "./commands/update.js";
|
|
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
|
+
}
|
|
9
19
|
|
|
10
20
|
const __dirname = fileURLToPath(new URL(".", import.meta.url));
|
|
11
21
|
|
|
@@ -14,6 +24,10 @@ const packageJson = JSON.parse(readFileSync(join(__dirname, "../package.json"),
|
|
|
14
24
|
|
|
15
25
|
const cli = cac("ck");
|
|
16
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
|
+
|
|
17
31
|
// New command
|
|
18
32
|
cli
|
|
19
33
|
.command("new", "Bootstrap a new ClaudeKit project")
|
|
@@ -34,11 +48,49 @@ cli
|
|
|
34
48
|
await updateCommand(options);
|
|
35
49
|
});
|
|
36
50
|
|
|
51
|
+
// Versions command
|
|
52
|
+
cli
|
|
53
|
+
.command("versions", "List available versions of ClaudeKit repositories")
|
|
54
|
+
.option("--kit <kit>", "Filter by specific kit (engineer, marketing)")
|
|
55
|
+
.option("--limit <limit>", "Number of releases to show (default: 30)")
|
|
56
|
+
.option("--all", "Show all releases including prereleases")
|
|
57
|
+
.action(async (options) => {
|
|
58
|
+
await versionCommand(options);
|
|
59
|
+
});
|
|
60
|
+
|
|
37
61
|
// Version
|
|
38
62
|
cli.version(packageJson.version);
|
|
39
63
|
|
|
40
64
|
// Help
|
|
41
65
|
cli.help();
|
|
42
66
|
|
|
43
|
-
// Parse
|
|
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
|
|
44
96
|
cli.parse();
|
package/src/lib/download.ts
CHANGED
|
@@ -5,6 +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 ignore from "ignore";
|
|
8
9
|
import ora from "ora";
|
|
9
10
|
import * as tar from "tar";
|
|
10
11
|
import unzipper from "unzipper";
|
|
@@ -19,6 +20,29 @@ import { logger } from "../utils/logger.js";
|
|
|
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
|
*/
|
|
@@ -91,6 +115,90 @@ 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
|
+
headers.Accept = "application/vnd.github+json";
|
|
141
|
+
} else {
|
|
142
|
+
headers.Accept = "application/octet-stream";
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const response = await fetch(url, { headers });
|
|
146
|
+
|
|
147
|
+
if (!response.ok) {
|
|
148
|
+
throw new DownloadError(`Failed to download: ${response.statusText}`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const totalSize = size || Number(response.headers.get("content-length")) || 0;
|
|
152
|
+
let downloadedSize = 0;
|
|
153
|
+
|
|
154
|
+
// Create progress bar only if we know the size
|
|
155
|
+
const progressBar =
|
|
156
|
+
totalSize > 0
|
|
157
|
+
? new cliProgress.SingleBar({
|
|
158
|
+
format: "Progress |{bar}| {percentage}% | {value}/{total} MB",
|
|
159
|
+
barCompleteChar: "\u2588",
|
|
160
|
+
barIncompleteChar: "\u2591",
|
|
161
|
+
hideCursor: true,
|
|
162
|
+
})
|
|
163
|
+
: null;
|
|
164
|
+
|
|
165
|
+
if (progressBar) {
|
|
166
|
+
progressBar.start(Math.round(totalSize / 1024 / 1024), 0);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const fileStream = createWriteStream(destPath);
|
|
170
|
+
const reader = response.body?.getReader();
|
|
171
|
+
|
|
172
|
+
if (!reader) {
|
|
173
|
+
throw new DownloadError("Failed to get response reader");
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
try {
|
|
177
|
+
while (true) {
|
|
178
|
+
const { done, value } = await reader.read();
|
|
179
|
+
|
|
180
|
+
if (done) break;
|
|
181
|
+
|
|
182
|
+
fileStream.write(value);
|
|
183
|
+
downloadedSize += value.length;
|
|
184
|
+
|
|
185
|
+
if (progressBar) {
|
|
186
|
+
progressBar.update(Math.round(downloadedSize / 1024 / 1024));
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
fileStream.end();
|
|
191
|
+
if (progressBar) progressBar.stop();
|
|
192
|
+
|
|
193
|
+
logger.success(`Downloaded ${name}`);
|
|
194
|
+
return destPath;
|
|
195
|
+
} catch (error) {
|
|
196
|
+
fileStream.close();
|
|
197
|
+
if (progressBar) progressBar.stop();
|
|
198
|
+
throw error;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
94
202
|
/**
|
|
95
203
|
* Extract archive to destination
|
|
96
204
|
*/
|
|
@@ -133,6 +241,14 @@ export class DownloadManager {
|
|
|
133
241
|
file: archivePath,
|
|
134
242
|
cwd: destDir,
|
|
135
243
|
strip: 1, // Strip the root directory from the archive
|
|
244
|
+
filter: (path: string) => {
|
|
245
|
+
// Exclude unwanted files
|
|
246
|
+
const shouldInclude = !this.shouldExclude(path);
|
|
247
|
+
if (!shouldInclude) {
|
|
248
|
+
logger.debug(`Excluding: ${path}`);
|
|
249
|
+
}
|
|
250
|
+
return shouldInclude;
|
|
251
|
+
},
|
|
136
252
|
});
|
|
137
253
|
}
|
|
138
254
|
|
|
@@ -140,7 +256,121 @@ export class DownloadManager {
|
|
|
140
256
|
* Extract zip archive
|
|
141
257
|
*/
|
|
142
258
|
private async extractZip(archivePath: string, destDir: string): Promise<void> {
|
|
143
|
-
|
|
259
|
+
const { readdir, stat, mkdir: mkdirPromise, copyFile, rm } = await import("node:fs/promises");
|
|
260
|
+
const { join: pathJoin } = await import("node:path");
|
|
261
|
+
|
|
262
|
+
// Extract to a temporary directory first
|
|
263
|
+
const tempExtractDir = `${destDir}-temp`;
|
|
264
|
+
await mkdirPromise(tempExtractDir, { recursive: true });
|
|
265
|
+
|
|
266
|
+
try {
|
|
267
|
+
// Extract zip to temp directory
|
|
268
|
+
await streamPipeline(
|
|
269
|
+
createReadStream(archivePath),
|
|
270
|
+
unzipper.Extract({ path: tempExtractDir }),
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
// Find the root directory in the zip (if any)
|
|
274
|
+
const entries = await readdir(tempExtractDir);
|
|
275
|
+
|
|
276
|
+
// If there's a single root directory, strip it
|
|
277
|
+
if (entries.length === 1) {
|
|
278
|
+
const rootEntry = entries[0];
|
|
279
|
+
const rootPath = pathJoin(tempExtractDir, rootEntry);
|
|
280
|
+
const rootStat = await stat(rootPath);
|
|
281
|
+
|
|
282
|
+
if (rootStat.isDirectory()) {
|
|
283
|
+
// Move contents from the root directory to the destination
|
|
284
|
+
await this.moveDirectoryContents(rootPath, destDir);
|
|
285
|
+
} else {
|
|
286
|
+
// Single file, just move it
|
|
287
|
+
await mkdirPromise(destDir, { recursive: true });
|
|
288
|
+
await copyFile(rootPath, pathJoin(destDir, rootEntry));
|
|
289
|
+
}
|
|
290
|
+
} else {
|
|
291
|
+
// Multiple entries at root, move them all
|
|
292
|
+
await this.moveDirectoryContents(tempExtractDir, destDir);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Clean up temp directory
|
|
296
|
+
await rm(tempExtractDir, { recursive: true, force: true });
|
|
297
|
+
} catch (error) {
|
|
298
|
+
// Clean up temp directory on error
|
|
299
|
+
try {
|
|
300
|
+
await rm(tempExtractDir, { recursive: true, force: true });
|
|
301
|
+
} catch {
|
|
302
|
+
// Ignore cleanup errors
|
|
303
|
+
}
|
|
304
|
+
throw error;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Move directory contents from source to destination, applying exclusion filters
|
|
310
|
+
*/
|
|
311
|
+
private async moveDirectoryContents(sourceDir: string, destDir: string): Promise<void> {
|
|
312
|
+
const { readdir, stat, mkdir: mkdirPromise, copyFile } = await import("node:fs/promises");
|
|
313
|
+
const { join: pathJoin, relative } = await import("node:path");
|
|
314
|
+
|
|
315
|
+
await mkdirPromise(destDir, { recursive: true });
|
|
316
|
+
|
|
317
|
+
const entries = await readdir(sourceDir);
|
|
318
|
+
|
|
319
|
+
for (const entry of entries) {
|
|
320
|
+
const sourcePath = pathJoin(sourceDir, entry);
|
|
321
|
+
const destPath = pathJoin(destDir, entry);
|
|
322
|
+
const relativePath = relative(sourceDir, sourcePath);
|
|
323
|
+
|
|
324
|
+
// Skip excluded files
|
|
325
|
+
if (this.shouldExclude(relativePath)) {
|
|
326
|
+
logger.debug(`Excluding: ${relativePath}`);
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const entryStat = await stat(sourcePath);
|
|
331
|
+
|
|
332
|
+
if (entryStat.isDirectory()) {
|
|
333
|
+
// Recursively copy directory
|
|
334
|
+
await this.copyDirectory(sourcePath, destPath);
|
|
335
|
+
} else {
|
|
336
|
+
// Copy file
|
|
337
|
+
await copyFile(sourcePath, destPath);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Recursively copy directory
|
|
344
|
+
*/
|
|
345
|
+
private async copyDirectory(sourceDir: string, destDir: string): Promise<void> {
|
|
346
|
+
const { readdir, stat, mkdir: mkdirPromise, copyFile } = await import("node:fs/promises");
|
|
347
|
+
const { join: pathJoin, relative } = await import("node:path");
|
|
348
|
+
|
|
349
|
+
await mkdirPromise(destDir, { recursive: true });
|
|
350
|
+
|
|
351
|
+
const entries = await readdir(sourceDir);
|
|
352
|
+
|
|
353
|
+
for (const entry of entries) {
|
|
354
|
+
const sourcePath = pathJoin(sourceDir, entry);
|
|
355
|
+
const destPath = pathJoin(destDir, entry);
|
|
356
|
+
const relativePath = relative(sourceDir, sourcePath);
|
|
357
|
+
|
|
358
|
+
// Skip excluded files
|
|
359
|
+
if (this.shouldExclude(relativePath)) {
|
|
360
|
+
logger.debug(`Excluding: ${relativePath}`);
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const entryStat = await stat(sourcePath);
|
|
365
|
+
|
|
366
|
+
if (entryStat.isDirectory()) {
|
|
367
|
+
// Recursively copy directory
|
|
368
|
+
await this.copyDirectory(sourcePath, destPath);
|
|
369
|
+
} else {
|
|
370
|
+
// Copy file
|
|
371
|
+
await copyFile(sourcePath, destPath);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
144
374
|
}
|
|
145
375
|
|
|
146
376
|
/**
|