claudekit-cli 1.3.0 → 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.
@@ -0,0 +1,57 @@
1
+ name: Claude Code Review
2
+
3
+ on:
4
+ pull_request:
5
+ types: [opened, synchronize]
6
+ # Optional: Only run on specific file changes
7
+ # paths:
8
+ # - "src/**/*.ts"
9
+ # - "src/**/*.tsx"
10
+ # - "src/**/*.js"
11
+ # - "src/**/*.jsx"
12
+
13
+ jobs:
14
+ claude-review:
15
+ # Optional: Filter by PR author
16
+ # if: |
17
+ # github.event.pull_request.user.login == 'external-contributor' ||
18
+ # github.event.pull_request.user.login == 'new-developer' ||
19
+ # github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR'
20
+
21
+ runs-on: ubuntu-latest
22
+ permissions:
23
+ contents: read
24
+ pull-requests: read
25
+ issues: read
26
+ id-token: write
27
+
28
+ steps:
29
+ - name: Checkout repository
30
+ uses: actions/checkout@v4
31
+ with:
32
+ fetch-depth: 1
33
+
34
+ - name: Run Claude Code Review
35
+ id: claude-review
36
+ uses: anthropics/claude-code-action@v1
37
+ with:
38
+ claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
39
+ prompt: |
40
+ REPO: ${{ github.repository }}
41
+ PR NUMBER: ${{ github.event.pull_request.number }}
42
+
43
+ Please review this pull request and provide feedback on:
44
+ - Code quality and best practices
45
+ - Potential bugs or issues
46
+ - Performance considerations
47
+ - Security concerns
48
+ - Test coverage
49
+
50
+ Use the repository's CLAUDE.md for guidance on style and conventions. Be constructive and helpful in your feedback.
51
+
52
+ Use `gh pr comment` with your Bash tool to leave your review as a comment on the PR.
53
+
54
+ # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
55
+ # or https://docs.claude.com/en/docs/claude-code/cli-reference for available options
56
+ claude_args: '--allowed-tools "Bash(gh issue view:*),Bash(gh search:*),Bash(gh issue list:*),Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*),Bash(gh pr list:*)"'
57
+
@@ -0,0 +1,50 @@
1
+ name: Claude Code
2
+
3
+ on:
4
+ issue_comment:
5
+ types: [created]
6
+ pull_request_review_comment:
7
+ types: [created]
8
+ issues:
9
+ types: [opened, assigned]
10
+ pull_request_review:
11
+ types: [submitted]
12
+
13
+ jobs:
14
+ claude:
15
+ if: |
16
+ (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
17
+ (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
18
+ (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
19
+ (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
20
+ runs-on: ubuntu-latest
21
+ permissions:
22
+ contents: read
23
+ pull-requests: read
24
+ issues: read
25
+ id-token: write
26
+ actions: read # Required for Claude to read CI results on PRs
27
+ steps:
28
+ - name: Checkout repository
29
+ uses: actions/checkout@v4
30
+ with:
31
+ fetch-depth: 1
32
+
33
+ - name: Run Claude Code
34
+ id: claude
35
+ uses: anthropics/claude-code-action@v1
36
+ with:
37
+ claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
38
+
39
+ # This is an optional setting that allows Claude to read CI results on PRs
40
+ additional_permissions: |
41
+ actions: read
42
+
43
+ # Optional: Give a custom prompt to Claude. If this is not specified, Claude will perform the instructions specified in the comment that tagged it.
44
+ # prompt: 'Update the pull request description to include a summary of changes.'
45
+
46
+ # Optional: Add claude_args to customize behavior and configuration
47
+ # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
48
+ # or https://docs.claude.com/en/docs/claude-code/cli-reference for available options
49
+ # claude_args: '--allowed-tools Bash(gh pr:*)'
50
+
package/CHANGELOG.md CHANGED
@@ -1,3 +1,10 @@
1
+ # [1.4.0](https://github.com/mrgoonie/claudekit-cli/compare/v1.3.0...v1.4.0) (2025-10-21)
2
+
3
+
4
+ ### Features
5
+
6
+ * add --exclude flag to new and update commands ([8a0d7a0](https://github.com/mrgoonie/claudekit-cli/commit/8a0d7a00de70823d4fecac26d4c7e82c4df2ab0f))
7
+
1
8
  # [1.3.0](https://github.com/mrgoonie/claudekit-cli/compare/v1.2.2...v1.3.0) (2025-10-21)
2
9
 
3
10
 
package/README.md CHANGED
@@ -79,6 +79,12 @@ ck new --dir my-project --kit engineer
79
79
 
80
80
  # Specific version
81
81
  ck new --kit engineer --version v1.0.0
82
+
83
+ # With exclude patterns
84
+ ck new --kit engineer --exclude "*.log" --exclude "temp/**"
85
+
86
+ # Multiple patterns
87
+ ck new --exclude "*.log" --exclude "*.tmp" --exclude "cache/**"
82
88
  ```
83
89
 
84
90
  ### Update Existing Project
@@ -94,6 +100,9 @@ ck update --kit engineer
94
100
 
95
101
  # Specific version
96
102
  ck update --kit engineer --version v1.0.0
103
+
104
+ # With exclude patterns
105
+ ck update --exclude "local-config/**" --exclude "*.local"
97
106
  ```
98
107
 
99
108
  ### List Available Versions
@@ -204,6 +213,95 @@ The following file patterns are protected and will not be overwritten during upd
204
213
  - `node_modules/**`, `.git/**`
205
214
  - `dist/**`, `build/**`
206
215
 
216
+ ## Excluding Files
217
+
218
+ Use the `--exclude` flag to skip specific files or directories during download and extraction. This is useful for:
219
+
220
+ - Excluding temporary or cache directories
221
+ - Skipping log files or debug output
222
+ - Omitting files you want to manage manually
223
+ - Avoiding unnecessary large files
224
+
225
+ ### Basic Usage
226
+
227
+ ```bash
228
+ # Exclude log files
229
+ ck new --exclude "*.log"
230
+
231
+ # Exclude multiple patterns
232
+ ck new --exclude "*.log" --exclude "temp/**" --exclude "cache/**"
233
+
234
+ # Common exclude patterns for updates
235
+ ck update --exclude "node_modules/**" --exclude "dist/**" --exclude ".env.*"
236
+ ```
237
+
238
+ ### Supported Glob Patterns
239
+
240
+ The `--exclude` flag accepts standard glob patterns:
241
+
242
+ - `*` - Match any characters except `/` (e.g., `*.log` matches all log files)
243
+ - `**` - Match any characters including `/` (e.g., `temp/**` matches all files in temp directory)
244
+ - `?` - Match single character (e.g., `file?.txt` matches `file1.txt`, `file2.txt`)
245
+ - `[abc]` - Match characters in brackets (e.g., `[Tt]emp` matches `Temp` or `temp`)
246
+ - `{a,b}` - Match alternatives (e.g., `*.{log,tmp}` matches `*.log` and `*.tmp`)
247
+
248
+ ### Common Exclude Patterns
249
+
250
+ ```bash
251
+ # Exclude all log files
252
+ --exclude "*.log" --exclude "**/*.log"
253
+
254
+ # Exclude temporary directories
255
+ --exclude "tmp/**" --exclude "temp/**" --exclude ".tmp/**"
256
+
257
+ # Exclude cache directories
258
+ --exclude "cache/**" --exclude ".cache/**" --exclude "**/.cache/**"
259
+
260
+ # Exclude build artifacts
261
+ --exclude "dist/**" --exclude "build/**" --exclude "out/**"
262
+
263
+ # Exclude local configuration
264
+ --exclude "*.local" --exclude "local/**" --exclude ".env.local"
265
+
266
+ # Exclude IDE/editor files
267
+ --exclude ".vscode/**" --exclude ".idea/**" --exclude "*.swp"
268
+ ```
269
+
270
+ ### Important Notes
271
+
272
+ **Additive Behavior:**
273
+ - User exclude patterns are ADDED to the default protected patterns
274
+ - They do not replace the built-in protections
275
+ - All patterns work together to determine which files to skip
276
+
277
+ **Security Restrictions:**
278
+ - Absolute paths (starting with `/`) are not allowed
279
+ - Path traversal patterns (containing `..`) are not allowed
280
+ - Patterns must be between 1-500 characters
281
+ - These restrictions prevent accidental or malicious file system access
282
+
283
+ **Pattern Matching:**
284
+ - Patterns are case-sensitive on Linux/macOS
285
+ - Patterns are case-insensitive on Windows
286
+ - Patterns are applied during both extraction and merge phases
287
+ - Excluded files are never written to disk, saving time and space
288
+
289
+ **Examples of Invalid Patterns:**
290
+
291
+ ```bash
292
+ # ❌ Absolute paths not allowed
293
+ ck new --exclude "/etc/passwd"
294
+
295
+ # ❌ Path traversal not allowed
296
+ ck new --exclude "../../secret"
297
+
298
+ # ❌ Empty patterns not allowed
299
+ ck new --exclude ""
300
+
301
+ # ✅ Correct way to exclude root-level files
302
+ ck new --exclude "secret.txt" --exclude "config.local.json"
303
+ ```
304
+
207
305
  ### Custom .claude Files
208
306
 
209
307
  When updating a project, the CLI automatically preserves your custom `.claude/` files that don't exist in the new release package. This allows you to maintain:
package/dist/index.js CHANGED
@@ -6400,6 +6400,66 @@ class CAC extends EventEmitter {
6400
6400
  }
6401
6401
  }
6402
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
+ };
6403
6463
 
6404
6464
  // src/commands/new.ts
6405
6465
  var import_fs_extra2 = __toESM(require_lib(), 1);
@@ -10922,16 +10982,19 @@ var coerce = {
10922
10982
  var NEVER = INVALID;
10923
10983
  // src/types.ts
10924
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");
10925
10986
  var NewCommandOptionsSchema = exports_external.object({
10926
10987
  dir: exports_external.string().default("."),
10927
10988
  kit: KitType.optional(),
10928
10989
  version: exports_external.string().optional(),
10929
- force: exports_external.boolean().default(false)
10990
+ force: exports_external.boolean().default(false),
10991
+ exclude: exports_external.array(ExcludePatternSchema).optional().default([])
10930
10992
  });
10931
10993
  var UpdateCommandOptionsSchema = exports_external.object({
10932
10994
  dir: exports_external.string().default("."),
10933
10995
  kit: KitType.optional(),
10934
- version: exports_external.string().optional()
10996
+ version: exports_external.string().optional(),
10997
+ exclude: exports_external.array(ExcludePatternSchema).optional().default([])
10935
10998
  });
10936
10999
  var VersionCommandOptionsSchema = exports_external.object({
10937
11000
  kit: KitType.optional(),
@@ -21213,9 +21276,21 @@ class DownloadManager {
21213
21276
  "*.log"
21214
21277
  ];
21215
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
+ }
21216
21292
  shouldExclude(filePath) {
21217
- const ig = import_ignore.default().add(DownloadManager.EXCLUDE_PATTERNS);
21218
- return ig.ignores(filePath);
21293
+ return this.ig.ignores(filePath);
21219
21294
  }
21220
21295
  isPathSafe(basePath, targetPath) {
21221
21296
  const resolvedBase = resolve(basePath);
@@ -21950,6 +22025,9 @@ async function newCommand(options) {
21950
22025
  logger.info(`Download source: ${downloadInfo.type}`);
21951
22026
  logger.debug(`Download URL: ${downloadInfo.url}`);
21952
22027
  const downloadManager = new DownloadManager;
22028
+ if (validOptions.exclude && validOptions.exclude.length > 0) {
22029
+ downloadManager.setExcludePatterns(validOptions.exclude);
22030
+ }
21953
22031
  const tempDir = await downloadManager.createTempDir();
21954
22032
  const { token } = await AuthManager.getToken();
21955
22033
  let archivePath;
@@ -21985,6 +22063,9 @@ async function newCommand(options) {
21985
22063
  await downloadManager.extractArchive(archivePath, extractDir);
21986
22064
  await downloadManager.validateExtraction(extractDir);
21987
22065
  const merger = new FileMerger;
22066
+ if (validOptions.exclude && validOptions.exclude.length > 0) {
22067
+ merger.addIgnorePatterns(validOptions.exclude);
22068
+ }
21988
22069
  await merger.merge(extractDir, resolvedDir, true);
21989
22070
  prompts.outro(`✨ Project created successfully at ${resolvedDir}`);
21990
22071
  prompts.note(`cd ${targetDir !== "." ? targetDir : "into the directory"}
@@ -22107,6 +22188,9 @@ async function updateCommand(options) {
22107
22188
  logger.info(`Download source: ${downloadInfo.type}`);
22108
22189
  logger.debug(`Download URL: ${downloadInfo.url}`);
22109
22190
  const downloadManager = new DownloadManager;
22191
+ if (validOptions.exclude && validOptions.exclude.length > 0) {
22192
+ downloadManager.setExcludePatterns(validOptions.exclude);
22193
+ }
22110
22194
  const tempDir = await downloadManager.createTempDir();
22111
22195
  const { token } = await AuthManager.getToken();
22112
22196
  let archivePath;
@@ -22148,6 +22232,9 @@ async function updateCommand(options) {
22148
22232
  merger.addIgnorePatterns(customClaudeFiles);
22149
22233
  logger.success(`Protected ${customClaudeFiles.length} custom .claude file(s)`);
22150
22234
  }
22235
+ if (validOptions.exclude && validOptions.exclude.length > 0) {
22236
+ merger.addIgnorePatterns(validOptions.exclude);
22237
+ }
22151
22238
  await merger.merge(extractDir, resolvedDir, false);
22152
22239
  prompts.outro(`✨ Project updated successfully at ${resolvedDir}`);
22153
22240
  const protectedNote = customClaudeFiles.length > 0 ? `Your project has been updated with the latest version.
@@ -22354,10 +22441,6 @@ class Logger2 {
22354
22441
  }
22355
22442
  }
22356
22443
  var logger2 = new Logger2;
22357
- // src/version.json
22358
- var version_default = {
22359
- version: "1.2.1"
22360
- };
22361
22444
 
22362
22445
  // src/index.ts
22363
22446
  if (process.stdout.setEncoding) {
@@ -22366,14 +22449,20 @@ if (process.stdout.setEncoding) {
22366
22449
  if (process.stderr.setEncoding) {
22367
22450
  process.stderr.setEncoding("utf8");
22368
22451
  }
22369
- var packageVersion = version_default.version;
22452
+ var packageVersion = package_default.version;
22370
22453
  var cli = cac("ck");
22371
22454
  cli.option("--verbose, -v", "Enable verbose logging for debugging");
22372
22455
  cli.option("--log-file <path>", "Write logs to file");
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) => {
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
+ }
22374
22460
  await newCommand(options);
22375
22461
  });
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) => {
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
+ }
22377
22466
  await updateCommand(options);
22378
22467
  });
22379
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) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claudekit-cli",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "description": "CLI tool for bootstrapping and updating ClaudeKit projects",
5
5
  "type": "module",
6
6
  "bin": {
@@ -110,6 +110,12 @@ export async function newCommand(options: NewCommandOptions): Promise<void> {
110
110
 
111
111
  // Download asset
112
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
+
113
119
  const tempDir = await downloadManager.createTempDir();
114
120
 
115
121
  // Get authentication token for API requests
@@ -157,6 +163,12 @@ export async function newCommand(options: NewCommandOptions): Promise<void> {
157
163
 
158
164
  // Copy files to target directory
159
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
+
160
172
  await merger.merge(extractDir, resolvedDir, true); // Skip confirmation for new projects
161
173
 
162
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}`);
package/src/index.ts CHANGED
@@ -1,11 +1,11 @@
1
1
  #!/usr/bin/env bun
2
2
 
3
3
  import { cac } from "cac";
4
+ import packageInfo from "../package.json" assert { type: "json" };
4
5
  import { newCommand } from "./commands/new.js";
5
6
  import { updateCommand } from "./commands/update.js";
6
7
  import { versionCommand } from "./commands/version.js";
7
8
  import { logger } from "./utils/logger.js";
8
- import versionInfo from "./version.json" assert { type: "json" };
9
9
 
10
10
  // Set proper output encoding to prevent unicode rendering issues
11
11
  if (process.stdout.setEncoding) {
@@ -15,7 +15,7 @@ if (process.stderr.setEncoding) {
15
15
  process.stderr.setEncoding("utf8");
16
16
  }
17
17
 
18
- const packageVersion = versionInfo.version;
18
+ const packageVersion = packageInfo.version;
19
19
 
20
20
  const cli = cac("ck");
21
21
 
@@ -30,7 +30,12 @@ cli
30
30
  .option("--kit <kit>", "Kit to use (engineer, marketing)")
31
31
  .option("--version <version>", "Specific version to download (default: latest)")
32
32
  .option("--force", "Overwrite existing files without confirmation")
33
+ .option("--exclude <pattern>", "Exclude files matching glob pattern (can be used multiple times)")
33
34
  .action(async (options) => {
35
+ // Normalize exclude to always be an array (CAC may pass string for single value)
36
+ if (options.exclude && !Array.isArray(options.exclude)) {
37
+ options.exclude = [options.exclude];
38
+ }
34
39
  await newCommand(options);
35
40
  });
36
41
 
@@ -40,7 +45,12 @@ cli
40
45
  .option("--dir <dir>", "Target directory (default: .)")
41
46
  .option("--kit <kit>", "Kit to use (engineer, marketing)")
42
47
  .option("--version <version>", "Specific version to download (default: latest)")
48
+ .option("--exclude <pattern>", "Exclude files matching glob pattern (can be used multiple times)")
43
49
  .action(async (options) => {
50
+ // Normalize exclude to always be an array (CAC may pass string for single value)
51
+ if (options.exclude && !Array.isArray(options.exclude)) {
52
+ options.exclude = [options.exclude];
53
+ }
44
54
  await updateCommand(options);
45
55
  });
46
56
 
@@ -41,12 +41,45 @@ export class DownloadManager {
41
41
  */
42
42
  private totalExtractedSize = 0;
43
43
 
44
+ /**
45
+ * Instance-level ignore object with combined default and user patterns
46
+ */
47
+ private ig: ReturnType<typeof ignore>;
48
+
49
+ /**
50
+ * Store user-defined exclude patterns
51
+ */
52
+ private userExcludePatterns: string[] = [];
53
+
54
+ /**
55
+ * Initialize DownloadManager with default exclude patterns
56
+ */
57
+ constructor() {
58
+ // Initialize ignore with default patterns
59
+ this.ig = ignore().add(DownloadManager.EXCLUDE_PATTERNS);
60
+ }
61
+
62
+ /**
63
+ * Set additional user-defined exclude patterns
64
+ * These are added to (not replace) the default EXCLUDE_PATTERNS
65
+ */
66
+ setExcludePatterns(patterns: string[]): void {
67
+ this.userExcludePatterns = patterns;
68
+ // Reinitialize ignore with both default and user patterns
69
+ this.ig = ignore().add([...DownloadManager.EXCLUDE_PATTERNS, ...this.userExcludePatterns]);
70
+
71
+ if (patterns.length > 0) {
72
+ logger.info(`Added ${patterns.length} custom exclude pattern(s)`);
73
+ patterns.forEach((p) => logger.debug(` - ${p}`));
74
+ }
75
+ }
76
+
44
77
  /**
45
78
  * Check if file path should be excluded
79
+ * Uses instance-level ignore with both default and user patterns
46
80
  */
47
81
  private shouldExclude(filePath: string): boolean {
48
- const ig = ignore().add(DownloadManager.EXCLUDE_PATTERNS);
49
- return ig.ignores(filePath);
82
+ return this.ig.ignores(filePath);
50
83
  }
51
84
 
52
85
  /**
package/src/types.ts CHANGED
@@ -4,12 +4,22 @@ import { z } from "zod";
4
4
  export const KitType = z.enum(["engineer", "marketing"]);
5
5
  export type KitType = z.infer<typeof KitType>;
6
6
 
7
+ // Exclude pattern validation schema
8
+ export const ExcludePatternSchema = z
9
+ .string()
10
+ .trim()
11
+ .min(1, "Exclude pattern cannot be empty")
12
+ .max(500, "Exclude pattern too long")
13
+ .refine((val) => !val.startsWith("/"), "Absolute paths not allowed in exclude patterns")
14
+ .refine((val) => !val.includes(".."), "Path traversal not allowed in exclude patterns");
15
+
7
16
  // Command options schemas
8
17
  export const NewCommandOptionsSchema = z.object({
9
18
  dir: z.string().default("."),
10
19
  kit: KitType.optional(),
11
20
  version: z.string().optional(),
12
21
  force: z.boolean().default(false),
22
+ exclude: z.array(ExcludePatternSchema).optional().default([]),
13
23
  });
14
24
  export type NewCommandOptions = z.infer<typeof NewCommandOptionsSchema>;
15
25
 
@@ -17,6 +27,7 @@ export const UpdateCommandOptionsSchema = z.object({
17
27
  dir: z.string().default("."),
18
28
  kit: KitType.optional(),
19
29
  version: z.string().optional(),
30
+ exclude: z.array(ExcludePatternSchema).optional().default([]),
20
31
  });
21
32
  export type UpdateCommandOptions = z.infer<typeof UpdateCommandOptionsSchema>;
22
33
 
@@ -5,6 +5,7 @@ import {
5
5
  ClaudeKitError,
6
6
  ConfigSchema,
7
7
  DownloadError,
8
+ ExcludePatternSchema,
8
9
  ExtractionError,
9
10
  GitHubError,
10
11
  GitHubReleaseAssetSchema,
@@ -29,6 +30,44 @@ describe("Types and Schemas", () => {
29
30
  });
30
31
  });
31
32
 
33
+ describe("ExcludePatternSchema", () => {
34
+ test("should accept valid glob patterns", () => {
35
+ const validPatterns = ["*.log", "**/*.tmp", "temp/**", "logs/*.txt", "cache/**/*"];
36
+ validPatterns.forEach((pattern) => {
37
+ expect(() => ExcludePatternSchema.parse(pattern)).not.toThrow();
38
+ });
39
+ });
40
+
41
+ test("should reject absolute paths", () => {
42
+ expect(() => ExcludePatternSchema.parse("/etc/passwd")).toThrow("Absolute paths not allowed");
43
+ expect(() => ExcludePatternSchema.parse("/var/log/**")).toThrow("Absolute paths not allowed");
44
+ });
45
+
46
+ test("should reject path traversal", () => {
47
+ expect(() => ExcludePatternSchema.parse("../../etc/passwd")).toThrow(
48
+ "Path traversal not allowed",
49
+ );
50
+ expect(() => ExcludePatternSchema.parse("../../../secret")).toThrow(
51
+ "Path traversal not allowed",
52
+ );
53
+ });
54
+
55
+ test("should reject empty patterns", () => {
56
+ expect(() => ExcludePatternSchema.parse("")).toThrow("Exclude pattern cannot be empty");
57
+ expect(() => ExcludePatternSchema.parse(" ")).toThrow("Exclude pattern cannot be empty");
58
+ });
59
+
60
+ test("should reject overly long patterns", () => {
61
+ const longPattern = "a".repeat(501);
62
+ expect(() => ExcludePatternSchema.parse(longPattern)).toThrow("Exclude pattern too long");
63
+ });
64
+
65
+ test("should trim whitespace", () => {
66
+ const result = ExcludePatternSchema.parse(" *.log ");
67
+ expect(result).toBe("*.log");
68
+ });
69
+ });
70
+
32
71
  describe("NewCommandOptionsSchema", () => {
33
72
  test("should validate correct options", () => {
34
73
  const result = NewCommandOptionsSchema.parse({
@@ -46,6 +85,7 @@ describe("Types and Schemas", () => {
46
85
  expect(result.dir).toBe(".");
47
86
  expect(result.kit).toBeUndefined();
48
87
  expect(result.version).toBeUndefined();
88
+ expect(result.exclude).toEqual([]);
49
89
  });
50
90
 
51
91
  test("should accept optional fields", () => {
@@ -53,6 +93,23 @@ describe("Types and Schemas", () => {
53
93
  expect(result.dir).toBe("./custom");
54
94
  expect(result.kit).toBeUndefined();
55
95
  });
96
+
97
+ test("should validate exclude patterns", () => {
98
+ const result = NewCommandOptionsSchema.parse({
99
+ dir: "./test",
100
+ exclude: ["*.log", "temp/**"],
101
+ });
102
+ expect(result.exclude).toEqual(["*.log", "temp/**"]);
103
+ });
104
+
105
+ test("should reject invalid exclude patterns", () => {
106
+ expect(() =>
107
+ NewCommandOptionsSchema.parse({
108
+ dir: "./test",
109
+ exclude: ["/etc/passwd"],
110
+ }),
111
+ ).toThrow();
112
+ });
56
113
  });
57
114
 
58
115
  describe("UpdateCommandOptionsSchema", () => {
@@ -70,6 +127,24 @@ describe("Types and Schemas", () => {
70
127
  test("should use default values", () => {
71
128
  const result = UpdateCommandOptionsSchema.parse({});
72
129
  expect(result.dir).toBe(".");
130
+ expect(result.exclude).toEqual([]);
131
+ });
132
+
133
+ test("should validate exclude patterns", () => {
134
+ const result = UpdateCommandOptionsSchema.parse({
135
+ dir: "./test",
136
+ exclude: ["*.log", "**/*.tmp"],
137
+ });
138
+ expect(result.exclude).toEqual(["*.log", "**/*.tmp"]);
139
+ });
140
+
141
+ test("should reject invalid exclude patterns", () => {
142
+ expect(() =>
143
+ UpdateCommandOptionsSchema.parse({
144
+ dir: "./test",
145
+ exclude: ["../../../etc"],
146
+ }),
147
+ ).toThrow();
73
148
  });
74
149
  });
75
150