claudekit-cli 1.2.2 → 1.3.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/CHANGELOG.md +16 -0
- package/biome.json +1 -1
- package/bun.lock +44 -429
- package/dist/index.js +87 -39
- package/package.json +9 -10
- package/src/commands/new.ts +27 -7
- package/src/index.ts +5 -9
- package/src/lib/download.ts +93 -12
- package/src/types.ts +1 -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/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) {
|
|
@@ -6408,7 +6403,7 @@ var cac = (name = "") => new CAC(name);
|
|
|
6408
6403
|
|
|
6409
6404
|
// src/commands/new.ts
|
|
6410
6405
|
var import_fs_extra2 = __toESM(require_lib(), 1);
|
|
6411
|
-
import { resolve } from "node:path";
|
|
6406
|
+
import { resolve as resolve2 } from "node:path";
|
|
6412
6407
|
|
|
6413
6408
|
// src/lib/auth.ts
|
|
6414
6409
|
import { execSync } from "node:child_process";
|
|
@@ -10930,7 +10925,8 @@ var KitType = exports_external.enum(["engineer", "marketing"]);
|
|
|
10930
10925
|
var NewCommandOptionsSchema = exports_external.object({
|
|
10931
10926
|
dir: exports_external.string().default("."),
|
|
10932
10927
|
kit: KitType.optional(),
|
|
10933
|
-
version: exports_external.string().optional()
|
|
10928
|
+
version: exports_external.string().optional(),
|
|
10929
|
+
force: exports_external.boolean().default(false)
|
|
10934
10930
|
});
|
|
10935
10931
|
var UpdateCommandOptionsSchema = exports_external.object({
|
|
10936
10932
|
dir: exports_external.string().default("."),
|
|
@@ -11328,7 +11324,7 @@ var import_ignore = __toESM(require_ignore(), 1);
|
|
|
11328
11324
|
import { createWriteStream as createWriteStream2 } from "node:fs";
|
|
11329
11325
|
import { mkdir as mkdir3 } from "node:fs/promises";
|
|
11330
11326
|
import { tmpdir } from "node:os";
|
|
11331
|
-
import { join as join3 } from "node:path";
|
|
11327
|
+
import { join as join3, relative, resolve } from "node:path";
|
|
11332
11328
|
|
|
11333
11329
|
// node_modules/@isaacs/fs-minipass/dist/esm/index.js
|
|
11334
11330
|
import EE from "events";
|
|
@@ -21204,6 +21200,7 @@ function createSpinner(options) {
|
|
|
21204
21200
|
|
|
21205
21201
|
// src/lib/download.ts
|
|
21206
21202
|
class DownloadManager {
|
|
21203
|
+
static MAX_EXTRACTION_SIZE = 500 * 1024 * 1024;
|
|
21207
21204
|
static EXCLUDE_PATTERNS = [
|
|
21208
21205
|
".git",
|
|
21209
21206
|
".git/**",
|
|
@@ -21215,10 +21212,26 @@ class DownloadManager {
|
|
|
21215
21212
|
"Thumbs.db",
|
|
21216
21213
|
"*.log"
|
|
21217
21214
|
];
|
|
21215
|
+
totalExtractedSize = 0;
|
|
21218
21216
|
shouldExclude(filePath) {
|
|
21219
21217
|
const ig = import_ignore.default().add(DownloadManager.EXCLUDE_PATTERNS);
|
|
21220
21218
|
return ig.ignores(filePath);
|
|
21221
21219
|
}
|
|
21220
|
+
isPathSafe(basePath, targetPath) {
|
|
21221
|
+
const resolvedBase = resolve(basePath);
|
|
21222
|
+
const resolvedTarget = resolve(targetPath);
|
|
21223
|
+
const relativePath = relative(resolvedBase, resolvedTarget);
|
|
21224
|
+
return !relativePath.startsWith("..") && !relativePath.startsWith("/") && resolvedTarget.startsWith(resolvedBase);
|
|
21225
|
+
}
|
|
21226
|
+
checkExtractionSize(fileSize) {
|
|
21227
|
+
this.totalExtractedSize += fileSize;
|
|
21228
|
+
if (this.totalExtractedSize > DownloadManager.MAX_EXTRACTION_SIZE) {
|
|
21229
|
+
throw new ExtractionError(`Archive exceeds maximum extraction size of ${this.formatBytes(DownloadManager.MAX_EXTRACTION_SIZE)}. Possible archive bomb detected.`);
|
|
21230
|
+
}
|
|
21231
|
+
}
|
|
21232
|
+
resetExtractionSize() {
|
|
21233
|
+
this.totalExtractedSize = 0;
|
|
21234
|
+
}
|
|
21222
21235
|
async downloadAsset(asset, destDir) {
|
|
21223
21236
|
try {
|
|
21224
21237
|
const destPath = join3(destDir, asset.name);
|
|
@@ -21328,6 +21341,7 @@ class DownloadManager {
|
|
|
21328
21341
|
async extractArchive(archivePath, destDir, archiveType) {
|
|
21329
21342
|
const spinner = createSpinner("Extracting files...").start();
|
|
21330
21343
|
try {
|
|
21344
|
+
this.resetExtractionSize();
|
|
21331
21345
|
const detectedType = archiveType || this.detectArchiveType(archivePath);
|
|
21332
21346
|
await mkdir3(destDir, { recursive: true });
|
|
21333
21347
|
if (detectedType === "tar.gz") {
|
|
@@ -21398,8 +21412,8 @@ class DownloadManager {
|
|
|
21398
21412
|
}
|
|
21399
21413
|
}
|
|
21400
21414
|
isWrapperDirectory(dirName) {
|
|
21401
|
-
const versionPattern = /^[\w-]+-v?\d+\.\d+\.\d
|
|
21402
|
-
const hashPattern = /^[\w-]+-[a-f0-9]{7,}$/;
|
|
21415
|
+
const versionPattern = /^[\w-]+-v?\d+\.\d+\.\d+(-[\w.]+)?$/;
|
|
21416
|
+
const hashPattern = /^[\w-]+-[a-f0-9]{7,40}$/;
|
|
21403
21417
|
return versionPattern.test(dirName) || hashPattern.test(dirName);
|
|
21404
21418
|
}
|
|
21405
21419
|
async extractZip(archivePath, destDir) {
|
|
@@ -21447,13 +21461,17 @@ class DownloadManager {
|
|
|
21447
21461
|
}
|
|
21448
21462
|
async moveDirectoryContents(sourceDir, destDir) {
|
|
21449
21463
|
const { readdir, stat, mkdir: mkdirPromise, copyFile } = await import("node:fs/promises");
|
|
21450
|
-
const { join: pathJoin, relative } = await import("node:path");
|
|
21464
|
+
const { join: pathJoin, relative: relative2 } = await import("node:path");
|
|
21451
21465
|
await mkdirPromise(destDir, { recursive: true });
|
|
21452
21466
|
const entries = await readdir(sourceDir);
|
|
21453
21467
|
for (const entry of entries) {
|
|
21454
21468
|
const sourcePath = pathJoin(sourceDir, entry);
|
|
21455
21469
|
const destPath = pathJoin(destDir, entry);
|
|
21456
|
-
const relativePath =
|
|
21470
|
+
const relativePath = relative2(sourceDir, sourcePath);
|
|
21471
|
+
if (!this.isPathSafe(destDir, destPath)) {
|
|
21472
|
+
logger.warning(`Skipping unsafe path: ${relativePath}`);
|
|
21473
|
+
throw new ExtractionError(`Path traversal attempt detected: ${relativePath}`);
|
|
21474
|
+
}
|
|
21457
21475
|
if (this.shouldExclude(relativePath)) {
|
|
21458
21476
|
logger.debug(`Excluding: ${relativePath}`);
|
|
21459
21477
|
continue;
|
|
@@ -21462,19 +21480,24 @@ class DownloadManager {
|
|
|
21462
21480
|
if (entryStat.isDirectory()) {
|
|
21463
21481
|
await this.copyDirectory(sourcePath, destPath);
|
|
21464
21482
|
} else {
|
|
21483
|
+
this.checkExtractionSize(entryStat.size);
|
|
21465
21484
|
await copyFile(sourcePath, destPath);
|
|
21466
21485
|
}
|
|
21467
21486
|
}
|
|
21468
21487
|
}
|
|
21469
21488
|
async copyDirectory(sourceDir, destDir) {
|
|
21470
21489
|
const { readdir, stat, mkdir: mkdirPromise, copyFile } = await import("node:fs/promises");
|
|
21471
|
-
const { join: pathJoin, relative } = await import("node:path");
|
|
21490
|
+
const { join: pathJoin, relative: relative2 } = await import("node:path");
|
|
21472
21491
|
await mkdirPromise(destDir, { recursive: true });
|
|
21473
21492
|
const entries = await readdir(sourceDir);
|
|
21474
21493
|
for (const entry of entries) {
|
|
21475
21494
|
const sourcePath = pathJoin(sourceDir, entry);
|
|
21476
21495
|
const destPath = pathJoin(destDir, entry);
|
|
21477
|
-
const relativePath =
|
|
21496
|
+
const relativePath = relative2(sourceDir, sourcePath);
|
|
21497
|
+
if (!this.isPathSafe(destDir, destPath)) {
|
|
21498
|
+
logger.warning(`Skipping unsafe path: ${relativePath}`);
|
|
21499
|
+
throw new ExtractionError(`Path traversal attempt detected: ${relativePath}`);
|
|
21500
|
+
}
|
|
21478
21501
|
if (this.shouldExclude(relativePath)) {
|
|
21479
21502
|
logger.debug(`Excluding: ${relativePath}`);
|
|
21480
21503
|
continue;
|
|
@@ -21483,6 +21506,7 @@ class DownloadManager {
|
|
|
21483
21506
|
if (entryStat.isDirectory()) {
|
|
21484
21507
|
await this.copyDirectory(sourcePath, destPath);
|
|
21485
21508
|
} else {
|
|
21509
|
+
this.checkExtractionSize(entryStat.size);
|
|
21486
21510
|
await copyFile(sourcePath, destPath);
|
|
21487
21511
|
}
|
|
21488
21512
|
}
|
|
@@ -21504,22 +21528,28 @@ class DownloadManager {
|
|
|
21504
21528
|
const entries = await readdir(extractDir);
|
|
21505
21529
|
logger.debug(`Extracted files: ${entries.join(", ")}`);
|
|
21506
21530
|
if (entries.length === 0) {
|
|
21507
|
-
|
|
21508
|
-
return false;
|
|
21531
|
+
throw new ExtractionError("Extraction resulted in no files");
|
|
21509
21532
|
}
|
|
21510
21533
|
const criticalPaths = [".claude", "CLAUDE.md"];
|
|
21534
|
+
const missingPaths = [];
|
|
21511
21535
|
for (const path8 of criticalPaths) {
|
|
21512
21536
|
try {
|
|
21513
21537
|
await access(pathJoin(extractDir, path8), constants2.F_OK);
|
|
21514
21538
|
logger.debug(`✓ Found: ${path8}`);
|
|
21515
21539
|
} catch {
|
|
21516
21540
|
logger.warning(`Expected path not found: ${path8}`);
|
|
21541
|
+
missingPaths.push(path8);
|
|
21517
21542
|
}
|
|
21518
21543
|
}
|
|
21519
|
-
|
|
21544
|
+
if (missingPaths.length > 0) {
|
|
21545
|
+
logger.warning(`Some expected paths are missing: ${missingPaths.join(", ")}. This may not be a ClaudeKit project.`);
|
|
21546
|
+
}
|
|
21547
|
+
logger.debug("Extraction validation passed");
|
|
21520
21548
|
} catch (error2) {
|
|
21521
|
-
|
|
21522
|
-
|
|
21549
|
+
if (error2 instanceof ExtractionError) {
|
|
21550
|
+
throw error2;
|
|
21551
|
+
}
|
|
21552
|
+
throw new ExtractionError(`Validation failed: ${error2 instanceof Error ? error2.message : "Unknown error"}`);
|
|
21523
21553
|
}
|
|
21524
21554
|
}
|
|
21525
21555
|
async createTempDir() {
|
|
@@ -21679,7 +21709,7 @@ class GitHubClient {
|
|
|
21679
21709
|
}
|
|
21680
21710
|
|
|
21681
21711
|
// src/lib/merge.ts
|
|
21682
|
-
import { join as join4, relative } from "node:path";
|
|
21712
|
+
import { join as join4, relative as relative2 } from "node:path";
|
|
21683
21713
|
var import_fs_extra = __toESM(require_lib(), 1);
|
|
21684
21714
|
var import_ignore2 = __toESM(require_ignore(), 1);
|
|
21685
21715
|
class FileMerger {
|
|
@@ -21705,7 +21735,7 @@ class FileMerger {
|
|
|
21705
21735
|
const conflicts = [];
|
|
21706
21736
|
const files = await this.getFiles(sourceDir);
|
|
21707
21737
|
for (const file of files) {
|
|
21708
|
-
const relativePath =
|
|
21738
|
+
const relativePath = relative2(sourceDir, file);
|
|
21709
21739
|
if (this.ig.ignores(relativePath)) {
|
|
21710
21740
|
continue;
|
|
21711
21741
|
}
|
|
@@ -21721,7 +21751,7 @@ class FileMerger {
|
|
|
21721
21751
|
let copiedCount = 0;
|
|
21722
21752
|
let skippedCount = 0;
|
|
21723
21753
|
for (const file of files) {
|
|
21724
|
-
const relativePath =
|
|
21754
|
+
const relativePath = relative2(sourceDir, file);
|
|
21725
21755
|
if (this.ig.ignores(relativePath)) {
|
|
21726
21756
|
logger.debug(`Skipping protected file: ${relativePath}`);
|
|
21727
21757
|
skippedCount++;
|
|
@@ -21859,27 +21889,42 @@ async function newCommand(options) {
|
|
|
21859
21889
|
prompts.intro("\uD83D\uDE80 ClaudeKit - Create New Project");
|
|
21860
21890
|
try {
|
|
21861
21891
|
const validOptions = NewCommandOptionsSchema.parse(options);
|
|
21892
|
+
const isNonInteractive = !process.stdin.isTTY || process.env.CI === "true" || process.env.NON_INTERACTIVE === "true";
|
|
21862
21893
|
const config = await ConfigManager.get();
|
|
21863
21894
|
let kit = validOptions.kit || config.defaults?.kit;
|
|
21864
21895
|
if (!kit) {
|
|
21896
|
+
if (isNonInteractive) {
|
|
21897
|
+
throw new Error("Kit must be specified via --kit flag in non-interactive mode");
|
|
21898
|
+
}
|
|
21865
21899
|
kit = await prompts.selectKit();
|
|
21866
21900
|
}
|
|
21867
21901
|
const kitConfig = AVAILABLE_KITS[kit];
|
|
21868
21902
|
logger.info(`Selected kit: ${kitConfig.name}`);
|
|
21869
21903
|
let targetDir = validOptions.dir || config.defaults?.dir || ".";
|
|
21870
21904
|
if (!validOptions.dir && !config.defaults?.dir) {
|
|
21871
|
-
|
|
21905
|
+
if (isNonInteractive) {
|
|
21906
|
+
targetDir = ".";
|
|
21907
|
+
} else {
|
|
21908
|
+
targetDir = await prompts.getDirectory(targetDir);
|
|
21909
|
+
}
|
|
21872
21910
|
}
|
|
21873
|
-
const resolvedDir =
|
|
21911
|
+
const resolvedDir = resolve2(targetDir);
|
|
21874
21912
|
logger.info(`Target directory: ${resolvedDir}`);
|
|
21875
21913
|
if (await import_fs_extra2.pathExists(resolvedDir)) {
|
|
21876
21914
|
const files = await import_fs_extra2.readdir(resolvedDir);
|
|
21877
21915
|
const isEmpty = files.length === 0;
|
|
21878
21916
|
if (!isEmpty) {
|
|
21879
|
-
|
|
21880
|
-
|
|
21881
|
-
|
|
21882
|
-
|
|
21917
|
+
if (isNonInteractive) {
|
|
21918
|
+
if (!validOptions.force) {
|
|
21919
|
+
throw new Error("Directory is not empty. Use --force flag to overwrite in non-interactive mode");
|
|
21920
|
+
}
|
|
21921
|
+
logger.info("Directory is not empty. Proceeding with --force flag");
|
|
21922
|
+
} else {
|
|
21923
|
+
const continueAnyway = await prompts.confirm("Directory is not empty. Files may be overwritten. Continue?");
|
|
21924
|
+
if (!continueAnyway) {
|
|
21925
|
+
logger.warning("Operation cancelled");
|
|
21926
|
+
return;
|
|
21927
|
+
}
|
|
21883
21928
|
}
|
|
21884
21929
|
}
|
|
21885
21930
|
}
|
|
@@ -21953,11 +21998,11 @@ bun run dev`, "Next steps");
|
|
|
21953
21998
|
|
|
21954
21999
|
// src/commands/update.ts
|
|
21955
22000
|
var import_fs_extra4 = __toESM(require_lib(), 1);
|
|
21956
|
-
import { resolve as
|
|
22001
|
+
import { resolve as resolve4 } from "node:path";
|
|
21957
22002
|
|
|
21958
22003
|
// src/utils/file-scanner.ts
|
|
21959
22004
|
var import_fs_extra3 = __toESM(require_lib(), 1);
|
|
21960
|
-
import { join as join5, relative as
|
|
22005
|
+
import { join as join5, relative as relative3, resolve as resolve3 } from "node:path";
|
|
21961
22006
|
class FileScanner {
|
|
21962
22007
|
static async getFiles(dirPath, relativeTo) {
|
|
21963
22008
|
const basePath = relativeTo || dirPath;
|
|
@@ -21982,7 +22027,7 @@ class FileScanner {
|
|
|
21982
22027
|
const subFiles = await FileScanner.getFiles(fullPath, basePath);
|
|
21983
22028
|
files.push(...subFiles);
|
|
21984
22029
|
} else if (stats.isFile()) {
|
|
21985
|
-
const relativePath =
|
|
22030
|
+
const relativePath = relative3(basePath, fullPath);
|
|
21986
22031
|
files.push(relativePath);
|
|
21987
22032
|
}
|
|
21988
22033
|
}
|
|
@@ -22010,8 +22055,8 @@ class FileScanner {
|
|
|
22010
22055
|
return customFiles;
|
|
22011
22056
|
}
|
|
22012
22057
|
static isSafePath(basePath, targetPath) {
|
|
22013
|
-
const resolvedBase =
|
|
22014
|
-
const resolvedTarget =
|
|
22058
|
+
const resolvedBase = resolve3(basePath);
|
|
22059
|
+
const resolvedTarget = resolve3(targetPath);
|
|
22015
22060
|
return resolvedTarget.startsWith(resolvedBase);
|
|
22016
22061
|
}
|
|
22017
22062
|
}
|
|
@@ -22033,7 +22078,7 @@ async function updateCommand(options) {
|
|
|
22033
22078
|
if (!validOptions.dir && !config.defaults?.dir) {
|
|
22034
22079
|
targetDir = await prompts.getDirectory(targetDir);
|
|
22035
22080
|
}
|
|
22036
|
-
const resolvedDir =
|
|
22081
|
+
const resolvedDir = resolve4(targetDir);
|
|
22037
22082
|
logger.info(`Target directory: ${resolvedDir}`);
|
|
22038
22083
|
if (!await import_fs_extra4.pathExists(resolvedDir)) {
|
|
22039
22084
|
logger.error(`Directory does not exist: ${resolvedDir}`);
|
|
@@ -22309,6 +22354,10 @@ class Logger2 {
|
|
|
22309
22354
|
}
|
|
22310
22355
|
}
|
|
22311
22356
|
var logger2 = new Logger2;
|
|
22357
|
+
// src/version.json
|
|
22358
|
+
var version_default = {
|
|
22359
|
+
version: "1.2.1"
|
|
22360
|
+
};
|
|
22312
22361
|
|
|
22313
22362
|
// src/index.ts
|
|
22314
22363
|
if (process.stdout.setEncoding) {
|
|
@@ -22317,12 +22366,11 @@ if (process.stdout.setEncoding) {
|
|
|
22317
22366
|
if (process.stderr.setEncoding) {
|
|
22318
22367
|
process.stderr.setEncoding("utf8");
|
|
22319
22368
|
}
|
|
22320
|
-
var
|
|
22321
|
-
var packageJson = JSON.parse(readFileSync(join6(__dirname2, "../package.json"), "utf-8"));
|
|
22369
|
+
var packageVersion = version_default.version;
|
|
22322
22370
|
var cli = cac("ck");
|
|
22323
22371
|
cli.option("--verbose, -v", "Enable verbose logging for debugging");
|
|
22324
22372
|
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) => {
|
|
22373
|
+
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").action(async (options) => {
|
|
22326
22374
|
await newCommand(options);
|
|
22327
22375
|
});
|
|
22328
22376
|
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) => {
|
|
@@ -22331,7 +22379,7 @@ cli.command("update", "Update existing ClaudeKit project").option("--dir <dir>",
|
|
|
22331
22379
|
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
22380
|
await versionCommand(options);
|
|
22333
22381
|
});
|
|
22334
|
-
cli.version(
|
|
22382
|
+
cli.version(packageVersion);
|
|
22335
22383
|
cli.help();
|
|
22336
22384
|
var parsed = cli.parse(process.argv, { run: false });
|
|
22337
22385
|
var envVerbose = process.env.CLAUDEKIT_VERBOSE === "1" || process.env.CLAUDEKIT_VERBOSE === "true";
|
|
@@ -22343,7 +22391,7 @@ if (parsed.options.logFile) {
|
|
|
22343
22391
|
logger2.setLogFile(parsed.options.logFile);
|
|
22344
22392
|
}
|
|
22345
22393
|
logger2.verbose("ClaudeKit CLI starting", {
|
|
22346
|
-
version:
|
|
22394
|
+
version: packageVersion,
|
|
22347
22395
|
command: parsed.args[0] || "none",
|
|
22348
22396
|
options: parsed.options,
|
|
22349
22397
|
cwd: process.cwd(),
|
package/package.json
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claudekit-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.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
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
|
|
3
|
-
import { readFileSync } from "node:fs";
|
|
4
|
-
import { join } from "node:path";
|
|
5
|
-
import { fileURLToPath } from "node:url";
|
|
6
3
|
import { cac } from "cac";
|
|
7
4
|
import { newCommand } from "./commands/new.js";
|
|
8
5
|
import { updateCommand } from "./commands/update.js";
|
|
9
6
|
import { versionCommand } from "./commands/version.js";
|
|
10
7
|
import { logger } from "./utils/logger.js";
|
|
8
|
+
import versionInfo from "./version.json" assert { type: "json" };
|
|
11
9
|
|
|
12
10
|
// Set proper output encoding to prevent unicode rendering issues
|
|
13
11
|
if (process.stdout.setEncoding) {
|
|
@@ -17,10 +15,7 @@ if (process.stderr.setEncoding) {
|
|
|
17
15
|
process.stderr.setEncoding("utf8");
|
|
18
16
|
}
|
|
19
17
|
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
// Read package.json for version
|
|
23
|
-
const packageJson = JSON.parse(readFileSync(join(__dirname, "../package.json"), "utf-8"));
|
|
18
|
+
const packageVersion = versionInfo.version;
|
|
24
19
|
|
|
25
20
|
const cli = cac("ck");
|
|
26
21
|
|
|
@@ -34,6 +29,7 @@ cli
|
|
|
34
29
|
.option("--dir <dir>", "Target directory (default: .)")
|
|
35
30
|
.option("--kit <kit>", "Kit to use (engineer, marketing)")
|
|
36
31
|
.option("--version <version>", "Specific version to download (default: latest)")
|
|
32
|
+
.option("--force", "Overwrite existing files without confirmation")
|
|
37
33
|
.action(async (options) => {
|
|
38
34
|
await newCommand(options);
|
|
39
35
|
});
|
|
@@ -59,7 +55,7 @@ cli
|
|
|
59
55
|
});
|
|
60
56
|
|
|
61
57
|
// Version
|
|
62
|
-
cli.version(
|
|
58
|
+
cli.version(packageVersion);
|
|
63
59
|
|
|
64
60
|
// Help
|
|
65
61
|
cli.help();
|
|
@@ -85,7 +81,7 @@ if (parsed.options.logFile) {
|
|
|
85
81
|
|
|
86
82
|
// Log startup info in verbose mode
|
|
87
83
|
logger.verbose("ClaudeKit CLI starting", {
|
|
88
|
-
version:
|
|
84
|
+
version: packageVersion,
|
|
89
85
|
command: parsed.args[0] || "none",
|
|
90
86
|
options: parsed.options,
|
|
91
87
|
cwd: process.cwd(),
|