claude-code-starter 0.14.1 → 0.16.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.
Files changed (2) hide show
  1. package/dist/cli.js +857 -110
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -2,19 +2,19 @@
2
2
 
3
3
  // src/cli.ts
4
4
  import { execSync, spawn } from "child_process";
5
- import fs3 from "fs";
6
- import path3 from "path";
5
+ import fs5 from "fs";
6
+ import path5 from "path";
7
7
  import { fileURLToPath } from "url";
8
8
  import ora from "ora";
9
- import pc from "picocolors";
10
- import prompts from "prompts";
9
+ import pc2 from "picocolors";
10
+ import prompts2 from "prompts";
11
11
 
12
12
  // src/analyzer.ts
13
13
  import fs from "fs";
14
14
  import path from "path";
15
15
  function analyzeRepository(rootDir) {
16
16
  const techStack = detectTechStack(rootDir);
17
- const fileCount = countSourceFiles(rootDir, techStack.languages);
17
+ const fileCount = countSourceFiles(rootDir);
18
18
  const packageJson = readPackageJson(rootDir);
19
19
  return {
20
20
  isExisting: fileCount > 0,
@@ -37,7 +37,7 @@ function detectTechStack(rootDir) {
37
37
  const linter = detectLinter(packageJson, files);
38
38
  const formatter = detectFormatter(packageJson, files);
39
39
  const bundler = detectBundler(packageJson, files);
40
- const isMonorepo = detectMonorepo(rootDir, files, packageJson);
40
+ const isMonorepo = detectMonorepo(files, packageJson);
41
41
  const hasDocker = files.includes("Dockerfile") || files.includes("docker-compose.yml") || files.includes("docker-compose.yaml");
42
42
  const { hasCICD, cicdPlatform } = detectCICD(rootDir, files);
43
43
  const { hasClaudeConfig, existingClaudeFiles } = detectExistingClaudeConfig(rootDir);
@@ -59,6 +59,12 @@ function detectTechStack(rootDir) {
59
59
  existingClaudeFiles
60
60
  };
61
61
  }
62
+ function getAllDeps(packageJson) {
63
+ return {
64
+ ...packageJson.dependencies || {},
65
+ ...packageJson.devDependencies || {}
66
+ };
67
+ }
62
68
  function readPackageJson(rootDir) {
63
69
  const packageJsonPath = path.join(rootDir, "package.json");
64
70
  if (!fs.existsSync(packageJsonPath)) return null;
@@ -271,10 +277,7 @@ function detectPackageManager(files) {
271
277
  }
272
278
  function detectTestingFramework(packageJson, files) {
273
279
  if (packageJson) {
274
- const allDeps = {
275
- ...packageJson.dependencies || {},
276
- ...packageJson.devDependencies || {}
277
- };
280
+ const allDeps = getAllDeps(packageJson);
278
281
  if (allDeps.vitest) return "vitest";
279
282
  if (allDeps.jest) return "jest";
280
283
  if (allDeps.mocha) return "mocha";
@@ -286,7 +289,8 @@ function detectTestingFramework(packageJson, files) {
286
289
  if (files.includes("pytest.ini") || files.includes("conftest.py")) return "pytest";
287
290
  if (files.includes("go.mod")) return "go-test";
288
291
  if (files.includes("Cargo.toml")) return "rust-test";
289
- if (files.includes("Gemfile")) return "rspec";
292
+ if (files.includes(".rspec")) return "rspec";
293
+ if (files.includes("Gemfile") && files.includes("spec")) return "rspec";
290
294
  return null;
291
295
  }
292
296
  function detectLinter(packageJson, files) {
@@ -297,10 +301,7 @@ function detectLinter(packageJson, files) {
297
301
  }
298
302
  if (files.includes("biome.json") || files.includes("biome.jsonc")) return "biome";
299
303
  if (packageJson) {
300
- const allDeps = {
301
- ...packageJson.dependencies || {},
302
- ...packageJson.devDependencies || {}
303
- };
304
+ const allDeps = getAllDeps(packageJson);
304
305
  if (allDeps.eslint) return "eslint";
305
306
  if (allDeps["@biomejs/biome"]) return "biome";
306
307
  }
@@ -316,16 +317,12 @@ function detectFormatter(packageJson, files) {
316
317
  }
317
318
  if (files.includes("biome.json") || files.includes("biome.jsonc")) return "biome";
318
319
  if (packageJson) {
319
- const allDeps = {
320
- ...packageJson.dependencies || {},
321
- ...packageJson.devDependencies || {}
322
- };
320
+ const allDeps = getAllDeps(packageJson);
323
321
  if (allDeps.prettier) return "prettier";
324
322
  if (allDeps["@biomejs/biome"]) return "biome";
325
323
  }
326
- if (files.includes("pyproject.toml")) {
327
- return "black";
328
- }
324
+ if (files.includes("ruff.toml") || files.includes(".ruff.toml")) return "ruff";
325
+ if (files.includes("pyproject.toml")) return "black";
329
326
  return null;
330
327
  }
331
328
  function detectBundler(packageJson, files) {
@@ -335,10 +332,7 @@ function detectBundler(packageJson, files) {
335
332
  if (files.some((f) => f.startsWith("rollup.config"))) return "rollup";
336
333
  if (files.some((f) => f.startsWith("esbuild"))) return "esbuild";
337
334
  if (packageJson) {
338
- const allDeps = {
339
- ...packageJson.dependencies || {},
340
- ...packageJson.devDependencies || {}
341
- };
335
+ const allDeps = getAllDeps(packageJson);
342
336
  if (allDeps.vite) return "vite";
343
337
  if (allDeps.webpack) return "webpack";
344
338
  if (allDeps.tsup) return "tsup";
@@ -350,16 +344,12 @@ function detectBundler(packageJson, files) {
350
344
  }
351
345
  return null;
352
346
  }
353
- function detectMonorepo(rootDir, files, packageJson) {
347
+ function detectMonorepo(files, packageJson) {
354
348
  if (files.includes("pnpm-workspace.yaml")) return true;
355
349
  if (files.includes("lerna.json")) return true;
356
350
  if (files.includes("nx.json")) return true;
357
351
  if (files.includes("turbo.json")) return true;
358
352
  if (packageJson?.workspaces) return true;
359
- const packagesDir = path.join(rootDir, "packages");
360
- const appsDir = path.join(rootDir, "apps");
361
- if (fs.existsSync(packagesDir) && fs.statSync(packagesDir).isDirectory()) return true;
362
- if (fs.existsSync(appsDir) && fs.statSync(appsDir).isDirectory()) return true;
363
353
  return false;
364
354
  }
365
355
  function detectCICD(rootDir, files) {
@@ -435,7 +425,7 @@ function listSourceFilesShallow(rootDir, extensions) {
435
425
  scan(rootDir, 0);
436
426
  return files;
437
427
  }
438
- function countSourceFiles(rootDir, _languages) {
428
+ function countSourceFiles(rootDir) {
439
429
  const extensions = [
440
430
  // JavaScript/TypeScript
441
431
  ".js",
@@ -512,13 +502,518 @@ function countSourceFiles(rootDir, _languages) {
512
502
  return count;
513
503
  }
514
504
 
515
- // src/generator.ts
505
+ // src/extras.ts
506
+ import pc from "picocolors";
507
+ import prompts from "prompts";
508
+
509
+ // src/hooks.ts
516
510
  import fs2 from "fs";
517
511
  import path2 from "path";
512
+ var HOOK_SCRIPT = String.raw`#!/usr/bin/env node
513
+ /**
514
+ * Block Dangerous Commands - PreToolUse Hook for Bash
515
+ * Blocks dangerous patterns before execution.
516
+ *
517
+ * SAFETY_LEVEL: 'critical' | 'high' | 'strict'
518
+ * critical - Only catastrophic: rm -rf ~, dd to disk, fork bombs
519
+ * high - + risky: force push main, secrets exposure, git reset --hard
520
+ * strict - + cautionary: any force push, sudo rm, docker prune
521
+ */
522
+
523
+ const fs = require('fs');
524
+ const path = require('path');
525
+
526
+ const SAFETY_LEVEL = 'high';
527
+
528
+ const PATTERNS = [
529
+ // CRITICAL — Catastrophic, unrecoverable
530
+
531
+ // Filesystem destruction
532
+ { level: 'critical', id: 'rm-home', regex: /\brm\s+(-.+\s+)*["']?~\/?["']?(\s|$|[;&|])/, reason: 'rm targeting home directory' },
533
+ { level: 'critical', id: 'rm-home-var', regex: /\brm\s+(-.+\s+)*["']?\$HOME["']?(\s|$|[;&|])/, reason: 'rm targeting $HOME' },
534
+ { level: 'critical', id: 'rm-home-trailing', regex: /\brm\s+.+\s+["']?(~\/?|\$HOME)["']?(\s*$|[;&|])/, reason: 'rm with trailing ~/ or $HOME' },
535
+ { level: 'critical', id: 'rm-root', regex: /\brm\s+(-.+\s+)*\/(\*|\s|$|[;&|])/, reason: 'rm targeting root filesystem' },
536
+ { level: 'critical', id: 'rm-system', regex: /\brm\s+(-.+\s+)*\/(etc|usr|var|bin|sbin|lib|boot|dev|proc|sys)(\/|\s|$)/, reason: 'rm targeting system directory' },
537
+ { level: 'critical', id: 'rm-cwd', regex: /\brm\s+(-.+\s+)*(\.\/?|\*|\.\/\*)(\s|$|[;&|])/, reason: 'rm deleting current directory contents' },
538
+
539
+ // Disk operations
540
+ { level: 'critical', id: 'dd-disk', regex: /\bdd\b.+of=\/dev\/(sd[a-z]|nvme|hd[a-z]|vd[a-z]|xvd[a-z])/, reason: 'dd writing to disk device' },
541
+ { level: 'critical', id: 'mkfs', regex: /\bmkfs(\.\w+)?\s+\/dev\/(sd[a-z]|nvme|hd[a-z]|vd[a-z])/, reason: 'mkfs formatting disk' },
542
+ { level: 'critical', id: 'fdisk', regex: /\b(fdisk|wipefs|parted)\s+\/dev\//, reason: 'disk partitioning/wiping operation' },
543
+
544
+ // Shell exploits
545
+ { level: 'critical', id: 'fork-bomb', regex: /:\(\)\s*\{.*:\s*\|\s*:.*&/, reason: 'fork bomb detected' },
546
+
547
+ // Git — history destruction
548
+ { level: 'critical', id: 'git-filter', regex: /\bgit\s+(filter-branch|filter-repo)\b/, reason: 'git history rewriting blocked' },
549
+ { level: 'critical', id: 'git-reflog-exp', regex: /\bgit\s+(reflog\s+expire|gc\s+--prune|prune)\b/, reason: 'removes git recovery safety net' },
550
+
551
+ // HIGH — Significant risk, data loss, security exposure
552
+
553
+ // Remote code execution
554
+ { level: 'high', id: 'curl-pipe-sh', regex: /\b(curl|wget)\b.+\|\s*(ba)?sh\b/, reason: 'piping URL to shell (RCE risk)' },
555
+
556
+ // Git — destructive operations
557
+ { level: 'high', id: 'git-force-main', regex: /\bgit\s+push\b(?!.+--force-with-lease).+(--force|-f)\b.+\b(main|master)\b/, reason: 'force push to main/master' },
558
+ { level: 'high', id: 'git-reset-hard', regex: /\bgit\s+reset\s+--hard/, reason: 'git reset --hard loses uncommitted work' },
559
+ { level: 'high', id: 'git-clean-f', regex: /\bgit\s+clean\s+(-\w*f|-f)/, reason: 'git clean -f deletes untracked files' },
560
+ { level: 'high', id: 'git-no-verify', regex: /\bgit\b.+--no-verify/, reason: '--no-verify skips safety hooks' },
561
+ { level: 'high', id: 'git-stash-destruct', regex: /\bgit\s+stash\s+(drop|clear|pop)\b/, reason: 'destructive git stash operation' },
562
+ { level: 'high', id: 'git-branch-D', regex: /\bgit\s+branch\s+(-D|--delete\s+--force)\b/, reason: 'git branch -D force-deletes branch' },
563
+ { level: 'high', id: 'git-checkout-force', regex: /\bgit\s+checkout\s+(-f|--\s+\.)/, reason: 'git checkout -f/-- . discards changes' },
564
+ { level: 'high', id: 'git-restore-destruct', regex: /\bgit\s+restore\s+(--staged\s+--worktree|\.)/, reason: 'git restore discards changes' },
565
+ { level: 'high', id: 'git-update-ref', regex: /\bgit\s+(update-ref|symbolic-ref|replace)\b/, reason: 'git ref manipulation blocked' },
566
+ { level: 'high', id: 'git-config-global', regex: /\bgit\s+config\s+--(global|system)\b/, reason: 'git global/system config blocked' },
567
+ { level: 'high', id: 'git-tag-delete', regex: /\bgit\s+tag\s+(-d|--delete)\b/, reason: 'git tag deletion blocked' },
568
+
569
+ // Git — write operations (user handles manually)
570
+ { level: 'high', id: 'git-push', regex: /\bgit\s+push\b/, reason: 'git push blocked — user handles manually' },
571
+ { level: 'high', id: 'git-pull', regex: /\bgit\s+pull\b/, reason: 'git pull blocked — user handles manually' },
572
+ { level: 'high', id: 'git-fetch', regex: /\bgit\s+fetch\b/, reason: 'git fetch blocked — user handles manually' },
573
+ { level: 'high', id: 'git-clone', regex: /\bgit\s+clone\b/, reason: 'git clone blocked — user handles manually' },
574
+ { level: 'high', id: 'git-add', regex: /\bgit\s+(add|stage)\b/, reason: 'git add/stage blocked — user handles manually' },
575
+ { level: 'high', id: 'git-commit', regex: /\bgit\s+commit\b/, reason: 'git commit blocked — user handles manually' },
576
+ { level: 'high', id: 'git-merge', regex: /\bgit\s+merge\b/, reason: 'git merge blocked — user handles manually' },
577
+ { level: 'high', id: 'git-rebase', regex: /\bgit\s+rebase\b/, reason: 'git rebase blocked — user handles manually' },
578
+ { level: 'high', id: 'git-reset', regex: /\bgit\s+reset\b/, reason: 'git reset blocked — user handles manually' },
579
+ { level: 'high', id: 'git-remote-mod', regex: /\bgit\s+remote\s+(add|set-url|remove)\b/, reason: 'git remote modification blocked' },
580
+ { level: 'high', id: 'git-submodule', regex: /\bgit\s+submodule\s+(add|update)\b/, reason: 'git submodule operation blocked' },
581
+
582
+ // Credentials & secrets
583
+ { level: 'high', id: 'chmod-777', regex: /\bchmod\b.+\b777\b/, reason: 'chmod 777 is a security risk' },
584
+ { level: 'high', id: 'cat-env', regex: /\b(cat|less|head|tail|more)\s+\.env\b/, reason: 'reading .env file exposes secrets' },
585
+ { level: 'high', id: 'cat-secrets', regex: /\b(cat|less|head|tail|more)\b.+(credentials|secrets?|\.pem|\.key|id_rsa|id_ed25519)/i, reason: 'reading secrets file' },
586
+ { level: 'high', id: 'env-dump', regex: /\b(printenv|^env)\s*([;&|]|$)/, reason: 'env dump may expose secrets' },
587
+ { level: 'high', id: 'echo-secret', regex: /\becho\b.+\$\w*(SECRET|KEY|TOKEN|PASSWORD|API_|PRIVATE)/i, reason: 'echoing secret variable' },
588
+ { level: 'high', id: 'rm-ssh', regex: /\brm\b.+\.ssh\/(id_|authorized_keys|known_hosts)/, reason: 'deleting SSH keys' },
589
+ { level: 'high', id: 'security-keychain', regex: /\bsecurity\s+find-generic-password\b/, reason: 'keychain access blocked' },
590
+ { level: 'high', id: 'gpg-export-secret', regex: /\bgpg\s+--export-secret-keys\b/, reason: 'GPG secret key export blocked' },
591
+ { level: 'high', id: 'history-cmd', regex: /\bhistory\b/, reason: 'history may expose secrets' },
592
+
593
+ // Destructive system commands
594
+ { level: 'high', id: 'elevated-priv', regex: /\b(sudo|doas|pkexec)\b/, reason: 'elevated privilege command blocked' },
595
+ { level: 'high', id: 'su-cmd', regex: /\bsu\b/, reason: 'su (switch user) blocked' },
596
+ { level: 'high', id: 'chmod-R', regex: /\bchmod\s+(-\w*R|-R)/, reason: 'recursive chmod blocked' },
597
+ { level: 'high', id: 'chown-R', regex: /\bchown\s+(-\w*R|-R)/, reason: 'recursive chown blocked' },
598
+ { level: 'high', id: 'kill-all', regex: /\bkill\s+-9\s+-1\b/, reason: 'kill all processes blocked' },
599
+ { level: 'high', id: 'killall', regex: /\b(killall|pkill\s+-9)\b/, reason: 'mass process killing blocked' },
600
+ { level: 'high', id: 'truncate-zero', regex: /\btruncate\s+-s\s*0\b/, reason: 'truncating file to zero blocked' },
601
+ { level: 'high', id: 'empty-file', regex: /\bcat\s+\/dev\/null\s*>/, reason: 'emptying file via /dev/null blocked' },
602
+ { level: 'high', id: 'crontab-r', regex: /\bcrontab\s+-r/, reason: 'removes all cron jobs' },
603
+
604
+ // Docker
605
+ { level: 'high', id: 'docker-vol-rm', regex: /\bdocker\s+volume\s+(rm|prune)/, reason: 'docker volume deletion loses data' },
606
+ { level: 'high', id: 'docker-push', regex: /\bdocker\s+push\b/, reason: 'docker push blocked' },
607
+ { level: 'high', id: 'docker-rm-all', regex: /\bdocker\s+rm\s+-f\b.+\$\(docker\s+ps/, reason: 'docker rm all containers blocked' },
608
+ { level: 'high', id: 'docker-sys-prune-a', regex: /\bdocker\s+system\s+prune\s+-a/, reason: 'docker system prune -a blocked' },
609
+ { level: 'high', id: 'docker-compose-destr', regex: /\bdocker[\s-]compose\s+down\s+(-v|--rmi)/, reason: 'docker-compose destructive down blocked' },
610
+
611
+ // Publishing & deployment
612
+ { level: 'high', id: 'npm-publish', regex: /\bnpm\s+(publish|unpublish|deprecate)\b/, reason: 'npm publishing blocked' },
613
+ { level: 'high', id: 'npm-audit-force', regex: /\bnpm\s+audit\s+fix\s+--force\b/, reason: 'npm audit fix --force can break deps' },
614
+ { level: 'high', id: 'cargo-publish', regex: /\bcargo\s+publish\b/, reason: 'cargo publish blocked' },
615
+ { level: 'high', id: 'pip-twine-upload', regex: /\b(pip|twine)\s+upload\b/, reason: 'Python package upload blocked' },
616
+ { level: 'high', id: 'gem-push', regex: /\bgem\s+push\b/, reason: 'gem push blocked' },
617
+ { level: 'high', id: 'pod-push', regex: /\bpod\s+trunk\s+push\b/, reason: 'pod trunk push blocked' },
618
+ { level: 'high', id: 'vercel-prod', regex: /\bvercel\b.+--prod/, reason: 'vercel production deploy blocked' },
619
+ { level: 'high', id: 'netlify-prod', regex: /\bnetlify\s+deploy\b.+--prod/, reason: 'netlify production deploy blocked' },
620
+ { level: 'high', id: 'fly-deploy', regex: /\bfly\s+deploy\b/, reason: 'fly deploy blocked' },
621
+ { level: 'high', id: 'firebase-deploy', regex: /\bfirebase\s+deploy\b/, reason: 'firebase deploy blocked' },
622
+ { level: 'high', id: 'terraform', regex: /\bterraform\s+(apply|destroy)\b/, reason: 'terraform apply/destroy blocked' },
623
+ { level: 'high', id: 'pulumi-cdktf', regex: /\b(pulumi|cdktf)\s+destroy\b/, reason: 'infrastructure destroy blocked' },
624
+ { level: 'high', id: 'kubectl-mutate', regex: /\bkubectl\s+(apply|delete|drain)\b/, reason: 'kubectl mutating operation blocked' },
625
+ { level: 'high', id: 'kubectl-scale-zero', regex: /\bkubectl\s+scale\b.+--replicas=0/, reason: 'kubectl scale to zero blocked' },
626
+ { level: 'high', id: 'helm-ops', regex: /\bhelm\s+(install|uninstall|upgrade)\b/, reason: 'helm operation blocked' },
627
+ { level: 'high', id: 'heroku', regex: /\bheroku\b/, reason: 'heroku command blocked' },
628
+ { level: 'high', id: 'eb-terminate', regex: /\beb\s+terminate\b/, reason: 'eb terminate blocked' },
629
+ { level: 'high', id: 'serverless-remove', regex: /\bserverless\s+remove\b/, reason: 'serverless remove blocked' },
630
+ { level: 'high', id: 'cap-prod-deploy', regex: /\bcap\s+production\s+deploy\b/, reason: 'production deploy blocked' },
631
+ { level: 'high', id: 'cloud-delete', regex: /\b(aws\s+cloudformation\s+delete-stack|gcloud\s+projects\s+delete|az\s+group\s+delete)\b/, reason: 'cloud resource deletion blocked' },
632
+
633
+ // Network & infrastructure
634
+ { level: 'high', id: 'curl-mutating', regex: /\bcurl\b.+-X\s*(POST|PUT|DELETE|PATCH)\b/, reason: 'mutating HTTP request blocked' },
635
+ { level: 'high', id: 'ssh-remote', regex: /\bssh\s/, reason: 'SSH remote connection blocked' },
636
+ { level: 'high', id: 'scp-remote', regex: /\bscp\s/, reason: 'SCP remote copy blocked' },
637
+ { level: 'high', id: 'rsync-delete', regex: /\brsync\b.+--delete/, reason: 'rsync --delete blocked' },
638
+ { level: 'high', id: 'firewall', regex: /\b(iptables\s+-F|ufw\s+disable)\b/, reason: 'firewall manipulation blocked' },
639
+ { level: 'high', id: 'network-kill', regex: /\bifconfig\s+\w+\s+down\b/, reason: 'network interface down blocked' },
640
+ { level: 'high', id: 'route-delete', regex: /\broute\s+del\s+default\b/, reason: 'default route deletion blocked' },
641
+
642
+ // Database
643
+ { level: 'high', id: 'sql-drop', regex: /\b(DROP\s+(DATABASE|TABLE)|TRUNCATE\s+TABLE)\b/i, reason: 'SQL drop/truncate blocked' },
644
+ { level: 'high', id: 'sql-mass-delete', regex: /\bDELETE\s+FROM\b.+\bWHERE\s+1\s*=\s*1/i, reason: 'SQL mass delete blocked' },
645
+ { level: 'high', id: 'redis-flush', regex: /\bredis-cli\s+(FLUSHALL|FLUSHDB)\b/, reason: 'redis flush blocked' },
646
+ { level: 'high', id: 'orm-reset', regex: /\b(prisma\s+migrate\s+reset|rails\s+db:(drop|reset)|django\s+flush)\b/, reason: 'ORM database reset blocked' },
647
+ { level: 'high', id: 'alembic-downgrade', regex: /\balembic\s+downgrade\s+base\b/, reason: 'alembic downgrade base blocked' },
648
+ { level: 'high', id: 'mongo-drop', regex: /\bmongosh\b.+dropDatabase/, reason: 'MongoDB drop database blocked' },
649
+
650
+ // STRICT — Cautionary, context-dependent
651
+ { level: 'strict', id: 'git-checkout-dot', regex: /\bgit\s+checkout\s+\./, reason: 'git checkout . discards changes' },
652
+ { level: 'strict', id: 'docker-prune', regex: /\bdocker\s+(system|image)\s+prune/, reason: 'docker prune removes images' },
653
+ ];
654
+
655
+ const LEVELS = { critical: 1, high: 2, strict: 3 };
656
+ const EMOJIS = { critical: '\u{1F6A8}', high: '\u26D4', strict: '\u26A0\uFE0F' };
657
+ const LOG_DIR = path.join(process.env.HOME || '/tmp', '.claude', 'hooks-logs');
658
+
659
+ function log(data) {
660
+ try {
661
+ if (!fs.existsSync(LOG_DIR)) fs.mkdirSync(LOG_DIR, { recursive: true });
662
+ const file = path.join(LOG_DIR, new Date().toISOString().slice(0, 10) + '.jsonl');
663
+ fs.appendFileSync(file, JSON.stringify({ ts: new Date().toISOString(), ...data }) + '\n');
664
+ } catch {}
665
+ }
666
+
667
+ function checkCommand(cmd, safetyLevel) {
668
+ safetyLevel = safetyLevel || SAFETY_LEVEL;
669
+ const threshold = LEVELS[safetyLevel] || 2;
670
+ for (const p of PATTERNS) {
671
+ if (LEVELS[p.level] <= threshold && p.regex.test(cmd)) {
672
+ return { blocked: true, pattern: p };
673
+ }
674
+ }
675
+ return { blocked: false, pattern: null };
676
+ }
677
+
678
+ async function main() {
679
+ let input = '';
680
+ for await (const chunk of process.stdin) input += chunk;
681
+
682
+ try {
683
+ const data = JSON.parse(input);
684
+ const { tool_name, tool_input, session_id, cwd, permission_mode } = data;
685
+ if (tool_name !== 'Bash') return console.log('{}');
686
+
687
+ const cmd = tool_input?.command || '';
688
+ const result = checkCommand(cmd);
689
+
690
+ if (result.blocked) {
691
+ const p = result.pattern;
692
+ log({ level: 'BLOCKED', id: p.id, priority: p.level, cmd, session_id, cwd, permission_mode });
693
+ return console.log(JSON.stringify({
694
+ hookSpecificOutput: {
695
+ hookEventName: 'PreToolUse',
696
+ permissionDecision: 'deny',
697
+ permissionDecisionReason: EMOJIS[p.level] + ' [' + p.id + '] ' + p.reason
698
+ }
699
+ }));
700
+ }
701
+ console.log('{}');
702
+ } catch (e) {
703
+ log({ level: 'ERROR', error: e.message });
704
+ console.log('{}');
705
+ }
706
+ }
707
+
708
+ if (require.main === module) {
709
+ main();
710
+ } else {
711
+ module.exports = { PATTERNS, LEVELS, SAFETY_LEVEL, checkCommand };
712
+ }
713
+ `;
714
+ function checkHookStatus(rootDir) {
715
+ const homeDir = process.env.HOME || "";
716
+ const projectScriptPath = path2.join(rootDir, ".claude", "hooks", "block-dangerous-commands.js");
717
+ const globalScriptPath = path2.join(homeDir, ".claude", "hooks", "block-dangerous-commands.js");
718
+ const result = {
719
+ projectInstalled: false,
720
+ globalInstalled: false,
721
+ projectMatchesOurs: false,
722
+ globalMatchesOurs: false
723
+ };
724
+ if (fs2.existsSync(projectScriptPath)) {
725
+ result.projectInstalled = true;
726
+ const content = fs2.readFileSync(projectScriptPath, "utf-8");
727
+ result.projectMatchesOurs = content.trim() === HOOK_SCRIPT.trim();
728
+ }
729
+ if (fs2.existsSync(globalScriptPath)) {
730
+ result.globalInstalled = true;
731
+ const content = fs2.readFileSync(globalScriptPath, "utf-8");
732
+ result.globalMatchesOurs = content.trim() === HOOK_SCRIPT.trim();
733
+ }
734
+ return result;
735
+ }
736
+ function installHook(rootDir) {
737
+ const hooksDir = path2.join(rootDir, ".claude", "hooks");
738
+ const hookPath = path2.join(hooksDir, "block-dangerous-commands.js");
739
+ const settingsPath = path2.join(rootDir, ".claude", "settings.json");
740
+ fs2.mkdirSync(hooksDir, { recursive: true });
741
+ fs2.writeFileSync(hookPath, HOOK_SCRIPT);
742
+ fs2.chmodSync(hookPath, 493);
743
+ try {
744
+ const existing = fs2.existsSync(settingsPath) ? JSON.parse(fs2.readFileSync(settingsPath, "utf-8")) : {};
745
+ const newEntry = {
746
+ matcher: "Bash",
747
+ hooks: [
748
+ {
749
+ type: "command",
750
+ command: "node .claude/hooks/block-dangerous-commands.js"
751
+ }
752
+ ]
753
+ };
754
+ const existingPreToolUse = Array.isArray(existing.hooks?.PreToolUse) ? existing.hooks.PreToolUse : [];
755
+ const alreadyInstalled = existingPreToolUse.some(
756
+ (e) => Array.isArray(e.hooks) && e.hooks.some((h) => h.command?.includes("block-dangerous-commands.js"))
757
+ );
758
+ existing.hooks = {
759
+ ...existing.hooks,
760
+ PreToolUse: alreadyInstalled ? existingPreToolUse : [...existingPreToolUse, newEntry]
761
+ };
762
+ fs2.writeFileSync(settingsPath, JSON.stringify(existing, null, 2));
763
+ } catch (err) {
764
+ const msg = err instanceof Error ? err.message : "unknown error";
765
+ console.error(` Warning: could not patch settings.json (${msg}) \u2014 add hook config manually`);
766
+ }
767
+ }
768
+ function installHookGlobal() {
769
+ const homeDir = process.env.HOME || "";
770
+ const hooksDir = path2.join(homeDir, ".claude", "hooks");
771
+ const hookPath = path2.join(hooksDir, "block-dangerous-commands.js");
772
+ const settingsPath = path2.join(homeDir, ".claude", "settings.json");
773
+ fs2.mkdirSync(hooksDir, { recursive: true });
774
+ fs2.writeFileSync(hookPath, HOOK_SCRIPT);
775
+ fs2.chmodSync(hookPath, 493);
776
+ try {
777
+ const existing = fs2.existsSync(settingsPath) ? JSON.parse(fs2.readFileSync(settingsPath, "utf-8")) : {};
778
+ const newEntry = {
779
+ matcher: "Bash",
780
+ hooks: [{ type: "command", command: "node ~/.claude/hooks/block-dangerous-commands.js" }]
781
+ };
782
+ const existingPreToolUse = Array.isArray(existing.hooks?.PreToolUse) ? existing.hooks.PreToolUse : [];
783
+ const alreadyInstalled = existingPreToolUse.some(
784
+ (e) => Array.isArray(e.hooks) && e.hooks.some((h) => h.command?.includes("block-dangerous-commands.js"))
785
+ );
786
+ existing.hooks = {
787
+ ...existing.hooks,
788
+ PreToolUse: alreadyInstalled ? existingPreToolUse : [...existingPreToolUse, newEntry]
789
+ };
790
+ fs2.writeFileSync(settingsPath, JSON.stringify(existing, null, 2));
791
+ } catch (err) {
792
+ const msg = err instanceof Error ? err.message : "unknown error";
793
+ console.error(` Warning: could not patch settings.json (${msg}) \u2014 add hook config manually`);
794
+ }
795
+ }
796
+ var STATUSLINE_SCRIPT = [
797
+ "#!/usr/bin/env bash",
798
+ "# Claude Code statusline \u2014 portable, no runtime dependency beyond jq",
799
+ "",
800
+ "set -euo pipefail",
801
+ "",
802
+ "# Colors (using $'...' so escapes resolve at assignment, not at output time)",
803
+ "RST=$'\\033[0m'",
804
+ "CYAN=$'\\033[36m'",
805
+ "MAGENTA=$'\\033[35m'",
806
+ "BLUE=$'\\033[34m'",
807
+ "GREEN=$'\\033[32m'",
808
+ "YELLOW=$'\\033[33m'",
809
+ "RED=$'\\033[31m'",
810
+ "",
811
+ "# Read JSON from stdin (Claude Code pipes session data)",
812
+ 'INPUT="$(cat)"',
813
+ "",
814
+ "# Parse fields with jq",
815
+ `CWD="$(echo "$INPUT" | jq -r '.workspace.current_dir // .cwd // ""')"`,
816
+ 'PROJECT="$(basename "$CWD")"',
817
+ `SESSION_ID="$(echo "$INPUT" | jq -r '.session_id // empty')"`,
818
+ `SESSION_NAME="$(echo "$INPUT" | jq -r '.session_name // empty')"`,
819
+ `REMAINING="$(echo "$INPUT" | jq -r '.context_window.remaining_percentage // empty')"`,
820
+ `MODEL="$(echo "$INPUT" | jq -r '.model.display_name // empty')"`,
821
+ "",
822
+ "# Line 1: [user] project [on branch]",
823
+ 'LINE1=""',
824
+ 'if [[ -n "${SSH_CONNECTION:-}" ]]; then',
825
+ ' LINE1+="${BLUE}$(whoami)${RST} "',
826
+ "fi",
827
+ 'LINE1+="${CYAN}${PROJECT}${RST}"',
828
+ "",
829
+ 'BRANCH="$(git branch --show-current 2>/dev/null || git rev-parse --short HEAD 2>/dev/null || true)"',
830
+ 'if [[ -n "$BRANCH" ]]; then',
831
+ ' LINE1+=" on ${MAGENTA}\u{1F331} ${BRANCH}${RST}"',
832
+ "fi",
833
+ "",
834
+ "# Line 2: session + context + model",
835
+ 'PARTS=""',
836
+ 'if [[ -n "$SESSION_ID" ]]; then',
837
+ ' if [[ -n "$SESSION_NAME" ]]; then',
838
+ ' PARTS+="${MAGENTA}${SESSION_NAME} \xB7 sid: ${SESSION_ID}${RST}"',
839
+ " else",
840
+ ' PARTS+="${MAGENTA}sid: ${SESSION_ID}${RST}"',
841
+ " fi",
842
+ "fi",
843
+ "",
844
+ 'if [[ -n "$REMAINING" ]]; then',
845
+ ' RND="${REMAINING%%.*}"',
846
+ " if (( RND < 20 )); then",
847
+ ' CTX_COLOR="$RED"',
848
+ " elif (( RND < 50 )); then",
849
+ ' CTX_COLOR="$YELLOW"',
850
+ " else",
851
+ ' CTX_COLOR="$GREEN"',
852
+ " fi",
853
+ ' [[ -n "$PARTS" ]] && PARTS+=" "',
854
+ ' PARTS+="${CTX_COLOR}[ctx: ${RND}%]${RST}"',
855
+ "fi",
856
+ "",
857
+ 'if [[ -n "$MODEL" ]]; then',
858
+ ' [[ -n "$PARTS" ]] && PARTS+=" "',
859
+ ' PARTS+="[${CYAN}${MODEL}${RST}]"',
860
+ "fi",
861
+ "",
862
+ 'echo "$LINE1"',
863
+ 'echo "$PARTS"'
864
+ ].join("\n");
865
+ function checkStatuslineStatus(rootDir) {
866
+ const homeDir = process.env.HOME || "";
867
+ const projectScriptPath = path2.join(rootDir, ".claude", "config", "statusline-command.sh");
868
+ const globalScriptPath = path2.join(homeDir, ".claude", "config", "statusline-command.sh");
869
+ const projectSettingsPath = path2.join(rootDir, ".claude", "settings.json");
870
+ const globalSettingsPath = path2.join(homeDir, ".claude", "settings.json");
871
+ const result = {
872
+ projectInstalled: false,
873
+ globalInstalled: false,
874
+ projectMatchesOurs: false,
875
+ globalMatchesOurs: false
876
+ };
877
+ try {
878
+ if (fs2.existsSync(projectSettingsPath)) {
879
+ const settings = JSON.parse(fs2.readFileSync(projectSettingsPath, "utf-8"));
880
+ if (settings.statusLine?.command) {
881
+ result.projectInstalled = true;
882
+ if (fs2.existsSync(projectScriptPath)) {
883
+ const content = fs2.readFileSync(projectScriptPath, "utf-8");
884
+ result.projectMatchesOurs = content.trim() === STATUSLINE_SCRIPT.trim();
885
+ }
886
+ }
887
+ }
888
+ } catch {
889
+ }
890
+ try {
891
+ if (fs2.existsSync(globalSettingsPath)) {
892
+ const settings = JSON.parse(fs2.readFileSync(globalSettingsPath, "utf-8"));
893
+ if (settings.statusLine?.command) {
894
+ result.globalInstalled = true;
895
+ if (fs2.existsSync(globalScriptPath)) {
896
+ const content = fs2.readFileSync(globalScriptPath, "utf-8");
897
+ result.globalMatchesOurs = content.trim() === STATUSLINE_SCRIPT.trim();
898
+ }
899
+ }
900
+ }
901
+ } catch {
902
+ }
903
+ return result;
904
+ }
905
+ function installStatusline(rootDir) {
906
+ const configDir = path2.join(rootDir, ".claude", "config");
907
+ const scriptPath = path2.join(configDir, "statusline-command.sh");
908
+ const settingsPath = path2.join(rootDir, ".claude", "settings.json");
909
+ fs2.mkdirSync(configDir, { recursive: true });
910
+ fs2.writeFileSync(scriptPath, STATUSLINE_SCRIPT);
911
+ fs2.chmodSync(scriptPath, 493);
912
+ patchSettings(settingsPath, {
913
+ statusLine: { type: "command", command: "bash .claude/config/statusline-command.sh" }
914
+ });
915
+ }
916
+ function installStatuslineGlobal() {
917
+ const homeDir = process.env.HOME || "";
918
+ const configDir = path2.join(homeDir, ".claude", "config");
919
+ const scriptPath = path2.join(configDir, "statusline-command.sh");
920
+ const settingsPath = path2.join(homeDir, ".claude", "settings.json");
921
+ fs2.mkdirSync(configDir, { recursive: true });
922
+ fs2.writeFileSync(scriptPath, STATUSLINE_SCRIPT);
923
+ fs2.chmodSync(scriptPath, 493);
924
+ patchSettings(settingsPath, {
925
+ statusLine: { type: "command", command: "bash ~/.claude/config/statusline-command.sh" }
926
+ });
927
+ }
928
+ function patchSettings(settingsPath, patch) {
929
+ try {
930
+ const existing = fs2.existsSync(settingsPath) ? JSON.parse(fs2.readFileSync(settingsPath, "utf-8")) : {};
931
+ Object.assign(existing, patch);
932
+ fs2.writeFileSync(settingsPath, JSON.stringify(existing, null, 2));
933
+ } catch (err) {
934
+ const msg = err instanceof Error ? err.message : "unknown error";
935
+ console.error(
936
+ ` Warning: could not patch settings.json (${msg}) \u2014 add statusLine config manually`
937
+ );
938
+ }
939
+ }
940
+
941
+ // src/extras.ts
942
+ var EXTRAS = [
943
+ {
944
+ id: "safety-hook",
945
+ name: "Safety hook",
946
+ description: "Block dangerous commands (git push, rm -rf, etc.)",
947
+ checkStatus: checkHookStatus,
948
+ installProject: installHook,
949
+ installGlobal: installHookGlobal,
950
+ projectPath: ".claude/hooks/block-dangerous-commands.js",
951
+ globalPath: "~/.claude/hooks/block-dangerous-commands.js"
952
+ },
953
+ {
954
+ id: "statusline",
955
+ name: "Custom statusline",
956
+ description: "Shows project, branch, context, model",
957
+ checkStatus: checkStatuslineStatus,
958
+ installProject: installStatusline,
959
+ installGlobal: installStatuslineGlobal,
960
+ projectPath: ".claude/config/statusline-command.sh",
961
+ globalPath: "~/.claude/config/statusline-command.sh"
962
+ }
963
+ ];
964
+ async function promptExtras(projectDir) {
965
+ for (const extra of EXTRAS) {
966
+ const status = extra.checkStatus(projectDir);
967
+ if (status.projectMatchesOurs || status.globalMatchesOurs) {
968
+ continue;
969
+ }
970
+ if (status.projectInstalled || status.globalInstalled) {
971
+ const where = status.globalInstalled ? "globally" : "in this project";
972
+ const { action } = await prompts({
973
+ type: "select",
974
+ name: "action",
975
+ message: `A different ${extra.name.toLowerCase()} is already configured ${where}. Replace it?`,
976
+ choices: [
977
+ { title: "Install for this project only", value: "project" },
978
+ { title: "Install globally (all projects)", value: "global" },
979
+ { title: "Skip \u2014 keep existing", value: "skip" }
980
+ ],
981
+ initial: 2
982
+ });
983
+ applyAction(action, extra, projectDir);
984
+ } else {
985
+ const { action } = await prompts({
986
+ type: "select",
987
+ name: "action",
988
+ message: `Add ${extra.name.toLowerCase()}? (${extra.description})`,
989
+ choices: [
990
+ { title: "Install for this project only", value: "project" },
991
+ { title: "Install globally (all projects)", value: "global" },
992
+ { title: "Skip", value: "skip" }
993
+ ],
994
+ initial: 0
995
+ });
996
+ applyAction(action, extra, projectDir);
997
+ }
998
+ }
999
+ }
1000
+ function applyAction(action, extra, projectDir) {
1001
+ if (action === "project") {
1002
+ extra.installProject(projectDir);
1003
+ console.log(pc.green(` + ${extra.projectPath}`));
1004
+ } else if (action === "global") {
1005
+ extra.installGlobal();
1006
+ console.log(pc.green(` + ${extra.globalPath} (global)`));
1007
+ }
1008
+ }
1009
+
1010
+ // src/generator.ts
1011
+ import fs3 from "fs";
1012
+ import path3 from "path";
518
1013
  function ensureDirectories(rootDir) {
519
1014
  const dirs = [".claude", ".claude/skills", ".claude/agents", ".claude/rules", ".claude/commands"];
520
1015
  for (const dir of dirs) {
521
- fs2.mkdirSync(path2.join(rootDir, dir), { recursive: true });
1016
+ fs3.mkdirSync(path3.join(rootDir, dir), { recursive: true });
522
1017
  }
523
1018
  }
524
1019
  function generateSettings(stack) {
@@ -593,16 +1088,17 @@ function generateSettings(stack) {
593
1088
  }
594
1089
  function writeSettings(rootDir, stack) {
595
1090
  const { path: settingsPath, content } = generateSettings(stack);
596
- const fullPath = path2.join(rootDir, settingsPath);
597
- const dir = path2.dirname(fullPath);
598
- fs2.mkdirSync(dir, { recursive: true });
599
- fs2.writeFileSync(fullPath, content);
1091
+ const fullPath = path3.join(rootDir, settingsPath);
1092
+ const dir = path3.dirname(fullPath);
1093
+ fs3.mkdirSync(dir, { recursive: true });
1094
+ fs3.writeFileSync(fullPath, content);
600
1095
  }
601
1096
 
602
1097
  // src/prompt.ts
603
- function getAnalysisPrompt(projectInfo) {
1098
+ function getAnalysisPrompt(projectInfo, options = { claudeMdMode: "replace", existingClaudeMd: null }) {
604
1099
  const context = buildContextSection(projectInfo);
605
1100
  const templateVars = buildTemplateVariables(projectInfo);
1101
+ const claudeMdInstructions = buildClaudeMdInstructions(options);
606
1102
  return `${ANALYSIS_PROMPT}
607
1103
 
608
1104
  ${SKILLS_PROMPT}
@@ -626,15 +1122,17 @@ ${context}
626
1122
 
627
1123
  ${templateVars}
628
1124
 
1125
+ ${claudeMdInstructions}
1126
+
629
1127
  ---
630
1128
 
631
1129
  ## Execute Now
632
1130
 
633
1131
  1. Read this entire prompt to understand all phases
634
1132
  2. Execute Phase 1 completely - read files, analyze code, gather all data
635
- 3. Execute Phase 2 - generate the CLAUDE.md (max 120 lines) using only discovered information
1133
+ ${options.claudeMdMode === "keep" ? `3. Skip CLAUDE.md generation \u2014 the existing file is being kept as-is` : options.claudeMdMode === "improve" ? `3. Execute Phase 2 \u2014 IMPROVE the existing CLAUDE.md (see Improvement Mode instructions above)` : `3. Execute Phase 2 - generate the CLAUDE.md (max 120 lines) using only discovered information`}
636
1134
  4. Execute Phase 3 - verify quality before writing
637
- 5. Use the Write tool to create \`.claude/CLAUDE.md\` with the final content
1135
+ ${options.claudeMdMode === "keep" ? `5. Skip writing CLAUDE.md \u2014 it is being preserved` : `5. Use the Write tool to create \`.claude/CLAUDE.md\` with the final content`}
638
1136
  6. Execute Phase 4 - generate ALL skill files (4 core + framework-specific if detected)
639
1137
  7. Execute Phase 5 - generate agent files
640
1138
  8. Execute Phase 6 - generate rule files
@@ -645,6 +1143,41 @@ ${templateVars}
645
1143
  Do NOT output file contents to stdout. Write all files to disk using the Write tool.
646
1144
  Generate ALL files in a single pass \u2014 do not stop after CLAUDE.md.`;
647
1145
  }
1146
+ function buildClaudeMdInstructions(options) {
1147
+ if (options.claudeMdMode === "keep") {
1148
+ return `---
1149
+
1150
+ ## CLAUDE.md Mode: KEEP
1151
+
1152
+ The user chose to keep their existing CLAUDE.md unchanged.
1153
+ **Do NOT read, modify, or overwrite \`.claude/CLAUDE.md\`.**
1154
+ Generate all other files (skills, agents, rules, commands) normally.
1155
+ Use the existing CLAUDE.md as the source of truth for cross-references.`;
1156
+ }
1157
+ if (options.claudeMdMode === "improve" && options.existingClaudeMd) {
1158
+ return `---
1159
+
1160
+ ## CLAUDE.md Mode: IMPROVE
1161
+
1162
+ The user has an existing CLAUDE.md and wants it improved, not replaced.
1163
+ Here is the current content:
1164
+
1165
+ \`\`\`markdown
1166
+ ${options.existingClaudeMd}
1167
+ \`\`\`
1168
+
1169
+ ### Improvement Rules
1170
+
1171
+ 1. **Preserve all manually-added content** \u2014 sections, notes, and custom rules the user wrote
1172
+ 2. **Enhance with discovered information** \u2014 fill gaps, add missing sections, improve specificity
1173
+ 3. **Fix generic content** \u2014 replace boilerplate with project-specific details found during Phase 1
1174
+ 4. **Update stale references** \u2014 fix file paths, commands, or patterns that no longer match the codebase
1175
+ 5. **Respect the 120-line cap** \u2014 if the file is already near the limit, prioritize density over additions
1176
+ 6. **Keep the user's structure** \u2014 if they organized sections differently from the template, keep their layout
1177
+ 7. **Do NOT remove content you don't understand** \u2014 if a section seems custom or domain-specific, preserve it`;
1178
+ }
1179
+ return "";
1180
+ }
648
1181
  function buildContextSection(projectInfo) {
649
1182
  const { name, description, techStack, fileCount } = projectInfo;
650
1183
  const lines = [];
@@ -1272,10 +1805,157 @@ Body: This command delegates to the code-reviewer agent for thorough review.
1272
1805
  3. If the agent is unavailable, perform a lightweight review: run the linter and check for obvious issues
1273
1806
  Do NOT duplicate the code-reviewer agent's checklist here \u2014 the agent has the full review criteria.`;
1274
1807
 
1808
+ // src/validator.ts
1809
+ import fs4 from "fs";
1810
+ import path4 from "path";
1811
+ function extractCommands(claudeMd) {
1812
+ const commands = [];
1813
+ const match = claudeMd.match(/## Common Commands[\s\S]*?```(?:bash)?\n([\s\S]*?)```/);
1814
+ if (!match) return commands;
1815
+ for (const line of match[1].split("\n")) {
1816
+ const trimmed = line.trim();
1817
+ if (!trimmed || trimmed.startsWith("#")) continue;
1818
+ const cmd = trimmed.split(/\s+#/)[0].trim();
1819
+ if (cmd.length > 3) commands.push(cmd);
1820
+ }
1821
+ return commands;
1822
+ }
1823
+ function extractConventionFingerprints(claudeMd) {
1824
+ const fingerprints = [];
1825
+ const startIdx = claudeMd.indexOf("## Code Conventions");
1826
+ if (startIdx === -1) return fingerprints;
1827
+ const rest = claudeMd.slice(startIdx + "## Code Conventions".length);
1828
+ const nextHeading = rest.match(/\n## [A-Z]/);
1829
+ const section = nextHeading ? claudeMd.slice(startIdx, startIdx + "## Code Conventions".length + nextHeading.index) : claudeMd.slice(startIdx);
1830
+ for (const kw of ["camelCase", "PascalCase", "kebab-case", "snake_case"]) {
1831
+ if (section.includes(kw)) fingerprints.push(kw);
1832
+ }
1833
+ if (/\bnamed exports?\b/i.test(section)) fingerprints.push("named export");
1834
+ if (/\bdefault exports?\b/i.test(section)) fingerprints.push("default export");
1835
+ if (section.includes("import type")) fingerprints.push("import type");
1836
+ for (const kw of [".skip()", ".only()", "console.log"]) {
1837
+ if (section.includes(kw)) fingerprints.push(kw);
1838
+ }
1839
+ return fingerprints;
1840
+ }
1841
+ var RULE_WORDS = /\b(verify|check|ensure|always|never|must|should|avoid)\b/i;
1842
+ function isConventionDuplication(line, fingerprints) {
1843
+ const trimmed = line.trim();
1844
+ if (!trimmed || trimmed.startsWith("#") || trimmed.includes("CLAUDE.md")) return false;
1845
+ if (!/^[-*]\s/.test(trimmed)) return false;
1846
+ const matchCount = fingerprints.filter((fp) => trimmed.includes(fp)).length;
1847
+ if (matchCount >= 2) return true;
1848
+ if (matchCount === 1 && RULE_WORDS.test(trimmed)) return true;
1849
+ return false;
1850
+ }
1851
+ function findLiteralCommand(line, commands) {
1852
+ const trimmed = line.trim();
1853
+ if (!trimmed || trimmed.startsWith("#") || trimmed.includes("CLAUDE.md")) return null;
1854
+ for (const cmd of commands) {
1855
+ if (trimmed.includes(cmd)) return cmd;
1856
+ }
1857
+ return null;
1858
+ }
1859
+ function separateFrontmatter(content) {
1860
+ const match = content.match(/^---\n[\s\S]*?\n---(?:\n|$)/);
1861
+ if (!match) {
1862
+ return { frontmatter: "", body: content };
1863
+ }
1864
+ return {
1865
+ frontmatter: match[0],
1866
+ body: content.slice(match[0].length)
1867
+ };
1868
+ }
1869
+ function processFile(filePath, commands, fingerprints) {
1870
+ const content = fs4.readFileSync(filePath, "utf-8");
1871
+ const { frontmatter, body } = separateFrontmatter(content);
1872
+ const lines = body.split("\n");
1873
+ const changes = [];
1874
+ const newLines = [];
1875
+ let inCodeBlock = false;
1876
+ for (const line of lines) {
1877
+ if (line.trim().startsWith("```")) {
1878
+ inCodeBlock = !inCodeBlock;
1879
+ newLines.push(line);
1880
+ continue;
1881
+ }
1882
+ if (inCodeBlock) {
1883
+ newLines.push(line);
1884
+ continue;
1885
+ }
1886
+ if (isConventionDuplication(line, fingerprints)) {
1887
+ changes.push({ file: filePath, original: line.trim(), replacement: null });
1888
+ continue;
1889
+ }
1890
+ const cmd = findLiteralCommand(line, commands);
1891
+ if (cmd) {
1892
+ const newLine = line.replace(cmd, "see Common Commands in CLAUDE.md");
1893
+ changes.push({ file: filePath, original: line.trim(), replacement: newLine.trim() });
1894
+ newLines.push(newLine);
1895
+ continue;
1896
+ }
1897
+ newLines.push(line);
1898
+ }
1899
+ if (changes.length > 0) {
1900
+ fs4.writeFileSync(filePath, frontmatter + newLines.join("\n"));
1901
+ }
1902
+ return changes;
1903
+ }
1904
+ function walkMdFiles(dir) {
1905
+ const files = [];
1906
+ if (!fs4.existsSync(dir)) return files;
1907
+ const entries = fs4.readdirSync(dir, { withFileTypes: true });
1908
+ for (const entry of entries) {
1909
+ const fullPath = path4.join(dir, entry.name);
1910
+ if (entry.isDirectory()) {
1911
+ files.push(...walkMdFiles(fullPath));
1912
+ } else if (entry.name.endsWith(".md")) {
1913
+ files.push(fullPath);
1914
+ }
1915
+ }
1916
+ return files;
1917
+ }
1918
+ function validateArtifacts(rootDir) {
1919
+ const result = {
1920
+ filesChecked: 0,
1921
+ filesModified: 0,
1922
+ duplicationsRemoved: 0,
1923
+ changes: []
1924
+ };
1925
+ const claudeMdPath = path4.join(rootDir, ".claude", "CLAUDE.md");
1926
+ if (!fs4.existsSync(claudeMdPath)) return result;
1927
+ try {
1928
+ const claudeMd = fs4.readFileSync(claudeMdPath, "utf-8");
1929
+ const commands = extractCommands(claudeMd);
1930
+ const fingerprints = extractConventionFingerprints(claudeMd);
1931
+ if (commands.length === 0 && fingerprints.length === 0) return result;
1932
+ const claudeDir = path4.join(rootDir, ".claude");
1933
+ const files = walkMdFiles(claudeDir).filter((f) => !f.endsWith("CLAUDE.md"));
1934
+ for (const filePath of files) {
1935
+ result.filesChecked++;
1936
+ try {
1937
+ const changes = processFile(filePath, commands, fingerprints);
1938
+ if (changes.length > 0) {
1939
+ result.filesModified++;
1940
+ result.duplicationsRemoved += changes.length;
1941
+ for (const change of changes) {
1942
+ change.file = path4.relative(rootDir, filePath);
1943
+ }
1944
+ result.changes.push(...changes);
1945
+ }
1946
+ } catch {
1947
+ }
1948
+ }
1949
+ } catch {
1950
+ return result;
1951
+ }
1952
+ return result;
1953
+ }
1954
+
1275
1955
  // src/cli.ts
1276
- var __dirname2 = path3.dirname(fileURLToPath(import.meta.url));
1956
+ var __dirname2 = path5.dirname(fileURLToPath(import.meta.url));
1277
1957
  var VERSION = JSON.parse(
1278
- fs3.readFileSync(path3.join(__dirname2, "..", "package.json"), "utf-8")
1958
+ fs5.readFileSync(path5.join(__dirname2, "..", "package.json"), "utf-8")
1279
1959
  ).version;
1280
1960
  function parseArgs(args) {
1281
1961
  return {
@@ -1291,21 +1971,21 @@ function getVersion() {
1291
1971
  }
1292
1972
  function showHelp() {
1293
1973
  console.log(`
1294
- ${pc.cyan("Claude Code Starter")} v${VERSION}
1974
+ ${pc2.cyan("Claude Code Starter")} v${VERSION}
1295
1975
 
1296
1976
  Bootstrap intelligent Claude Code configurations for any repository.
1297
1977
 
1298
- ${pc.bold("USAGE")}
1978
+ ${pc2.bold("USAGE")}
1299
1979
  npx claude-code-starter [OPTIONS]
1300
1980
 
1301
- ${pc.bold("OPTIONS")}
1981
+ ${pc2.bold("OPTIONS")}
1302
1982
  -h, --help Show this help message
1303
1983
  -v, --version Show version number
1304
1984
  -f, --force Force overwrite existing .claude files
1305
1985
  -y, --no-interactive Skip interactive prompts (use defaults)
1306
1986
  -V, --verbose Show detailed output
1307
1987
 
1308
- ${pc.bold("WHAT IT DOES")}
1988
+ ${pc2.bold("WHAT IT DOES")}
1309
1989
  1. Analyzes your repository's tech stack
1310
1990
  2. Launches Claude CLI to deeply analyze your codebase
1311
1991
  3. Generates all .claude/ configuration files:
@@ -1313,55 +1993,55 @@ ${pc.bold("WHAT IT DOES")}
1313
1993
  - Skills for your frameworks and workflows
1314
1994
  - Agents for code review and testing
1315
1995
  - Rules matching your code style
1316
- - Commands for task management
1996
+ - Commands for analysis and code review
1317
1997
 
1318
- ${pc.bold("REQUIREMENTS")}
1998
+ ${pc2.bold("REQUIREMENTS")}
1319
1999
  Claude CLI must be installed: https://claude.ai/download
1320
2000
 
1321
- ${pc.bold("MORE INFO")}
2001
+ ${pc2.bold("MORE INFO")}
1322
2002
  https://github.com/cassmtnr/claude-code-starter
1323
2003
  `);
1324
2004
  }
1325
2005
  function showBanner() {
1326
2006
  console.log();
1327
- console.log(pc.bold("Claude Code Starter") + pc.gray(` v${VERSION}`));
1328
- console.log(pc.gray("Intelligent AI-Assisted Development Setup"));
2007
+ console.log(pc2.bold("Claude Code Starter") + pc2.gray(` v${VERSION}`));
2008
+ console.log(pc2.gray("Intelligent AI-Assisted Development Setup"));
1329
2009
  console.log();
1330
2010
  }
1331
2011
  function showTechStack(projectInfo, verbose) {
1332
2012
  const { techStack } = projectInfo;
1333
- console.log(pc.bold("Tech Stack"));
2013
+ console.log(pc2.bold("Tech Stack"));
1334
2014
  console.log();
1335
2015
  if (techStack.primaryLanguage) {
1336
- console.log(` ${pc.bold("Language:")} ${formatLanguage(techStack.primaryLanguage)}`);
2016
+ console.log(` ${pc2.bold("Language:")} ${formatLanguage(techStack.primaryLanguage)}`);
1337
2017
  }
1338
2018
  if (techStack.primaryFramework) {
1339
- console.log(` ${pc.bold("Framework:")} ${formatFramework(techStack.primaryFramework)}`);
2019
+ console.log(` ${pc2.bold("Framework:")} ${formatFramework(techStack.primaryFramework)}`);
1340
2020
  }
1341
2021
  if (techStack.packageManager) {
1342
- console.log(` ${pc.bold("Package Manager:")} ${techStack.packageManager}`);
2022
+ console.log(` ${pc2.bold("Package Manager:")} ${techStack.packageManager}`);
1343
2023
  }
1344
2024
  if (techStack.testingFramework) {
1345
- console.log(` ${pc.bold("Testing:")} ${techStack.testingFramework}`);
2025
+ console.log(` ${pc2.bold("Testing:")} ${techStack.testingFramework}`);
1346
2026
  }
1347
2027
  if (verbose) {
1348
2028
  if (techStack.linter) {
1349
- console.log(` ${pc.bold("Linter:")} ${techStack.linter}`);
2029
+ console.log(` ${pc2.bold("Linter:")} ${techStack.linter}`);
1350
2030
  }
1351
2031
  if (techStack.formatter) {
1352
- console.log(` ${pc.bold("Formatter:")} ${techStack.formatter}`);
2032
+ console.log(` ${pc2.bold("Formatter:")} ${techStack.formatter}`);
1353
2033
  }
1354
2034
  if (techStack.bundler) {
1355
- console.log(` ${pc.bold("Bundler:")} ${techStack.bundler}`);
2035
+ console.log(` ${pc2.bold("Bundler:")} ${techStack.bundler}`);
1356
2036
  }
1357
2037
  if (techStack.isMonorepo) {
1358
- console.log(` ${pc.bold("Monorepo:")} yes`);
2038
+ console.log(` ${pc2.bold("Monorepo:")} yes`);
1359
2039
  }
1360
2040
  if (techStack.hasDocker) {
1361
- console.log(` ${pc.bold("Docker:")} yes`);
2041
+ console.log(` ${pc2.bold("Docker:")} yes`);
1362
2042
  }
1363
2043
  if (techStack.hasCICD) {
1364
- console.log(` ${pc.bold("CI/CD:")} ${techStack.cicdPlatform}`);
2044
+ console.log(` ${pc2.bold("CI/CD:")} ${techStack.cicdPlatform}`);
1365
2045
  }
1366
2046
  }
1367
2047
  console.log();
@@ -1437,9 +2117,9 @@ async function promptNewProject(args) {
1437
2117
  if (!args.interactive) {
1438
2118
  return null;
1439
2119
  }
1440
- console.log(pc.yellow("New project detected - let's set it up!"));
2120
+ console.log(pc2.yellow("New project detected - let's set it up!"));
1441
2121
  console.log();
1442
- const descResponse = await prompts({
2122
+ const descResponse = await prompts2({
1443
2123
  type: "text",
1444
2124
  name: "description",
1445
2125
  message: "What are you building?",
@@ -1448,7 +2128,7 @@ async function promptNewProject(args) {
1448
2128
  if (!descResponse.description) {
1449
2129
  return null;
1450
2130
  }
1451
- const langResponse = await prompts({
2131
+ const langResponse = await prompts2({
1452
2132
  type: "select",
1453
2133
  name: "primaryLanguage",
1454
2134
  message: "Primary language?",
@@ -1469,34 +2149,34 @@ async function promptNewProject(args) {
1469
2149
  });
1470
2150
  const lang = langResponse.primaryLanguage || "typescript";
1471
2151
  const fwChoices = frameworkChoices[lang] || defaultFrameworkChoices;
1472
- const fwResponse = await prompts({
2152
+ const fwResponse = await prompts2({
1473
2153
  type: "select",
1474
2154
  name: "framework",
1475
2155
  message: "Framework?",
1476
2156
  choices: fwChoices
1477
2157
  });
1478
2158
  const pmChoices = getPackageManagerChoices(lang);
1479
- const pmResponse = await prompts({
2159
+ const pmResponse = await prompts2({
1480
2160
  type: "select",
1481
2161
  name: "packageManager",
1482
2162
  message: "Package manager?",
1483
2163
  choices: pmChoices
1484
2164
  });
1485
2165
  const testChoices = getTestingFrameworkChoices(lang);
1486
- const testResponse = await prompts({
2166
+ const testResponse = await prompts2({
1487
2167
  type: "select",
1488
2168
  name: "testingFramework",
1489
2169
  message: "Testing framework?",
1490
2170
  choices: testChoices
1491
2171
  });
1492
2172
  const lintChoices = getLinterFormatterChoices(lang);
1493
- const lintResponse = await prompts({
2173
+ const lintResponse = await prompts2({
1494
2174
  type: "select",
1495
2175
  name: "linter",
1496
2176
  message: "Linter/Formatter?",
1497
2177
  choices: lintChoices
1498
2178
  });
1499
- const typeResponse = await prompts({
2179
+ const typeResponse = await prompts2({
1500
2180
  type: "select",
1501
2181
  name: "projectType",
1502
2182
  message: "Project type?",
@@ -1604,7 +2284,6 @@ function getLinterFormatterChoices(lang) {
1604
2284
  return [
1605
2285
  { title: "Biome", value: "biome" },
1606
2286
  { title: "ESLint + Prettier", value: "eslint" },
1607
- { title: "ESLint", value: "eslint" },
1608
2287
  { title: "None", value: null }
1609
2288
  ];
1610
2289
  }
@@ -1735,12 +2414,12 @@ function checkClaudeCli() {
1735
2414
  return false;
1736
2415
  }
1737
2416
  }
1738
- function runClaudeAnalysis(projectDir, projectInfo) {
2417
+ function runClaudeAnalysis(projectDir, projectInfo, options = { claudeMdMode: "replace", existingClaudeMd: null }) {
1739
2418
  return new Promise((resolve) => {
1740
- const prompt = getAnalysisPrompt(projectInfo);
1741
- console.log(pc.cyan("Launching Claude for deep project analysis..."));
2419
+ const prompt = getAnalysisPrompt(projectInfo, options);
2420
+ console.log(pc2.cyan("Launching Claude for deep project analysis..."));
1742
2421
  console.log(
1743
- pc.gray("Claude will read your codebase and generate all .claude/ configuration files")
2422
+ pc2.gray("Claude will read your codebase and generate all .claude/ configuration files")
1744
2423
  );
1745
2424
  console.log();
1746
2425
  const spinner = ora({
@@ -1755,6 +2434,8 @@ function runClaudeAnalysis(projectDir, projectInfo) {
1755
2434
  "claude",
1756
2435
  [
1757
2436
  "-p",
2437
+ "--verbose",
2438
+ "--output-format=stream-json",
1758
2439
  "--allowedTools",
1759
2440
  "Read",
1760
2441
  "--allowedTools",
@@ -1771,8 +2452,43 @@ function runClaudeAnalysis(projectDir, projectInfo) {
1771
2452
  stdio: ["pipe", "pipe", "pipe"]
1772
2453
  }
1773
2454
  );
2455
+ let stdoutBuffer = "";
2456
+ child.stdout.on("data", (chunk) => {
2457
+ stdoutBuffer += chunk.toString();
2458
+ const lines = stdoutBuffer.split("\n");
2459
+ stdoutBuffer = lines.pop() || "";
2460
+ for (const line of lines) {
2461
+ if (!line.trim()) continue;
2462
+ try {
2463
+ const event = JSON.parse(line);
2464
+ if (event.type === "assistant" && Array.isArray(event.message?.content)) {
2465
+ for (const block of event.message.content) {
2466
+ if (block.type === "tool_use" && block.name && block.input) {
2467
+ const toolName = block.name;
2468
+ const toolInput = block.input;
2469
+ const filePath = toolInput.file_path || toolInput.path || toolInput.pattern || "";
2470
+ const shortPath = filePath.split("/").slice(-2).join("/");
2471
+ const action = toolName === "Write" || toolName === "Edit" ? "Writing" : "Reading";
2472
+ if (shortPath) {
2473
+ spinner.text = `${action} ${shortPath}...`;
2474
+ } else {
2475
+ spinner.text = `Using ${toolName}...`;
2476
+ }
2477
+ }
2478
+ }
2479
+ }
2480
+ } catch {
2481
+ }
2482
+ }
2483
+ });
2484
+ child.stdin.on("error", () => {
2485
+ });
1774
2486
  child.stdin.write(prompt);
1775
2487
  child.stdin.end();
2488
+ let stderrOutput = "";
2489
+ child.stderr.on("data", (chunk) => {
2490
+ stderrOutput += chunk.toString();
2491
+ });
1776
2492
  child.on("error", (err) => {
1777
2493
  spinner.fail(`Failed to launch Claude CLI: ${err.message}`);
1778
2494
  resolve(false);
@@ -1783,23 +2499,26 @@ function runClaudeAnalysis(projectDir, projectInfo) {
1783
2499
  resolve(true);
1784
2500
  } else {
1785
2501
  spinner.fail(`Claude exited with code ${code}`);
2502
+ if (stderrOutput.trim()) {
2503
+ console.error(pc2.gray(stderrOutput.trim()));
2504
+ }
1786
2505
  resolve(false);
1787
2506
  }
1788
2507
  });
1789
2508
  });
1790
2509
  }
1791
2510
  function getGeneratedFiles(projectDir) {
1792
- const claudeDir = path3.join(projectDir, ".claude");
2511
+ const claudeDir = path5.join(projectDir, ".claude");
1793
2512
  const files = [];
1794
2513
  function walk(dir) {
1795
- if (!fs3.existsSync(dir)) return;
1796
- const entries = fs3.readdirSync(dir, { withFileTypes: true });
2514
+ if (!fs5.existsSync(dir)) return;
2515
+ const entries = fs5.readdirSync(dir, { withFileTypes: true });
1797
2516
  for (const entry of entries) {
1798
- const fullPath = path3.join(dir, entry.name);
2517
+ const fullPath = path5.join(dir, entry.name);
1799
2518
  if (entry.isDirectory()) {
1800
2519
  walk(fullPath);
1801
2520
  } else {
1802
- files.push(path3.relative(projectDir, fullPath));
2521
+ files.push(path5.relative(projectDir, fullPath));
1803
2522
  }
1804
2523
  }
1805
2524
  }
@@ -1818,7 +2537,7 @@ async function main() {
1818
2537
  }
1819
2538
  showBanner();
1820
2539
  const projectDir = process.cwd();
1821
- console.log(pc.gray("Analyzing repository..."));
2540
+ console.log(pc2.gray("Analyzing repository..."));
1822
2541
  console.log();
1823
2542
  const projectInfo = analyzeRepository(projectDir);
1824
2543
  showTechStack(projectInfo, args.verbose);
@@ -1846,63 +2565,86 @@ async function main() {
1846
2565
  projectInfo.description = preferences.description;
1847
2566
  }
1848
2567
  } else {
1849
- console.log(pc.gray(`Existing project with ${projectInfo.fileCount} source files`));
2568
+ console.log(pc2.gray(`Existing project with ${projectInfo.fileCount} source files`));
1850
2569
  console.log();
1851
2570
  }
1852
- if (projectInfo.techStack.hasClaudeConfig && !args.force) {
1853
- console.log(pc.yellow("Existing .claude/ configuration detected"));
1854
- console.log();
1855
- if (args.interactive) {
1856
- const { proceed } = await prompts({
1857
- type: "confirm",
1858
- name: "proceed",
1859
- message: "Update existing configuration? (preserves task state)",
1860
- initial: true
2571
+ let claudeMdMode = "replace";
2572
+ let existingClaudeMd = null;
2573
+ const claudeMdPath = path5.join(projectDir, ".claude", "CLAUDE.md");
2574
+ if (fs5.existsSync(claudeMdPath)) {
2575
+ existingClaudeMd = fs5.readFileSync(claudeMdPath, "utf-8");
2576
+ if (args.force) {
2577
+ claudeMdMode = "replace";
2578
+ } else if (args.interactive) {
2579
+ console.log(pc2.yellow("Existing CLAUDE.md detected"));
2580
+ console.log();
2581
+ const { mode } = await prompts2({
2582
+ type: "select",
2583
+ name: "mode",
2584
+ message: "How should we handle the existing CLAUDE.md?",
2585
+ choices: [
2586
+ { title: "Improve \u2014 scan and enhance the existing file", value: "improve" },
2587
+ { title: "Replace \u2014 generate a new one from scratch", value: "replace" },
2588
+ { title: "Keep \u2014 leave CLAUDE.md as-is, regenerate other files", value: "keep" }
2589
+ ],
2590
+ initial: 0
1861
2591
  });
1862
- if (!proceed) {
1863
- console.log(pc.gray("Cancelled. Use --force to overwrite."));
2592
+ if (mode === void 0) {
2593
+ console.log(pc2.gray("Cancelled."));
1864
2594
  process.exit(0);
1865
2595
  }
2596
+ claudeMdMode = mode;
1866
2597
  }
1867
2598
  console.log();
1868
2599
  }
1869
2600
  if (!checkClaudeCli()) {
1870
- console.error(pc.red("Claude CLI is required but not found."));
1871
- console.error(pc.gray("Install it from: https://claude.ai/download"));
2601
+ console.error(pc2.red("Claude CLI is required but not found."));
2602
+ console.error(pc2.gray("Install it from: https://claude.ai/download"));
1872
2603
  process.exit(1);
1873
2604
  }
1874
- console.log(pc.gray("Setting up .claude/ directory structure..."));
2605
+ console.log(pc2.gray("Setting up .claude/ directory structure..."));
1875
2606
  console.log();
1876
2607
  writeSettings(projectDir, projectInfo.techStack);
1877
2608
  ensureDirectories(projectDir);
1878
- console.log(pc.green("Created:"));
1879
- console.log(pc.green(" + .claude/settings.json"));
2609
+ console.log(pc2.green("Created:"));
2610
+ console.log(pc2.green(" + .claude/settings.json"));
1880
2611
  console.log();
1881
- const success = await runClaudeAnalysis(projectDir, projectInfo);
2612
+ const success = await runClaudeAnalysis(projectDir, projectInfo, {
2613
+ claudeMdMode,
2614
+ existingClaudeMd: claudeMdMode === "improve" ? existingClaudeMd : null
2615
+ });
1882
2616
  if (!success) {
1883
- console.error(pc.red("Claude analysis failed. Please try again."));
2617
+ console.error(pc2.red("Claude analysis failed. Please try again."));
1884
2618
  process.exit(1);
1885
2619
  }
2620
+ const validation = validateArtifacts(projectDir);
2621
+ if (validation.duplicationsRemoved > 0) {
2622
+ console.log(
2623
+ pc2.gray(
2624
+ ` Deduplication: removed ${validation.duplicationsRemoved} redundancies from ${validation.filesModified} files`
2625
+ )
2626
+ );
2627
+ }
1886
2628
  const generatedFiles = getGeneratedFiles(projectDir);
1887
2629
  console.log();
1888
- console.log(pc.green(`Done! (${generatedFiles.length} files)`));
2630
+ console.log(pc2.green(`Done! (${generatedFiles.length} files)`));
1889
2631
  console.log();
1890
- console.log(pc.bold("Generated for your stack:"));
2632
+ console.log(pc2.bold("Generated for your stack:"));
1891
2633
  const skills = generatedFiles.filter((f) => f.includes("/skills/"));
1892
2634
  const agents = generatedFiles.filter((f) => f.includes("/agents/"));
1893
2635
  const rules = generatedFiles.filter((f) => f.includes("/rules/"));
1894
2636
  const commands = generatedFiles.filter((f) => f.includes("/commands/"));
1895
2637
  if (generatedFiles.some((f) => f.endsWith("CLAUDE.md"))) {
1896
- console.log(pc.cyan(" CLAUDE.md (deep analysis by Claude)"));
2638
+ console.log(pc2.cyan(" CLAUDE.md (deep analysis by Claude)"));
1897
2639
  }
1898
2640
  if (skills.length > 0) {
1899
2641
  console.log(
1900
- ` ${skills.length} skills (${skills.map((s) => path3.basename(s, ".md")).join(", ")})`
2642
+ ` ${skills.length} skills (${skills.map((s) => path5.basename(s, ".md")).join(", ")})`
1901
2643
  );
1902
2644
  }
1903
2645
  if (agents.length > 0) {
1904
2646
  console.log(
1905
- ` ${agents.length} agents (${agents.map((a) => path3.basename(a, ".md")).join(", ")})`
2647
+ ` ${agents.length} agents (${agents.map((a) => path5.basename(a, ".md")).join(", ")})`
1906
2648
  );
1907
2649
  }
1908
2650
  if (rules.length > 0) {
@@ -1912,19 +2654,24 @@ async function main() {
1912
2654
  console.log(` ${commands.length} commands`);
1913
2655
  }
1914
2656
  console.log();
1915
- console.log(`${pc.cyan("Next step:")} Run ${pc.bold("claude")} to start working!`);
2657
+ if (args.interactive) {
2658
+ console.log();
2659
+ await promptExtras(projectDir);
2660
+ }
2661
+ console.log();
2662
+ console.log(`${pc2.cyan("Next step:")} Run ${pc2.bold("claude")} to start working!`);
1916
2663
  console.log();
1917
2664
  console.log(
1918
- pc.gray(
2665
+ pc2.gray(
1919
2666
  "Your .claude/ files were generated by deep analysis - review them with: ls -la .claude/"
1920
2667
  )
1921
2668
  );
1922
2669
  }
1923
2670
  try {
1924
- const isMain = process.argv[1] && fs3.realpathSync(process.argv[1]) === fileURLToPath(import.meta.url);
2671
+ const isMain = process.argv[1] && fs5.realpathSync(process.argv[1]) === fileURLToPath(import.meta.url);
1925
2672
  if (isMain) {
1926
2673
  main().catch((err) => {
1927
- console.error(pc.red("Error:"), err.message);
2674
+ console.error(pc2.red("Error:"), err.message);
1928
2675
  if (process.env.DEBUG) {
1929
2676
  console.error(err.stack);
1930
2677
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-starter",
3
- "version": "0.14.1",
3
+ "version": "0.16.0",
4
4
  "description": "A lightweight starter kit for AI-assisted development with Claude Code",
5
5
  "keywords": [
6
6
  "claude",