claudekit-cli 1.2.2 → 1.4.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 +3 -3
- package/.github/workflows/claude-code-review.yml +57 -0
- package/.github/workflows/claude.yml +50 -0
- package/CHANGELOG.md +23 -0
- package/README.md +98 -0
- package/biome.json +1 -1
- package/bun.lock +44 -429
- package/dist/index.js +180 -43
- package/package.json +9 -10
- package/src/commands/new.ts +39 -7
- package/src/commands/update.ts +11 -0
- package/src/index.ts +15 -9
- package/src/lib/download.ts +128 -14
- package/src/types.ts +12 -0
- package/src/version.json +3 -0
- package/test-integration/demo/.mcp.json +13 -0
- package/test-integration/demo/.repomixignore +15 -0
- package/test-integration/demo/CLAUDE.md +34 -0
- package/tests/integration/cli.test.ts +252 -0
- package/tests/lib/download.test.ts +230 -8
- package/tests/types.test.ts +75 -0
package/dist/index.js
CHANGED
|
@@ -5800,11 +5800,6 @@ var require_ignore = __commonJS((exports, module) => {
|
|
|
5800
5800
|
}
|
|
5801
5801
|
});
|
|
5802
5802
|
|
|
5803
|
-
// src/index.ts
|
|
5804
|
-
import { readFileSync } from "fs";
|
|
5805
|
-
import { join as join6 } from "path";
|
|
5806
|
-
import { fileURLToPath } from "url";
|
|
5807
|
-
|
|
5808
5803
|
// node_modules/cac/dist/index.mjs
|
|
5809
5804
|
import { EventEmitter } from "events";
|
|
5810
5805
|
function toArr(any) {
|
|
@@ -6405,10 +6400,70 @@ class CAC extends EventEmitter {
|
|
|
6405
6400
|
}
|
|
6406
6401
|
}
|
|
6407
6402
|
var cac = (name = "") => new CAC(name);
|
|
6403
|
+
// package.json
|
|
6404
|
+
var package_default = {
|
|
6405
|
+
name: "claudekit-cli",
|
|
6406
|
+
version: "1.3.0",
|
|
6407
|
+
description: "CLI tool for bootstrapping and updating ClaudeKit projects",
|
|
6408
|
+
type: "module",
|
|
6409
|
+
bin: {
|
|
6410
|
+
ck: "./dist/index.js"
|
|
6411
|
+
},
|
|
6412
|
+
scripts: {
|
|
6413
|
+
dev: "bun run src/index.ts >> logs.txt 2>&1",
|
|
6414
|
+
build: "bun build src/index.ts --outdir dist --target node --external keytar --external @octokit/rest >> logs.txt 2>&1",
|
|
6415
|
+
compile: "bun build src/index.ts --compile --outfile ck >> logs.txt 2>&1",
|
|
6416
|
+
test: "bun test >> logs.txt 2>&1",
|
|
6417
|
+
"test:watch": "bun test --watch >> logs.txt 2>&1",
|
|
6418
|
+
lint: "biome check . >> logs.txt 2>&1",
|
|
6419
|
+
format: "biome format --write . >> logs.txt 2>&1",
|
|
6420
|
+
typecheck: "tsc --noEmit >> logs.txt 2>&1"
|
|
6421
|
+
},
|
|
6422
|
+
keywords: [
|
|
6423
|
+
"cli",
|
|
6424
|
+
"claudekit",
|
|
6425
|
+
"boilerplate",
|
|
6426
|
+
"bootstrap",
|
|
6427
|
+
"template"
|
|
6428
|
+
],
|
|
6429
|
+
author: "ClaudeKit",
|
|
6430
|
+
license: "MIT",
|
|
6431
|
+
engines: {
|
|
6432
|
+
bun: ">=1.0.0"
|
|
6433
|
+
},
|
|
6434
|
+
dependencies: {
|
|
6435
|
+
"@clack/prompts": "^0.7.0",
|
|
6436
|
+
"@octokit/rest": "^22.0.0",
|
|
6437
|
+
cac: "^6.7.14",
|
|
6438
|
+
"cli-progress": "^3.12.0",
|
|
6439
|
+
"extract-zip": "^2.0.1",
|
|
6440
|
+
"fs-extra": "^11.2.0",
|
|
6441
|
+
ignore: "^5.3.2",
|
|
6442
|
+
keytar: "^7.9.0",
|
|
6443
|
+
ora: "^9.0.0",
|
|
6444
|
+
picocolors: "^1.1.1",
|
|
6445
|
+
tar: "^7.4.3",
|
|
6446
|
+
tmp: "^0.2.3",
|
|
6447
|
+
zod: "^3.23.8"
|
|
6448
|
+
},
|
|
6449
|
+
devDependencies: {
|
|
6450
|
+
"@biomejs/biome": "^1.9.4",
|
|
6451
|
+
"@semantic-release/changelog": "^6.0.3",
|
|
6452
|
+
"@semantic-release/git": "^10.0.1",
|
|
6453
|
+
"@types/bun": "latest",
|
|
6454
|
+
"@types/cli-progress": "^3.11.6",
|
|
6455
|
+
"@types/fs-extra": "^11.0.4",
|
|
6456
|
+
"@types/node": "^22.10.1",
|
|
6457
|
+
"@types/tar": "^6.1.13",
|
|
6458
|
+
"@types/tmp": "^0.2.6",
|
|
6459
|
+
"semantic-release": "^24.2.0",
|
|
6460
|
+
typescript: "^5.7.2"
|
|
6461
|
+
}
|
|
6462
|
+
};
|
|
6408
6463
|
|
|
6409
6464
|
// src/commands/new.ts
|
|
6410
6465
|
var import_fs_extra2 = __toESM(require_lib(), 1);
|
|
6411
|
-
import { resolve } from "node:path";
|
|
6466
|
+
import { resolve as resolve2 } from "node:path";
|
|
6412
6467
|
|
|
6413
6468
|
// src/lib/auth.ts
|
|
6414
6469
|
import { execSync } from "node:child_process";
|
|
@@ -10927,15 +10982,19 @@ var coerce = {
|
|
|
10927
10982
|
var NEVER = INVALID;
|
|
10928
10983
|
// src/types.ts
|
|
10929
10984
|
var KitType = exports_external.enum(["engineer", "marketing"]);
|
|
10985
|
+
var ExcludePatternSchema = exports_external.string().trim().min(1, "Exclude pattern cannot be empty").max(500, "Exclude pattern too long").refine((val) => !val.startsWith("/"), "Absolute paths not allowed in exclude patterns").refine((val) => !val.includes(".."), "Path traversal not allowed in exclude patterns");
|
|
10930
10986
|
var NewCommandOptionsSchema = exports_external.object({
|
|
10931
10987
|
dir: exports_external.string().default("."),
|
|
10932
10988
|
kit: KitType.optional(),
|
|
10933
|
-
version: exports_external.string().optional()
|
|
10989
|
+
version: exports_external.string().optional(),
|
|
10990
|
+
force: exports_external.boolean().default(false),
|
|
10991
|
+
exclude: exports_external.array(ExcludePatternSchema).optional().default([])
|
|
10934
10992
|
});
|
|
10935
10993
|
var UpdateCommandOptionsSchema = exports_external.object({
|
|
10936
10994
|
dir: exports_external.string().default("."),
|
|
10937
10995
|
kit: KitType.optional(),
|
|
10938
|
-
version: exports_external.string().optional()
|
|
10996
|
+
version: exports_external.string().optional(),
|
|
10997
|
+
exclude: exports_external.array(ExcludePatternSchema).optional().default([])
|
|
10939
10998
|
});
|
|
10940
10999
|
var VersionCommandOptionsSchema = exports_external.object({
|
|
10941
11000
|
kit: KitType.optional(),
|
|
@@ -11328,7 +11387,7 @@ var import_ignore = __toESM(require_ignore(), 1);
|
|
|
11328
11387
|
import { createWriteStream as createWriteStream2 } from "node:fs";
|
|
11329
11388
|
import { mkdir as mkdir3 } from "node:fs/promises";
|
|
11330
11389
|
import { tmpdir } from "node:os";
|
|
11331
|
-
import { join as join3 } from "node:path";
|
|
11390
|
+
import { join as join3, relative, resolve } from "node:path";
|
|
11332
11391
|
|
|
11333
11392
|
// node_modules/@isaacs/fs-minipass/dist/esm/index.js
|
|
11334
11393
|
import EE from "events";
|
|
@@ -21204,6 +21263,7 @@ function createSpinner(options) {
|
|
|
21204
21263
|
|
|
21205
21264
|
// src/lib/download.ts
|
|
21206
21265
|
class DownloadManager {
|
|
21266
|
+
static MAX_EXTRACTION_SIZE = 500 * 1024 * 1024;
|
|
21207
21267
|
static EXCLUDE_PATTERNS = [
|
|
21208
21268
|
".git",
|
|
21209
21269
|
".git/**",
|
|
@@ -21215,9 +21275,37 @@ class DownloadManager {
|
|
|
21215
21275
|
"Thumbs.db",
|
|
21216
21276
|
"*.log"
|
|
21217
21277
|
];
|
|
21278
|
+
totalExtractedSize = 0;
|
|
21279
|
+
ig;
|
|
21280
|
+
userExcludePatterns = [];
|
|
21281
|
+
constructor() {
|
|
21282
|
+
this.ig = import_ignore.default().add(DownloadManager.EXCLUDE_PATTERNS);
|
|
21283
|
+
}
|
|
21284
|
+
setExcludePatterns(patterns) {
|
|
21285
|
+
this.userExcludePatterns = patterns;
|
|
21286
|
+
this.ig = import_ignore.default().add([...DownloadManager.EXCLUDE_PATTERNS, ...this.userExcludePatterns]);
|
|
21287
|
+
if (patterns.length > 0) {
|
|
21288
|
+
logger.info(`Added ${patterns.length} custom exclude pattern(s)`);
|
|
21289
|
+
patterns.forEach((p) => logger.debug(` - ${p}`));
|
|
21290
|
+
}
|
|
21291
|
+
}
|
|
21218
21292
|
shouldExclude(filePath) {
|
|
21219
|
-
|
|
21220
|
-
|
|
21293
|
+
return this.ig.ignores(filePath);
|
|
21294
|
+
}
|
|
21295
|
+
isPathSafe(basePath, targetPath) {
|
|
21296
|
+
const resolvedBase = resolve(basePath);
|
|
21297
|
+
const resolvedTarget = resolve(targetPath);
|
|
21298
|
+
const relativePath = relative(resolvedBase, resolvedTarget);
|
|
21299
|
+
return !relativePath.startsWith("..") && !relativePath.startsWith("/") && resolvedTarget.startsWith(resolvedBase);
|
|
21300
|
+
}
|
|
21301
|
+
checkExtractionSize(fileSize) {
|
|
21302
|
+
this.totalExtractedSize += fileSize;
|
|
21303
|
+
if (this.totalExtractedSize > DownloadManager.MAX_EXTRACTION_SIZE) {
|
|
21304
|
+
throw new ExtractionError(`Archive exceeds maximum extraction size of ${this.formatBytes(DownloadManager.MAX_EXTRACTION_SIZE)}. Possible archive bomb detected.`);
|
|
21305
|
+
}
|
|
21306
|
+
}
|
|
21307
|
+
resetExtractionSize() {
|
|
21308
|
+
this.totalExtractedSize = 0;
|
|
21221
21309
|
}
|
|
21222
21310
|
async downloadAsset(asset, destDir) {
|
|
21223
21311
|
try {
|
|
@@ -21328,6 +21416,7 @@ class DownloadManager {
|
|
|
21328
21416
|
async extractArchive(archivePath, destDir, archiveType) {
|
|
21329
21417
|
const spinner = createSpinner("Extracting files...").start();
|
|
21330
21418
|
try {
|
|
21419
|
+
this.resetExtractionSize();
|
|
21331
21420
|
const detectedType = archiveType || this.detectArchiveType(archivePath);
|
|
21332
21421
|
await mkdir3(destDir, { recursive: true });
|
|
21333
21422
|
if (detectedType === "tar.gz") {
|
|
@@ -21398,8 +21487,8 @@ class DownloadManager {
|
|
|
21398
21487
|
}
|
|
21399
21488
|
}
|
|
21400
21489
|
isWrapperDirectory(dirName) {
|
|
21401
|
-
const versionPattern = /^[\w-]+-v?\d+\.\d+\.\d
|
|
21402
|
-
const hashPattern = /^[\w-]+-[a-f0-9]{7,}$/;
|
|
21490
|
+
const versionPattern = /^[\w-]+-v?\d+\.\d+\.\d+(-[\w.]+)?$/;
|
|
21491
|
+
const hashPattern = /^[\w-]+-[a-f0-9]{7,40}$/;
|
|
21403
21492
|
return versionPattern.test(dirName) || hashPattern.test(dirName);
|
|
21404
21493
|
}
|
|
21405
21494
|
async extractZip(archivePath, destDir) {
|
|
@@ -21447,13 +21536,17 @@ class DownloadManager {
|
|
|
21447
21536
|
}
|
|
21448
21537
|
async moveDirectoryContents(sourceDir, destDir) {
|
|
21449
21538
|
const { readdir, stat, mkdir: mkdirPromise, copyFile } = await import("node:fs/promises");
|
|
21450
|
-
const { join: pathJoin, relative } = await import("node:path");
|
|
21539
|
+
const { join: pathJoin, relative: relative2 } = await import("node:path");
|
|
21451
21540
|
await mkdirPromise(destDir, { recursive: true });
|
|
21452
21541
|
const entries = await readdir(sourceDir);
|
|
21453
21542
|
for (const entry of entries) {
|
|
21454
21543
|
const sourcePath = pathJoin(sourceDir, entry);
|
|
21455
21544
|
const destPath = pathJoin(destDir, entry);
|
|
21456
|
-
const relativePath =
|
|
21545
|
+
const relativePath = relative2(sourceDir, sourcePath);
|
|
21546
|
+
if (!this.isPathSafe(destDir, destPath)) {
|
|
21547
|
+
logger.warning(`Skipping unsafe path: ${relativePath}`);
|
|
21548
|
+
throw new ExtractionError(`Path traversal attempt detected: ${relativePath}`);
|
|
21549
|
+
}
|
|
21457
21550
|
if (this.shouldExclude(relativePath)) {
|
|
21458
21551
|
logger.debug(`Excluding: ${relativePath}`);
|
|
21459
21552
|
continue;
|
|
@@ -21462,19 +21555,24 @@ class DownloadManager {
|
|
|
21462
21555
|
if (entryStat.isDirectory()) {
|
|
21463
21556
|
await this.copyDirectory(sourcePath, destPath);
|
|
21464
21557
|
} else {
|
|
21558
|
+
this.checkExtractionSize(entryStat.size);
|
|
21465
21559
|
await copyFile(sourcePath, destPath);
|
|
21466
21560
|
}
|
|
21467
21561
|
}
|
|
21468
21562
|
}
|
|
21469
21563
|
async copyDirectory(sourceDir, destDir) {
|
|
21470
21564
|
const { readdir, stat, mkdir: mkdirPromise, copyFile } = await import("node:fs/promises");
|
|
21471
|
-
const { join: pathJoin, relative } = await import("node:path");
|
|
21565
|
+
const { join: pathJoin, relative: relative2 } = await import("node:path");
|
|
21472
21566
|
await mkdirPromise(destDir, { recursive: true });
|
|
21473
21567
|
const entries = await readdir(sourceDir);
|
|
21474
21568
|
for (const entry of entries) {
|
|
21475
21569
|
const sourcePath = pathJoin(sourceDir, entry);
|
|
21476
21570
|
const destPath = pathJoin(destDir, entry);
|
|
21477
|
-
const relativePath =
|
|
21571
|
+
const relativePath = relative2(sourceDir, sourcePath);
|
|
21572
|
+
if (!this.isPathSafe(destDir, destPath)) {
|
|
21573
|
+
logger.warning(`Skipping unsafe path: ${relativePath}`);
|
|
21574
|
+
throw new ExtractionError(`Path traversal attempt detected: ${relativePath}`);
|
|
21575
|
+
}
|
|
21478
21576
|
if (this.shouldExclude(relativePath)) {
|
|
21479
21577
|
logger.debug(`Excluding: ${relativePath}`);
|
|
21480
21578
|
continue;
|
|
@@ -21483,6 +21581,7 @@ class DownloadManager {
|
|
|
21483
21581
|
if (entryStat.isDirectory()) {
|
|
21484
21582
|
await this.copyDirectory(sourcePath, destPath);
|
|
21485
21583
|
} else {
|
|
21584
|
+
this.checkExtractionSize(entryStat.size);
|
|
21486
21585
|
await copyFile(sourcePath, destPath);
|
|
21487
21586
|
}
|
|
21488
21587
|
}
|
|
@@ -21504,22 +21603,28 @@ class DownloadManager {
|
|
|
21504
21603
|
const entries = await readdir(extractDir);
|
|
21505
21604
|
logger.debug(`Extracted files: ${entries.join(", ")}`);
|
|
21506
21605
|
if (entries.length === 0) {
|
|
21507
|
-
|
|
21508
|
-
return false;
|
|
21606
|
+
throw new ExtractionError("Extraction resulted in no files");
|
|
21509
21607
|
}
|
|
21510
21608
|
const criticalPaths = [".claude", "CLAUDE.md"];
|
|
21609
|
+
const missingPaths = [];
|
|
21511
21610
|
for (const path8 of criticalPaths) {
|
|
21512
21611
|
try {
|
|
21513
21612
|
await access(pathJoin(extractDir, path8), constants2.F_OK);
|
|
21514
21613
|
logger.debug(`✓ Found: ${path8}`);
|
|
21515
21614
|
} catch {
|
|
21516
21615
|
logger.warning(`Expected path not found: ${path8}`);
|
|
21616
|
+
missingPaths.push(path8);
|
|
21517
21617
|
}
|
|
21518
21618
|
}
|
|
21519
|
-
|
|
21619
|
+
if (missingPaths.length > 0) {
|
|
21620
|
+
logger.warning(`Some expected paths are missing: ${missingPaths.join(", ")}. This may not be a ClaudeKit project.`);
|
|
21621
|
+
}
|
|
21622
|
+
logger.debug("Extraction validation passed");
|
|
21520
21623
|
} catch (error2) {
|
|
21521
|
-
|
|
21522
|
-
|
|
21624
|
+
if (error2 instanceof ExtractionError) {
|
|
21625
|
+
throw error2;
|
|
21626
|
+
}
|
|
21627
|
+
throw new ExtractionError(`Validation failed: ${error2 instanceof Error ? error2.message : "Unknown error"}`);
|
|
21523
21628
|
}
|
|
21524
21629
|
}
|
|
21525
21630
|
async createTempDir() {
|
|
@@ -21679,7 +21784,7 @@ class GitHubClient {
|
|
|
21679
21784
|
}
|
|
21680
21785
|
|
|
21681
21786
|
// src/lib/merge.ts
|
|
21682
|
-
import { join as join4, relative } from "node:path";
|
|
21787
|
+
import { join as join4, relative as relative2 } from "node:path";
|
|
21683
21788
|
var import_fs_extra = __toESM(require_lib(), 1);
|
|
21684
21789
|
var import_ignore2 = __toESM(require_ignore(), 1);
|
|
21685
21790
|
class FileMerger {
|
|
@@ -21705,7 +21810,7 @@ class FileMerger {
|
|
|
21705
21810
|
const conflicts = [];
|
|
21706
21811
|
const files = await this.getFiles(sourceDir);
|
|
21707
21812
|
for (const file of files) {
|
|
21708
|
-
const relativePath =
|
|
21813
|
+
const relativePath = relative2(sourceDir, file);
|
|
21709
21814
|
if (this.ig.ignores(relativePath)) {
|
|
21710
21815
|
continue;
|
|
21711
21816
|
}
|
|
@@ -21721,7 +21826,7 @@ class FileMerger {
|
|
|
21721
21826
|
let copiedCount = 0;
|
|
21722
21827
|
let skippedCount = 0;
|
|
21723
21828
|
for (const file of files) {
|
|
21724
|
-
const relativePath =
|
|
21829
|
+
const relativePath = relative2(sourceDir, file);
|
|
21725
21830
|
if (this.ig.ignores(relativePath)) {
|
|
21726
21831
|
logger.debug(`Skipping protected file: ${relativePath}`);
|
|
21727
21832
|
skippedCount++;
|
|
@@ -21859,27 +21964,42 @@ async function newCommand(options) {
|
|
|
21859
21964
|
prompts.intro("\uD83D\uDE80 ClaudeKit - Create New Project");
|
|
21860
21965
|
try {
|
|
21861
21966
|
const validOptions = NewCommandOptionsSchema.parse(options);
|
|
21967
|
+
const isNonInteractive = !process.stdin.isTTY || process.env.CI === "true" || process.env.NON_INTERACTIVE === "true";
|
|
21862
21968
|
const config = await ConfigManager.get();
|
|
21863
21969
|
let kit = validOptions.kit || config.defaults?.kit;
|
|
21864
21970
|
if (!kit) {
|
|
21971
|
+
if (isNonInteractive) {
|
|
21972
|
+
throw new Error("Kit must be specified via --kit flag in non-interactive mode");
|
|
21973
|
+
}
|
|
21865
21974
|
kit = await prompts.selectKit();
|
|
21866
21975
|
}
|
|
21867
21976
|
const kitConfig = AVAILABLE_KITS[kit];
|
|
21868
21977
|
logger.info(`Selected kit: ${kitConfig.name}`);
|
|
21869
21978
|
let targetDir = validOptions.dir || config.defaults?.dir || ".";
|
|
21870
21979
|
if (!validOptions.dir && !config.defaults?.dir) {
|
|
21871
|
-
|
|
21980
|
+
if (isNonInteractive) {
|
|
21981
|
+
targetDir = ".";
|
|
21982
|
+
} else {
|
|
21983
|
+
targetDir = await prompts.getDirectory(targetDir);
|
|
21984
|
+
}
|
|
21872
21985
|
}
|
|
21873
|
-
const resolvedDir =
|
|
21986
|
+
const resolvedDir = resolve2(targetDir);
|
|
21874
21987
|
logger.info(`Target directory: ${resolvedDir}`);
|
|
21875
21988
|
if (await import_fs_extra2.pathExists(resolvedDir)) {
|
|
21876
21989
|
const files = await import_fs_extra2.readdir(resolvedDir);
|
|
21877
21990
|
const isEmpty = files.length === 0;
|
|
21878
21991
|
if (!isEmpty) {
|
|
21879
|
-
|
|
21880
|
-
|
|
21881
|
-
|
|
21882
|
-
|
|
21992
|
+
if (isNonInteractive) {
|
|
21993
|
+
if (!validOptions.force) {
|
|
21994
|
+
throw new Error("Directory is not empty. Use --force flag to overwrite in non-interactive mode");
|
|
21995
|
+
}
|
|
21996
|
+
logger.info("Directory is not empty. Proceeding with --force flag");
|
|
21997
|
+
} else {
|
|
21998
|
+
const continueAnyway = await prompts.confirm("Directory is not empty. Files may be overwritten. Continue?");
|
|
21999
|
+
if (!continueAnyway) {
|
|
22000
|
+
logger.warning("Operation cancelled");
|
|
22001
|
+
return;
|
|
22002
|
+
}
|
|
21883
22003
|
}
|
|
21884
22004
|
}
|
|
21885
22005
|
}
|
|
@@ -21905,6 +22025,9 @@ async function newCommand(options) {
|
|
|
21905
22025
|
logger.info(`Download source: ${downloadInfo.type}`);
|
|
21906
22026
|
logger.debug(`Download URL: ${downloadInfo.url}`);
|
|
21907
22027
|
const downloadManager = new DownloadManager;
|
|
22028
|
+
if (validOptions.exclude && validOptions.exclude.length > 0) {
|
|
22029
|
+
downloadManager.setExcludePatterns(validOptions.exclude);
|
|
22030
|
+
}
|
|
21908
22031
|
const tempDir = await downloadManager.createTempDir();
|
|
21909
22032
|
const { token } = await AuthManager.getToken();
|
|
21910
22033
|
let archivePath;
|
|
@@ -21940,6 +22063,9 @@ async function newCommand(options) {
|
|
|
21940
22063
|
await downloadManager.extractArchive(archivePath, extractDir);
|
|
21941
22064
|
await downloadManager.validateExtraction(extractDir);
|
|
21942
22065
|
const merger = new FileMerger;
|
|
22066
|
+
if (validOptions.exclude && validOptions.exclude.length > 0) {
|
|
22067
|
+
merger.addIgnorePatterns(validOptions.exclude);
|
|
22068
|
+
}
|
|
21943
22069
|
await merger.merge(extractDir, resolvedDir, true);
|
|
21944
22070
|
prompts.outro(`✨ Project created successfully at ${resolvedDir}`);
|
|
21945
22071
|
prompts.note(`cd ${targetDir !== "." ? targetDir : "into the directory"}
|
|
@@ -21953,11 +22079,11 @@ bun run dev`, "Next steps");
|
|
|
21953
22079
|
|
|
21954
22080
|
// src/commands/update.ts
|
|
21955
22081
|
var import_fs_extra4 = __toESM(require_lib(), 1);
|
|
21956
|
-
import { resolve as
|
|
22082
|
+
import { resolve as resolve4 } from "node:path";
|
|
21957
22083
|
|
|
21958
22084
|
// src/utils/file-scanner.ts
|
|
21959
22085
|
var import_fs_extra3 = __toESM(require_lib(), 1);
|
|
21960
|
-
import { join as join5, relative as
|
|
22086
|
+
import { join as join5, relative as relative3, resolve as resolve3 } from "node:path";
|
|
21961
22087
|
class FileScanner {
|
|
21962
22088
|
static async getFiles(dirPath, relativeTo) {
|
|
21963
22089
|
const basePath = relativeTo || dirPath;
|
|
@@ -21982,7 +22108,7 @@ class FileScanner {
|
|
|
21982
22108
|
const subFiles = await FileScanner.getFiles(fullPath, basePath);
|
|
21983
22109
|
files.push(...subFiles);
|
|
21984
22110
|
} else if (stats.isFile()) {
|
|
21985
|
-
const relativePath =
|
|
22111
|
+
const relativePath = relative3(basePath, fullPath);
|
|
21986
22112
|
files.push(relativePath);
|
|
21987
22113
|
}
|
|
21988
22114
|
}
|
|
@@ -22010,8 +22136,8 @@ class FileScanner {
|
|
|
22010
22136
|
return customFiles;
|
|
22011
22137
|
}
|
|
22012
22138
|
static isSafePath(basePath, targetPath) {
|
|
22013
|
-
const resolvedBase =
|
|
22014
|
-
const resolvedTarget =
|
|
22139
|
+
const resolvedBase = resolve3(basePath);
|
|
22140
|
+
const resolvedTarget = resolve3(targetPath);
|
|
22015
22141
|
return resolvedTarget.startsWith(resolvedBase);
|
|
22016
22142
|
}
|
|
22017
22143
|
}
|
|
@@ -22033,7 +22159,7 @@ async function updateCommand(options) {
|
|
|
22033
22159
|
if (!validOptions.dir && !config.defaults?.dir) {
|
|
22034
22160
|
targetDir = await prompts.getDirectory(targetDir);
|
|
22035
22161
|
}
|
|
22036
|
-
const resolvedDir =
|
|
22162
|
+
const resolvedDir = resolve4(targetDir);
|
|
22037
22163
|
logger.info(`Target directory: ${resolvedDir}`);
|
|
22038
22164
|
if (!await import_fs_extra4.pathExists(resolvedDir)) {
|
|
22039
22165
|
logger.error(`Directory does not exist: ${resolvedDir}`);
|
|
@@ -22062,6 +22188,9 @@ async function updateCommand(options) {
|
|
|
22062
22188
|
logger.info(`Download source: ${downloadInfo.type}`);
|
|
22063
22189
|
logger.debug(`Download URL: ${downloadInfo.url}`);
|
|
22064
22190
|
const downloadManager = new DownloadManager;
|
|
22191
|
+
if (validOptions.exclude && validOptions.exclude.length > 0) {
|
|
22192
|
+
downloadManager.setExcludePatterns(validOptions.exclude);
|
|
22193
|
+
}
|
|
22065
22194
|
const tempDir = await downloadManager.createTempDir();
|
|
22066
22195
|
const { token } = await AuthManager.getToken();
|
|
22067
22196
|
let archivePath;
|
|
@@ -22103,6 +22232,9 @@ async function updateCommand(options) {
|
|
|
22103
22232
|
merger.addIgnorePatterns(customClaudeFiles);
|
|
22104
22233
|
logger.success(`Protected ${customClaudeFiles.length} custom .claude file(s)`);
|
|
22105
22234
|
}
|
|
22235
|
+
if (validOptions.exclude && validOptions.exclude.length > 0) {
|
|
22236
|
+
merger.addIgnorePatterns(validOptions.exclude);
|
|
22237
|
+
}
|
|
22106
22238
|
await merger.merge(extractDir, resolvedDir, false);
|
|
22107
22239
|
prompts.outro(`✨ Project updated successfully at ${resolvedDir}`);
|
|
22108
22240
|
const protectedNote = customClaudeFiles.length > 0 ? `Your project has been updated with the latest version.
|
|
@@ -22317,21 +22449,26 @@ if (process.stdout.setEncoding) {
|
|
|
22317
22449
|
if (process.stderr.setEncoding) {
|
|
22318
22450
|
process.stderr.setEncoding("utf8");
|
|
22319
22451
|
}
|
|
22320
|
-
var
|
|
22321
|
-
var packageJson = JSON.parse(readFileSync(join6(__dirname2, "../package.json"), "utf-8"));
|
|
22452
|
+
var packageVersion = package_default.version;
|
|
22322
22453
|
var cli = cac("ck");
|
|
22323
22454
|
cli.option("--verbose, -v", "Enable verbose logging for debugging");
|
|
22324
22455
|
cli.option("--log-file <path>", "Write logs to file");
|
|
22325
|
-
cli.command("new", "Bootstrap a new ClaudeKit project").option("--dir <dir>", "Target directory (default: .)").option("--kit <kit>", "Kit to use (engineer, marketing)").option("--version <version>", "Specific version to download (default: latest)").action(async (options) => {
|
|
22456
|
+
cli.command("new", "Bootstrap a new ClaudeKit project").option("--dir <dir>", "Target directory (default: .)").option("--kit <kit>", "Kit to use (engineer, marketing)").option("--version <version>", "Specific version to download (default: latest)").option("--force", "Overwrite existing files without confirmation").option("--exclude <pattern>", "Exclude files matching glob pattern (can be used multiple times)").action(async (options) => {
|
|
22457
|
+
if (options.exclude && !Array.isArray(options.exclude)) {
|
|
22458
|
+
options.exclude = [options.exclude];
|
|
22459
|
+
}
|
|
22326
22460
|
await newCommand(options);
|
|
22327
22461
|
});
|
|
22328
|
-
cli.command("update", "Update existing ClaudeKit project").option("--dir <dir>", "Target directory (default: .)").option("--kit <kit>", "Kit to use (engineer, marketing)").option("--version <version>", "Specific version to download (default: latest)").action(async (options) => {
|
|
22462
|
+
cli.command("update", "Update existing ClaudeKit project").option("--dir <dir>", "Target directory (default: .)").option("--kit <kit>", "Kit to use (engineer, marketing)").option("--version <version>", "Specific version to download (default: latest)").option("--exclude <pattern>", "Exclude files matching glob pattern (can be used multiple times)").action(async (options) => {
|
|
22463
|
+
if (options.exclude && !Array.isArray(options.exclude)) {
|
|
22464
|
+
options.exclude = [options.exclude];
|
|
22465
|
+
}
|
|
22329
22466
|
await updateCommand(options);
|
|
22330
22467
|
});
|
|
22331
22468
|
cli.command("versions", "List available versions of ClaudeKit repositories").option("--kit <kit>", "Filter by specific kit (engineer, marketing)").option("--limit <limit>", "Number of releases to show (default: 30)").option("--all", "Show all releases including prereleases").action(async (options) => {
|
|
22332
22469
|
await versionCommand(options);
|
|
22333
22470
|
});
|
|
22334
|
-
cli.version(
|
|
22471
|
+
cli.version(packageVersion);
|
|
22335
22472
|
cli.help();
|
|
22336
22473
|
var parsed = cli.parse(process.argv, { run: false });
|
|
22337
22474
|
var envVerbose = process.env.CLAUDEKIT_VERBOSE === "1" || process.env.CLAUDEKIT_VERBOSE === "true";
|
|
@@ -22343,7 +22480,7 @@ if (parsed.options.logFile) {
|
|
|
22343
22480
|
logger2.setLogFile(parsed.options.logFile);
|
|
22344
22481
|
}
|
|
22345
22482
|
logger2.verbose("ClaudeKit CLI starting", {
|
|
22346
|
-
version:
|
|
22483
|
+
version: packageVersion,
|
|
22347
22484
|
command: parsed.args[0] || "none",
|
|
22348
22485
|
options: parsed.options,
|
|
22349
22486
|
cwd: process.cwd(),
|
package/package.json
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claudekit-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "CLI tool for bootstrapping and updating ClaudeKit projects",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"ck": "./dist/index.js"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
|
-
"dev": "bun run src/index.ts",
|
|
11
|
-
"build": "bun build src/index.ts --outdir dist --target node --external
|
|
12
|
-
"compile": "bun build src/index.ts --compile --outfile ck",
|
|
13
|
-
"test": "bun test",
|
|
14
|
-
"test:watch": "bun test --watch",
|
|
15
|
-
"lint": "biome check .",
|
|
16
|
-
"format": "biome format --write .",
|
|
17
|
-
"typecheck": "tsc --noEmit"
|
|
10
|
+
"dev": "bun run src/index.ts >> logs.txt 2>&1",
|
|
11
|
+
"build": "bun build src/index.ts --outdir dist --target node --external keytar --external @octokit/rest >> logs.txt 2>&1",
|
|
12
|
+
"compile": "bun build src/index.ts --compile --outfile ck >> logs.txt 2>&1",
|
|
13
|
+
"test": "bun test >> logs.txt 2>&1",
|
|
14
|
+
"test:watch": "bun test --watch >> logs.txt 2>&1",
|
|
15
|
+
"lint": "biome check . >> logs.txt 2>&1",
|
|
16
|
+
"format": "biome format --write . >> logs.txt 2>&1",
|
|
17
|
+
"typecheck": "tsc --noEmit >> logs.txt 2>&1"
|
|
18
18
|
},
|
|
19
19
|
"keywords": [
|
|
20
20
|
"cli",
|
|
@@ -53,7 +53,6 @@
|
|
|
53
53
|
"@types/node": "^22.10.1",
|
|
54
54
|
"@types/tar": "^6.1.13",
|
|
55
55
|
"@types/tmp": "^0.2.6",
|
|
56
|
-
"@types/unzipper": "^0.10.10",
|
|
57
56
|
"semantic-release": "^24.2.0",
|
|
58
57
|
"typescript": "^5.7.2"
|
|
59
58
|
}
|
package/src/commands/new.ts
CHANGED
|
@@ -19,12 +19,19 @@ export async function newCommand(options: NewCommandOptions): Promise<void> {
|
|
|
19
19
|
// Validate and parse options
|
|
20
20
|
const validOptions = NewCommandOptionsSchema.parse(options);
|
|
21
21
|
|
|
22
|
+
// Detect non-interactive mode
|
|
23
|
+
const isNonInteractive =
|
|
24
|
+
!process.stdin.isTTY || process.env.CI === "true" || process.env.NON_INTERACTIVE === "true";
|
|
25
|
+
|
|
22
26
|
// Load config for defaults
|
|
23
27
|
const config = await ConfigManager.get();
|
|
24
28
|
|
|
25
29
|
// Get kit selection
|
|
26
30
|
let kit = validOptions.kit || config.defaults?.kit;
|
|
27
31
|
if (!kit) {
|
|
32
|
+
if (isNonInteractive) {
|
|
33
|
+
throw new Error("Kit must be specified via --kit flag in non-interactive mode");
|
|
34
|
+
}
|
|
28
35
|
kit = await prompts.selectKit();
|
|
29
36
|
}
|
|
30
37
|
|
|
@@ -34,7 +41,11 @@ export async function newCommand(options: NewCommandOptions): Promise<void> {
|
|
|
34
41
|
// Get target directory
|
|
35
42
|
let targetDir = validOptions.dir || config.defaults?.dir || ".";
|
|
36
43
|
if (!validOptions.dir && !config.defaults?.dir) {
|
|
37
|
-
|
|
44
|
+
if (isNonInteractive) {
|
|
45
|
+
targetDir = ".";
|
|
46
|
+
} else {
|
|
47
|
+
targetDir = await prompts.getDirectory(targetDir);
|
|
48
|
+
}
|
|
38
49
|
}
|
|
39
50
|
|
|
40
51
|
const resolvedDir = resolve(targetDir);
|
|
@@ -45,12 +56,21 @@ export async function newCommand(options: NewCommandOptions): Promise<void> {
|
|
|
45
56
|
const files = await readdir(resolvedDir);
|
|
46
57
|
const isEmpty = files.length === 0;
|
|
47
58
|
if (!isEmpty) {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
59
|
+
if (isNonInteractive) {
|
|
60
|
+
if (!validOptions.force) {
|
|
61
|
+
throw new Error(
|
|
62
|
+
"Directory is not empty. Use --force flag to overwrite in non-interactive mode",
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
logger.info("Directory is not empty. Proceeding with --force flag");
|
|
66
|
+
} else {
|
|
67
|
+
const continueAnyway = await prompts.confirm(
|
|
68
|
+
"Directory is not empty. Files may be overwritten. Continue?",
|
|
69
|
+
);
|
|
70
|
+
if (!continueAnyway) {
|
|
71
|
+
logger.warning("Operation cancelled");
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
54
74
|
}
|
|
55
75
|
}
|
|
56
76
|
}
|
|
@@ -90,6 +110,12 @@ export async function newCommand(options: NewCommandOptions): Promise<void> {
|
|
|
90
110
|
|
|
91
111
|
// Download asset
|
|
92
112
|
const downloadManager = new DownloadManager();
|
|
113
|
+
|
|
114
|
+
// Apply user exclude patterns if provided
|
|
115
|
+
if (validOptions.exclude && validOptions.exclude.length > 0) {
|
|
116
|
+
downloadManager.setExcludePatterns(validOptions.exclude);
|
|
117
|
+
}
|
|
118
|
+
|
|
93
119
|
const tempDir = await downloadManager.createTempDir();
|
|
94
120
|
|
|
95
121
|
// Get authentication token for API requests
|
|
@@ -137,6 +163,12 @@ export async function newCommand(options: NewCommandOptions): Promise<void> {
|
|
|
137
163
|
|
|
138
164
|
// Copy files to target directory
|
|
139
165
|
const merger = new FileMerger();
|
|
166
|
+
|
|
167
|
+
// Apply user exclude patterns if provided
|
|
168
|
+
if (validOptions.exclude && validOptions.exclude.length > 0) {
|
|
169
|
+
merger.addIgnorePatterns(validOptions.exclude);
|
|
170
|
+
}
|
|
171
|
+
|
|
140
172
|
await merger.merge(extractDir, resolvedDir, true); // Skip confirmation for new projects
|
|
141
173
|
|
|
142
174
|
prompts.outro(`✨ Project created successfully at ${resolvedDir}`);
|
package/src/commands/update.ts
CHANGED
|
@@ -83,6 +83,12 @@ export async function updateCommand(options: UpdateCommandOptions): Promise<void
|
|
|
83
83
|
|
|
84
84
|
// Download asset
|
|
85
85
|
const downloadManager = new DownloadManager();
|
|
86
|
+
|
|
87
|
+
// Apply user exclude patterns if provided
|
|
88
|
+
if (validOptions.exclude && validOptions.exclude.length > 0) {
|
|
89
|
+
downloadManager.setExcludePatterns(validOptions.exclude);
|
|
90
|
+
}
|
|
91
|
+
|
|
86
92
|
const tempDir = await downloadManager.createTempDir();
|
|
87
93
|
|
|
88
94
|
// Get authentication token for API requests
|
|
@@ -141,6 +147,11 @@ export async function updateCommand(options: UpdateCommandOptions): Promise<void
|
|
|
141
147
|
logger.success(`Protected ${customClaudeFiles.length} custom .claude file(s)`);
|
|
142
148
|
}
|
|
143
149
|
|
|
150
|
+
// Apply user exclude patterns if provided
|
|
151
|
+
if (validOptions.exclude && validOptions.exclude.length > 0) {
|
|
152
|
+
merger.addIgnorePatterns(validOptions.exclude);
|
|
153
|
+
}
|
|
154
|
+
|
|
144
155
|
await merger.merge(extractDir, resolvedDir, false); // Show confirmation for updates
|
|
145
156
|
|
|
146
157
|
prompts.outro(`✨ Project updated successfully at ${resolvedDir}`);
|