claude-code-starter 0.15.0 → 0.17.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 +1694 -304
  2. package/package.json +2 -2
package/dist/cli.js CHANGED
@@ -2,19 +2,20 @@
2
2
 
3
3
  // src/cli.ts
4
4
  import { execSync, spawn } from "child_process";
5
- import fs5 from "fs";
6
- import path5 from "path";
5
+ import fs7 from "fs";
6
+ import os2 from "os";
7
+ import path7 from "path";
7
8
  import { fileURLToPath } from "url";
8
9
  import ora from "ora";
9
- import pc from "picocolors";
10
- import prompts from "prompts";
10
+ import pc2 from "picocolors";
11
+ import prompts2 from "prompts";
11
12
 
12
13
  // src/analyzer.ts
13
14
  import fs from "fs";
14
15
  import path from "path";
15
16
  function analyzeRepository(rootDir) {
16
17
  const techStack = detectTechStack(rootDir);
17
- const fileCount = countSourceFiles(rootDir, techStack.languages);
18
+ const fileCount = countSourceFiles(rootDir);
18
19
  const packageJson = readPackageJson(rootDir);
19
20
  return {
20
21
  isExisting: fileCount > 0,
@@ -33,11 +34,11 @@ function detectTechStack(rootDir) {
33
34
  const frameworks = detectFrameworks(packageJson, files, rootDir);
34
35
  const primaryFramework = frameworks[0] || null;
35
36
  const packageManager = detectPackageManager(files);
36
- const testingFramework = detectTestingFramework(packageJson, files);
37
+ const testingFramework = detectTestingFramework(packageJson, files, rootDir);
37
38
  const linter = detectLinter(packageJson, files);
38
- const formatter = detectFormatter(packageJson, files);
39
+ const formatter = detectFormatter(packageJson, files, rootDir);
39
40
  const bundler = detectBundler(packageJson, files);
40
- const isMonorepo = detectMonorepo(rootDir, files, packageJson);
41
+ const isMonorepo = detectMonorepo(files, packageJson);
41
42
  const hasDocker = files.includes("Dockerfile") || files.includes("docker-compose.yml") || files.includes("docker-compose.yaml");
42
43
  const { hasCICD, cicdPlatform } = detectCICD(rootDir, files);
43
44
  const { hasClaudeConfig, existingClaudeFiles } = detectExistingClaudeConfig(rootDir);
@@ -59,6 +60,12 @@ function detectTechStack(rootDir) {
59
60
  existingClaudeFiles
60
61
  };
61
62
  }
63
+ function getAllDeps(packageJson) {
64
+ return {
65
+ ...packageJson.dependencies || {},
66
+ ...packageJson.devDependencies || {}
67
+ };
68
+ }
62
69
  function readPackageJson(rootDir) {
63
70
  const packageJsonPath = path.join(rootDir, "package.json");
64
71
  if (!fs.existsSync(packageJsonPath)) return null;
@@ -269,12 +276,9 @@ function detectPackageManager(files) {
269
276
  if (files.includes("build.gradle") || files.includes("build.gradle.kts")) return "gradle";
270
277
  return null;
271
278
  }
272
- function detectTestingFramework(packageJson, files) {
279
+ function detectTestingFramework(packageJson, files, rootDir) {
273
280
  if (packageJson) {
274
- const allDeps = {
275
- ...packageJson.dependencies || {},
276
- ...packageJson.devDependencies || {}
277
- };
281
+ const allDeps = getAllDeps(packageJson);
278
282
  if (allDeps.vitest) return "vitest";
279
283
  if (allDeps.jest) return "jest";
280
284
  if (allDeps.mocha) return "mocha";
@@ -286,7 +290,15 @@ function detectTestingFramework(packageJson, files) {
286
290
  if (files.includes("pytest.ini") || files.includes("conftest.py")) return "pytest";
287
291
  if (files.includes("go.mod")) return "go-test";
288
292
  if (files.includes("Cargo.toml")) return "rust-test";
289
- if (files.includes("Gemfile")) return "rspec";
293
+ if (files.includes(".rspec")) return "rspec";
294
+ if (files.includes("Gemfile")) {
295
+ if (files.includes("spec")) return "rspec";
296
+ try {
297
+ const gemfile = fs.readFileSync(path.join(rootDir, "Gemfile"), "utf-8").toLowerCase();
298
+ if (gemfile.includes("rspec")) return "rspec";
299
+ } catch {
300
+ }
301
+ }
290
302
  return null;
291
303
  }
292
304
  function detectLinter(packageJson, files) {
@@ -297,10 +309,7 @@ function detectLinter(packageJson, files) {
297
309
  }
298
310
  if (files.includes("biome.json") || files.includes("biome.jsonc")) return "biome";
299
311
  if (packageJson) {
300
- const allDeps = {
301
- ...packageJson.dependencies || {},
302
- ...packageJson.devDependencies || {}
303
- };
312
+ const allDeps = getAllDeps(packageJson);
304
313
  if (allDeps.eslint) return "eslint";
305
314
  if (allDeps["@biomejs/biome"]) return "biome";
306
315
  }
@@ -308,7 +317,7 @@ function detectLinter(packageJson, files) {
308
317
  if (files.includes(".flake8") || files.includes("setup.cfg")) return "flake8";
309
318
  return null;
310
319
  }
311
- function detectFormatter(packageJson, files) {
320
+ function detectFormatter(packageJson, files, rootDir) {
312
321
  if (files.some(
313
322
  (f) => f.startsWith(".prettierrc") || f === "prettier.config.js" || f === "prettier.config.mjs"
314
323
  )) {
@@ -316,15 +325,23 @@ function detectFormatter(packageJson, files) {
316
325
  }
317
326
  if (files.includes("biome.json") || files.includes("biome.jsonc")) return "biome";
318
327
  if (packageJson) {
319
- const allDeps = {
320
- ...packageJson.dependencies || {},
321
- ...packageJson.devDependencies || {}
322
- };
328
+ const allDeps = getAllDeps(packageJson);
323
329
  if (allDeps.prettier) return "prettier";
324
330
  if (allDeps["@biomejs/biome"]) return "biome";
325
331
  }
332
+ if (files.includes("ruff.toml") || files.includes(".ruff.toml")) return "ruff";
326
333
  if (files.includes("pyproject.toml")) {
327
- return "black";
334
+ try {
335
+ const pyproject = fs.readFileSync(path.join(rootDir, "pyproject.toml"), "utf-8");
336
+ if (pyproject.includes("[tool.ruff.format]") || pyproject.includes("[tool.ruff]")) {
337
+ return "ruff";
338
+ }
339
+ if (pyproject.includes("[tool.black]") || pyproject.includes("black")) {
340
+ return "black";
341
+ }
342
+ } catch {
343
+ }
344
+ return null;
328
345
  }
329
346
  return null;
330
347
  }
@@ -335,10 +352,7 @@ function detectBundler(packageJson, files) {
335
352
  if (files.some((f) => f.startsWith("rollup.config"))) return "rollup";
336
353
  if (files.some((f) => f.startsWith("esbuild"))) return "esbuild";
337
354
  if (packageJson) {
338
- const allDeps = {
339
- ...packageJson.dependencies || {},
340
- ...packageJson.devDependencies || {}
341
- };
355
+ const allDeps = getAllDeps(packageJson);
342
356
  if (allDeps.vite) return "vite";
343
357
  if (allDeps.webpack) return "webpack";
344
358
  if (allDeps.tsup) return "tsup";
@@ -350,16 +364,12 @@ function detectBundler(packageJson, files) {
350
364
  }
351
365
  return null;
352
366
  }
353
- function detectMonorepo(rootDir, files, packageJson) {
367
+ function detectMonorepo(files, packageJson) {
354
368
  if (files.includes("pnpm-workspace.yaml")) return true;
355
369
  if (files.includes("lerna.json")) return true;
356
370
  if (files.includes("nx.json")) return true;
357
371
  if (files.includes("turbo.json")) return true;
358
372
  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
373
  return false;
364
374
  }
365
375
  function detectCICD(rootDir, files) {
@@ -435,7 +445,7 @@ function listSourceFilesShallow(rootDir, extensions) {
435
445
  scan(rootDir, 0);
436
446
  return files;
437
447
  }
438
- function countSourceFiles(rootDir, _languages) {
448
+ function countSourceFiles(rootDir) {
439
449
  const extensions = [
440
450
  // JavaScript/TypeScript
441
451
  ".js",
@@ -512,96 +522,13 @@ function countSourceFiles(rootDir, _languages) {
512
522
  return count;
513
523
  }
514
524
 
515
- // src/generator.ts
516
- import fs2 from "fs";
517
- import path2 from "path";
518
- function ensureDirectories(rootDir) {
519
- const dirs = [".claude", ".claude/skills", ".claude/agents", ".claude/rules", ".claude/commands"];
520
- for (const dir of dirs) {
521
- fs2.mkdirSync(path2.join(rootDir, dir), { recursive: true });
522
- }
523
- }
524
- function generateSettings(stack) {
525
- const permissions = ["Read(**)", "Edit(**)", "Write(.claude/**)", "Bash(git:*)"];
526
- const pkgManagers = ["npm", "yarn", "pnpm", "bun", "npx"];
527
- for (const pm of pkgManagers) {
528
- permissions.push(`Bash(${pm}:*)`);
529
- }
530
- if (stack.languages.includes("typescript") || stack.languages.includes("javascript")) {
531
- permissions.push("Bash(node:*)", "Bash(tsc:*)");
532
- }
533
- if (stack.languages.includes("python")) {
534
- permissions.push(
535
- "Bash(python:*)",
536
- "Bash(pip:*)",
537
- "Bash(poetry:*)",
538
- "Bash(pytest:*)",
539
- "Bash(uvicorn:*)"
540
- );
541
- }
542
- if (stack.languages.includes("go")) {
543
- permissions.push("Bash(go:*)");
544
- }
545
- if (stack.languages.includes("rust")) {
546
- permissions.push("Bash(cargo:*)", "Bash(rustc:*)");
547
- }
548
- if (stack.languages.includes("ruby")) {
549
- permissions.push("Bash(ruby:*)", "Bash(bundle:*)", "Bash(rails:*)", "Bash(rake:*)");
550
- }
551
- if (stack.testingFramework) {
552
- const testCommands = {
553
- jest: ["jest:*"],
554
- vitest: ["vitest:*"],
555
- playwright: ["playwright:*"],
556
- cypress: ["cypress:*"],
557
- pytest: ["pytest:*"],
558
- rspec: ["rspec:*"]
559
- };
560
- const cmds = testCommands[stack.testingFramework];
561
- if (cmds) {
562
- permissions.push(...cmds.map((c) => `Bash(${c})`));
563
- }
564
- }
565
- if (stack.linter) {
566
- permissions.push(`Bash(${stack.linter}:*)`);
567
- }
568
- if (stack.formatter) {
569
- permissions.push(`Bash(${stack.formatter}:*)`);
570
- }
571
- permissions.push(
572
- "Bash(ls:*)",
573
- "Bash(mkdir:*)",
574
- "Bash(cat:*)",
575
- "Bash(echo:*)",
576
- "Bash(grep:*)",
577
- "Bash(find:*)"
578
- );
579
- if (stack.hasDocker) {
580
- permissions.push("Bash(docker:*)", "Bash(docker-compose:*)");
581
- }
582
- const settings = {
583
- $schema: "https://json.schemastore.org/claude-code-settings.json",
584
- permissions: {
585
- allow: [...new Set(permissions)]
586
- // Deduplicate
587
- }
588
- };
589
- return {
590
- path: ".claude/settings.json",
591
- content: JSON.stringify(settings, null, 2)
592
- };
593
- }
594
- function writeSettings(rootDir, stack) {
595
- 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);
600
- }
525
+ // src/extras.ts
526
+ import pc from "picocolors";
527
+ import prompts from "prompts";
601
528
 
602
529
  // src/hooks.ts
603
- import fs3 from "fs";
604
- import path3 from "path";
530
+ import fs2 from "fs";
531
+ import path2 from "path";
605
532
  var HOOK_SCRIPT = String.raw`#!/usr/bin/env node
606
533
  /**
607
534
  * Block Dangerous Commands - PreToolUse Hook for Bash
@@ -765,77 +692,1003 @@ function checkCommand(cmd, safetyLevel) {
765
692
  return { blocked: true, pattern: p };
766
693
  }
767
694
  }
768
- return { blocked: false, pattern: null };
695
+ return { blocked: false, pattern: null };
696
+ }
697
+
698
+ async function main() {
699
+ let input = '';
700
+ for await (const chunk of process.stdin) input += chunk;
701
+
702
+ try {
703
+ const data = JSON.parse(input);
704
+ const { tool_name, tool_input, session_id, cwd, permission_mode } = data;
705
+ if (tool_name !== 'Bash') return console.log('{}');
706
+
707
+ const cmd = tool_input?.command || '';
708
+ const result = checkCommand(cmd);
709
+
710
+ if (result.blocked) {
711
+ const p = result.pattern;
712
+ log({ level: 'BLOCKED', id: p.id, priority: p.level, cmd, session_id, cwd, permission_mode });
713
+ return console.log(JSON.stringify({
714
+ hookSpecificOutput: {
715
+ hookEventName: 'PreToolUse',
716
+ permissionDecision: 'deny',
717
+ permissionDecisionReason: EMOJIS[p.level] + ' [' + p.id + '] ' + p.reason
718
+ }
719
+ }));
720
+ }
721
+ console.log('{}');
722
+ } catch (e) {
723
+ log({ level: 'ERROR', error: e.message });
724
+ console.log('{}');
725
+ }
726
+ }
727
+
728
+ if (require.main === module) {
729
+ main();
730
+ } else {
731
+ module.exports = { PATTERNS, LEVELS, SAFETY_LEVEL, checkCommand };
732
+ }
733
+ `;
734
+ function checkHookStatus(rootDir) {
735
+ const homeDir = process.env.HOME || "";
736
+ const projectScriptPath = path2.join(rootDir, ".claude", "hooks", "block-dangerous-commands.js");
737
+ const globalScriptPath = path2.join(homeDir, ".claude", "hooks", "block-dangerous-commands.js");
738
+ const result = {
739
+ projectInstalled: false,
740
+ globalInstalled: false,
741
+ projectMatchesOurs: false,
742
+ globalMatchesOurs: false
743
+ };
744
+ if (fs2.existsSync(projectScriptPath)) {
745
+ result.projectInstalled = true;
746
+ try {
747
+ const content = fs2.readFileSync(projectScriptPath, "utf-8");
748
+ result.projectMatchesOurs = content.trim() === HOOK_SCRIPT.trim();
749
+ } catch {
750
+ }
751
+ }
752
+ if (fs2.existsSync(globalScriptPath)) {
753
+ result.globalInstalled = true;
754
+ try {
755
+ const content = fs2.readFileSync(globalScriptPath, "utf-8");
756
+ result.globalMatchesOurs = content.trim() === HOOK_SCRIPT.trim();
757
+ } catch {
758
+ }
759
+ }
760
+ return result;
761
+ }
762
+ function installHook(rootDir) {
763
+ const hooksDir = path2.join(rootDir, ".claude", "hooks");
764
+ const hookPath = path2.join(hooksDir, "block-dangerous-commands.js");
765
+ const settingsPath = path2.join(rootDir, ".claude", "settings.json");
766
+ fs2.mkdirSync(hooksDir, { recursive: true });
767
+ fs2.writeFileSync(hookPath, HOOK_SCRIPT);
768
+ fs2.chmodSync(hookPath, 493);
769
+ try {
770
+ const existing = fs2.existsSync(settingsPath) ? JSON.parse(fs2.readFileSync(settingsPath, "utf-8")) : {};
771
+ const newEntry = {
772
+ matcher: "Bash",
773
+ hooks: [
774
+ {
775
+ type: "command",
776
+ command: "node .claude/hooks/block-dangerous-commands.js"
777
+ }
778
+ ]
779
+ };
780
+ const existingPreToolUse = Array.isArray(existing.hooks?.PreToolUse) ? existing.hooks.PreToolUse : [];
781
+ const alreadyInstalled = existingPreToolUse.some(
782
+ (e) => Array.isArray(e.hooks) && e.hooks.some((h) => h.command?.includes("block-dangerous-commands.js"))
783
+ );
784
+ existing.hooks = {
785
+ ...existing.hooks,
786
+ PreToolUse: alreadyInstalled ? existingPreToolUse : [...existingPreToolUse, newEntry]
787
+ };
788
+ fs2.writeFileSync(settingsPath, JSON.stringify(existing, null, 2));
789
+ } catch (err) {
790
+ const msg = err instanceof Error ? err.message : "unknown error";
791
+ console.error(` Warning: could not patch settings.json (${msg}) \u2014 add hook config manually`);
792
+ }
793
+ }
794
+ function installHookGlobal() {
795
+ const homeDir = process.env.HOME || "";
796
+ const hooksDir = path2.join(homeDir, ".claude", "hooks");
797
+ const hookPath = path2.join(hooksDir, "block-dangerous-commands.js");
798
+ const settingsPath = path2.join(homeDir, ".claude", "settings.json");
799
+ fs2.mkdirSync(hooksDir, { recursive: true });
800
+ fs2.writeFileSync(hookPath, HOOK_SCRIPT);
801
+ fs2.chmodSync(hookPath, 493);
802
+ try {
803
+ const existing = fs2.existsSync(settingsPath) ? JSON.parse(fs2.readFileSync(settingsPath, "utf-8")) : {};
804
+ const newEntry = {
805
+ matcher: "Bash",
806
+ hooks: [{ type: "command", command: "node ~/.claude/hooks/block-dangerous-commands.js" }]
807
+ };
808
+ const existingPreToolUse = Array.isArray(existing.hooks?.PreToolUse) ? existing.hooks.PreToolUse : [];
809
+ const alreadyInstalled = existingPreToolUse.some(
810
+ (e) => Array.isArray(e.hooks) && e.hooks.some((h) => h.command?.includes("block-dangerous-commands.js"))
811
+ );
812
+ existing.hooks = {
813
+ ...existing.hooks,
814
+ PreToolUse: alreadyInstalled ? existingPreToolUse : [...existingPreToolUse, newEntry]
815
+ };
816
+ fs2.writeFileSync(settingsPath, JSON.stringify(existing, null, 2));
817
+ } catch (err) {
818
+ const msg = err instanceof Error ? err.message : "unknown error";
819
+ console.error(` Warning: could not patch settings.json (${msg}) \u2014 add hook config manually`);
820
+ }
821
+ }
822
+ var STATUSLINE_SCRIPT = [
823
+ "#!/usr/bin/env bash",
824
+ "# Claude Code statusline \u2014 portable, no runtime dependency beyond jq",
825
+ "",
826
+ "set -euo pipefail",
827
+ "",
828
+ "# Colors (using $'...' so escapes resolve at assignment, not at output time)",
829
+ "RST=$'\\033[0m'",
830
+ "CYAN=$'\\033[36m'",
831
+ "MAGENTA=$'\\033[35m'",
832
+ "BLUE=$'\\033[34m'",
833
+ "GREEN=$'\\033[32m'",
834
+ "YELLOW=$'\\033[33m'",
835
+ "RED=$'\\033[31m'",
836
+ "",
837
+ "# Read JSON from stdin (Claude Code pipes session data)",
838
+ 'INPUT="$(cat)"',
839
+ "",
840
+ "# Parse fields with jq",
841
+ `CWD="$(echo "$INPUT" | jq -r '.workspace.current_dir // .cwd // ""')"`,
842
+ 'PROJECT="$(basename "$CWD")"',
843
+ `SESSION_ID="$(echo "$INPUT" | jq -r '.session_id // empty')"`,
844
+ `SESSION_NAME="$(echo "$INPUT" | jq -r '.session_name // empty')"`,
845
+ `REMAINING="$(echo "$INPUT" | jq -r '.context_window.remaining_percentage // empty')"`,
846
+ `MODEL="$(echo "$INPUT" | jq -r '.model.display_name // empty')"`,
847
+ "",
848
+ "# Line 1: [user] project [on branch]",
849
+ 'LINE1=""',
850
+ 'if [[ -n "${SSH_CONNECTION:-}" ]]; then',
851
+ ' LINE1+="${BLUE}$(whoami)${RST} "',
852
+ "fi",
853
+ 'LINE1+="${CYAN}${PROJECT}${RST}"',
854
+ "",
855
+ 'BRANCH="$(git branch --show-current 2>/dev/null || git rev-parse --short HEAD 2>/dev/null || true)"',
856
+ 'if [[ -n "$BRANCH" ]]; then',
857
+ ' LINE1+=" on ${MAGENTA}\u{1F331} ${BRANCH}${RST}"',
858
+ "fi",
859
+ "",
860
+ "# Line 2: session + context + model",
861
+ 'PARTS=""',
862
+ 'if [[ -n "$SESSION_ID" ]]; then',
863
+ ' if [[ -n "$SESSION_NAME" ]]; then',
864
+ ' PARTS+="${MAGENTA}${SESSION_NAME} \xB7 sid: ${SESSION_ID}${RST}"',
865
+ " else",
866
+ ' PARTS+="${MAGENTA}sid: ${SESSION_ID}${RST}"',
867
+ " fi",
868
+ "fi",
869
+ "",
870
+ 'if [[ -n "$REMAINING" ]]; then',
871
+ ' RND="${REMAINING%%.*}"',
872
+ " if (( RND < 20 )); then",
873
+ ' CTX_COLOR="$RED"',
874
+ " elif (( RND < 50 )); then",
875
+ ' CTX_COLOR="$YELLOW"',
876
+ " else",
877
+ ' CTX_COLOR="$GREEN"',
878
+ " fi",
879
+ ' [[ -n "$PARTS" ]] && PARTS+=" "',
880
+ ' PARTS+="${CTX_COLOR}[ctx: ${RND}%]${RST}"',
881
+ "fi",
882
+ "",
883
+ 'if [[ -n "$MODEL" ]]; then',
884
+ ' [[ -n "$PARTS" ]] && PARTS+=" "',
885
+ ' PARTS+="[${CYAN}${MODEL}${RST}]"',
886
+ "fi",
887
+ "",
888
+ 'echo "$LINE1"',
889
+ 'echo "$PARTS"'
890
+ ].join("\n");
891
+ function checkStatuslineStatus(rootDir) {
892
+ const homeDir = process.env.HOME || "";
893
+ const projectScriptPath = path2.join(rootDir, ".claude", "config", "statusline-command.sh");
894
+ const globalScriptPath = path2.join(homeDir, ".claude", "config", "statusline-command.sh");
895
+ const projectSettingsPath = path2.join(rootDir, ".claude", "settings.json");
896
+ const globalSettingsPath = path2.join(homeDir, ".claude", "settings.json");
897
+ const result = {
898
+ projectInstalled: false,
899
+ globalInstalled: false,
900
+ projectMatchesOurs: false,
901
+ globalMatchesOurs: false
902
+ };
903
+ try {
904
+ if (fs2.existsSync(projectSettingsPath)) {
905
+ const settings = JSON.parse(fs2.readFileSync(projectSettingsPath, "utf-8"));
906
+ if (settings.statusLine?.command) {
907
+ result.projectInstalled = true;
908
+ if (fs2.existsSync(projectScriptPath)) {
909
+ const content = fs2.readFileSync(projectScriptPath, "utf-8");
910
+ result.projectMatchesOurs = content.trim() === STATUSLINE_SCRIPT.trim();
911
+ }
912
+ }
913
+ }
914
+ } catch {
915
+ }
916
+ try {
917
+ if (fs2.existsSync(globalSettingsPath)) {
918
+ const settings = JSON.parse(fs2.readFileSync(globalSettingsPath, "utf-8"));
919
+ if (settings.statusLine?.command) {
920
+ result.globalInstalled = true;
921
+ if (fs2.existsSync(globalScriptPath)) {
922
+ const content = fs2.readFileSync(globalScriptPath, "utf-8");
923
+ result.globalMatchesOurs = content.trim() === STATUSLINE_SCRIPT.trim();
924
+ }
925
+ }
926
+ }
927
+ } catch {
928
+ }
929
+ return result;
930
+ }
931
+ function installStatusline(rootDir) {
932
+ const configDir = path2.join(rootDir, ".claude", "config");
933
+ const scriptPath = path2.join(configDir, "statusline-command.sh");
934
+ const settingsPath = path2.join(rootDir, ".claude", "settings.json");
935
+ fs2.mkdirSync(configDir, { recursive: true });
936
+ fs2.writeFileSync(scriptPath, STATUSLINE_SCRIPT);
937
+ fs2.chmodSync(scriptPath, 493);
938
+ patchSettings(settingsPath, {
939
+ statusLine: { type: "command", command: "bash .claude/config/statusline-command.sh" }
940
+ });
941
+ }
942
+ function installStatuslineGlobal() {
943
+ const homeDir = process.env.HOME || "";
944
+ const configDir = path2.join(homeDir, ".claude", "config");
945
+ const scriptPath = path2.join(configDir, "statusline-command.sh");
946
+ const settingsPath = path2.join(homeDir, ".claude", "settings.json");
947
+ fs2.mkdirSync(configDir, { recursive: true });
948
+ fs2.writeFileSync(scriptPath, STATUSLINE_SCRIPT);
949
+ fs2.chmodSync(scriptPath, 493);
950
+ patchSettings(settingsPath, {
951
+ statusLine: { type: "command", command: "bash ~/.claude/config/statusline-command.sh" }
952
+ });
953
+ }
954
+ var SENSITIVE_FILES_HOOK = String.raw`#!/usr/bin/env node
955
+ /**
956
+ * Protect Sensitive Files - PreToolUse Hook for Write/Edit
957
+ * Warns before modifying sensitive files (migrations, env, credentials, lock files).
958
+ */
959
+
960
+ const path = require('path');
961
+
962
+ const SENSITIVE_PATTERNS = [
963
+ { pattern: /\/migrations?\//i, reason: 'migration file — changes may affect database schema' },
964
+ { pattern: /\.env(\.\w+)?$/, reason: 'environment file — may contain secrets' },
965
+ { pattern: /\/secrets?\//i, reason: 'secrets directory — may contain credentials' },
966
+ { pattern: /\/credentials?\//i, reason: 'credentials directory' },
967
+ { pattern: /\.(pem|key|cert|crt)$/, reason: 'certificate/key file' },
968
+ { pattern: /package-lock\.json$/, reason: 'lock file — should be managed by package manager' },
969
+ { pattern: /yarn\.lock$/, reason: 'lock file — should be managed by package manager' },
970
+ { pattern: /pnpm-lock\.yaml$/, reason: 'lock file — should be managed by package manager' },
971
+ { pattern: /bun\.lock$/, reason: 'lock file — should be managed by package manager' },
972
+ { pattern: /Cargo\.lock$/, reason: 'lock file — should be managed by cargo' },
973
+ { pattern: /poetry\.lock$/, reason: 'lock file — should be managed by poetry' },
974
+ ];
975
+
976
+ async function main() {
977
+ let input = '';
978
+ for await (const chunk of process.stdin) input += chunk;
979
+
980
+ try {
981
+ const data = JSON.parse(input);
982
+ const { tool_name, tool_input } = data;
983
+
984
+ if (tool_name !== 'Write' && tool_name !== 'Edit') {
985
+ return console.log('{}');
986
+ }
987
+
988
+ const filePath = tool_input?.file_path || tool_input?.path || '';
989
+ const normalized = filePath.replace(/\\/g, '/');
990
+
991
+ for (const { pattern, reason } of SENSITIVE_PATTERNS) {
992
+ if (pattern.test(normalized)) {
993
+ return console.log(JSON.stringify({
994
+ hookSpecificOutput: {
995
+ hookEventName: 'PreToolUse',
996
+ permissionDecision: 'ask',
997
+ permissionDecisionReason: '\u26A0\uFE0F Sensitive file: ' + reason + ' (' + path.basename(filePath) + ')'
998
+ }
999
+ }));
1000
+ }
1001
+ }
1002
+
1003
+ console.log('{}');
1004
+ } catch {
1005
+ console.log('{}');
1006
+ }
1007
+ }
1008
+
1009
+ if (require.main === module) main();
1010
+ `;
1011
+ function checkSensitiveHookStatus(rootDir) {
1012
+ const homeDir = process.env.HOME || "";
1013
+ const projectPath = path2.join(rootDir, ".claude", "hooks", "protect-sensitive-files.js");
1014
+ const globalPath = path2.join(homeDir, ".claude", "hooks", "protect-sensitive-files.js");
1015
+ const result = {
1016
+ projectInstalled: false,
1017
+ globalInstalled: false,
1018
+ projectMatchesOurs: false,
1019
+ globalMatchesOurs: false
1020
+ };
1021
+ if (fs2.existsSync(projectPath)) {
1022
+ result.projectInstalled = true;
1023
+ try {
1024
+ result.projectMatchesOurs = fs2.readFileSync(projectPath, "utf-8").trim() === SENSITIVE_FILES_HOOK.trim();
1025
+ } catch {
1026
+ }
1027
+ }
1028
+ if (fs2.existsSync(globalPath)) {
1029
+ result.globalInstalled = true;
1030
+ try {
1031
+ result.globalMatchesOurs = fs2.readFileSync(globalPath, "utf-8").trim() === SENSITIVE_FILES_HOOK.trim();
1032
+ } catch {
1033
+ }
1034
+ }
1035
+ return result;
1036
+ }
1037
+ function installSensitiveHook(rootDir) {
1038
+ const hooksDir = path2.join(rootDir, ".claude", "hooks");
1039
+ const hookPath = path2.join(hooksDir, "protect-sensitive-files.js");
1040
+ const settingsPath = path2.join(rootDir, ".claude", "settings.json");
1041
+ fs2.mkdirSync(hooksDir, { recursive: true });
1042
+ fs2.writeFileSync(hookPath, SENSITIVE_FILES_HOOK);
1043
+ fs2.chmodSync(hookPath, 493);
1044
+ patchHook(settingsPath, "Write", "node .claude/hooks/protect-sensitive-files.js");
1045
+ patchHook(settingsPath, "Edit", "node .claude/hooks/protect-sensitive-files.js");
1046
+ }
1047
+ function installSensitiveHookGlobal() {
1048
+ const homeDir = process.env.HOME || "";
1049
+ const hooksDir = path2.join(homeDir, ".claude", "hooks");
1050
+ const hookPath = path2.join(hooksDir, "protect-sensitive-files.js");
1051
+ const settingsPath = path2.join(homeDir, ".claude", "settings.json");
1052
+ fs2.mkdirSync(hooksDir, { recursive: true });
1053
+ fs2.writeFileSync(hookPath, SENSITIVE_FILES_HOOK);
1054
+ fs2.chmodSync(hookPath, 493);
1055
+ patchHook(settingsPath, "Write", "node ~/.claude/hooks/protect-sensitive-files.js");
1056
+ patchHook(settingsPath, "Edit", "node ~/.claude/hooks/protect-sensitive-files.js");
1057
+ }
1058
+ function patchHook(settingsPath, matcher, command) {
1059
+ try {
1060
+ const existing = fs2.existsSync(settingsPath) ? JSON.parse(fs2.readFileSync(settingsPath, "utf-8")) : {};
1061
+ const newEntry = {
1062
+ matcher,
1063
+ hooks: [{ type: "command", command }]
1064
+ };
1065
+ const existingPreToolUse = Array.isArray(existing.hooks?.PreToolUse) ? existing.hooks.PreToolUse : [];
1066
+ const alreadyInstalled = existingPreToolUse.some(
1067
+ (e) => e.matcher === matcher && Array.isArray(e.hooks) && e.hooks.some((h) => h.command === command)
1068
+ );
1069
+ if (!alreadyInstalled) {
1070
+ existing.hooks = {
1071
+ ...existing.hooks,
1072
+ PreToolUse: [...existingPreToolUse, newEntry]
1073
+ };
1074
+ fs2.writeFileSync(settingsPath, JSON.stringify(existing, null, 2));
1075
+ }
1076
+ } catch (err) {
1077
+ const msg = err instanceof Error ? err.message : "unknown error";
1078
+ console.error(` Warning: could not patch settings.json (${msg}) \u2014 add hook config manually`);
1079
+ }
1080
+ }
1081
+ function patchSettings(settingsPath, patch) {
1082
+ try {
1083
+ const existing = fs2.existsSync(settingsPath) ? JSON.parse(fs2.readFileSync(settingsPath, "utf-8")) : {};
1084
+ Object.assign(existing, patch);
1085
+ fs2.writeFileSync(settingsPath, JSON.stringify(existing, null, 2));
1086
+ } catch (err) {
1087
+ const msg = err instanceof Error ? err.message : "unknown error";
1088
+ console.error(
1089
+ ` Warning: could not patch settings.json (${msg}) \u2014 add statusLine config manually`
1090
+ );
1091
+ }
1092
+ }
1093
+
1094
+ // src/extras.ts
1095
+ var EXTRAS = [
1096
+ {
1097
+ id: "safety-hook",
1098
+ name: "Safety hook",
1099
+ description: "Block dangerous commands (git push, rm -rf, etc.)",
1100
+ checkStatus: checkHookStatus,
1101
+ installProject: installHook,
1102
+ installGlobal: installHookGlobal,
1103
+ projectPath: ".claude/hooks/block-dangerous-commands.js",
1104
+ globalPath: "~/.claude/hooks/block-dangerous-commands.js"
1105
+ },
1106
+ {
1107
+ id: "statusline",
1108
+ name: "Custom statusline",
1109
+ description: "Shows project, branch, context, model",
1110
+ checkStatus: checkStatuslineStatus,
1111
+ installProject: installStatusline,
1112
+ installGlobal: installStatuslineGlobal,
1113
+ projectPath: ".claude/config/statusline-command.sh",
1114
+ globalPath: "~/.claude/config/statusline-command.sh"
1115
+ },
1116
+ {
1117
+ id: "sensitive-files",
1118
+ name: "Sensitive file protection",
1119
+ description: "Warns before editing migrations, env, lock files, credentials",
1120
+ checkStatus: checkSensitiveHookStatus,
1121
+ installProject: installSensitiveHook,
1122
+ installGlobal: installSensitiveHookGlobal,
1123
+ projectPath: ".claude/hooks/protect-sensitive-files.js",
1124
+ globalPath: "~/.claude/hooks/protect-sensitive-files.js"
1125
+ }
1126
+ ];
1127
+ async function promptExtras(projectDir) {
1128
+ for (const extra of EXTRAS) {
1129
+ const status = extra.checkStatus(projectDir);
1130
+ if (status.projectMatchesOurs || status.globalMatchesOurs) {
1131
+ continue;
1132
+ }
1133
+ if (status.projectInstalled || status.globalInstalled) {
1134
+ const where = status.globalInstalled ? "globally" : "in this project";
1135
+ const { action } = await prompts({
1136
+ type: "select",
1137
+ name: "action",
1138
+ message: `A different ${extra.name.toLowerCase()} is already configured ${where}. Replace it?`,
1139
+ choices: [
1140
+ { title: "Install for this project only", value: "project" },
1141
+ { title: "Install globally (all projects)", value: "global" },
1142
+ { title: "Skip \u2014 keep existing", value: "skip" }
1143
+ ],
1144
+ initial: 2
1145
+ });
1146
+ applyAction(action, extra, projectDir);
1147
+ } else {
1148
+ const { action } = await prompts({
1149
+ type: "select",
1150
+ name: "action",
1151
+ message: `Add ${extra.name.toLowerCase()}? (${extra.description})`,
1152
+ choices: [
1153
+ { title: "Install for this project only", value: "project" },
1154
+ { title: "Install globally (all projects)", value: "global" },
1155
+ { title: "Skip", value: "skip" }
1156
+ ],
1157
+ initial: 0
1158
+ });
1159
+ applyAction(action, extra, projectDir);
1160
+ }
1161
+ }
1162
+ }
1163
+ function applyAction(action, extra, projectDir) {
1164
+ if (action === "project") {
1165
+ extra.installProject(projectDir);
1166
+ console.log(pc.green(` + ${extra.projectPath}`));
1167
+ } else if (action === "global") {
1168
+ extra.installGlobal();
1169
+ console.log(pc.green(` + ${extra.globalPath} (global)`));
1170
+ }
1171
+ }
1172
+
1173
+ // src/generator.ts
1174
+ import fs3 from "fs";
1175
+ import path3 from "path";
1176
+ function ensureDirectories(rootDir) {
1177
+ const dirs = [".claude", ".claude/skills", ".claude/agents", ".claude/rules", ".claude/commands"];
1178
+ for (const dir of dirs) {
1179
+ fs3.mkdirSync(path3.join(rootDir, dir), { recursive: true });
1180
+ }
1181
+ }
1182
+ function generateSettings(stack) {
1183
+ const permissions = ["Read(**)", "Edit(**)", "Write(.claude/**)", "Bash(git:*)"];
1184
+ const pkgManagers = ["npm", "yarn", "pnpm", "bun", "npx"];
1185
+ for (const pm of pkgManagers) {
1186
+ permissions.push(`Bash(${pm}:*)`);
1187
+ }
1188
+ if (stack.languages.includes("typescript") || stack.languages.includes("javascript")) {
1189
+ permissions.push("Bash(node:*)", "Bash(tsc:*)");
1190
+ }
1191
+ if (stack.languages.includes("python")) {
1192
+ permissions.push(
1193
+ "Bash(python:*)",
1194
+ "Bash(pip:*)",
1195
+ "Bash(poetry:*)",
1196
+ "Bash(pytest:*)",
1197
+ "Bash(uvicorn:*)"
1198
+ );
1199
+ }
1200
+ if (stack.languages.includes("go")) {
1201
+ permissions.push("Bash(go:*)");
1202
+ }
1203
+ if (stack.languages.includes("rust")) {
1204
+ permissions.push("Bash(cargo:*)", "Bash(rustc:*)");
1205
+ }
1206
+ if (stack.languages.includes("ruby")) {
1207
+ permissions.push("Bash(ruby:*)", "Bash(bundle:*)", "Bash(rails:*)", "Bash(rake:*)");
1208
+ }
1209
+ if (stack.testingFramework) {
1210
+ const testCommands = {
1211
+ jest: ["jest:*"],
1212
+ vitest: ["vitest:*"],
1213
+ playwright: ["playwright:*"],
1214
+ cypress: ["cypress:*"],
1215
+ pytest: ["pytest:*"],
1216
+ rspec: ["rspec:*"]
1217
+ };
1218
+ const cmds = testCommands[stack.testingFramework];
1219
+ if (cmds) {
1220
+ permissions.push(...cmds.map((c) => `Bash(${c})`));
1221
+ }
1222
+ }
1223
+ if (stack.linter) {
1224
+ permissions.push(`Bash(${stack.linter}:*)`);
1225
+ }
1226
+ if (stack.formatter) {
1227
+ permissions.push(`Bash(${stack.formatter}:*)`);
1228
+ }
1229
+ permissions.push(
1230
+ "Bash(ls:*)",
1231
+ "Bash(mkdir:*)",
1232
+ "Bash(cat:*)",
1233
+ "Bash(echo:*)",
1234
+ "Bash(grep:*)",
1235
+ "Bash(find:*)"
1236
+ );
1237
+ if (stack.hasDocker) {
1238
+ permissions.push("Bash(docker:*)", "Bash(docker-compose:*)");
1239
+ }
1240
+ const settings = {
1241
+ $schema: "https://json.schemastore.org/claude-code-settings.json",
1242
+ permissions: {
1243
+ allow: [...new Set(permissions)]
1244
+ // Deduplicate
1245
+ }
1246
+ };
1247
+ return {
1248
+ path: ".claude/settings.json",
1249
+ content: JSON.stringify(settings, null, 2)
1250
+ };
1251
+ }
1252
+ function writeSettings(rootDir, stack, force = false) {
1253
+ const { path: settingsPath, content } = generateSettings(stack);
1254
+ const fullPath = path3.join(rootDir, settingsPath);
1255
+ const dir = path3.dirname(fullPath);
1256
+ fs3.mkdirSync(dir, { recursive: true });
1257
+ if (!force && fs3.existsSync(fullPath)) {
1258
+ try {
1259
+ const existing = JSON.parse(fs3.readFileSync(fullPath, "utf-8"));
1260
+ const generated = JSON.parse(content);
1261
+ const existingAllow = existing.permissions?.allow || [];
1262
+ const generatedAllow = generated.permissions?.allow || [];
1263
+ const mergedAllow = [.../* @__PURE__ */ new Set([...existingAllow, ...generatedAllow])];
1264
+ const merged = {
1265
+ ...existing,
1266
+ $schema: generated.$schema,
1267
+ permissions: {
1268
+ ...existing.permissions,
1269
+ allow: mergedAllow
1270
+ }
1271
+ };
1272
+ fs3.writeFileSync(fullPath, JSON.stringify(merged, null, 2));
1273
+ return;
1274
+ } catch {
1275
+ }
1276
+ }
1277
+ fs3.writeFileSync(fullPath, content);
1278
+ }
1279
+
1280
+ // src/health.ts
1281
+ import fs4 from "fs";
1282
+ import os from "os";
1283
+ import path4 from "path";
1284
+ function checkHealth(projectDir) {
1285
+ const items = [];
1286
+ items.push(checkClaudeMdExists(projectDir));
1287
+ items.push(checkClaudeMdLength(projectDir));
1288
+ items.push(checkClaudeMdReferences(projectDir));
1289
+ items.push(checkSettingsExists(projectDir));
1290
+ items.push(checkAgentsExist(projectDir));
1291
+ items.push(checkSkillsExist(projectDir));
1292
+ items.push(checkRulesHaveFilters(projectDir));
1293
+ items.push(checkCommandsExist(projectDir));
1294
+ items.push(checkSafetyHook(projectDir));
1295
+ items.push(checkNoDuplication(projectDir));
1296
+ const score = items.reduce((sum, item) => sum + item.score, 0);
1297
+ const maxScore = items.reduce((sum, item) => sum + item.maxScore, 0);
1298
+ return { score, maxScore, items };
1299
+ }
1300
+ function checkClaudeMdExists(projectDir) {
1301
+ const exists = fs4.existsSync(path4.join(projectDir, ".claude", "CLAUDE.md"));
1302
+ return {
1303
+ name: "CLAUDE.md exists",
1304
+ passed: exists,
1305
+ score: exists ? 10 : 0,
1306
+ maxScore: 10,
1307
+ message: exists ? "CLAUDE.md found" : "Missing .claude/CLAUDE.md \u2014 run claude-code-starter to generate"
1308
+ };
1309
+ }
1310
+ function checkClaudeMdLength(projectDir) {
1311
+ const claudeMdPath = path4.join(projectDir, ".claude", "CLAUDE.md");
1312
+ if (!fs4.existsSync(claudeMdPath)) {
1313
+ return {
1314
+ name: "CLAUDE.md length",
1315
+ passed: false,
1316
+ score: 0,
1317
+ maxScore: 5,
1318
+ message: "CLAUDE.md not found"
1319
+ };
1320
+ }
1321
+ const lines = fs4.readFileSync(claudeMdPath, "utf-8").split("\n").length;
1322
+ const ok = lines <= 120;
1323
+ return {
1324
+ name: "CLAUDE.md length",
1325
+ passed: ok,
1326
+ score: ok ? 5 : 2,
1327
+ maxScore: 5,
1328
+ message: ok ? `${lines} lines (within 120-line cap)` : `${lines} lines \u2014 exceeds 120-line cap, consider trimming`
1329
+ };
1330
+ }
1331
+ function checkClaudeMdReferences(projectDir) {
1332
+ const claudeMdPath = path4.join(projectDir, ".claude", "CLAUDE.md");
1333
+ if (!fs4.existsSync(claudeMdPath)) {
1334
+ return {
1335
+ name: "CLAUDE.md file references",
1336
+ passed: false,
1337
+ score: 0,
1338
+ maxScore: 10,
1339
+ message: "CLAUDE.md not found"
1340
+ };
1341
+ }
1342
+ const content = fs4.readFileSync(claudeMdPath, "utf-8");
1343
+ const fileRefs = content.match(/`([^`]+\.\w{1,5})`/g) || [];
1344
+ const uniqueRefs = [...new Set(fileRefs.map((r) => r.replace(/`/g, "").split(" ")[0]))];
1345
+ let valid = 0;
1346
+ let invalid = 0;
1347
+ const brokenRefs = [];
1348
+ for (const ref of uniqueRefs) {
1349
+ if (ref.includes("*") || ref.includes("://") || ref.startsWith("$") || ref.startsWith(".env"))
1350
+ continue;
1351
+ const filePart = ref.split("(")[0].trim();
1352
+ if (filePart.length === 0) continue;
1353
+ const fullPath = path4.join(projectDir, filePart);
1354
+ if (fs4.existsSync(fullPath)) {
1355
+ valid++;
1356
+ } else {
1357
+ invalid++;
1358
+ brokenRefs.push(filePart);
1359
+ }
1360
+ }
1361
+ const total = valid + invalid;
1362
+ if (total === 0) {
1363
+ return {
1364
+ name: "CLAUDE.md file references",
1365
+ passed: true,
1366
+ score: 10,
1367
+ maxScore: 10,
1368
+ message: "No file references to validate"
1369
+ };
1370
+ }
1371
+ const passed = invalid === 0;
1372
+ const score = total > 0 ? Math.round(valid / total * 10) : 10;
1373
+ const message = passed ? `All ${valid} file references are valid` : `${invalid}/${total} file references are broken: ${brokenRefs.slice(0, 3).join(", ")}${brokenRefs.length > 3 ? "..." : ""}`;
1374
+ return { name: "CLAUDE.md file references", passed, score, maxScore: 10, message };
1375
+ }
1376
+ function checkSettingsExists(projectDir) {
1377
+ const settingsPath = path4.join(projectDir, ".claude", "settings.json");
1378
+ if (!fs4.existsSync(settingsPath)) {
1379
+ return {
1380
+ name: "settings.json",
1381
+ passed: false,
1382
+ score: 0,
1383
+ maxScore: 5,
1384
+ message: "Missing .claude/settings.json"
1385
+ };
1386
+ }
1387
+ try {
1388
+ const settings = JSON.parse(fs4.readFileSync(settingsPath, "utf-8"));
1389
+ const hasPermissions = Array.isArray(settings.permissions?.allow) && settings.permissions.allow.length > 0;
1390
+ return {
1391
+ name: "settings.json",
1392
+ passed: hasPermissions,
1393
+ score: hasPermissions ? 5 : 3,
1394
+ maxScore: 5,
1395
+ message: hasPermissions ? `${settings.permissions.allow.length} permissions configured` : "settings.json exists but no permissions configured"
1396
+ };
1397
+ } catch {
1398
+ return {
1399
+ name: "settings.json",
1400
+ passed: false,
1401
+ score: 1,
1402
+ maxScore: 5,
1403
+ message: "settings.json exists but is malformed"
1404
+ };
1405
+ }
1406
+ }
1407
+ function checkAgentsExist(projectDir) {
1408
+ const agentsDir = path4.join(projectDir, ".claude", "agents");
1409
+ const agents = listMdFiles(agentsDir);
1410
+ const passed = agents.length >= 2;
1411
+ return {
1412
+ name: "Agents",
1413
+ passed,
1414
+ score: Math.min(agents.length * 2, 10),
1415
+ maxScore: 10,
1416
+ message: agents.length > 0 ? `${agents.length} agents: ${agents.map((a) => path4.basename(a, ".md")).join(", ")}` : "No agents found \u2014 run claude-code-starter to generate"
1417
+ };
1418
+ }
1419
+ function checkSkillsExist(projectDir) {
1420
+ const skillsDir = path4.join(projectDir, ".claude", "skills");
1421
+ const skills = listMdFiles(skillsDir);
1422
+ const passed = skills.length >= 4;
1423
+ return {
1424
+ name: "Skills",
1425
+ passed,
1426
+ score: Math.min(skills.length, 5),
1427
+ maxScore: 5,
1428
+ message: skills.length > 0 ? `${skills.length} skills found` : "No skills found \u2014 run claude-code-starter to generate"
1429
+ };
1430
+ }
1431
+ function checkRulesHaveFilters(projectDir) {
1432
+ const rulesDir = path4.join(projectDir, ".claude", "rules");
1433
+ const rules = listMdFiles(rulesDir);
1434
+ if (rules.length === 0) {
1435
+ return {
1436
+ name: "Rules have paths filters",
1437
+ passed: true,
1438
+ score: 5,
1439
+ maxScore: 5,
1440
+ message: "No rules to check"
1441
+ };
1442
+ }
1443
+ let withFilter = 0;
1444
+ let withoutFilter = 0;
1445
+ for (const rule of rules) {
1446
+ try {
1447
+ const content = fs4.readFileSync(rule, "utf-8");
1448
+ if (content.includes("paths:")) {
1449
+ withFilter++;
1450
+ } else {
1451
+ withoutFilter++;
1452
+ }
1453
+ } catch {
1454
+ withoutFilter++;
1455
+ }
1456
+ }
1457
+ const passed = withoutFilter === 0;
1458
+ return {
1459
+ name: "Rules have paths filters",
1460
+ passed,
1461
+ score: passed ? 5 : Math.round(withFilter / rules.length * 5),
1462
+ maxScore: 5,
1463
+ message: passed ? `All ${withFilter} rules have paths: filters` : `${withoutFilter}/${rules.length} rules missing paths: filter \u2014 they load every session`
1464
+ };
1465
+ }
1466
+ function checkCommandsExist(projectDir) {
1467
+ const commandsDir = path4.join(projectDir, ".claude", "commands");
1468
+ const commands = listMdFiles(commandsDir);
1469
+ const passed = commands.length >= 2;
1470
+ return {
1471
+ name: "Commands",
1472
+ passed,
1473
+ score: Math.min(commands.length * 2, 5),
1474
+ maxScore: 5,
1475
+ message: commands.length > 0 ? `${commands.length} commands: ${commands.map((c) => `/${path4.basename(c, ".md")}`).join(", ")}` : "No commands found"
1476
+ };
1477
+ }
1478
+ function checkSafetyHook(projectDir) {
1479
+ const hookPath = path4.join(projectDir, ".claude", "hooks", "block-dangerous-commands.js");
1480
+ const globalHookPath = path4.join(
1481
+ process.env.HOME || os.homedir(),
1482
+ ".claude",
1483
+ "hooks",
1484
+ "block-dangerous-commands.js"
1485
+ );
1486
+ const installed = fs4.existsSync(hookPath) || fs4.existsSync(globalHookPath);
1487
+ return {
1488
+ name: "Safety hook",
1489
+ passed: installed,
1490
+ score: installed ? 5 : 0,
1491
+ maxScore: 5,
1492
+ message: installed ? "Safety hook installed" : "No safety hook \u2014 consider installing via --refresh"
1493
+ };
1494
+ }
1495
+ function checkNoDuplication(projectDir) {
1496
+ const claudeMdPath = path4.join(projectDir, ".claude", "CLAUDE.md");
1497
+ if (!fs4.existsSync(claudeMdPath)) {
1498
+ return {
1499
+ name: "No duplication",
1500
+ passed: true,
1501
+ score: 10,
1502
+ maxScore: 10,
1503
+ message: "CLAUDE.md not found \u2014 nothing to check"
1504
+ };
1505
+ }
1506
+ const claudeMd = fs4.readFileSync(claudeMdPath, "utf-8");
1507
+ const conventionSection = claudeMd.indexOf("## Code Conventions");
1508
+ if (conventionSection === -1) {
1509
+ return {
1510
+ name: "No duplication",
1511
+ passed: true,
1512
+ score: 10,
1513
+ maxScore: 10,
1514
+ message: "No conventions section to check"
1515
+ };
1516
+ }
1517
+ const keywords = ["camelCase", "PascalCase", "named export", "default export", "import type"];
1518
+ const claudeDir = path4.join(projectDir, ".claude");
1519
+ const files = walkMdFiles(claudeDir).filter((f) => !f.endsWith("CLAUDE.md"));
1520
+ let duplications = 0;
1521
+ for (const file of files) {
1522
+ try {
1523
+ const content = fs4.readFileSync(file, "utf-8");
1524
+ for (const kw of keywords) {
1525
+ if (claudeMd.includes(kw) && content.includes(kw)) {
1526
+ duplications++;
1527
+ break;
1528
+ }
1529
+ }
1530
+ } catch {
1531
+ }
1532
+ }
1533
+ const passed = duplications === 0;
1534
+ return {
1535
+ name: "No duplication",
1536
+ passed,
1537
+ score: passed ? 10 : Math.max(0, 10 - duplications * 2),
1538
+ maxScore: 10,
1539
+ message: passed ? "No convention duplication detected" : `${duplications} files duplicate conventions from CLAUDE.md \u2014 run validation`
1540
+ };
1541
+ }
1542
+ function listMdFiles(dir) {
1543
+ if (!fs4.existsSync(dir)) return [];
1544
+ try {
1545
+ return fs4.readdirSync(dir).filter((f) => f.endsWith(".md")).map((f) => path4.join(dir, f));
1546
+ } catch {
1547
+ return [];
1548
+ }
1549
+ }
1550
+ function walkMdFiles(dir) {
1551
+ const files = [];
1552
+ if (!fs4.existsSync(dir)) return files;
1553
+ try {
1554
+ for (const entry of fs4.readdirSync(dir, { withFileTypes: true })) {
1555
+ const fullPath = path4.join(dir, entry.name);
1556
+ if (entry.isDirectory()) {
1557
+ files.push(...walkMdFiles(fullPath));
1558
+ } else if (entry.name.endsWith(".md")) {
1559
+ files.push(fullPath);
1560
+ }
1561
+ }
1562
+ } catch {
1563
+ }
1564
+ return files;
1565
+ }
1566
+
1567
+ // src/portability.ts
1568
+ import fs5 from "fs";
1569
+ import path5 from "path";
1570
+ function exportConfig(projectDir, outputPath) {
1571
+ const claudeDir = path5.join(projectDir, ".claude");
1572
+ const config = {
1573
+ version: "1.0",
1574
+ exportDate: (/* @__PURE__ */ new Date()).toISOString(),
1575
+ projectName: path5.basename(projectDir),
1576
+ techStack: {},
1577
+ claudeMd: readFileOrNull(path5.join(claudeDir, "CLAUDE.md")),
1578
+ settings: readJsonOrNull(path5.join(claudeDir, "settings.json")),
1579
+ skills: readDirFiles(path5.join(claudeDir, "skills")),
1580
+ agents: readDirFiles(path5.join(claudeDir, "agents")),
1581
+ rules: readDirFiles(path5.join(claudeDir, "rules")),
1582
+ commands: readDirFiles(path5.join(claudeDir, "commands")),
1583
+ hooks: readDirFiles(path5.join(claudeDir, "hooks"))
1584
+ };
1585
+ fs5.writeFileSync(outputPath, JSON.stringify(config, null, 2));
1586
+ return config;
1587
+ }
1588
+ function importConfig(inputPath, projectDir, force = false) {
1589
+ let config;
1590
+ try {
1591
+ const content = fs5.readFileSync(inputPath, "utf-8");
1592
+ config = JSON.parse(content);
1593
+ } catch {
1594
+ return [];
1595
+ }
1596
+ const written = [];
1597
+ const claudeDir = path5.join(projectDir, ".claude");
1598
+ if (config.claudeMd) {
1599
+ const dest = path5.join(claudeDir, "CLAUDE.md");
1600
+ if (force || !fs5.existsSync(dest)) {
1601
+ fs5.mkdirSync(claudeDir, { recursive: true });
1602
+ fs5.writeFileSync(dest, config.claudeMd);
1603
+ written.push(".claude/CLAUDE.md");
1604
+ }
1605
+ }
1606
+ if (config.settings) {
1607
+ const dest = path5.join(claudeDir, "settings.json");
1608
+ if (force || !fs5.existsSync(dest)) {
1609
+ fs5.mkdirSync(claudeDir, { recursive: true });
1610
+ fs5.writeFileSync(dest, JSON.stringify(config.settings, null, 2));
1611
+ written.push(".claude/settings.json");
1612
+ }
1613
+ }
1614
+ const dirs = [
1615
+ ["skills", config.skills],
1616
+ ["agents", config.agents],
1617
+ ["rules", config.rules],
1618
+ ["commands", config.commands],
1619
+ ["hooks", config.hooks]
1620
+ ];
1621
+ for (const [dirName, files] of dirs) {
1622
+ if (!files || Object.keys(files).length === 0) continue;
1623
+ const dirPath = path5.join(claudeDir, dirName);
1624
+ fs5.mkdirSync(dirPath, { recursive: true });
1625
+ for (const [fileName, fileContent] of Object.entries(files)) {
1626
+ const dest = path5.resolve(dirPath, fileName);
1627
+ if (!dest.startsWith(dirPath + path5.sep) && dest !== dirPath) continue;
1628
+ if (force || !fs5.existsSync(dest)) {
1629
+ fs5.writeFileSync(dest, fileContent);
1630
+ written.push(`.claude/${dirName}/${fileName}`);
1631
+ }
1632
+ }
1633
+ }
1634
+ return written;
769
1635
  }
770
-
771
- async function main() {
772
- let input = '';
773
- for await (const chunk of process.stdin) input += chunk;
774
-
1636
+ function loadTemplate(templatePath) {
775
1637
  try {
776
- const data = JSON.parse(input);
777
- const { tool_name, tool_input, session_id, cwd, permission_mode } = data;
778
- if (tool_name !== 'Bash') return console.log('{}');
779
-
780
- const cmd = tool_input?.command || '';
781
- const result = checkCommand(cmd);
782
-
783
- if (result.blocked) {
784
- const p = result.pattern;
785
- log({ level: 'BLOCKED', id: p.id, priority: p.level, cmd, session_id, cwd, permission_mode });
786
- return console.log(JSON.stringify({
787
- hookSpecificOutput: {
788
- hookEventName: 'PreToolUse',
789
- permissionDecision: 'deny',
790
- permissionDecisionReason: EMOJIS[p.level] + ' [' + p.id + '] ' + p.reason
791
- }
792
- }));
793
- }
794
- console.log('{}');
795
- } catch (e) {
796
- log({ level: 'ERROR', error: e.message });
797
- console.log('{}');
1638
+ if (!fs5.existsSync(templatePath)) return null;
1639
+ const content = fs5.readFileSync(templatePath, "utf-8");
1640
+ const config = JSON.parse(content);
1641
+ if (!config.version || typeof config.skills !== "object") return null;
1642
+ return config;
1643
+ } catch {
1644
+ return null;
798
1645
  }
799
1646
  }
800
-
801
- if (require.main === module) {
802
- main();
803
- } else {
804
- module.exports = { PATTERNS, LEVELS, SAFETY_LEVEL, checkCommand };
1647
+ function findProjectTemplate(projectDir) {
1648
+ const templatePath = path5.join(projectDir, ".claude-template.json");
1649
+ if (fs5.existsSync(templatePath)) return templatePath;
1650
+ return null;
805
1651
  }
806
- `;
807
- function installHook(rootDir) {
808
- const hooksDir = path3.join(rootDir, ".claude", "hooks");
809
- const hookPath = path3.join(hooksDir, "block-dangerous-commands.js");
810
- const settingsPath = path3.join(rootDir, ".claude", "settings.json");
811
- fs3.mkdirSync(hooksDir, { recursive: true });
812
- fs3.writeFileSync(hookPath, HOOK_SCRIPT);
813
- fs3.chmodSync(hookPath, 493);
1652
+ function readFileOrNull(filePath) {
814
1653
  try {
815
- const existing = fs3.existsSync(settingsPath) ? JSON.parse(fs3.readFileSync(settingsPath, "utf-8")) : {};
816
- existing.hooks = {
817
- ...existing.hooks,
818
- PreToolUse: [
819
- {
820
- matcher: "Bash",
821
- hooks: [
822
- {
823
- type: "command",
824
- command: "node .claude/hooks/block-dangerous-commands.js"
825
- }
826
- ]
1654
+ return fs5.readFileSync(filePath, "utf-8");
1655
+ } catch {
1656
+ return null;
1657
+ }
1658
+ }
1659
+ function readJsonOrNull(filePath) {
1660
+ try {
1661
+ return JSON.parse(fs5.readFileSync(filePath, "utf-8"));
1662
+ } catch {
1663
+ return null;
1664
+ }
1665
+ }
1666
+ function readDirFiles(dirPath) {
1667
+ const result = {};
1668
+ if (!fs5.existsSync(dirPath)) return result;
1669
+ try {
1670
+ for (const entry of fs5.readdirSync(dirPath)) {
1671
+ try {
1672
+ const fullPath = path5.join(dirPath, entry);
1673
+ if (fs5.statSync(fullPath).isFile()) {
1674
+ result[entry] = fs5.readFileSync(fullPath, "utf-8");
827
1675
  }
828
- ]
829
- };
830
- fs3.writeFileSync(settingsPath, JSON.stringify(existing, null, 2));
1676
+ } catch {
1677
+ }
1678
+ }
831
1679
  } catch {
832
1680
  }
1681
+ return result;
833
1682
  }
834
1683
 
835
1684
  // src/prompt.ts
836
- function getAnalysisPrompt(projectInfo) {
1685
+ function getAnalysisPrompt(projectInfo, options = { claudeMdMode: "replace", existingClaudeMd: null }) {
837
1686
  const context = buildContextSection(projectInfo);
838
1687
  const templateVars = buildTemplateVariables(projectInfo);
1688
+ const claudeMdInstructions = buildClaudeMdInstructions(options);
1689
+ const memorySection = options.noMemory ? "" : `
1690
+
1691
+ ${MEMORY_PROMPT}`;
839
1692
  return `${ANALYSIS_PROMPT}
840
1693
 
841
1694
  ${SKILLS_PROMPT}
@@ -844,7 +1697,7 @@ ${AGENTS_PROMPT}
844
1697
 
845
1698
  ${RULES_PROMPT}
846
1699
 
847
- ${COMMANDS_PROMPT}
1700
+ ${COMMANDS_PROMPT}${memorySection}
848
1701
 
849
1702
  ---
850
1703
 
@@ -859,25 +1712,63 @@ ${context}
859
1712
 
860
1713
  ${templateVars}
861
1714
 
1715
+ ${claudeMdInstructions}
1716
+
862
1717
  ---
863
1718
 
864
1719
  ## Execute Now
865
1720
 
866
1721
  1. Read this entire prompt to understand all phases
867
1722
  2. Execute Phase 1 completely - read files, analyze code, gather all data
868
- 3. Execute Phase 2 - generate the CLAUDE.md (max 120 lines) using only discovered information
1723
+ ${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`}
869
1724
  4. Execute Phase 3 - verify quality before writing
870
- 5. Use the Write tool to create \`.claude/CLAUDE.md\` with the final content
1725
+ ${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`}
871
1726
  6. Execute Phase 4 - generate ALL skill files (4 core + framework-specific if detected)
872
- 7. Execute Phase 5 - generate agent files
1727
+ 7. Execute Phase 5 - generate ALL agent files (6 agents)
873
1728
  8. Execute Phase 6 - generate rule files
874
- 9. Execute Phase 7 - generate command files (2 commands: analyze, code-review)
875
- 10. Run the Anti-Redundancy Enforcement checks one final time across ALL generated files \u2014 if any convention is restated, any command is duplicated, or any rule lacks a \`paths:\` filter, fix it before proceeding
876
- 11. Output a brief summary of what was generated and any gaps found
1729
+ 9. Execute Phase 7 - generate ALL command files (6 commands)
1730
+ ${options.noMemory ? `10. Skip memory seeding (--no-memory flag)` : `10. Execute Phase 8 - seed initial memory files`}
1731
+ 11. Run the Anti-Redundancy Enforcement checks one final time across ALL generated files \u2014 if any convention is restated, any command is duplicated, or any rule lacks a \`paths:\` filter, fix it before proceeding
1732
+ 12. Output a brief summary of what was generated and any gaps found
877
1733
 
878
1734
  Do NOT output file contents to stdout. Write all files to disk using the Write tool.
879
1735
  Generate ALL files in a single pass \u2014 do not stop after CLAUDE.md.`;
880
1736
  }
1737
+ function buildClaudeMdInstructions(options) {
1738
+ if (options.claudeMdMode === "keep") {
1739
+ return `---
1740
+
1741
+ ## CLAUDE.md Mode: KEEP
1742
+
1743
+ The user chose to keep their existing CLAUDE.md unchanged.
1744
+ **Do NOT read, modify, or overwrite \`.claude/CLAUDE.md\`.**
1745
+ Generate all other files (skills, agents, rules, commands) normally.
1746
+ Use the existing CLAUDE.md as the source of truth for cross-references.`;
1747
+ }
1748
+ if (options.claudeMdMode === "improve" && options.existingClaudeMd) {
1749
+ return `---
1750
+
1751
+ ## CLAUDE.md Mode: IMPROVE
1752
+
1753
+ The user has an existing CLAUDE.md and wants it improved, not replaced.
1754
+ Here is the current content:
1755
+
1756
+ \`\`\`markdown
1757
+ ${options.existingClaudeMd}
1758
+ \`\`\`
1759
+
1760
+ ### Improvement Rules
1761
+
1762
+ 1. **Preserve all manually-added content** \u2014 sections, notes, and custom rules the user wrote
1763
+ 2. **Enhance with discovered information** \u2014 fill gaps, add missing sections, improve specificity
1764
+ 3. **Fix generic content** \u2014 replace boilerplate with project-specific details found during Phase 1
1765
+ 4. **Update stale references** \u2014 fix file paths, commands, or patterns that no longer match the codebase
1766
+ 5. **Respect the 120-line cap** \u2014 if the file is already near the limit, prioritize density over additions
1767
+ 6. **Keep the user's structure** \u2014 if they organized sections differently from the template, keep their layout
1768
+ 7. **Do NOT remove content you don't understand** \u2014 if a section seems custom or domain-specific, preserve it`;
1769
+ }
1770
+ return "";
1771
+ }
881
1772
  function buildContextSection(projectInfo) {
882
1773
  const { name, description, techStack, fileCount } = projectInfo;
883
1774
  const lines = [];
@@ -1288,7 +2179,7 @@ var SKILLS_PROMPT = `---
1288
2179
  ## Phase 4: Generate Skills
1289
2180
 
1290
2181
  Write each skill file to \`.claude/skills/\` using the Write tool. Every skill must have
1291
- YAML frontmatter with \`name\`, \`description\`, and optionally \`globs\` for file matching.
2182
+ YAML frontmatter with \`name\`, \`description\`, and \`globs\` for auto-triggering when relevant files are open.
1292
2183
 
1293
2184
  **Tailor ALL skills to this specific project** \u2014 use the actual file patterns and
1294
2185
  conventions discovered during Phase 1.
@@ -1350,12 +2241,34 @@ Generate the matching skill ONLY if the framework was detected in the tech stack
1350
2241
 
1351
2242
  - **Rails detected** \u2192 Write \`.claude/skills/rails-patterns.md\` \u2014 MVC, ActiveRecord, concerns, service objects, jobs, mailers, strong parameters.
1352
2243
 
1353
- - **Spring detected** \u2192 Write \`.claude/skills/spring-patterns.md\` \u2014 Beans, controllers, services, repositories, AOP, dependency injection, configuration properties.`;
2244
+ - **Spring detected** \u2192 Write \`.claude/skills/spring-patterns.md\` \u2014 Beans, controllers, services, repositories, AOP, dependency injection, configuration properties.
2245
+
2246
+ ### 4.3 Project-Specific Skills (ONLY if detected)
2247
+
2248
+ Generate additional skills based on detected infrastructure:
2249
+
2250
+ - **Database/ORM detected** (Prisma, Drizzle, TypeORM, SQLAlchemy, Mongoose) \u2192 Write \`.claude/skills/database-patterns.md\` with globs targeting migration/schema files. Content: migration workflow, schema change process, query optimization patterns for the detected ORM, seed data conventions.
2251
+
2252
+ - **Docker detected** \u2192 Write \`.claude/skills/docker-patterns.md\` with globs: \`["**/Dockerfile*", "**/docker-compose*", "**/.dockerignore"]\`. Content: multi-stage build patterns, compose service definitions, volume mounting, health checks, image optimization.
2253
+
2254
+ - **Monorepo detected** \u2192 Write \`.claude/skills/monorepo-patterns.md\` with globs targeting workspace configs. Content: cross-package changes, shared dependency management, workspace protocol, package publishing order.
2255
+
2256
+ - **CI/CD detected** \u2192 Write \`.claude/skills/cicd-patterns.md\` with globs targeting workflow files. Content: workflow modification guidelines, secret handling, deployment patterns, caching strategies for the detected CI platform.
2257
+
2258
+ ### 4.4 Skill Globs Reference
2259
+
2260
+ Every skill MUST include a \`globs\` field in its frontmatter for auto-triggering:
2261
+
2262
+ - \`iterative-development.md\` \u2192 omit globs (methodology, invoked manually)
2263
+ - \`code-deduplication.md\` \u2192 omit globs (methodology, invoked manually)
2264
+ - \`security.md\` \u2192 \`globs: ["**/.env*", "**/secrets/**", "**/auth/**", "**/middleware/**"]\`
2265
+ - \`testing-methodology.md\` \u2192 \`globs: ["**/*.test.*", "**/*.spec.*", "**/tests/**", "**/test/**"]\`
2266
+ - Framework skills \u2192 framework-specific globs (e.g., Next.js: \`["**/app/**", "**/pages/**", "next.config.*"]\`)`;
1354
2267
  var AGENTS_PROMPT = `---
1355
2268
 
1356
2269
  ## Phase 5: Generate Agents
1357
2270
 
1358
- Write 2 agent files to \`.claude/agents/\`.
2271
+ Write 6 agent files to \`.claude/agents/\`.
1359
2272
 
1360
2273
  ### \`.claude/agents/code-reviewer.md\`
1361
2274
 
@@ -1410,7 +2323,121 @@ Body content \u2014 instructions for the test writer agent:
1410
2323
  - Include edge cases: empty inputs, nulls, errors, boundaries
1411
2324
  - Mock external dependencies following project patterns
1412
2325
  - Run tests after writing to verify they pass
1413
- - Do NOT duplicate the testing-methodology skill content. The skill covers test design (what to test, edge cases, organization); this agent covers writing and running tests (framework syntax, assertions, execution).`;
2326
+ - Do NOT duplicate the testing-methodology skill content. The skill covers test design (what to test, edge cases, organization); this agent covers writing and running tests (framework syntax, assertions, execution).
2327
+
2328
+ ### \`.claude/agents/code-simplifier.md\`
2329
+
2330
+ YAML frontmatter:
2331
+ \`\`\`yaml
2332
+ ---
2333
+ name: code-simplifier
2334
+ description: Simplifies and refines code for clarity, consistency, and maintainability while preserving all functionality
2335
+ tools:
2336
+ - Read
2337
+ - Grep
2338
+ - Glob
2339
+ - Write
2340
+ - Edit
2341
+ - "Bash({lint_command})"
2342
+ - "Bash({test_command})"
2343
+ model: sonnet
2344
+ ---
2345
+ \`\`\`
2346
+
2347
+ Body content \u2014 instructions for the code simplifier agent:
2348
+ - Focus on recently modified code (use \`git diff --name-only\` to identify changed files)
2349
+ - Look for: duplicated logic, overly complex conditionals, dead code, inconsistent patterns
2350
+ - Simplify without changing behavior \u2014 preserve ALL existing functionality
2351
+ - Follow conventions in CLAUDE.md
2352
+ - Specific simplifications: extract repeated code into helpers, flatten nested conditionals, remove unused variables/imports, replace verbose patterns with idiomatic equivalents
2353
+ - Run the linter after modifications
2354
+ - Run tests after modifications to verify nothing breaks
2355
+ - Do NOT add features, refactor beyond the changed area, or make "improvements" beyond simplification
2356
+
2357
+ ### \`.claude/agents/explore.md\`
2358
+
2359
+ YAML frontmatter:
2360
+ \`\`\`yaml
2361
+ ---
2362
+ name: explore
2363
+ description: Fast codebase exploration \u2014 find files, search code, answer architecture questions
2364
+ tools:
2365
+ - Read
2366
+ - Grep
2367
+ - Glob
2368
+ disallowed_tools:
2369
+ - Write
2370
+ - Edit
2371
+ - Bash
2372
+ model: haiku
2373
+ ---
2374
+ \`\`\`
2375
+
2376
+ Body content \u2014 instructions for the explore agent:
2377
+ - Use Glob for file pattern matching, Grep for content search, Read for file contents
2378
+ - Answer questions about: where things are defined, how modules connect, what patterns are used
2379
+ - Report findings in a structured format: file paths, relevant code snippets, relationships
2380
+ - When asked "how does X work?": trace the code path from entry point to implementation
2381
+ - When asked "where is X?": search broadly first (Glob for files, Grep for content), then narrow
2382
+ - Never modify files \u2014 this agent is strictly read-only
2383
+ - Be thorough but fast \u2014 use targeted searches, not exhaustive reads
2384
+
2385
+ ### \`.claude/agents/plan.md\`
2386
+
2387
+ YAML frontmatter:
2388
+ \`\`\`yaml
2389
+ ---
2390
+ name: plan
2391
+ description: Designs implementation plans with step-by-step approach and trade-off analysis
2392
+ tools:
2393
+ - Read
2394
+ - Grep
2395
+ - Glob
2396
+ disallowed_tools:
2397
+ - Write
2398
+ - Edit
2399
+ - Bash
2400
+ model: sonnet
2401
+ ---
2402
+ \`\`\`
2403
+
2404
+ Body content \u2014 instructions for the plan agent:
2405
+ - Read relevant source files to understand current architecture
2406
+ - Identify affected files, dependencies, and potential risks
2407
+ - Produce a step-by-step plan with: files to create/modify, approach for each, testing strategy
2408
+ - Consider trade-offs: complexity vs simplicity, performance vs readability
2409
+ - Flag breaking changes or migration requirements
2410
+ - Follow conventions in CLAUDE.md
2411
+ - Output format: numbered steps, each with file path, action (create/modify/delete), and description
2412
+ - Include a "Risks & Considerations" section at the end
2413
+
2414
+ ### \`.claude/agents/docs-writer.md\`
2415
+
2416
+ YAML frontmatter:
2417
+ \`\`\`yaml
2418
+ ---
2419
+ name: docs-writer
2420
+ description: Generates and updates documentation from code analysis
2421
+ tools:
2422
+ - Read
2423
+ - Grep
2424
+ - Glob
2425
+ - Write
2426
+ - Edit
2427
+ disallowed_tools:
2428
+ - Bash
2429
+ model: sonnet
2430
+ ---
2431
+ \`\`\`
2432
+
2433
+ Body content \u2014 instructions for the docs writer agent:
2434
+ - Analyze code changes (specified files or recent changes)
2435
+ - Update relevant documentation: README, API docs, architecture docs, changelogs
2436
+ - Generate JSDoc/docstrings for new public functions, classes, and interfaces
2437
+ - Maintain consistency with existing documentation style
2438
+ - Never fabricate information \u2014 only document what is verifiable from the code
2439
+ - When updating existing docs, preserve the author's structure and voice
2440
+ - For new documentation, follow the project's existing documentation patterns`;
1414
2441
  var RULES_PROMPT = `---
1415
2442
 
1416
2443
  ## Phase 6: Generate Rules
@@ -1475,7 +2502,7 @@ var COMMANDS_PROMPT = `---
1475
2502
 
1476
2503
  ## Phase 7: Generate Commands
1477
2504
 
1478
- Write 2 command files to \`.claude/commands/\`. Each needs YAML frontmatter with
2505
+ Write 6 command files to \`.claude/commands/\`. Each needs YAML frontmatter with
1479
2506
  \`allowed-tools\`, \`description\`, and optionally \`argument-hint\`.
1480
2507
 
1481
2508
  Do NOT generate task management commands (\`task.md\`, \`status.md\`, \`done.md\`) \u2014
@@ -1503,11 +2530,138 @@ Body: This command delegates to the code-reviewer agent for thorough review.
1503
2530
  1. Run \`git diff\` and \`git diff --cached\` to identify staged and unstaged changes
1504
2531
  2. Spawn the \`code-reviewer\` agent to perform the full review
1505
2532
  3. If the agent is unavailable, perform a lightweight review: run the linter and check for obvious issues
1506
- Do NOT duplicate the code-reviewer agent's checklist here \u2014 the agent has the full review criteria.`;
2533
+ Do NOT duplicate the code-reviewer agent's checklist here \u2014 the agent has the full review criteria.
2534
+
2535
+ ### \`.claude/commands/commit.md\`
2536
+ \`\`\`yaml
2537
+ ---
2538
+ allowed-tools: ["Read", "Grep", "Glob", "Bash(git status)", "Bash(git diff)", "Bash(git diff --cached)", "Bash(git log --oneline -10)"]
2539
+ description: "Generate a conventional commit message from staged changes"
2540
+ ---
2541
+ \`\`\`
2542
+ Body:
2543
+ 1. Run \`git diff --cached\` to see staged changes (if nothing staged, run \`git diff\` and suggest what to stage)
2544
+ 2. Run \`git log --oneline -10\` to match existing commit style
2545
+ 3. Analyze the nature of changes: feat, fix, refactor, chore, docs, test, style, perf, ci, build
2546
+ 4. Determine scope from the files changed (e.g., \`cli\`, \`analyzer\`, \`hooks\`)
2547
+ 5. Generate a conventional commit message: \`type(scope): subject\`
2548
+ 6. Include a body if changes are substantial (>50 lines changed)
2549
+ 7. Follow commit conventions in CLAUDE.md
2550
+ 8. Present the message for user review \u2014 do NOT run git commit
2551
+
2552
+ ### \`.claude/commands/fix.md\`
2553
+ \`\`\`yaml
2554
+ ---
2555
+ allowed-tools: ["Read", "Grep", "Glob", "Write", "Edit", "Bash({test_command})", "Bash({lint_command})"]
2556
+ description: "Diagnose and fix a failing test or error"
2557
+ argument-hint: "<error message or test name>"
2558
+ ---
2559
+ \`\`\`
2560
+ Body:
2561
+ 1. If argument is a test name: run that specific test to reproduce the failure
2562
+ 2. If argument is an error message: search codebase for related code
2563
+ 3. Follow 4-phase debugging methodology:
2564
+ - **Reproduce**: Run the failing test/command to see the exact error
2565
+ - **Locate**: Trace the error from the failure point to the source
2566
+ - **Diagnose**: Understand WHY the code fails (not just WHERE)
2567
+ - **Fix**: Apply the minimal fix that resolves the root cause
2568
+ 4. Re-run the failing test to verify the fix
2569
+ 5. Run the full test suite to check for regressions (see Common Commands in CLAUDE.md)
2570
+
2571
+ ### \`.claude/commands/explain.md\`
2572
+ \`\`\`yaml
2573
+ ---
2574
+ allowed-tools: ["Read", "Grep", "Glob"]
2575
+ description: "Deep explanation of a file, module, or concept"
2576
+ argument-hint: "<file path, module name, or concept>"
2577
+ ---
2578
+ \`\`\`
2579
+ Body:
2580
+ 1. Read the specified file or search for the module/concept
2581
+ 2. Trace dependencies (what it imports) and dependents (what imports it)
2582
+ 3. Explain in a structured format:
2583
+ - **Purpose**: What this code does and why it exists
2584
+ - **How It Works**: Step-by-step walkthrough of the logic
2585
+ - **Dependencies**: What it relies on (internal and external)
2586
+ - **Public API**: Exported functions, classes, types with brief descriptions
2587
+ - **Gotchas**: Non-obvious behavior, edge cases, known limitations
2588
+ 4. Use actual code references with file paths
2589
+ 5. Tailor the explanation depth to the complexity of the code
2590
+
2591
+ ### \`.claude/commands/refactor.md\`
2592
+ \`\`\`yaml
2593
+ ---
2594
+ allowed-tools: ["Read", "Grep", "Glob", "Write", "Edit", "Bash({test_command})", "Bash({lint_command})"]
2595
+ description: "Targeted refactoring of a specific area"
2596
+ argument-hint: "<file path or description of what to refactor>"
2597
+ ---
2598
+ \`\`\`
2599
+ Body:
2600
+ 1. Read the target code and understand its current structure
2601
+ 2. Search for ALL references to affected functions/variables/types (Grep the entire project)
2602
+ 3. Plan the refactoring: what changes, what stays, what tests cover it
2603
+ 4. Apply changes incrementally:
2604
+ - Make the structural change
2605
+ - Update all references (imports, usages, tests, docs)
2606
+ - Run linter
2607
+ - Run tests
2608
+ 5. Verify no stale references remain (Grep for old names)
2609
+ 6. Follow conventions in CLAUDE.md`;
2610
+ var MEMORY_PROMPT = `---
2611
+
2612
+ ## Phase 8: Seed Initial Memory
2613
+
2614
+ Claude Code has a persistent memory system at \`.claude/memory/\`. Seed it with
2615
+ factual information discovered during Phase 1 that would be useful in future conversations.
2616
+
2617
+ **Only write memories that cannot be easily derived from reading the code or CLAUDE.md.**
2618
+
2619
+ ### Memory File Format
2620
+
2621
+ Each memory file uses this frontmatter format:
2622
+ \`\`\`markdown
2623
+ ---
2624
+ name: {memory name}
2625
+ description: {one-line description}
2626
+ type: {project | reference}
2627
+ ---
2628
+
2629
+ {memory content}
2630
+ \`\`\`
2631
+
2632
+ ### What to Seed
2633
+
2634
+ 1. **Project memory** (type: \`project\`) \u2014 Write 1-2 files for:
2635
+ - Architecture pattern and rationale (e.g., "Clean Architecture with feature-based modules \u2014 chosen for testability and team scaling")
2636
+ - Primary domain and business context (e.g., "E-commerce platform for B2B wholesale \u2014 domain entities are Company, Order, Product, PriceList")
2637
+
2638
+ 2. **Reference memory** (type: \`reference\`) \u2014 Write 1-2 files for:
2639
+ - CI/CD platform and deployment patterns (e.g., "GitHub Actions deploys to Vercel on push to main, preview deploys on PRs")
2640
+ - External system pointers found in README or config (e.g., "API docs at /docs/api.md, issue tracker is GitHub Issues")
2641
+
2642
+ ### What NOT to Seed
2643
+
2644
+ - Anything already in CLAUDE.md (commands, conventions, file structure)
2645
+ - Anything derivable from config files (package.json, tsconfig, etc.)
2646
+ - Generic information (e.g., "this is a TypeScript project")
2647
+ - Ephemeral state (current bugs, in-progress work)
2648
+
2649
+ ### Where to Write
2650
+
2651
+ Write memory files to \`.claude/memory/\`:
2652
+ - \`.claude/memory/architecture.md\`
2653
+ - \`.claude/memory/domain.md\`
2654
+ - \`.claude/memory/deployment.md\`
2655
+ - \`.claude/memory/references.md\`
2656
+
2657
+ Only write files for which you have genuine, project-specific information.
2658
+ Write a \`.claude/memory/MEMORY.md\` index file with one-line pointers to each memory file.
2659
+
2660
+ Skip this phase entirely if the project is too new or simple to have meaningful memory seeds.`;
1507
2661
 
1508
2662
  // src/validator.ts
1509
- import fs4 from "fs";
1510
- import path4 from "path";
2663
+ import fs6 from "fs";
2664
+ import path6 from "path";
1511
2665
  function extractCommands(claudeMd) {
1512
2666
  const commands = [];
1513
2667
  const match = claudeMd.match(/## Common Commands[\s\S]*?```(?:bash)?\n([\s\S]*?)```/);
@@ -1567,7 +2721,7 @@ function separateFrontmatter(content) {
1567
2721
  };
1568
2722
  }
1569
2723
  function processFile(filePath, commands, fingerprints) {
1570
- const content = fs4.readFileSync(filePath, "utf-8");
2724
+ const content = fs6.readFileSync(filePath, "utf-8");
1571
2725
  const { frontmatter, body } = separateFrontmatter(content);
1572
2726
  const lines = body.split("\n");
1573
2727
  const changes = [];
@@ -1597,18 +2751,18 @@ function processFile(filePath, commands, fingerprints) {
1597
2751
  newLines.push(line);
1598
2752
  }
1599
2753
  if (changes.length > 0) {
1600
- fs4.writeFileSync(filePath, frontmatter + newLines.join("\n"));
2754
+ fs6.writeFileSync(filePath, frontmatter + newLines.join("\n"));
1601
2755
  }
1602
2756
  return changes;
1603
2757
  }
1604
- function walkMdFiles(dir) {
2758
+ function walkMdFiles2(dir) {
1605
2759
  const files = [];
1606
- if (!fs4.existsSync(dir)) return files;
1607
- const entries = fs4.readdirSync(dir, { withFileTypes: true });
2760
+ if (!fs6.existsSync(dir)) return files;
2761
+ const entries = fs6.readdirSync(dir, { withFileTypes: true });
1608
2762
  for (const entry of entries) {
1609
- const fullPath = path4.join(dir, entry.name);
2763
+ const fullPath = path6.join(dir, entry.name);
1610
2764
  if (entry.isDirectory()) {
1611
- files.push(...walkMdFiles(fullPath));
2765
+ files.push(...walkMdFiles2(fullPath));
1612
2766
  } else if (entry.name.endsWith(".md")) {
1613
2767
  files.push(fullPath);
1614
2768
  }
@@ -1622,25 +2776,28 @@ function validateArtifacts(rootDir) {
1622
2776
  duplicationsRemoved: 0,
1623
2777
  changes: []
1624
2778
  };
1625
- const claudeMdPath = path4.join(rootDir, ".claude", "CLAUDE.md");
1626
- if (!fs4.existsSync(claudeMdPath)) return result;
2779
+ const claudeMdPath = path6.join(rootDir, ".claude", "CLAUDE.md");
2780
+ if (!fs6.existsSync(claudeMdPath)) return result;
1627
2781
  try {
1628
- const claudeMd = fs4.readFileSync(claudeMdPath, "utf-8");
2782
+ const claudeMd = fs6.readFileSync(claudeMdPath, "utf-8");
1629
2783
  const commands = extractCommands(claudeMd);
1630
2784
  const fingerprints = extractConventionFingerprints(claudeMd);
1631
2785
  if (commands.length === 0 && fingerprints.length === 0) return result;
1632
- const claudeDir = path4.join(rootDir, ".claude");
1633
- const files = walkMdFiles(claudeDir).filter((f) => !f.endsWith("CLAUDE.md"));
2786
+ const claudeDir = path6.join(rootDir, ".claude");
2787
+ const files = walkMdFiles2(claudeDir).filter((f) => !f.endsWith("CLAUDE.md"));
1634
2788
  for (const filePath of files) {
1635
2789
  result.filesChecked++;
1636
- const changes = processFile(filePath, commands, fingerprints);
1637
- if (changes.length > 0) {
1638
- result.filesModified++;
1639
- result.duplicationsRemoved += changes.length;
1640
- for (const change of changes) {
1641
- change.file = path4.relative(rootDir, filePath);
2790
+ try {
2791
+ const changes = processFile(filePath, commands, fingerprints);
2792
+ if (changes.length > 0) {
2793
+ result.filesModified++;
2794
+ result.duplicationsRemoved += changes.length;
2795
+ for (const change of changes) {
2796
+ change.file = path6.relative(rootDir, filePath);
2797
+ }
2798
+ result.changes.push(...changes);
1642
2799
  }
1643
- result.changes.push(...changes);
2800
+ } catch {
1644
2801
  }
1645
2802
  }
1646
2803
  } catch {
@@ -1650,39 +2807,97 @@ function validateArtifacts(rootDir) {
1650
2807
  }
1651
2808
 
1652
2809
  // src/cli.ts
1653
- var __dirname2 = path5.dirname(fileURLToPath(import.meta.url));
1654
- var VERSION = JSON.parse(
1655
- fs5.readFileSync(path5.join(__dirname2, "..", "package.json"), "utf-8")
1656
- ).version;
2810
+ var VERSION;
2811
+ if (true) {
2812
+ VERSION = "0.17.0";
2813
+ } else {
2814
+ try {
2815
+ const __dirname2 = path7.dirname(fileURLToPath(import.meta.url));
2816
+ VERSION = JSON.parse(
2817
+ fs7.readFileSync(path7.join(__dirname2, "..", "package.json"), "utf-8")
2818
+ ).version;
2819
+ } catch {
2820
+ VERSION = "unknown";
2821
+ }
2822
+ }
1657
2823
  function parseArgs(args) {
2824
+ const findValue = (flag) => {
2825
+ const idx = args.indexOf(flag);
2826
+ if (idx !== -1 && idx + 1 < args.length) return args[idx + 1];
2827
+ return null;
2828
+ };
2829
+ const profileValue = findValue("--profile");
2830
+ const validProfiles = ["solo", "team", "ci"];
2831
+ const profile = profileValue && validProfiles.includes(profileValue) ? profileValue : null;
2832
+ let interactive = !args.includes("--no-interactive") && !args.includes("-y");
2833
+ if (profile === "ci") interactive = false;
1658
2834
  return {
1659
2835
  help: args.includes("-h") || args.includes("--help"),
1660
2836
  version: args.includes("-v") || args.includes("--version"),
1661
2837
  force: args.includes("-f") || args.includes("--force"),
1662
- interactive: !args.includes("--no-interactive") && !args.includes("-y"),
1663
- verbose: args.includes("--verbose") || args.includes("-V")
2838
+ interactive,
2839
+ verbose: args.includes("--verbose") || args.includes("-V"),
2840
+ refresh: args.includes("--refresh"),
2841
+ tune: args.includes("--tune"),
2842
+ check: args.includes("--check"),
2843
+ noMemory: args.includes("--no-memory") || profile === "ci",
2844
+ exportPath: findValue("--export"),
2845
+ importPath: findValue("--import"),
2846
+ template: findValue("--template"),
2847
+ profile
1664
2848
  };
1665
2849
  }
1666
2850
  function getVersion() {
1667
2851
  return VERSION;
1668
2852
  }
2853
+ function isNewerVersion(current, latest) {
2854
+ const parse = (v) => v.replace(/^v/, "").split(".").map(Number);
2855
+ const [cMajor, cMinor, cPatch] = parse(current);
2856
+ const [lMajor, lMinor, lPatch] = parse(latest);
2857
+ if (lMajor !== cMajor) return lMajor > cMajor;
2858
+ if (lMinor !== cMinor) return lMinor > cMinor;
2859
+ return lPatch > cPatch;
2860
+ }
2861
+ function checkForUpdate() {
2862
+ try {
2863
+ const latest = execSync("npm view claude-code-starter version", {
2864
+ encoding: "utf-8",
2865
+ timeout: 5e3,
2866
+ stdio: ["ignore", "pipe", "ignore"]
2867
+ }).trim();
2868
+ if (latest && isNewerVersion(VERSION, latest)) {
2869
+ console.log(pc2.yellow(` Update available: ${VERSION} \u2192 ${latest}`));
2870
+ console.log(pc2.yellow(" Run: npm install -g claude-code-starter@latest"));
2871
+ console.log();
2872
+ }
2873
+ } catch {
2874
+ }
2875
+ }
1669
2876
  function showHelp() {
1670
2877
  console.log(`
1671
- ${pc.cyan("Claude Code Starter")} v${VERSION}
2878
+ ${pc2.cyan("Claude Code Starter")} v${VERSION}
1672
2879
 
1673
2880
  Bootstrap intelligent Claude Code configurations for any repository.
1674
2881
 
1675
- ${pc.bold("USAGE")}
2882
+ ${pc2.bold("USAGE")}
1676
2883
  npx claude-code-starter [OPTIONS]
1677
2884
 
1678
- ${pc.bold("OPTIONS")}
1679
- -h, --help Show this help message
1680
- -v, --version Show version number
1681
- -f, --force Force overwrite existing .claude files
1682
- -y, --no-interactive Skip interactive prompts (use defaults)
1683
- -V, --verbose Show detailed output
1684
-
1685
- ${pc.bold("WHAT IT DOES")}
2885
+ ${pc2.bold("OPTIONS")}
2886
+ -h, --help Show this help message
2887
+ -v, --version Show version number
2888
+ -f, --force Force overwrite existing .claude files
2889
+ -y, --no-interactive Skip interactive prompts (use defaults)
2890
+ -V, --verbose Show detailed output
2891
+ --refresh Refresh settings.json, hooks, and statusline without re-running Claude analysis
2892
+ --tune Re-analyze existing .claude/ setup and show health report
2893
+ --check Audit .claude/ directory and exit with score (CI-friendly)
2894
+ --no-memory Skip memory seeding during analysis
2895
+ --export <path> Export .claude/ config as portable JSON archive
2896
+ --import <path> Import a config archive into .claude/
2897
+ --template <path> Bootstrap from a template file
2898
+ --profile <name> Generation profile: solo, team, or ci
2899
+
2900
+ ${pc2.bold("WHAT IT DOES")}
1686
2901
  1. Analyzes your repository's tech stack
1687
2902
  2. Launches Claude CLI to deeply analyze your codebase
1688
2903
  3. Generates all .claude/ configuration files:
@@ -1692,53 +2907,53 @@ ${pc.bold("WHAT IT DOES")}
1692
2907
  - Rules matching your code style
1693
2908
  - Commands for analysis and code review
1694
2909
 
1695
- ${pc.bold("REQUIREMENTS")}
2910
+ ${pc2.bold("REQUIREMENTS")}
1696
2911
  Claude CLI must be installed: https://claude.ai/download
1697
2912
 
1698
- ${pc.bold("MORE INFO")}
2913
+ ${pc2.bold("MORE INFO")}
1699
2914
  https://github.com/cassmtnr/claude-code-starter
1700
2915
  `);
1701
2916
  }
1702
2917
  function showBanner() {
1703
2918
  console.log();
1704
- console.log(pc.bold("Claude Code Starter") + pc.gray(` v${VERSION}`));
1705
- console.log(pc.gray("Intelligent AI-Assisted Development Setup"));
2919
+ console.log(pc2.bold("Claude Code Starter") + pc2.gray(` v${VERSION}`));
2920
+ console.log(pc2.gray("Intelligent AI-Assisted Development Setup"));
1706
2921
  console.log();
1707
2922
  }
1708
2923
  function showTechStack(projectInfo, verbose) {
1709
2924
  const { techStack } = projectInfo;
1710
- console.log(pc.bold("Tech Stack"));
2925
+ console.log(pc2.bold("Tech Stack"));
1711
2926
  console.log();
1712
2927
  if (techStack.primaryLanguage) {
1713
- console.log(` ${pc.bold("Language:")} ${formatLanguage(techStack.primaryLanguage)}`);
2928
+ console.log(` ${pc2.bold("Language:")} ${formatLanguage(techStack.primaryLanguage)}`);
1714
2929
  }
1715
2930
  if (techStack.primaryFramework) {
1716
- console.log(` ${pc.bold("Framework:")} ${formatFramework(techStack.primaryFramework)}`);
2931
+ console.log(` ${pc2.bold("Framework:")} ${formatFramework(techStack.primaryFramework)}`);
1717
2932
  }
1718
2933
  if (techStack.packageManager) {
1719
- console.log(` ${pc.bold("Package Manager:")} ${techStack.packageManager}`);
2934
+ console.log(` ${pc2.bold("Package Manager:")} ${techStack.packageManager}`);
1720
2935
  }
1721
2936
  if (techStack.testingFramework) {
1722
- console.log(` ${pc.bold("Testing:")} ${techStack.testingFramework}`);
2937
+ console.log(` ${pc2.bold("Testing:")} ${techStack.testingFramework}`);
1723
2938
  }
1724
2939
  if (verbose) {
1725
2940
  if (techStack.linter) {
1726
- console.log(` ${pc.bold("Linter:")} ${techStack.linter}`);
2941
+ console.log(` ${pc2.bold("Linter:")} ${techStack.linter}`);
1727
2942
  }
1728
2943
  if (techStack.formatter) {
1729
- console.log(` ${pc.bold("Formatter:")} ${techStack.formatter}`);
2944
+ console.log(` ${pc2.bold("Formatter:")} ${techStack.formatter}`);
1730
2945
  }
1731
2946
  if (techStack.bundler) {
1732
- console.log(` ${pc.bold("Bundler:")} ${techStack.bundler}`);
2947
+ console.log(` ${pc2.bold("Bundler:")} ${techStack.bundler}`);
1733
2948
  }
1734
2949
  if (techStack.isMonorepo) {
1735
- console.log(` ${pc.bold("Monorepo:")} yes`);
2950
+ console.log(` ${pc2.bold("Monorepo:")} yes`);
1736
2951
  }
1737
2952
  if (techStack.hasDocker) {
1738
- console.log(` ${pc.bold("Docker:")} yes`);
2953
+ console.log(` ${pc2.bold("Docker:")} yes`);
1739
2954
  }
1740
2955
  if (techStack.hasCICD) {
1741
- console.log(` ${pc.bold("CI/CD:")} ${techStack.cicdPlatform}`);
2956
+ console.log(` ${pc2.bold("CI/CD:")} ${techStack.cicdPlatform}`);
1742
2957
  }
1743
2958
  }
1744
2959
  console.log();
@@ -1814,9 +3029,9 @@ async function promptNewProject(args) {
1814
3029
  if (!args.interactive) {
1815
3030
  return null;
1816
3031
  }
1817
- console.log(pc.yellow("New project detected - let's set it up!"));
3032
+ console.log(pc2.yellow("New project detected - let's set it up!"));
1818
3033
  console.log();
1819
- const descResponse = await prompts({
3034
+ const descResponse = await prompts2({
1820
3035
  type: "text",
1821
3036
  name: "description",
1822
3037
  message: "What are you building?",
@@ -1825,7 +3040,7 @@ async function promptNewProject(args) {
1825
3040
  if (!descResponse.description) {
1826
3041
  return null;
1827
3042
  }
1828
- const langResponse = await prompts({
3043
+ const langResponse = await prompts2({
1829
3044
  type: "select",
1830
3045
  name: "primaryLanguage",
1831
3046
  message: "Primary language?",
@@ -1846,34 +3061,34 @@ async function promptNewProject(args) {
1846
3061
  });
1847
3062
  const lang = langResponse.primaryLanguage || "typescript";
1848
3063
  const fwChoices = frameworkChoices[lang] || defaultFrameworkChoices;
1849
- const fwResponse = await prompts({
3064
+ const fwResponse = await prompts2({
1850
3065
  type: "select",
1851
3066
  name: "framework",
1852
3067
  message: "Framework?",
1853
3068
  choices: fwChoices
1854
3069
  });
1855
3070
  const pmChoices = getPackageManagerChoices(lang);
1856
- const pmResponse = await prompts({
3071
+ const pmResponse = await prompts2({
1857
3072
  type: "select",
1858
3073
  name: "packageManager",
1859
3074
  message: "Package manager?",
1860
3075
  choices: pmChoices
1861
3076
  });
1862
3077
  const testChoices = getTestingFrameworkChoices(lang);
1863
- const testResponse = await prompts({
3078
+ const testResponse = await prompts2({
1864
3079
  type: "select",
1865
3080
  name: "testingFramework",
1866
3081
  message: "Testing framework?",
1867
3082
  choices: testChoices
1868
3083
  });
1869
3084
  const lintChoices = getLinterFormatterChoices(lang);
1870
- const lintResponse = await prompts({
3085
+ const lintResponse = await prompts2({
1871
3086
  type: "select",
1872
3087
  name: "linter",
1873
3088
  message: "Linter/Formatter?",
1874
3089
  choices: lintChoices
1875
3090
  });
1876
- const typeResponse = await prompts({
3091
+ const typeResponse = await prompts2({
1877
3092
  type: "select",
1878
3093
  name: "projectType",
1879
3094
  message: "Project type?",
@@ -1981,7 +3196,6 @@ function getLinterFormatterChoices(lang) {
1981
3196
  return [
1982
3197
  { title: "Biome", value: "biome" },
1983
3198
  { title: "ESLint + Prettier", value: "eslint" },
1984
- { title: "ESLint", value: "eslint" },
1985
3199
  { title: "None", value: null }
1986
3200
  ];
1987
3201
  }
@@ -2112,12 +3326,12 @@ function checkClaudeCli() {
2112
3326
  return false;
2113
3327
  }
2114
3328
  }
2115
- function runClaudeAnalysis(projectDir, projectInfo) {
3329
+ function runClaudeAnalysis(projectDir, projectInfo, options = { claudeMdMode: "replace", existingClaudeMd: null }) {
2116
3330
  return new Promise((resolve) => {
2117
- const prompt = getAnalysisPrompt(projectInfo);
2118
- console.log(pc.cyan("Launching Claude for deep project analysis..."));
3331
+ const prompt = getAnalysisPrompt(projectInfo, options);
3332
+ console.log(pc2.cyan("Launching Claude for deep project analysis..."));
2119
3333
  console.log(
2120
- pc.gray("Claude will read your codebase and generate all .claude/ configuration files")
3334
+ pc2.gray("Claude will read your codebase and generate all .claude/ configuration files")
2121
3335
  );
2122
3336
  console.log();
2123
3337
  const spinner = ora({
@@ -2132,6 +3346,8 @@ function runClaudeAnalysis(projectDir, projectInfo) {
2132
3346
  "claude",
2133
3347
  [
2134
3348
  "-p",
3349
+ "--verbose",
3350
+ "--output-format=stream-json",
2135
3351
  "--allowedTools",
2136
3352
  "Read",
2137
3353
  "--allowedTools",
@@ -2148,35 +3364,80 @@ function runClaudeAnalysis(projectDir, projectInfo) {
2148
3364
  stdio: ["pipe", "pipe", "pipe"]
2149
3365
  }
2150
3366
  );
3367
+ const cleanup = () => {
3368
+ child.kill("SIGTERM");
3369
+ };
3370
+ process.on("SIGINT", cleanup);
3371
+ process.on("SIGTERM", cleanup);
3372
+ let stdoutBuffer = "";
3373
+ child.stdout.on("data", (chunk) => {
3374
+ stdoutBuffer += chunk.toString();
3375
+ const lines = stdoutBuffer.split("\n");
3376
+ stdoutBuffer = lines.pop() || "";
3377
+ for (const line of lines) {
3378
+ if (!line.trim()) continue;
3379
+ try {
3380
+ const event = JSON.parse(line);
3381
+ if (event.type === "assistant" && Array.isArray(event.message?.content)) {
3382
+ for (const block of event.message.content) {
3383
+ if (block.type === "tool_use" && block.name && block.input) {
3384
+ const toolName = block.name;
3385
+ const toolInput = block.input;
3386
+ const filePath = toolInput.file_path || toolInput.path || toolInput.pattern || "";
3387
+ const shortPath = filePath.split("/").slice(-2).join("/");
3388
+ const action = toolName === "Write" || toolName === "Edit" ? "Writing" : "Reading";
3389
+ if (shortPath) {
3390
+ spinner.text = `${action} ${shortPath}...`;
3391
+ } else {
3392
+ spinner.text = `Using ${toolName}...`;
3393
+ }
3394
+ }
3395
+ }
3396
+ }
3397
+ } catch {
3398
+ }
3399
+ }
3400
+ });
3401
+ child.stdin.on("error", () => {
3402
+ });
2151
3403
  child.stdin.write(prompt);
2152
3404
  child.stdin.end();
3405
+ let stderrOutput = "";
3406
+ child.stderr.on("data", (chunk) => {
3407
+ stderrOutput += chunk.toString();
3408
+ });
2153
3409
  child.on("error", (err) => {
2154
3410
  spinner.fail(`Failed to launch Claude CLI: ${err.message}`);
2155
3411
  resolve(false);
2156
3412
  });
2157
3413
  child.on("close", (code) => {
3414
+ process.off("SIGINT", cleanup);
3415
+ process.off("SIGTERM", cleanup);
2158
3416
  if (code === 0) {
2159
3417
  spinner.succeed("Claude analysis complete!");
2160
3418
  resolve(true);
2161
3419
  } else {
2162
3420
  spinner.fail(`Claude exited with code ${code}`);
3421
+ if (stderrOutput.trim()) {
3422
+ console.error(pc2.gray(stderrOutput.trim()));
3423
+ }
2163
3424
  resolve(false);
2164
3425
  }
2165
3426
  });
2166
3427
  });
2167
3428
  }
2168
3429
  function getGeneratedFiles(projectDir) {
2169
- const claudeDir = path5.join(projectDir, ".claude");
3430
+ const claudeDir = path7.join(projectDir, ".claude");
2170
3431
  const files = [];
2171
3432
  function walk(dir) {
2172
- if (!fs5.existsSync(dir)) return;
2173
- const entries = fs5.readdirSync(dir, { withFileTypes: true });
3433
+ if (!fs7.existsSync(dir)) return;
3434
+ const entries = fs7.readdirSync(dir, { withFileTypes: true });
2174
3435
  for (const entry of entries) {
2175
- const fullPath = path5.join(dir, entry.name);
3436
+ const fullPath = path7.join(dir, entry.name);
2176
3437
  if (entry.isDirectory()) {
2177
3438
  walk(fullPath);
2178
3439
  } else {
2179
- files.push(path5.relative(projectDir, fullPath));
3440
+ files.push(path7.relative(projectDir, fullPath));
2180
3441
  }
2181
3442
  }
2182
3443
  }
@@ -2194,11 +3455,132 @@ async function main() {
2194
3455
  process.exit(0);
2195
3456
  }
2196
3457
  showBanner();
3458
+ checkForUpdate();
2197
3459
  const projectDir = process.cwd();
2198
- console.log(pc.gray("Analyzing repository..."));
3460
+ if (args.check) {
3461
+ const claudeDir = path7.join(projectDir, ".claude");
3462
+ if (!fs7.existsSync(claudeDir)) {
3463
+ console.error(pc2.red("No .claude/ directory found. Run claude-code-starter first."));
3464
+ process.exit(1);
3465
+ }
3466
+ const result = checkHealth(projectDir);
3467
+ console.log(pc2.bold("Health Check"));
3468
+ console.log();
3469
+ for (const item of result.items) {
3470
+ const icon = item.passed ? pc2.green("PASS") : pc2.red("FAIL");
3471
+ console.log(` ${icon} ${item.name} (${item.score}/${item.maxScore})`);
3472
+ console.log(pc2.gray(` ${item.message}`));
3473
+ }
3474
+ console.log();
3475
+ const pct = Math.round(result.score / result.maxScore * 100);
3476
+ const scoreColor = pct >= 70 ? pc2.green : pct >= 40 ? pc2.yellow : pc2.red;
3477
+ console.log(scoreColor(`Score: ${result.score}/${result.maxScore} (${pct}%)`));
3478
+ process.exit(pct >= 60 ? 0 : 1);
3479
+ }
3480
+ if (args.tune) {
3481
+ const claudeDir = path7.join(projectDir, ".claude");
3482
+ if (!fs7.existsSync(claudeDir)) {
3483
+ console.error(pc2.red("No .claude/ directory found. Run claude-code-starter first."));
3484
+ process.exit(1);
3485
+ }
3486
+ console.log(pc2.gray("Analyzing existing .claude/ configuration..."));
3487
+ console.log();
3488
+ const projectInfo2 = analyzeRepository(projectDir);
3489
+ showTechStack(projectInfo2, args.verbose);
3490
+ const result = checkHealth(projectDir);
3491
+ console.log(pc2.bold("Configuration Health"));
3492
+ console.log();
3493
+ for (const item of result.items) {
3494
+ const icon = item.passed ? pc2.green("PASS") : pc2.yellow("WARN");
3495
+ console.log(` ${icon} ${item.name} (${item.score}/${item.maxScore})`);
3496
+ console.log(pc2.gray(` ${item.message}`));
3497
+ }
3498
+ console.log();
3499
+ const pct = Math.round(result.score / result.maxScore * 100);
3500
+ const scoreColor = pct >= 70 ? pc2.green : pct >= 40 ? pc2.yellow : pc2.red;
3501
+ console.log(scoreColor(`Score: ${result.score}/${result.maxScore} (${pct}%)`));
3502
+ const failing = result.items.filter((i) => !i.passed);
3503
+ if (failing.length > 0) {
3504
+ console.log();
3505
+ console.log(pc2.bold("Suggestions:"));
3506
+ for (const item of failing) {
3507
+ console.log(` - ${item.message}`);
3508
+ }
3509
+ console.log();
3510
+ console.log(
3511
+ pc2.gray("Run claude-code-starter again to regenerate, or --refresh for settings only")
3512
+ );
3513
+ }
3514
+ return;
3515
+ }
3516
+ if (args.exportPath) {
3517
+ const claudeDir = path7.join(projectDir, ".claude");
3518
+ if (!fs7.existsSync(claudeDir)) {
3519
+ console.error(pc2.red("No .claude/ directory found. Nothing to export."));
3520
+ process.exit(1);
3521
+ }
3522
+ const config = exportConfig(projectDir, args.exportPath);
3523
+ const fileCount = Object.keys(config.skills).length + Object.keys(config.agents).length + Object.keys(config.rules).length + Object.keys(config.commands).length + Object.keys(config.hooks).length + (config.claudeMd ? 1 : 0) + (config.settings ? 1 : 0);
3524
+ console.log(pc2.green(`Exported ${fileCount} files to ${args.exportPath}`));
3525
+ return;
3526
+ }
3527
+ if (args.importPath) {
3528
+ if (!fs7.existsSync(args.importPath)) {
3529
+ console.error(pc2.red(`File not found: ${args.importPath}`));
3530
+ process.exit(1);
3531
+ }
3532
+ const written = importConfig(args.importPath, projectDir, args.force);
3533
+ if (written.length === 0) {
3534
+ console.log(pc2.yellow("No files written (all already exist). Use -f to overwrite."));
3535
+ } else {
3536
+ console.log(pc2.green(`Imported ${written.length} files:`));
3537
+ for (const file of written) {
3538
+ console.log(pc2.green(` + ${file}`));
3539
+ }
3540
+ }
3541
+ return;
3542
+ }
3543
+ const templatePath = args.template || findProjectTemplate(projectDir);
3544
+ if (templatePath) {
3545
+ const template = loadTemplate(templatePath);
3546
+ if (!template) {
3547
+ console.error(pc2.red(`Invalid or missing template: ${templatePath}`));
3548
+ process.exit(1);
3549
+ }
3550
+ const tmpPath = path7.join(os2.tmpdir(), `.claude-template-import-${Date.now()}.json`);
3551
+ try {
3552
+ fs7.writeFileSync(tmpPath, JSON.stringify(template, null, 2));
3553
+ const written = importConfig(tmpPath, projectDir, args.force);
3554
+ console.log(pc2.green(`Applied template: ${written.length} files written`));
3555
+ for (const file of written) {
3556
+ console.log(pc2.green(` + ${file}`));
3557
+ }
3558
+ } finally {
3559
+ try {
3560
+ fs7.unlinkSync(tmpPath);
3561
+ } catch {
3562
+ }
3563
+ }
3564
+ return;
3565
+ }
3566
+ console.log(pc2.gray("Analyzing repository..."));
2199
3567
  console.log();
2200
3568
  const projectInfo = analyzeRepository(projectDir);
2201
3569
  showTechStack(projectInfo, args.verbose);
3570
+ if (args.refresh) {
3571
+ console.log(pc2.gray("Setting up .claude/ directory structure..."));
3572
+ console.log();
3573
+ writeSettings(projectDir, projectInfo.techStack);
3574
+ ensureDirectories(projectDir);
3575
+ console.log(pc2.green("Updated:"));
3576
+ console.log(pc2.green(" + .claude/settings.json"));
3577
+ console.log();
3578
+ await promptExtras(projectDir);
3579
+ console.log();
3580
+ console.log(pc2.green("Done!"));
3581
+ console.log();
3582
+ return;
3583
+ }
2202
3584
  let preferences = null;
2203
3585
  if (!projectInfo.isExisting) {
2204
3586
  preferences = await promptNewProject(args);
@@ -2223,71 +3605,87 @@ async function main() {
2223
3605
  projectInfo.description = preferences.description;
2224
3606
  }
2225
3607
  } else {
2226
- console.log(pc.gray(`Existing project with ${projectInfo.fileCount} source files`));
3608
+ console.log(pc2.gray(`Existing project with ${projectInfo.fileCount} source files`));
2227
3609
  console.log();
2228
3610
  }
2229
- if (projectInfo.techStack.hasClaudeConfig && !args.force) {
2230
- console.log(pc.yellow("Existing .claude/ configuration detected"));
2231
- console.log();
2232
- if (args.interactive) {
2233
- const { proceed } = await prompts({
2234
- type: "confirm",
2235
- name: "proceed",
2236
- message: "Update existing configuration?",
2237
- initial: true
3611
+ let claudeMdMode = "replace";
3612
+ let existingClaudeMd = null;
3613
+ const claudeMdPath = path7.join(projectDir, ".claude", "CLAUDE.md");
3614
+ if (fs7.existsSync(claudeMdPath)) {
3615
+ existingClaudeMd = fs7.readFileSync(claudeMdPath, "utf-8");
3616
+ if (args.force) {
3617
+ claudeMdMode = "replace";
3618
+ } else if (args.interactive) {
3619
+ console.log(pc2.yellow("Existing CLAUDE.md detected"));
3620
+ console.log();
3621
+ const { mode } = await prompts2({
3622
+ type: "select",
3623
+ name: "mode",
3624
+ message: "How should we handle the existing CLAUDE.md?",
3625
+ choices: [
3626
+ { title: "Improve \u2014 scan and enhance the existing file", value: "improve" },
3627
+ { title: "Replace \u2014 generate a new one from scratch", value: "replace" },
3628
+ { title: "Keep \u2014 leave CLAUDE.md as-is, regenerate other files", value: "keep" }
3629
+ ],
3630
+ initial: 0
2238
3631
  });
2239
- if (!proceed) {
2240
- console.log(pc.gray("Cancelled. Use --force to overwrite."));
3632
+ if (mode === void 0) {
3633
+ console.log(pc2.gray("Cancelled."));
2241
3634
  process.exit(0);
2242
3635
  }
3636
+ claudeMdMode = mode;
2243
3637
  }
2244
3638
  console.log();
2245
3639
  }
2246
3640
  if (!checkClaudeCli()) {
2247
- console.error(pc.red("Claude CLI is required but not found."));
2248
- console.error(pc.gray("Install it from: https://claude.ai/download"));
3641
+ console.error(pc2.red("Claude CLI is required but not found."));
3642
+ console.error(pc2.gray("Install it from: https://claude.ai/download"));
2249
3643
  process.exit(1);
2250
3644
  }
2251
- console.log(pc.gray("Setting up .claude/ directory structure..."));
3645
+ console.log(pc2.gray("Setting up .claude/ directory structure..."));
2252
3646
  console.log();
2253
- writeSettings(projectDir, projectInfo.techStack);
3647
+ writeSettings(projectDir, projectInfo.techStack, args.force);
2254
3648
  ensureDirectories(projectDir);
2255
- console.log(pc.green("Created:"));
2256
- console.log(pc.green(" + .claude/settings.json"));
3649
+ console.log(pc2.green("Created:"));
3650
+ console.log(pc2.green(" + .claude/settings.json"));
2257
3651
  console.log();
2258
- const success = await runClaudeAnalysis(projectDir, projectInfo);
3652
+ const success = await runClaudeAnalysis(projectDir, projectInfo, {
3653
+ claudeMdMode,
3654
+ existingClaudeMd: claudeMdMode === "improve" ? existingClaudeMd : null,
3655
+ noMemory: args.noMemory
3656
+ });
2259
3657
  if (!success) {
2260
- console.error(pc.red("Claude analysis failed. Please try again."));
3658
+ console.error(pc2.red("Claude analysis failed. Please try again."));
2261
3659
  process.exit(1);
2262
3660
  }
2263
3661
  const validation = validateArtifacts(projectDir);
2264
3662
  if (validation.duplicationsRemoved > 0) {
2265
3663
  console.log(
2266
- pc.gray(
3664
+ pc2.gray(
2267
3665
  ` Deduplication: removed ${validation.duplicationsRemoved} redundancies from ${validation.filesModified} files`
2268
3666
  )
2269
3667
  );
2270
3668
  }
2271
3669
  const generatedFiles = getGeneratedFiles(projectDir);
2272
3670
  console.log();
2273
- console.log(pc.green(`Done! (${generatedFiles.length} files)`));
3671
+ console.log(pc2.green(`Done! (${generatedFiles.length} files)`));
2274
3672
  console.log();
2275
- console.log(pc.bold("Generated for your stack:"));
3673
+ console.log(pc2.bold("Generated for your stack:"));
2276
3674
  const skills = generatedFiles.filter((f) => f.includes("/skills/"));
2277
3675
  const agents = generatedFiles.filter((f) => f.includes("/agents/"));
2278
3676
  const rules = generatedFiles.filter((f) => f.includes("/rules/"));
2279
3677
  const commands = generatedFiles.filter((f) => f.includes("/commands/"));
2280
3678
  if (generatedFiles.some((f) => f.endsWith("CLAUDE.md"))) {
2281
- console.log(pc.cyan(" CLAUDE.md (deep analysis by Claude)"));
3679
+ console.log(pc2.cyan(" CLAUDE.md (deep analysis by Claude)"));
2282
3680
  }
2283
3681
  if (skills.length > 0) {
2284
3682
  console.log(
2285
- ` ${skills.length} skills (${skills.map((s) => path5.basename(s, ".md")).join(", ")})`
3683
+ ` ${skills.length} skills (${skills.map((s) => path7.basename(s, ".md")).join(", ")})`
2286
3684
  );
2287
3685
  }
2288
3686
  if (agents.length > 0) {
2289
3687
  console.log(
2290
- ` ${agents.length} agents (${agents.map((a) => path5.basename(a, ".md")).join(", ")})`
3688
+ ` ${agents.length} agents (${agents.map((a) => path7.basename(a, ".md")).join(", ")})`
2291
3689
  );
2292
3690
  }
2293
3691
  if (rules.length > 0) {
@@ -2299,32 +3697,22 @@ async function main() {
2299
3697
  console.log();
2300
3698
  if (args.interactive) {
2301
3699
  console.log();
2302
- const { installSafetyHook } = await prompts({
2303
- type: "confirm",
2304
- name: "installSafetyHook",
2305
- message: "Add a safety hook to block dangerous commands? (git push, rm -rf, etc.)",
2306
- initial: true
2307
- });
2308
- if (installSafetyHook) {
2309
- installHook(projectDir);
2310
- console.log(pc.green(" + .claude/hooks/block-dangerous-commands.js"));
2311
- console.log(pc.gray(" Blocks destructive Bash commands before execution"));
2312
- }
3700
+ await promptExtras(projectDir);
2313
3701
  }
2314
3702
  console.log();
2315
- console.log(`${pc.cyan("Next step:")} Run ${pc.bold("claude")} to start working!`);
3703
+ console.log(`${pc2.cyan("Next step:")} Run ${pc2.bold("claude")} to start working!`);
2316
3704
  console.log();
2317
3705
  console.log(
2318
- pc.gray(
3706
+ pc2.gray(
2319
3707
  "Your .claude/ files were generated by deep analysis - review them with: ls -la .claude/"
2320
3708
  )
2321
3709
  );
2322
3710
  }
2323
3711
  try {
2324
- const isMain = process.argv[1] && fs5.realpathSync(process.argv[1]) === fileURLToPath(import.meta.url);
3712
+ const isMain = process.argv[1] && fs7.realpathSync(process.argv[1]) === fileURLToPath(import.meta.url);
2325
3713
  if (isMain) {
2326
3714
  main().catch((err) => {
2327
- console.error(pc.red("Error:"), err.message);
3715
+ console.error(pc2.red("Error:"), err.message);
2328
3716
  if (process.env.DEBUG) {
2329
3717
  console.error(err.stack);
2330
3718
  }
@@ -2335,9 +3723,11 @@ try {
2335
3723
  }
2336
3724
  export {
2337
3725
  checkClaudeCli,
3726
+ checkForUpdate,
2338
3727
  formatFramework,
2339
3728
  formatLanguage,
2340
3729
  getVersion,
3730
+ isNewerVersion,
2341
3731
  mapFormatter,
2342
3732
  parseArgs,
2343
3733
  promptNewProject,