claudekit-cli 1.2.1 → 1.2.2
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/CHANGELOG.md +7 -0
- package/README.md +2 -0
- package/bun.lock +17 -7
- package/dist/index.js +2637 -20
- package/package.json +2 -2
- package/src/commands/new.ts +3 -0
- package/src/commands/update.ts +3 -0
- package/src/lib/download.ts +154 -26
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claudekit-cli",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.2",
|
|
4
4
|
"description": "CLI tool for bootstrapping and updating ClaudeKit projects",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
"@octokit/rest": "^22.0.0",
|
|
34
34
|
"cac": "^6.7.14",
|
|
35
35
|
"cli-progress": "^3.12.0",
|
|
36
|
+
"extract-zip": "^2.0.1",
|
|
36
37
|
"fs-extra": "^11.2.0",
|
|
37
38
|
"ignore": "^5.3.2",
|
|
38
39
|
"keytar": "^7.9.0",
|
|
@@ -40,7 +41,6 @@
|
|
|
40
41
|
"picocolors": "^1.1.1",
|
|
41
42
|
"tar": "^7.4.3",
|
|
42
43
|
"tmp": "^0.2.3",
|
|
43
|
-
"unzipper": "^0.12.3",
|
|
44
44
|
"zod": "^3.23.8"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
package/src/commands/new.ts
CHANGED
|
@@ -132,6 +132,9 @@ export async function newCommand(options: NewCommandOptions): Promise<void> {
|
|
|
132
132
|
const extractDir = `${tempDir}/extracted`;
|
|
133
133
|
await downloadManager.extractArchive(archivePath, extractDir);
|
|
134
134
|
|
|
135
|
+
// Validate extraction
|
|
136
|
+
await downloadManager.validateExtraction(extractDir);
|
|
137
|
+
|
|
135
138
|
// Copy files to target directory
|
|
136
139
|
const merger = new FileMerger();
|
|
137
140
|
await merger.merge(extractDir, resolvedDir, true); // Skip confirmation for new projects
|
package/src/commands/update.ts
CHANGED
|
@@ -125,6 +125,9 @@ export async function updateCommand(options: UpdateCommandOptions): Promise<void
|
|
|
125
125
|
const extractDir = `${tempDir}/extracted`;
|
|
126
126
|
await downloadManager.extractArchive(archivePath, extractDir);
|
|
127
127
|
|
|
128
|
+
// Validate extraction
|
|
129
|
+
await downloadManager.validateExtraction(extractDir);
|
|
130
|
+
|
|
128
131
|
// Identify custom .claude files to preserve
|
|
129
132
|
logger.info("Scanning for custom .claude files...");
|
|
130
133
|
const customClaudeFiles = await FileScanner.findCustomFiles(resolvedDir, extractDir, ".claude");
|
package/src/lib/download.ts
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createWriteStream } from "node:fs";
|
|
2
2
|
import { mkdir } from "node:fs/promises";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
|
-
import { pipeline } from "node:stream";
|
|
6
|
-
import { promisify } from "node:util";
|
|
7
5
|
import cliProgress from "cli-progress";
|
|
6
|
+
import extractZip from "extract-zip";
|
|
8
7
|
import ignore from "ignore";
|
|
9
8
|
import * as tar from "tar";
|
|
10
|
-
import unzipper from "unzipper";
|
|
11
9
|
import {
|
|
12
10
|
type ArchiveType,
|
|
13
11
|
DownloadError,
|
|
@@ -17,8 +15,6 @@ import {
|
|
|
17
15
|
import { logger } from "../utils/logger.js";
|
|
18
16
|
import { createSpinner } from "../utils/safe-spinner.js";
|
|
19
17
|
|
|
20
|
-
const streamPipeline = promisify(pipeline);
|
|
21
|
-
|
|
22
18
|
export class DownloadManager {
|
|
23
19
|
/**
|
|
24
20
|
* Patterns to exclude from extraction
|
|
@@ -239,19 +235,95 @@ export class DownloadManager {
|
|
|
239
235
|
* Extract tar.gz archive
|
|
240
236
|
*/
|
|
241
237
|
private async extractTarGz(archivePath: string, destDir: string): Promise<void> {
|
|
242
|
-
await
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
238
|
+
const { readdir, stat, mkdir: mkdirPromise, copyFile, rm } = await import("node:fs/promises");
|
|
239
|
+
const { join: pathJoin } = await import("node:path");
|
|
240
|
+
|
|
241
|
+
// Extract to a temporary directory first
|
|
242
|
+
const tempExtractDir = `${destDir}-temp`;
|
|
243
|
+
await mkdirPromise(tempExtractDir, { recursive: true });
|
|
244
|
+
|
|
245
|
+
try {
|
|
246
|
+
// Extract without stripping first
|
|
247
|
+
await tar.extract({
|
|
248
|
+
file: archivePath,
|
|
249
|
+
cwd: tempExtractDir,
|
|
250
|
+
strip: 0, // Don't strip yet - we'll decide based on wrapper detection
|
|
251
|
+
filter: (path: string) => {
|
|
252
|
+
// Exclude unwanted files
|
|
253
|
+
const shouldInclude = !this.shouldExclude(path);
|
|
254
|
+
if (!shouldInclude) {
|
|
255
|
+
logger.debug(`Excluding: ${path}`);
|
|
256
|
+
}
|
|
257
|
+
return shouldInclude;
|
|
258
|
+
},
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
logger.debug(`Extracted TAR.GZ to temp: ${tempExtractDir}`);
|
|
262
|
+
|
|
263
|
+
// Apply same wrapper detection logic as zip
|
|
264
|
+
const entries = await readdir(tempExtractDir);
|
|
265
|
+
logger.debug(`Root entries: ${entries.join(", ")}`);
|
|
266
|
+
|
|
267
|
+
if (entries.length === 1) {
|
|
268
|
+
const rootEntry = entries[0];
|
|
269
|
+
const rootPath = pathJoin(tempExtractDir, rootEntry);
|
|
270
|
+
const rootStat = await stat(rootPath);
|
|
271
|
+
|
|
272
|
+
if (rootStat.isDirectory()) {
|
|
273
|
+
// Check contents of root directory
|
|
274
|
+
const rootContents = await readdir(rootPath);
|
|
275
|
+
logger.debug(`Root directory '${rootEntry}' contains: ${rootContents.join(", ")}`);
|
|
276
|
+
|
|
277
|
+
// Only strip if root is a version/release wrapper
|
|
278
|
+
const isWrapper = this.isWrapperDirectory(rootEntry);
|
|
279
|
+
logger.debug(`Is wrapper directory: ${isWrapper}`);
|
|
280
|
+
|
|
281
|
+
if (isWrapper) {
|
|
282
|
+
// Strip wrapper and move contents
|
|
283
|
+
logger.debug(`Stripping wrapper directory: ${rootEntry}`);
|
|
284
|
+
await this.moveDirectoryContents(rootPath, destDir);
|
|
285
|
+
} else {
|
|
286
|
+
// Keep root directory - move everything including root
|
|
287
|
+
logger.debug("Preserving complete directory structure");
|
|
288
|
+
await this.moveDirectoryContents(tempExtractDir, destDir);
|
|
289
|
+
}
|
|
290
|
+
} else {
|
|
291
|
+
// Single file, just move it
|
|
292
|
+
await mkdirPromise(destDir, { recursive: true });
|
|
293
|
+
await copyFile(rootPath, pathJoin(destDir, rootEntry));
|
|
251
294
|
}
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
295
|
+
} else {
|
|
296
|
+
// Multiple entries at root, move them all
|
|
297
|
+
logger.debug("Multiple root entries - moving all");
|
|
298
|
+
await this.moveDirectoryContents(tempExtractDir, destDir);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
logger.debug(`Moved contents to: ${destDir}`);
|
|
302
|
+
|
|
303
|
+
// Clean up temp directory
|
|
304
|
+
await rm(tempExtractDir, { recursive: true, force: true });
|
|
305
|
+
} catch (error) {
|
|
306
|
+
// Clean up temp directory on error
|
|
307
|
+
try {
|
|
308
|
+
await rm(tempExtractDir, { recursive: true, force: true });
|
|
309
|
+
} catch {
|
|
310
|
+
// Ignore cleanup errors
|
|
311
|
+
}
|
|
312
|
+
throw error;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Check if directory name is a version/release wrapper
|
|
318
|
+
* Examples: claudekit-engineer-v1.0.0, claudekit-engineer-1.0.0, repo-abc1234
|
|
319
|
+
*/
|
|
320
|
+
private isWrapperDirectory(dirName: string): boolean {
|
|
321
|
+
// Match version patterns: project-v1.0.0, project-1.0.0
|
|
322
|
+
const versionPattern = /^[\w-]+-v?\d+\.\d+\.\d+/;
|
|
323
|
+
// Match commit hash patterns: project-abc1234
|
|
324
|
+
const hashPattern = /^[\w-]+-[a-f0-9]{7,}$/;
|
|
325
|
+
|
|
326
|
+
return versionPattern.test(dirName) || hashPattern.test(dirName);
|
|
255
327
|
}
|
|
256
328
|
|
|
257
329
|
/**
|
|
@@ -266,24 +338,39 @@ export class DownloadManager {
|
|
|
266
338
|
await mkdirPromise(tempExtractDir, { recursive: true });
|
|
267
339
|
|
|
268
340
|
try {
|
|
269
|
-
// Extract zip to temp directory
|
|
270
|
-
await
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
);
|
|
341
|
+
// Extract zip to temp directory using extract-zip
|
|
342
|
+
await extractZip(archivePath, { dir: tempExtractDir });
|
|
343
|
+
|
|
344
|
+
logger.debug(`Extracted ZIP to temp: ${tempExtractDir}`);
|
|
274
345
|
|
|
275
346
|
// Find the root directory in the zip (if any)
|
|
276
347
|
const entries = await readdir(tempExtractDir);
|
|
348
|
+
logger.debug(`Root entries: ${entries.join(", ")}`);
|
|
277
349
|
|
|
278
|
-
// If there's a single root directory,
|
|
350
|
+
// If there's a single root directory, check if it's a wrapper
|
|
279
351
|
if (entries.length === 1) {
|
|
280
352
|
const rootEntry = entries[0];
|
|
281
353
|
const rootPath = pathJoin(tempExtractDir, rootEntry);
|
|
282
354
|
const rootStat = await stat(rootPath);
|
|
283
355
|
|
|
284
356
|
if (rootStat.isDirectory()) {
|
|
285
|
-
//
|
|
286
|
-
await
|
|
357
|
+
// Check contents of root directory
|
|
358
|
+
const rootContents = await readdir(rootPath);
|
|
359
|
+
logger.debug(`Root directory '${rootEntry}' contains: ${rootContents.join(", ")}`);
|
|
360
|
+
|
|
361
|
+
// Only strip if root is a version/release wrapper
|
|
362
|
+
const isWrapper = this.isWrapperDirectory(rootEntry);
|
|
363
|
+
logger.debug(`Is wrapper directory: ${isWrapper}`);
|
|
364
|
+
|
|
365
|
+
if (isWrapper) {
|
|
366
|
+
// Strip wrapper and move contents
|
|
367
|
+
logger.debug(`Stripping wrapper directory: ${rootEntry}`);
|
|
368
|
+
await this.moveDirectoryContents(rootPath, destDir);
|
|
369
|
+
} else {
|
|
370
|
+
// Keep root directory - move everything including root
|
|
371
|
+
logger.debug("Preserving complete directory structure");
|
|
372
|
+
await this.moveDirectoryContents(tempExtractDir, destDir);
|
|
373
|
+
}
|
|
287
374
|
} else {
|
|
288
375
|
// Single file, just move it
|
|
289
376
|
await mkdirPromise(destDir, { recursive: true });
|
|
@@ -291,9 +378,12 @@ export class DownloadManager {
|
|
|
291
378
|
}
|
|
292
379
|
} else {
|
|
293
380
|
// Multiple entries at root, move them all
|
|
381
|
+
logger.debug("Multiple root entries - moving all");
|
|
294
382
|
await this.moveDirectoryContents(tempExtractDir, destDir);
|
|
295
383
|
}
|
|
296
384
|
|
|
385
|
+
logger.debug(`Moved contents to: ${destDir}`);
|
|
386
|
+
|
|
297
387
|
// Clean up temp directory
|
|
298
388
|
await rm(tempExtractDir, { recursive: true, force: true });
|
|
299
389
|
} catch (error) {
|
|
@@ -388,6 +478,44 @@ export class DownloadManager {
|
|
|
388
478
|
throw new ExtractionError(`Cannot detect archive type from filename: ${filename}`);
|
|
389
479
|
}
|
|
390
480
|
|
|
481
|
+
/**
|
|
482
|
+
* Validate extraction results
|
|
483
|
+
*/
|
|
484
|
+
async validateExtraction(extractDir: string): Promise<boolean> {
|
|
485
|
+
const { readdir, access } = await import("node:fs/promises");
|
|
486
|
+
const { join: pathJoin } = await import("node:path");
|
|
487
|
+
const { constants } = await import("node:fs");
|
|
488
|
+
|
|
489
|
+
try {
|
|
490
|
+
// Check if extract directory exists and is not empty
|
|
491
|
+
const entries = await readdir(extractDir);
|
|
492
|
+
logger.debug(`Extracted files: ${entries.join(", ")}`);
|
|
493
|
+
|
|
494
|
+
if (entries.length === 0) {
|
|
495
|
+
logger.warning("Extraction resulted in no files");
|
|
496
|
+
return false;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// Verify critical paths exist
|
|
500
|
+
const criticalPaths = [".claude", "CLAUDE.md"];
|
|
501
|
+
for (const path of criticalPaths) {
|
|
502
|
+
try {
|
|
503
|
+
await access(pathJoin(extractDir, path), constants.F_OK);
|
|
504
|
+
logger.debug(`✓ Found: ${path}`);
|
|
505
|
+
} catch {
|
|
506
|
+
logger.warning(`Expected path not found: ${path}`);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
return true;
|
|
511
|
+
} catch (error) {
|
|
512
|
+
logger.error(
|
|
513
|
+
`Validation failed: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
514
|
+
);
|
|
515
|
+
return false;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
391
519
|
/**
|
|
392
520
|
* Create temporary download directory
|
|
393
521
|
*/
|