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.
- package/dist/cli.js +857 -110
- 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
|
|
6
|
-
import
|
|
5
|
+
import fs5 from "fs";
|
|
6
|
+
import path5 from "path";
|
|
7
7
|
import { fileURLToPath } from "url";
|
|
8
8
|
import ora from "ora";
|
|
9
|
-
import
|
|
10
|
-
import
|
|
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
|
|
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(
|
|
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("
|
|
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("
|
|
327
|
-
|
|
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(
|
|
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
|
|
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/
|
|
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
|
-
|
|
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 =
|
|
597
|
-
const dir =
|
|
598
|
-
|
|
599
|
-
|
|
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 =
|
|
1956
|
+
var __dirname2 = path5.dirname(fileURLToPath(import.meta.url));
|
|
1277
1957
|
var VERSION = JSON.parse(
|
|
1278
|
-
|
|
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
|
-
${
|
|
1974
|
+
${pc2.cyan("Claude Code Starter")} v${VERSION}
|
|
1295
1975
|
|
|
1296
1976
|
Bootstrap intelligent Claude Code configurations for any repository.
|
|
1297
1977
|
|
|
1298
|
-
${
|
|
1978
|
+
${pc2.bold("USAGE")}
|
|
1299
1979
|
npx claude-code-starter [OPTIONS]
|
|
1300
1980
|
|
|
1301
|
-
${
|
|
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
|
-
${
|
|
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
|
|
1996
|
+
- Commands for analysis and code review
|
|
1317
1997
|
|
|
1318
|
-
${
|
|
1998
|
+
${pc2.bold("REQUIREMENTS")}
|
|
1319
1999
|
Claude CLI must be installed: https://claude.ai/download
|
|
1320
2000
|
|
|
1321
|
-
${
|
|
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(
|
|
1328
|
-
console.log(
|
|
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(
|
|
2013
|
+
console.log(pc2.bold("Tech Stack"));
|
|
1334
2014
|
console.log();
|
|
1335
2015
|
if (techStack.primaryLanguage) {
|
|
1336
|
-
console.log(` ${
|
|
2016
|
+
console.log(` ${pc2.bold("Language:")} ${formatLanguage(techStack.primaryLanguage)}`);
|
|
1337
2017
|
}
|
|
1338
2018
|
if (techStack.primaryFramework) {
|
|
1339
|
-
console.log(` ${
|
|
2019
|
+
console.log(` ${pc2.bold("Framework:")} ${formatFramework(techStack.primaryFramework)}`);
|
|
1340
2020
|
}
|
|
1341
2021
|
if (techStack.packageManager) {
|
|
1342
|
-
console.log(` ${
|
|
2022
|
+
console.log(` ${pc2.bold("Package Manager:")} ${techStack.packageManager}`);
|
|
1343
2023
|
}
|
|
1344
2024
|
if (techStack.testingFramework) {
|
|
1345
|
-
console.log(` ${
|
|
2025
|
+
console.log(` ${pc2.bold("Testing:")} ${techStack.testingFramework}`);
|
|
1346
2026
|
}
|
|
1347
2027
|
if (verbose) {
|
|
1348
2028
|
if (techStack.linter) {
|
|
1349
|
-
console.log(` ${
|
|
2029
|
+
console.log(` ${pc2.bold("Linter:")} ${techStack.linter}`);
|
|
1350
2030
|
}
|
|
1351
2031
|
if (techStack.formatter) {
|
|
1352
|
-
console.log(` ${
|
|
2032
|
+
console.log(` ${pc2.bold("Formatter:")} ${techStack.formatter}`);
|
|
1353
2033
|
}
|
|
1354
2034
|
if (techStack.bundler) {
|
|
1355
|
-
console.log(` ${
|
|
2035
|
+
console.log(` ${pc2.bold("Bundler:")} ${techStack.bundler}`);
|
|
1356
2036
|
}
|
|
1357
2037
|
if (techStack.isMonorepo) {
|
|
1358
|
-
console.log(` ${
|
|
2038
|
+
console.log(` ${pc2.bold("Monorepo:")} yes`);
|
|
1359
2039
|
}
|
|
1360
2040
|
if (techStack.hasDocker) {
|
|
1361
|
-
console.log(` ${
|
|
2041
|
+
console.log(` ${pc2.bold("Docker:")} yes`);
|
|
1362
2042
|
}
|
|
1363
2043
|
if (techStack.hasCICD) {
|
|
1364
|
-
console.log(` ${
|
|
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(
|
|
2120
|
+
console.log(pc2.yellow("New project detected - let's set it up!"));
|
|
1441
2121
|
console.log();
|
|
1442
|
-
const descResponse = await
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
2419
|
+
const prompt = getAnalysisPrompt(projectInfo, options);
|
|
2420
|
+
console.log(pc2.cyan("Launching Claude for deep project analysis..."));
|
|
1742
2421
|
console.log(
|
|
1743
|
-
|
|
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 =
|
|
2511
|
+
const claudeDir = path5.join(projectDir, ".claude");
|
|
1793
2512
|
const files = [];
|
|
1794
2513
|
function walk(dir) {
|
|
1795
|
-
if (!
|
|
1796
|
-
const entries =
|
|
2514
|
+
if (!fs5.existsSync(dir)) return;
|
|
2515
|
+
const entries = fs5.readdirSync(dir, { withFileTypes: true });
|
|
1797
2516
|
for (const entry of entries) {
|
|
1798
|
-
const fullPath =
|
|
2517
|
+
const fullPath = path5.join(dir, entry.name);
|
|
1799
2518
|
if (entry.isDirectory()) {
|
|
1800
2519
|
walk(fullPath);
|
|
1801
2520
|
} else {
|
|
1802
|
-
files.push(
|
|
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(
|
|
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(
|
|
2568
|
+
console.log(pc2.gray(`Existing project with ${projectInfo.fileCount} source files`));
|
|
1850
2569
|
console.log();
|
|
1851
2570
|
}
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
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 (
|
|
1863
|
-
console.log(
|
|
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(
|
|
1871
|
-
console.error(
|
|
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(
|
|
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(
|
|
1879
|
-
console.log(
|
|
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(
|
|
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(
|
|
2630
|
+
console.log(pc2.green(`Done! (${generatedFiles.length} files)`));
|
|
1889
2631
|
console.log();
|
|
1890
|
-
console.log(
|
|
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(
|
|
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) =>
|
|
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) =>
|
|
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
|
-
|
|
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
|
-
|
|
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] &&
|
|
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(
|
|
2674
|
+
console.error(pc2.red("Error:"), err.message);
|
|
1928
2675
|
if (process.env.DEBUG) {
|
|
1929
2676
|
console.error(err.stack);
|
|
1930
2677
|
}
|