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
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");
|
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,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
|
/**
|
package/src/lib/github.ts
CHANGED
|
@@ -154,4 +154,60 @@ 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)
|
|
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
|
+
// First priority: Look for official ClaudeKit package assets
|
|
172
|
+
const packageAsset = release.assets.find(
|
|
173
|
+
(a) =>
|
|
174
|
+
a.name.toLowerCase().includes("claudekit") &&
|
|
175
|
+
a.name.toLowerCase().includes("package") &&
|
|
176
|
+
a.name.endsWith(".zip"),
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
if (packageAsset) {
|
|
180
|
+
logger.debug(`Using ClaudeKit package asset: ${packageAsset.name}`);
|
|
181
|
+
return {
|
|
182
|
+
type: "asset",
|
|
183
|
+
url: packageAsset.browser_download_url,
|
|
184
|
+
name: packageAsset.name,
|
|
185
|
+
size: packageAsset.size,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Second priority: Look for any custom uploaded assets
|
|
190
|
+
const customAsset = release.assets.find(
|
|
191
|
+
(a) => a.name.endsWith(".tar.gz") || a.name.endsWith(".tgz") || a.name.endsWith(".zip"),
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
if (customAsset) {
|
|
195
|
+
logger.debug(`Using custom asset: ${customAsset.name}`);
|
|
196
|
+
return {
|
|
197
|
+
type: "asset",
|
|
198
|
+
url: customAsset.browser_download_url,
|
|
199
|
+
name: customAsset.name,
|
|
200
|
+
size: customAsset.size,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Fall back to GitHub's automatic tarball
|
|
205
|
+
logger.debug("Using GitHub automatic tarball");
|
|
206
|
+
return {
|
|
207
|
+
type: "tarball",
|
|
208
|
+
url: release.tarball_url,
|
|
209
|
+
name: `${release.tag_name}.tar.gz`,
|
|
210
|
+
size: undefined, // Size unknown for automatic tarballs
|
|
211
|
+
};
|
|
212
|
+
}
|
|
157
213
|
}
|
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
|
}
|
package/src/types.ts
CHANGED
|
@@ -60,6 +60,8 @@ export const GitHubReleaseSchema = z.object({
|
|
|
60
60
|
prerelease: z.boolean(),
|
|
61
61
|
assets: z.array(GitHubReleaseAssetSchema),
|
|
62
62
|
published_at: z.string().optional(),
|
|
63
|
+
tarball_url: z.string().url(),
|
|
64
|
+
zipball_url: z.string().url(),
|
|
63
65
|
});
|
|
64
66
|
export type GitHubRelease = z.infer<typeof GitHubReleaseSchema>;
|
|
65
67
|
|
|
@@ -77,13 +79,13 @@ export const AVAILABLE_KITS: Record<KitType, KitConfig> = {
|
|
|
77
79
|
engineer: {
|
|
78
80
|
name: "ClaudeKit Engineer",
|
|
79
81
|
repo: "claudekit-engineer",
|
|
80
|
-
owner: "
|
|
82
|
+
owner: "claudekit",
|
|
81
83
|
description: "Engineering toolkit for building with Claude",
|
|
82
84
|
},
|
|
83
85
|
marketing: {
|
|
84
86
|
name: "ClaudeKit Marketing",
|
|
85
87
|
repo: "claudekit-marketing",
|
|
86
|
-
owner: "
|
|
88
|
+
owner: "claudekit",
|
|
87
89
|
description: "[Coming Soon] Marketing toolkit",
|
|
88
90
|
},
|
|
89
91
|
};
|