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