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/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
- const ig = import_ignore.default().add(DownloadManager.EXCLUDE_PATTERNS);
21220
- return ig.ignores(filePath);
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 = relative(sourceDir, sourcePath);
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 = relative(sourceDir, sourcePath);
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
- logger.warning("Extraction resulted in no files");
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
- return true;
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
- logger.error(`Validation failed: ${error2 instanceof Error ? error2.message : "Unknown error"}`);
21522
- return false;
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 = relative(sourceDir, file);
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 = relative(sourceDir, file);
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
- targetDir = await prompts.getDirectory(targetDir);
21980
+ if (isNonInteractive) {
21981
+ targetDir = ".";
21982
+ } else {
21983
+ targetDir = await prompts.getDirectory(targetDir);
21984
+ }
21872
21985
  }
21873
- const resolvedDir = resolve(targetDir);
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
- const continueAnyway = await prompts.confirm("Directory is not empty. Files may be overwritten. Continue?");
21880
- if (!continueAnyway) {
21881
- logger.warning("Operation cancelled");
21882
- return;
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 resolve3 } from "node:path";
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 relative2, resolve as resolve2 } from "node:path";
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 = relative2(basePath, fullPath);
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 = resolve2(basePath);
22014
- const resolvedTarget = resolve2(targetPath);
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 = resolve3(targetDir);
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 __dirname2 = fileURLToPath(new URL(".", import.meta.url));
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(packageJson.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: packageJson.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.2.2",
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 unzipper --external keytar --external @octokit/rest",
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
  }
@@ -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
- targetDir = await prompts.getDirectory(targetDir);
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
- const continueAnyway = await prompts.confirm(
49
- "Directory is not empty. Files may be overwritten. Continue?",
50
- );
51
- if (!continueAnyway) {
52
- logger.warning("Operation cancelled");
53
- return;
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}`);
@@ -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}`);