claude-code-starter 0.15.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +563 -216
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -6,15 +6,15 @@ import fs5 from "fs";
|
|
|
6
6
|
import path5 from "path";
|
|
7
7
|
import { fileURLToPath } from "url";
|
|
8
8
|
import ora from "ora";
|
|
9
|
-
import
|
|
10
|
-
import
|
|
9
|
+
import pc2 from "picocolors";
|
|
10
|
+
import prompts2 from "prompts";
|
|
11
11
|
|
|
12
12
|
// src/analyzer.ts
|
|
13
13
|
import fs from "fs";
|
|
14
14
|
import path from "path";
|
|
15
15
|
function analyzeRepository(rootDir) {
|
|
16
16
|
const techStack = detectTechStack(rootDir);
|
|
17
|
-
const fileCount = countSourceFiles(rootDir
|
|
17
|
+
const fileCount = countSourceFiles(rootDir);
|
|
18
18
|
const packageJson = readPackageJson(rootDir);
|
|
19
19
|
return {
|
|
20
20
|
isExisting: fileCount > 0,
|
|
@@ -37,7 +37,7 @@ function detectTechStack(rootDir) {
|
|
|
37
37
|
const linter = detectLinter(packageJson, files);
|
|
38
38
|
const formatter = detectFormatter(packageJson, files);
|
|
39
39
|
const bundler = detectBundler(packageJson, files);
|
|
40
|
-
const isMonorepo = detectMonorepo(
|
|
40
|
+
const isMonorepo = detectMonorepo(files, packageJson);
|
|
41
41
|
const hasDocker = files.includes("Dockerfile") || files.includes("docker-compose.yml") || files.includes("docker-compose.yaml");
|
|
42
42
|
const { hasCICD, cicdPlatform } = detectCICD(rootDir, files);
|
|
43
43
|
const { hasClaudeConfig, existingClaudeFiles } = detectExistingClaudeConfig(rootDir);
|
|
@@ -59,6 +59,12 @@ function detectTechStack(rootDir) {
|
|
|
59
59
|
existingClaudeFiles
|
|
60
60
|
};
|
|
61
61
|
}
|
|
62
|
+
function getAllDeps(packageJson) {
|
|
63
|
+
return {
|
|
64
|
+
...packageJson.dependencies || {},
|
|
65
|
+
...packageJson.devDependencies || {}
|
|
66
|
+
};
|
|
67
|
+
}
|
|
62
68
|
function readPackageJson(rootDir) {
|
|
63
69
|
const packageJsonPath = path.join(rootDir, "package.json");
|
|
64
70
|
if (!fs.existsSync(packageJsonPath)) return null;
|
|
@@ -271,10 +277,7 @@ function detectPackageManager(files) {
|
|
|
271
277
|
}
|
|
272
278
|
function detectTestingFramework(packageJson, files) {
|
|
273
279
|
if (packageJson) {
|
|
274
|
-
const allDeps =
|
|
275
|
-
...packageJson.dependencies || {},
|
|
276
|
-
...packageJson.devDependencies || {}
|
|
277
|
-
};
|
|
280
|
+
const allDeps = getAllDeps(packageJson);
|
|
278
281
|
if (allDeps.vitest) return "vitest";
|
|
279
282
|
if (allDeps.jest) return "jest";
|
|
280
283
|
if (allDeps.mocha) return "mocha";
|
|
@@ -286,7 +289,8 @@ function detectTestingFramework(packageJson, files) {
|
|
|
286
289
|
if (files.includes("pytest.ini") || files.includes("conftest.py")) return "pytest";
|
|
287
290
|
if (files.includes("go.mod")) return "go-test";
|
|
288
291
|
if (files.includes("Cargo.toml")) return "rust-test";
|
|
289
|
-
if (files.includes("
|
|
292
|
+
if (files.includes(".rspec")) return "rspec";
|
|
293
|
+
if (files.includes("Gemfile") && files.includes("spec")) return "rspec";
|
|
290
294
|
return null;
|
|
291
295
|
}
|
|
292
296
|
function detectLinter(packageJson, files) {
|
|
@@ -297,10 +301,7 @@ function detectLinter(packageJson, files) {
|
|
|
297
301
|
}
|
|
298
302
|
if (files.includes("biome.json") || files.includes("biome.jsonc")) return "biome";
|
|
299
303
|
if (packageJson) {
|
|
300
|
-
const allDeps =
|
|
301
|
-
...packageJson.dependencies || {},
|
|
302
|
-
...packageJson.devDependencies || {}
|
|
303
|
-
};
|
|
304
|
+
const allDeps = getAllDeps(packageJson);
|
|
304
305
|
if (allDeps.eslint) return "eslint";
|
|
305
306
|
if (allDeps["@biomejs/biome"]) return "biome";
|
|
306
307
|
}
|
|
@@ -316,16 +317,12 @@ function detectFormatter(packageJson, files) {
|
|
|
316
317
|
}
|
|
317
318
|
if (files.includes("biome.json") || files.includes("biome.jsonc")) return "biome";
|
|
318
319
|
if (packageJson) {
|
|
319
|
-
const allDeps =
|
|
320
|
-
...packageJson.dependencies || {},
|
|
321
|
-
...packageJson.devDependencies || {}
|
|
322
|
-
};
|
|
320
|
+
const allDeps = getAllDeps(packageJson);
|
|
323
321
|
if (allDeps.prettier) return "prettier";
|
|
324
322
|
if (allDeps["@biomejs/biome"]) return "biome";
|
|
325
323
|
}
|
|
326
|
-
if (files.includes("
|
|
327
|
-
|
|
328
|
-
}
|
|
324
|
+
if (files.includes("ruff.toml") || files.includes(".ruff.toml")) return "ruff";
|
|
325
|
+
if (files.includes("pyproject.toml")) return "black";
|
|
329
326
|
return null;
|
|
330
327
|
}
|
|
331
328
|
function detectBundler(packageJson, files) {
|
|
@@ -335,10 +332,7 @@ function detectBundler(packageJson, files) {
|
|
|
335
332
|
if (files.some((f) => f.startsWith("rollup.config"))) return "rollup";
|
|
336
333
|
if (files.some((f) => f.startsWith("esbuild"))) return "esbuild";
|
|
337
334
|
if (packageJson) {
|
|
338
|
-
const allDeps =
|
|
339
|
-
...packageJson.dependencies || {},
|
|
340
|
-
...packageJson.devDependencies || {}
|
|
341
|
-
};
|
|
335
|
+
const allDeps = getAllDeps(packageJson);
|
|
342
336
|
if (allDeps.vite) return "vite";
|
|
343
337
|
if (allDeps.webpack) return "webpack";
|
|
344
338
|
if (allDeps.tsup) return "tsup";
|
|
@@ -350,16 +344,12 @@ function detectBundler(packageJson, files) {
|
|
|
350
344
|
}
|
|
351
345
|
return null;
|
|
352
346
|
}
|
|
353
|
-
function detectMonorepo(
|
|
347
|
+
function detectMonorepo(files, packageJson) {
|
|
354
348
|
if (files.includes("pnpm-workspace.yaml")) return true;
|
|
355
349
|
if (files.includes("lerna.json")) return true;
|
|
356
350
|
if (files.includes("nx.json")) return true;
|
|
357
351
|
if (files.includes("turbo.json")) return true;
|
|
358
352
|
if (packageJson?.workspaces) return true;
|
|
359
|
-
const packagesDir = path.join(rootDir, "packages");
|
|
360
|
-
const appsDir = path.join(rootDir, "apps");
|
|
361
|
-
if (fs.existsSync(packagesDir) && fs.statSync(packagesDir).isDirectory()) return true;
|
|
362
|
-
if (fs.existsSync(appsDir) && fs.statSync(appsDir).isDirectory()) return true;
|
|
363
353
|
return false;
|
|
364
354
|
}
|
|
365
355
|
function detectCICD(rootDir, files) {
|
|
@@ -435,7 +425,7 @@ function listSourceFilesShallow(rootDir, extensions) {
|
|
|
435
425
|
scan(rootDir, 0);
|
|
436
426
|
return files;
|
|
437
427
|
}
|
|
438
|
-
function countSourceFiles(rootDir
|
|
428
|
+
function countSourceFiles(rootDir) {
|
|
439
429
|
const extensions = [
|
|
440
430
|
// JavaScript/TypeScript
|
|
441
431
|
".js",
|
|
@@ -512,96 +502,13 @@ function countSourceFiles(rootDir, _languages) {
|
|
|
512
502
|
return count;
|
|
513
503
|
}
|
|
514
504
|
|
|
515
|
-
// src/
|
|
516
|
-
import
|
|
517
|
-
import
|
|
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
|
-
}
|
|
505
|
+
// src/extras.ts
|
|
506
|
+
import pc from "picocolors";
|
|
507
|
+
import prompts from "prompts";
|
|
601
508
|
|
|
602
509
|
// src/hooks.ts
|
|
603
|
-
import
|
|
604
|
-
import
|
|
510
|
+
import fs2 from "fs";
|
|
511
|
+
import path2 from "path";
|
|
605
512
|
var HOOK_SCRIPT = String.raw`#!/usr/bin/env node
|
|
606
513
|
/**
|
|
607
514
|
* Block Dangerous Commands - PreToolUse Hook for Bash
|
|
@@ -804,38 +711,394 @@ if (require.main === module) {
|
|
|
804
711
|
module.exports = { PATTERNS, LEVELS, SAFETY_LEVEL, checkCommand };
|
|
805
712
|
}
|
|
806
713
|
`;
|
|
714
|
+
function checkHookStatus(rootDir) {
|
|
715
|
+
const homeDir = process.env.HOME || "";
|
|
716
|
+
const projectScriptPath = path2.join(rootDir, ".claude", "hooks", "block-dangerous-commands.js");
|
|
717
|
+
const globalScriptPath = path2.join(homeDir, ".claude", "hooks", "block-dangerous-commands.js");
|
|
718
|
+
const result = {
|
|
719
|
+
projectInstalled: false,
|
|
720
|
+
globalInstalled: false,
|
|
721
|
+
projectMatchesOurs: false,
|
|
722
|
+
globalMatchesOurs: false
|
|
723
|
+
};
|
|
724
|
+
if (fs2.existsSync(projectScriptPath)) {
|
|
725
|
+
result.projectInstalled = true;
|
|
726
|
+
const content = fs2.readFileSync(projectScriptPath, "utf-8");
|
|
727
|
+
result.projectMatchesOurs = content.trim() === HOOK_SCRIPT.trim();
|
|
728
|
+
}
|
|
729
|
+
if (fs2.existsSync(globalScriptPath)) {
|
|
730
|
+
result.globalInstalled = true;
|
|
731
|
+
const content = fs2.readFileSync(globalScriptPath, "utf-8");
|
|
732
|
+
result.globalMatchesOurs = content.trim() === HOOK_SCRIPT.trim();
|
|
733
|
+
}
|
|
734
|
+
return result;
|
|
735
|
+
}
|
|
807
736
|
function installHook(rootDir) {
|
|
808
|
-
const hooksDir =
|
|
809
|
-
const hookPath =
|
|
810
|
-
const settingsPath =
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
737
|
+
const hooksDir = path2.join(rootDir, ".claude", "hooks");
|
|
738
|
+
const hookPath = path2.join(hooksDir, "block-dangerous-commands.js");
|
|
739
|
+
const settingsPath = path2.join(rootDir, ".claude", "settings.json");
|
|
740
|
+
fs2.mkdirSync(hooksDir, { recursive: true });
|
|
741
|
+
fs2.writeFileSync(hookPath, HOOK_SCRIPT);
|
|
742
|
+
fs2.chmodSync(hookPath, 493);
|
|
814
743
|
try {
|
|
815
|
-
const existing =
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
744
|
+
const existing = fs2.existsSync(settingsPath) ? JSON.parse(fs2.readFileSync(settingsPath, "utf-8")) : {};
|
|
745
|
+
const newEntry = {
|
|
746
|
+
matcher: "Bash",
|
|
747
|
+
hooks: [
|
|
819
748
|
{
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
{
|
|
823
|
-
type: "command",
|
|
824
|
-
command: "node .claude/hooks/block-dangerous-commands.js"
|
|
825
|
-
}
|
|
826
|
-
]
|
|
749
|
+
type: "command",
|
|
750
|
+
command: "node .claude/hooks/block-dangerous-commands.js"
|
|
827
751
|
}
|
|
828
752
|
]
|
|
829
753
|
};
|
|
830
|
-
|
|
754
|
+
const existingPreToolUse = Array.isArray(existing.hooks?.PreToolUse) ? existing.hooks.PreToolUse : [];
|
|
755
|
+
const alreadyInstalled = existingPreToolUse.some(
|
|
756
|
+
(e) => Array.isArray(e.hooks) && e.hooks.some((h) => h.command?.includes("block-dangerous-commands.js"))
|
|
757
|
+
);
|
|
758
|
+
existing.hooks = {
|
|
759
|
+
...existing.hooks,
|
|
760
|
+
PreToolUse: alreadyInstalled ? existingPreToolUse : [...existingPreToolUse, newEntry]
|
|
761
|
+
};
|
|
762
|
+
fs2.writeFileSync(settingsPath, JSON.stringify(existing, null, 2));
|
|
763
|
+
} catch (err) {
|
|
764
|
+
const msg = err instanceof Error ? err.message : "unknown error";
|
|
765
|
+
console.error(` Warning: could not patch settings.json (${msg}) \u2014 add hook config manually`);
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
function installHookGlobal() {
|
|
769
|
+
const homeDir = process.env.HOME || "";
|
|
770
|
+
const hooksDir = path2.join(homeDir, ".claude", "hooks");
|
|
771
|
+
const hookPath = path2.join(hooksDir, "block-dangerous-commands.js");
|
|
772
|
+
const settingsPath = path2.join(homeDir, ".claude", "settings.json");
|
|
773
|
+
fs2.mkdirSync(hooksDir, { recursive: true });
|
|
774
|
+
fs2.writeFileSync(hookPath, HOOK_SCRIPT);
|
|
775
|
+
fs2.chmodSync(hookPath, 493);
|
|
776
|
+
try {
|
|
777
|
+
const existing = fs2.existsSync(settingsPath) ? JSON.parse(fs2.readFileSync(settingsPath, "utf-8")) : {};
|
|
778
|
+
const newEntry = {
|
|
779
|
+
matcher: "Bash",
|
|
780
|
+
hooks: [{ type: "command", command: "node ~/.claude/hooks/block-dangerous-commands.js" }]
|
|
781
|
+
};
|
|
782
|
+
const existingPreToolUse = Array.isArray(existing.hooks?.PreToolUse) ? existing.hooks.PreToolUse : [];
|
|
783
|
+
const alreadyInstalled = existingPreToolUse.some(
|
|
784
|
+
(e) => Array.isArray(e.hooks) && e.hooks.some((h) => h.command?.includes("block-dangerous-commands.js"))
|
|
785
|
+
);
|
|
786
|
+
existing.hooks = {
|
|
787
|
+
...existing.hooks,
|
|
788
|
+
PreToolUse: alreadyInstalled ? existingPreToolUse : [...existingPreToolUse, newEntry]
|
|
789
|
+
};
|
|
790
|
+
fs2.writeFileSync(settingsPath, JSON.stringify(existing, null, 2));
|
|
791
|
+
} catch (err) {
|
|
792
|
+
const msg = err instanceof Error ? err.message : "unknown error";
|
|
793
|
+
console.error(` Warning: could not patch settings.json (${msg}) \u2014 add hook config manually`);
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
var STATUSLINE_SCRIPT = [
|
|
797
|
+
"#!/usr/bin/env bash",
|
|
798
|
+
"# Claude Code statusline \u2014 portable, no runtime dependency beyond jq",
|
|
799
|
+
"",
|
|
800
|
+
"set -euo pipefail",
|
|
801
|
+
"",
|
|
802
|
+
"# Colors (using $'...' so escapes resolve at assignment, not at output time)",
|
|
803
|
+
"RST=$'\\033[0m'",
|
|
804
|
+
"CYAN=$'\\033[36m'",
|
|
805
|
+
"MAGENTA=$'\\033[35m'",
|
|
806
|
+
"BLUE=$'\\033[34m'",
|
|
807
|
+
"GREEN=$'\\033[32m'",
|
|
808
|
+
"YELLOW=$'\\033[33m'",
|
|
809
|
+
"RED=$'\\033[31m'",
|
|
810
|
+
"",
|
|
811
|
+
"# Read JSON from stdin (Claude Code pipes session data)",
|
|
812
|
+
'INPUT="$(cat)"',
|
|
813
|
+
"",
|
|
814
|
+
"# Parse fields with jq",
|
|
815
|
+
`CWD="$(echo "$INPUT" | jq -r '.workspace.current_dir // .cwd // ""')"`,
|
|
816
|
+
'PROJECT="$(basename "$CWD")"',
|
|
817
|
+
`SESSION_ID="$(echo "$INPUT" | jq -r '.session_id // empty')"`,
|
|
818
|
+
`SESSION_NAME="$(echo "$INPUT" | jq -r '.session_name // empty')"`,
|
|
819
|
+
`REMAINING="$(echo "$INPUT" | jq -r '.context_window.remaining_percentage // empty')"`,
|
|
820
|
+
`MODEL="$(echo "$INPUT" | jq -r '.model.display_name // empty')"`,
|
|
821
|
+
"",
|
|
822
|
+
"# Line 1: [user] project [on branch]",
|
|
823
|
+
'LINE1=""',
|
|
824
|
+
'if [[ -n "${SSH_CONNECTION:-}" ]]; then',
|
|
825
|
+
' LINE1+="${BLUE}$(whoami)${RST} "',
|
|
826
|
+
"fi",
|
|
827
|
+
'LINE1+="${CYAN}${PROJECT}${RST}"',
|
|
828
|
+
"",
|
|
829
|
+
'BRANCH="$(git branch --show-current 2>/dev/null || git rev-parse --short HEAD 2>/dev/null || true)"',
|
|
830
|
+
'if [[ -n "$BRANCH" ]]; then',
|
|
831
|
+
' LINE1+=" on ${MAGENTA}\u{1F331} ${BRANCH}${RST}"',
|
|
832
|
+
"fi",
|
|
833
|
+
"",
|
|
834
|
+
"# Line 2: session + context + model",
|
|
835
|
+
'PARTS=""',
|
|
836
|
+
'if [[ -n "$SESSION_ID" ]]; then',
|
|
837
|
+
' if [[ -n "$SESSION_NAME" ]]; then',
|
|
838
|
+
' PARTS+="${MAGENTA}${SESSION_NAME} \xB7 sid: ${SESSION_ID}${RST}"',
|
|
839
|
+
" else",
|
|
840
|
+
' PARTS+="${MAGENTA}sid: ${SESSION_ID}${RST}"',
|
|
841
|
+
" fi",
|
|
842
|
+
"fi",
|
|
843
|
+
"",
|
|
844
|
+
'if [[ -n "$REMAINING" ]]; then',
|
|
845
|
+
' RND="${REMAINING%%.*}"',
|
|
846
|
+
" if (( RND < 20 )); then",
|
|
847
|
+
' CTX_COLOR="$RED"',
|
|
848
|
+
" elif (( RND < 50 )); then",
|
|
849
|
+
' CTX_COLOR="$YELLOW"',
|
|
850
|
+
" else",
|
|
851
|
+
' CTX_COLOR="$GREEN"',
|
|
852
|
+
" fi",
|
|
853
|
+
' [[ -n "$PARTS" ]] && PARTS+=" "',
|
|
854
|
+
' PARTS+="${CTX_COLOR}[ctx: ${RND}%]${RST}"',
|
|
855
|
+
"fi",
|
|
856
|
+
"",
|
|
857
|
+
'if [[ -n "$MODEL" ]]; then',
|
|
858
|
+
' [[ -n "$PARTS" ]] && PARTS+=" "',
|
|
859
|
+
' PARTS+="[${CYAN}${MODEL}${RST}]"',
|
|
860
|
+
"fi",
|
|
861
|
+
"",
|
|
862
|
+
'echo "$LINE1"',
|
|
863
|
+
'echo "$PARTS"'
|
|
864
|
+
].join("\n");
|
|
865
|
+
function checkStatuslineStatus(rootDir) {
|
|
866
|
+
const homeDir = process.env.HOME || "";
|
|
867
|
+
const projectScriptPath = path2.join(rootDir, ".claude", "config", "statusline-command.sh");
|
|
868
|
+
const globalScriptPath = path2.join(homeDir, ".claude", "config", "statusline-command.sh");
|
|
869
|
+
const projectSettingsPath = path2.join(rootDir, ".claude", "settings.json");
|
|
870
|
+
const globalSettingsPath = path2.join(homeDir, ".claude", "settings.json");
|
|
871
|
+
const result = {
|
|
872
|
+
projectInstalled: false,
|
|
873
|
+
globalInstalled: false,
|
|
874
|
+
projectMatchesOurs: false,
|
|
875
|
+
globalMatchesOurs: false
|
|
876
|
+
};
|
|
877
|
+
try {
|
|
878
|
+
if (fs2.existsSync(projectSettingsPath)) {
|
|
879
|
+
const settings = JSON.parse(fs2.readFileSync(projectSettingsPath, "utf-8"));
|
|
880
|
+
if (settings.statusLine?.command) {
|
|
881
|
+
result.projectInstalled = true;
|
|
882
|
+
if (fs2.existsSync(projectScriptPath)) {
|
|
883
|
+
const content = fs2.readFileSync(projectScriptPath, "utf-8");
|
|
884
|
+
result.projectMatchesOurs = content.trim() === STATUSLINE_SCRIPT.trim();
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
} catch {
|
|
889
|
+
}
|
|
890
|
+
try {
|
|
891
|
+
if (fs2.existsSync(globalSettingsPath)) {
|
|
892
|
+
const settings = JSON.parse(fs2.readFileSync(globalSettingsPath, "utf-8"));
|
|
893
|
+
if (settings.statusLine?.command) {
|
|
894
|
+
result.globalInstalled = true;
|
|
895
|
+
if (fs2.existsSync(globalScriptPath)) {
|
|
896
|
+
const content = fs2.readFileSync(globalScriptPath, "utf-8");
|
|
897
|
+
result.globalMatchesOurs = content.trim() === STATUSLINE_SCRIPT.trim();
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
}
|
|
831
901
|
} catch {
|
|
832
902
|
}
|
|
903
|
+
return result;
|
|
904
|
+
}
|
|
905
|
+
function installStatusline(rootDir) {
|
|
906
|
+
const configDir = path2.join(rootDir, ".claude", "config");
|
|
907
|
+
const scriptPath = path2.join(configDir, "statusline-command.sh");
|
|
908
|
+
const settingsPath = path2.join(rootDir, ".claude", "settings.json");
|
|
909
|
+
fs2.mkdirSync(configDir, { recursive: true });
|
|
910
|
+
fs2.writeFileSync(scriptPath, STATUSLINE_SCRIPT);
|
|
911
|
+
fs2.chmodSync(scriptPath, 493);
|
|
912
|
+
patchSettings(settingsPath, {
|
|
913
|
+
statusLine: { type: "command", command: "bash .claude/config/statusline-command.sh" }
|
|
914
|
+
});
|
|
915
|
+
}
|
|
916
|
+
function installStatuslineGlobal() {
|
|
917
|
+
const homeDir = process.env.HOME || "";
|
|
918
|
+
const configDir = path2.join(homeDir, ".claude", "config");
|
|
919
|
+
const scriptPath = path2.join(configDir, "statusline-command.sh");
|
|
920
|
+
const settingsPath = path2.join(homeDir, ".claude", "settings.json");
|
|
921
|
+
fs2.mkdirSync(configDir, { recursive: true });
|
|
922
|
+
fs2.writeFileSync(scriptPath, STATUSLINE_SCRIPT);
|
|
923
|
+
fs2.chmodSync(scriptPath, 493);
|
|
924
|
+
patchSettings(settingsPath, {
|
|
925
|
+
statusLine: { type: "command", command: "bash ~/.claude/config/statusline-command.sh" }
|
|
926
|
+
});
|
|
927
|
+
}
|
|
928
|
+
function patchSettings(settingsPath, patch) {
|
|
929
|
+
try {
|
|
930
|
+
const existing = fs2.existsSync(settingsPath) ? JSON.parse(fs2.readFileSync(settingsPath, "utf-8")) : {};
|
|
931
|
+
Object.assign(existing, patch);
|
|
932
|
+
fs2.writeFileSync(settingsPath, JSON.stringify(existing, null, 2));
|
|
933
|
+
} catch (err) {
|
|
934
|
+
const msg = err instanceof Error ? err.message : "unknown error";
|
|
935
|
+
console.error(
|
|
936
|
+
` Warning: could not patch settings.json (${msg}) \u2014 add statusLine config manually`
|
|
937
|
+
);
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
// src/extras.ts
|
|
942
|
+
var EXTRAS = [
|
|
943
|
+
{
|
|
944
|
+
id: "safety-hook",
|
|
945
|
+
name: "Safety hook",
|
|
946
|
+
description: "Block dangerous commands (git push, rm -rf, etc.)",
|
|
947
|
+
checkStatus: checkHookStatus,
|
|
948
|
+
installProject: installHook,
|
|
949
|
+
installGlobal: installHookGlobal,
|
|
950
|
+
projectPath: ".claude/hooks/block-dangerous-commands.js",
|
|
951
|
+
globalPath: "~/.claude/hooks/block-dangerous-commands.js"
|
|
952
|
+
},
|
|
953
|
+
{
|
|
954
|
+
id: "statusline",
|
|
955
|
+
name: "Custom statusline",
|
|
956
|
+
description: "Shows project, branch, context, model",
|
|
957
|
+
checkStatus: checkStatuslineStatus,
|
|
958
|
+
installProject: installStatusline,
|
|
959
|
+
installGlobal: installStatuslineGlobal,
|
|
960
|
+
projectPath: ".claude/config/statusline-command.sh",
|
|
961
|
+
globalPath: "~/.claude/config/statusline-command.sh"
|
|
962
|
+
}
|
|
963
|
+
];
|
|
964
|
+
async function promptExtras(projectDir) {
|
|
965
|
+
for (const extra of EXTRAS) {
|
|
966
|
+
const status = extra.checkStatus(projectDir);
|
|
967
|
+
if (status.projectMatchesOurs || status.globalMatchesOurs) {
|
|
968
|
+
continue;
|
|
969
|
+
}
|
|
970
|
+
if (status.projectInstalled || status.globalInstalled) {
|
|
971
|
+
const where = status.globalInstalled ? "globally" : "in this project";
|
|
972
|
+
const { action } = await prompts({
|
|
973
|
+
type: "select",
|
|
974
|
+
name: "action",
|
|
975
|
+
message: `A different ${extra.name.toLowerCase()} is already configured ${where}. Replace it?`,
|
|
976
|
+
choices: [
|
|
977
|
+
{ title: "Install for this project only", value: "project" },
|
|
978
|
+
{ title: "Install globally (all projects)", value: "global" },
|
|
979
|
+
{ title: "Skip \u2014 keep existing", value: "skip" }
|
|
980
|
+
],
|
|
981
|
+
initial: 2
|
|
982
|
+
});
|
|
983
|
+
applyAction(action, extra, projectDir);
|
|
984
|
+
} else {
|
|
985
|
+
const { action } = await prompts({
|
|
986
|
+
type: "select",
|
|
987
|
+
name: "action",
|
|
988
|
+
message: `Add ${extra.name.toLowerCase()}? (${extra.description})`,
|
|
989
|
+
choices: [
|
|
990
|
+
{ title: "Install for this project only", value: "project" },
|
|
991
|
+
{ title: "Install globally (all projects)", value: "global" },
|
|
992
|
+
{ title: "Skip", value: "skip" }
|
|
993
|
+
],
|
|
994
|
+
initial: 0
|
|
995
|
+
});
|
|
996
|
+
applyAction(action, extra, projectDir);
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
function applyAction(action, extra, projectDir) {
|
|
1001
|
+
if (action === "project") {
|
|
1002
|
+
extra.installProject(projectDir);
|
|
1003
|
+
console.log(pc.green(` + ${extra.projectPath}`));
|
|
1004
|
+
} else if (action === "global") {
|
|
1005
|
+
extra.installGlobal();
|
|
1006
|
+
console.log(pc.green(` + ${extra.globalPath} (global)`));
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
// src/generator.ts
|
|
1011
|
+
import fs3 from "fs";
|
|
1012
|
+
import path3 from "path";
|
|
1013
|
+
function ensureDirectories(rootDir) {
|
|
1014
|
+
const dirs = [".claude", ".claude/skills", ".claude/agents", ".claude/rules", ".claude/commands"];
|
|
1015
|
+
for (const dir of dirs) {
|
|
1016
|
+
fs3.mkdirSync(path3.join(rootDir, dir), { recursive: true });
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
function generateSettings(stack) {
|
|
1020
|
+
const permissions = ["Read(**)", "Edit(**)", "Write(.claude/**)", "Bash(git:*)"];
|
|
1021
|
+
const pkgManagers = ["npm", "yarn", "pnpm", "bun", "npx"];
|
|
1022
|
+
for (const pm of pkgManagers) {
|
|
1023
|
+
permissions.push(`Bash(${pm}:*)`);
|
|
1024
|
+
}
|
|
1025
|
+
if (stack.languages.includes("typescript") || stack.languages.includes("javascript")) {
|
|
1026
|
+
permissions.push("Bash(node:*)", "Bash(tsc:*)");
|
|
1027
|
+
}
|
|
1028
|
+
if (stack.languages.includes("python")) {
|
|
1029
|
+
permissions.push(
|
|
1030
|
+
"Bash(python:*)",
|
|
1031
|
+
"Bash(pip:*)",
|
|
1032
|
+
"Bash(poetry:*)",
|
|
1033
|
+
"Bash(pytest:*)",
|
|
1034
|
+
"Bash(uvicorn:*)"
|
|
1035
|
+
);
|
|
1036
|
+
}
|
|
1037
|
+
if (stack.languages.includes("go")) {
|
|
1038
|
+
permissions.push("Bash(go:*)");
|
|
1039
|
+
}
|
|
1040
|
+
if (stack.languages.includes("rust")) {
|
|
1041
|
+
permissions.push("Bash(cargo:*)", "Bash(rustc:*)");
|
|
1042
|
+
}
|
|
1043
|
+
if (stack.languages.includes("ruby")) {
|
|
1044
|
+
permissions.push("Bash(ruby:*)", "Bash(bundle:*)", "Bash(rails:*)", "Bash(rake:*)");
|
|
1045
|
+
}
|
|
1046
|
+
if (stack.testingFramework) {
|
|
1047
|
+
const testCommands = {
|
|
1048
|
+
jest: ["jest:*"],
|
|
1049
|
+
vitest: ["vitest:*"],
|
|
1050
|
+
playwright: ["playwright:*"],
|
|
1051
|
+
cypress: ["cypress:*"],
|
|
1052
|
+
pytest: ["pytest:*"],
|
|
1053
|
+
rspec: ["rspec:*"]
|
|
1054
|
+
};
|
|
1055
|
+
const cmds = testCommands[stack.testingFramework];
|
|
1056
|
+
if (cmds) {
|
|
1057
|
+
permissions.push(...cmds.map((c) => `Bash(${c})`));
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
if (stack.linter) {
|
|
1061
|
+
permissions.push(`Bash(${stack.linter}:*)`);
|
|
1062
|
+
}
|
|
1063
|
+
if (stack.formatter) {
|
|
1064
|
+
permissions.push(`Bash(${stack.formatter}:*)`);
|
|
1065
|
+
}
|
|
1066
|
+
permissions.push(
|
|
1067
|
+
"Bash(ls:*)",
|
|
1068
|
+
"Bash(mkdir:*)",
|
|
1069
|
+
"Bash(cat:*)",
|
|
1070
|
+
"Bash(echo:*)",
|
|
1071
|
+
"Bash(grep:*)",
|
|
1072
|
+
"Bash(find:*)"
|
|
1073
|
+
);
|
|
1074
|
+
if (stack.hasDocker) {
|
|
1075
|
+
permissions.push("Bash(docker:*)", "Bash(docker-compose:*)");
|
|
1076
|
+
}
|
|
1077
|
+
const settings = {
|
|
1078
|
+
$schema: "https://json.schemastore.org/claude-code-settings.json",
|
|
1079
|
+
permissions: {
|
|
1080
|
+
allow: [...new Set(permissions)]
|
|
1081
|
+
// Deduplicate
|
|
1082
|
+
}
|
|
1083
|
+
};
|
|
1084
|
+
return {
|
|
1085
|
+
path: ".claude/settings.json",
|
|
1086
|
+
content: JSON.stringify(settings, null, 2)
|
|
1087
|
+
};
|
|
1088
|
+
}
|
|
1089
|
+
function writeSettings(rootDir, stack) {
|
|
1090
|
+
const { path: settingsPath, content } = generateSettings(stack);
|
|
1091
|
+
const fullPath = path3.join(rootDir, settingsPath);
|
|
1092
|
+
const dir = path3.dirname(fullPath);
|
|
1093
|
+
fs3.mkdirSync(dir, { recursive: true });
|
|
1094
|
+
fs3.writeFileSync(fullPath, content);
|
|
833
1095
|
}
|
|
834
1096
|
|
|
835
1097
|
// src/prompt.ts
|
|
836
|
-
function getAnalysisPrompt(projectInfo) {
|
|
1098
|
+
function getAnalysisPrompt(projectInfo, options = { claudeMdMode: "replace", existingClaudeMd: null }) {
|
|
837
1099
|
const context = buildContextSection(projectInfo);
|
|
838
1100
|
const templateVars = buildTemplateVariables(projectInfo);
|
|
1101
|
+
const claudeMdInstructions = buildClaudeMdInstructions(options);
|
|
839
1102
|
return `${ANALYSIS_PROMPT}
|
|
840
1103
|
|
|
841
1104
|
${SKILLS_PROMPT}
|
|
@@ -859,15 +1122,17 @@ ${context}
|
|
|
859
1122
|
|
|
860
1123
|
${templateVars}
|
|
861
1124
|
|
|
1125
|
+
${claudeMdInstructions}
|
|
1126
|
+
|
|
862
1127
|
---
|
|
863
1128
|
|
|
864
1129
|
## Execute Now
|
|
865
1130
|
|
|
866
1131
|
1. Read this entire prompt to understand all phases
|
|
867
1132
|
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
|
|
1133
|
+
${options.claudeMdMode === "keep" ? `3. Skip CLAUDE.md generation \u2014 the existing file is being kept as-is` : options.claudeMdMode === "improve" ? `3. Execute Phase 2 \u2014 IMPROVE the existing CLAUDE.md (see Improvement Mode instructions above)` : `3. Execute Phase 2 - generate the CLAUDE.md (max 120 lines) using only discovered information`}
|
|
869
1134
|
4. Execute Phase 3 - verify quality before writing
|
|
870
|
-
5. Use the Write tool to create \`.claude/CLAUDE.md\` with the final content
|
|
1135
|
+
${options.claudeMdMode === "keep" ? `5. Skip writing CLAUDE.md \u2014 it is being preserved` : `5. Use the Write tool to create \`.claude/CLAUDE.md\` with the final content`}
|
|
871
1136
|
6. Execute Phase 4 - generate ALL skill files (4 core + framework-specific if detected)
|
|
872
1137
|
7. Execute Phase 5 - generate agent files
|
|
873
1138
|
8. Execute Phase 6 - generate rule files
|
|
@@ -878,6 +1143,41 @@ ${templateVars}
|
|
|
878
1143
|
Do NOT output file contents to stdout. Write all files to disk using the Write tool.
|
|
879
1144
|
Generate ALL files in a single pass \u2014 do not stop after CLAUDE.md.`;
|
|
880
1145
|
}
|
|
1146
|
+
function buildClaudeMdInstructions(options) {
|
|
1147
|
+
if (options.claudeMdMode === "keep") {
|
|
1148
|
+
return `---
|
|
1149
|
+
|
|
1150
|
+
## CLAUDE.md Mode: KEEP
|
|
1151
|
+
|
|
1152
|
+
The user chose to keep their existing CLAUDE.md unchanged.
|
|
1153
|
+
**Do NOT read, modify, or overwrite \`.claude/CLAUDE.md\`.**
|
|
1154
|
+
Generate all other files (skills, agents, rules, commands) normally.
|
|
1155
|
+
Use the existing CLAUDE.md as the source of truth for cross-references.`;
|
|
1156
|
+
}
|
|
1157
|
+
if (options.claudeMdMode === "improve" && options.existingClaudeMd) {
|
|
1158
|
+
return `---
|
|
1159
|
+
|
|
1160
|
+
## CLAUDE.md Mode: IMPROVE
|
|
1161
|
+
|
|
1162
|
+
The user has an existing CLAUDE.md and wants it improved, not replaced.
|
|
1163
|
+
Here is the current content:
|
|
1164
|
+
|
|
1165
|
+
\`\`\`markdown
|
|
1166
|
+
${options.existingClaudeMd}
|
|
1167
|
+
\`\`\`
|
|
1168
|
+
|
|
1169
|
+
### Improvement Rules
|
|
1170
|
+
|
|
1171
|
+
1. **Preserve all manually-added content** \u2014 sections, notes, and custom rules the user wrote
|
|
1172
|
+
2. **Enhance with discovered information** \u2014 fill gaps, add missing sections, improve specificity
|
|
1173
|
+
3. **Fix generic content** \u2014 replace boilerplate with project-specific details found during Phase 1
|
|
1174
|
+
4. **Update stale references** \u2014 fix file paths, commands, or patterns that no longer match the codebase
|
|
1175
|
+
5. **Respect the 120-line cap** \u2014 if the file is already near the limit, prioritize density over additions
|
|
1176
|
+
6. **Keep the user's structure** \u2014 if they organized sections differently from the template, keep their layout
|
|
1177
|
+
7. **Do NOT remove content you don't understand** \u2014 if a section seems custom or domain-specific, preserve it`;
|
|
1178
|
+
}
|
|
1179
|
+
return "";
|
|
1180
|
+
}
|
|
881
1181
|
function buildContextSection(projectInfo) {
|
|
882
1182
|
const { name, description, techStack, fileCount } = projectInfo;
|
|
883
1183
|
const lines = [];
|
|
@@ -1633,14 +1933,17 @@ function validateArtifacts(rootDir) {
|
|
|
1633
1933
|
const files = walkMdFiles(claudeDir).filter((f) => !f.endsWith("CLAUDE.md"));
|
|
1634
1934
|
for (const filePath of files) {
|
|
1635
1935
|
result.filesChecked++;
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
change
|
|
1936
|
+
try {
|
|
1937
|
+
const changes = processFile(filePath, commands, fingerprints);
|
|
1938
|
+
if (changes.length > 0) {
|
|
1939
|
+
result.filesModified++;
|
|
1940
|
+
result.duplicationsRemoved += changes.length;
|
|
1941
|
+
for (const change of changes) {
|
|
1942
|
+
change.file = path4.relative(rootDir, filePath);
|
|
1943
|
+
}
|
|
1944
|
+
result.changes.push(...changes);
|
|
1642
1945
|
}
|
|
1643
|
-
|
|
1946
|
+
} catch {
|
|
1644
1947
|
}
|
|
1645
1948
|
}
|
|
1646
1949
|
} catch {
|
|
@@ -1668,21 +1971,21 @@ function getVersion() {
|
|
|
1668
1971
|
}
|
|
1669
1972
|
function showHelp() {
|
|
1670
1973
|
console.log(`
|
|
1671
|
-
${
|
|
1974
|
+
${pc2.cyan("Claude Code Starter")} v${VERSION}
|
|
1672
1975
|
|
|
1673
1976
|
Bootstrap intelligent Claude Code configurations for any repository.
|
|
1674
1977
|
|
|
1675
|
-
${
|
|
1978
|
+
${pc2.bold("USAGE")}
|
|
1676
1979
|
npx claude-code-starter [OPTIONS]
|
|
1677
1980
|
|
|
1678
|
-
${
|
|
1981
|
+
${pc2.bold("OPTIONS")}
|
|
1679
1982
|
-h, --help Show this help message
|
|
1680
1983
|
-v, --version Show version number
|
|
1681
1984
|
-f, --force Force overwrite existing .claude files
|
|
1682
1985
|
-y, --no-interactive Skip interactive prompts (use defaults)
|
|
1683
1986
|
-V, --verbose Show detailed output
|
|
1684
1987
|
|
|
1685
|
-
${
|
|
1988
|
+
${pc2.bold("WHAT IT DOES")}
|
|
1686
1989
|
1. Analyzes your repository's tech stack
|
|
1687
1990
|
2. Launches Claude CLI to deeply analyze your codebase
|
|
1688
1991
|
3. Generates all .claude/ configuration files:
|
|
@@ -1692,53 +1995,53 @@ ${pc.bold("WHAT IT DOES")}
|
|
|
1692
1995
|
- Rules matching your code style
|
|
1693
1996
|
- Commands for analysis and code review
|
|
1694
1997
|
|
|
1695
|
-
${
|
|
1998
|
+
${pc2.bold("REQUIREMENTS")}
|
|
1696
1999
|
Claude CLI must be installed: https://claude.ai/download
|
|
1697
2000
|
|
|
1698
|
-
${
|
|
2001
|
+
${pc2.bold("MORE INFO")}
|
|
1699
2002
|
https://github.com/cassmtnr/claude-code-starter
|
|
1700
2003
|
`);
|
|
1701
2004
|
}
|
|
1702
2005
|
function showBanner() {
|
|
1703
2006
|
console.log();
|
|
1704
|
-
console.log(
|
|
1705
|
-
console.log(
|
|
2007
|
+
console.log(pc2.bold("Claude Code Starter") + pc2.gray(` v${VERSION}`));
|
|
2008
|
+
console.log(pc2.gray("Intelligent AI-Assisted Development Setup"));
|
|
1706
2009
|
console.log();
|
|
1707
2010
|
}
|
|
1708
2011
|
function showTechStack(projectInfo, verbose) {
|
|
1709
2012
|
const { techStack } = projectInfo;
|
|
1710
|
-
console.log(
|
|
2013
|
+
console.log(pc2.bold("Tech Stack"));
|
|
1711
2014
|
console.log();
|
|
1712
2015
|
if (techStack.primaryLanguage) {
|
|
1713
|
-
console.log(` ${
|
|
2016
|
+
console.log(` ${pc2.bold("Language:")} ${formatLanguage(techStack.primaryLanguage)}`);
|
|
1714
2017
|
}
|
|
1715
2018
|
if (techStack.primaryFramework) {
|
|
1716
|
-
console.log(` ${
|
|
2019
|
+
console.log(` ${pc2.bold("Framework:")} ${formatFramework(techStack.primaryFramework)}`);
|
|
1717
2020
|
}
|
|
1718
2021
|
if (techStack.packageManager) {
|
|
1719
|
-
console.log(` ${
|
|
2022
|
+
console.log(` ${pc2.bold("Package Manager:")} ${techStack.packageManager}`);
|
|
1720
2023
|
}
|
|
1721
2024
|
if (techStack.testingFramework) {
|
|
1722
|
-
console.log(` ${
|
|
2025
|
+
console.log(` ${pc2.bold("Testing:")} ${techStack.testingFramework}`);
|
|
1723
2026
|
}
|
|
1724
2027
|
if (verbose) {
|
|
1725
2028
|
if (techStack.linter) {
|
|
1726
|
-
console.log(` ${
|
|
2029
|
+
console.log(` ${pc2.bold("Linter:")} ${techStack.linter}`);
|
|
1727
2030
|
}
|
|
1728
2031
|
if (techStack.formatter) {
|
|
1729
|
-
console.log(` ${
|
|
2032
|
+
console.log(` ${pc2.bold("Formatter:")} ${techStack.formatter}`);
|
|
1730
2033
|
}
|
|
1731
2034
|
if (techStack.bundler) {
|
|
1732
|
-
console.log(` ${
|
|
2035
|
+
console.log(` ${pc2.bold("Bundler:")} ${techStack.bundler}`);
|
|
1733
2036
|
}
|
|
1734
2037
|
if (techStack.isMonorepo) {
|
|
1735
|
-
console.log(` ${
|
|
2038
|
+
console.log(` ${pc2.bold("Monorepo:")} yes`);
|
|
1736
2039
|
}
|
|
1737
2040
|
if (techStack.hasDocker) {
|
|
1738
|
-
console.log(` ${
|
|
2041
|
+
console.log(` ${pc2.bold("Docker:")} yes`);
|
|
1739
2042
|
}
|
|
1740
2043
|
if (techStack.hasCICD) {
|
|
1741
|
-
console.log(` ${
|
|
2044
|
+
console.log(` ${pc2.bold("CI/CD:")} ${techStack.cicdPlatform}`);
|
|
1742
2045
|
}
|
|
1743
2046
|
}
|
|
1744
2047
|
console.log();
|
|
@@ -1814,9 +2117,9 @@ async function promptNewProject(args) {
|
|
|
1814
2117
|
if (!args.interactive) {
|
|
1815
2118
|
return null;
|
|
1816
2119
|
}
|
|
1817
|
-
console.log(
|
|
2120
|
+
console.log(pc2.yellow("New project detected - let's set it up!"));
|
|
1818
2121
|
console.log();
|
|
1819
|
-
const descResponse = await
|
|
2122
|
+
const descResponse = await prompts2({
|
|
1820
2123
|
type: "text",
|
|
1821
2124
|
name: "description",
|
|
1822
2125
|
message: "What are you building?",
|
|
@@ -1825,7 +2128,7 @@ async function promptNewProject(args) {
|
|
|
1825
2128
|
if (!descResponse.description) {
|
|
1826
2129
|
return null;
|
|
1827
2130
|
}
|
|
1828
|
-
const langResponse = await
|
|
2131
|
+
const langResponse = await prompts2({
|
|
1829
2132
|
type: "select",
|
|
1830
2133
|
name: "primaryLanguage",
|
|
1831
2134
|
message: "Primary language?",
|
|
@@ -1846,34 +2149,34 @@ async function promptNewProject(args) {
|
|
|
1846
2149
|
});
|
|
1847
2150
|
const lang = langResponse.primaryLanguage || "typescript";
|
|
1848
2151
|
const fwChoices = frameworkChoices[lang] || defaultFrameworkChoices;
|
|
1849
|
-
const fwResponse = await
|
|
2152
|
+
const fwResponse = await prompts2({
|
|
1850
2153
|
type: "select",
|
|
1851
2154
|
name: "framework",
|
|
1852
2155
|
message: "Framework?",
|
|
1853
2156
|
choices: fwChoices
|
|
1854
2157
|
});
|
|
1855
2158
|
const pmChoices = getPackageManagerChoices(lang);
|
|
1856
|
-
const pmResponse = await
|
|
2159
|
+
const pmResponse = await prompts2({
|
|
1857
2160
|
type: "select",
|
|
1858
2161
|
name: "packageManager",
|
|
1859
2162
|
message: "Package manager?",
|
|
1860
2163
|
choices: pmChoices
|
|
1861
2164
|
});
|
|
1862
2165
|
const testChoices = getTestingFrameworkChoices(lang);
|
|
1863
|
-
const testResponse = await
|
|
2166
|
+
const testResponse = await prompts2({
|
|
1864
2167
|
type: "select",
|
|
1865
2168
|
name: "testingFramework",
|
|
1866
2169
|
message: "Testing framework?",
|
|
1867
2170
|
choices: testChoices
|
|
1868
2171
|
});
|
|
1869
2172
|
const lintChoices = getLinterFormatterChoices(lang);
|
|
1870
|
-
const lintResponse = await
|
|
2173
|
+
const lintResponse = await prompts2({
|
|
1871
2174
|
type: "select",
|
|
1872
2175
|
name: "linter",
|
|
1873
2176
|
message: "Linter/Formatter?",
|
|
1874
2177
|
choices: lintChoices
|
|
1875
2178
|
});
|
|
1876
|
-
const typeResponse = await
|
|
2179
|
+
const typeResponse = await prompts2({
|
|
1877
2180
|
type: "select",
|
|
1878
2181
|
name: "projectType",
|
|
1879
2182
|
message: "Project type?",
|
|
@@ -1981,7 +2284,6 @@ function getLinterFormatterChoices(lang) {
|
|
|
1981
2284
|
return [
|
|
1982
2285
|
{ title: "Biome", value: "biome" },
|
|
1983
2286
|
{ title: "ESLint + Prettier", value: "eslint" },
|
|
1984
|
-
{ title: "ESLint", value: "eslint" },
|
|
1985
2287
|
{ title: "None", value: null }
|
|
1986
2288
|
];
|
|
1987
2289
|
}
|
|
@@ -2112,12 +2414,12 @@ function checkClaudeCli() {
|
|
|
2112
2414
|
return false;
|
|
2113
2415
|
}
|
|
2114
2416
|
}
|
|
2115
|
-
function runClaudeAnalysis(projectDir, projectInfo) {
|
|
2417
|
+
function runClaudeAnalysis(projectDir, projectInfo, options = { claudeMdMode: "replace", existingClaudeMd: null }) {
|
|
2116
2418
|
return new Promise((resolve) => {
|
|
2117
|
-
const prompt = getAnalysisPrompt(projectInfo);
|
|
2118
|
-
console.log(
|
|
2419
|
+
const prompt = getAnalysisPrompt(projectInfo, options);
|
|
2420
|
+
console.log(pc2.cyan("Launching Claude for deep project analysis..."));
|
|
2119
2421
|
console.log(
|
|
2120
|
-
|
|
2422
|
+
pc2.gray("Claude will read your codebase and generate all .claude/ configuration files")
|
|
2121
2423
|
);
|
|
2122
2424
|
console.log();
|
|
2123
2425
|
const spinner = ora({
|
|
@@ -2132,6 +2434,8 @@ function runClaudeAnalysis(projectDir, projectInfo) {
|
|
|
2132
2434
|
"claude",
|
|
2133
2435
|
[
|
|
2134
2436
|
"-p",
|
|
2437
|
+
"--verbose",
|
|
2438
|
+
"--output-format=stream-json",
|
|
2135
2439
|
"--allowedTools",
|
|
2136
2440
|
"Read",
|
|
2137
2441
|
"--allowedTools",
|
|
@@ -2148,8 +2452,43 @@ function runClaudeAnalysis(projectDir, projectInfo) {
|
|
|
2148
2452
|
stdio: ["pipe", "pipe", "pipe"]
|
|
2149
2453
|
}
|
|
2150
2454
|
);
|
|
2455
|
+
let stdoutBuffer = "";
|
|
2456
|
+
child.stdout.on("data", (chunk) => {
|
|
2457
|
+
stdoutBuffer += chunk.toString();
|
|
2458
|
+
const lines = stdoutBuffer.split("\n");
|
|
2459
|
+
stdoutBuffer = lines.pop() || "";
|
|
2460
|
+
for (const line of lines) {
|
|
2461
|
+
if (!line.trim()) continue;
|
|
2462
|
+
try {
|
|
2463
|
+
const event = JSON.parse(line);
|
|
2464
|
+
if (event.type === "assistant" && Array.isArray(event.message?.content)) {
|
|
2465
|
+
for (const block of event.message.content) {
|
|
2466
|
+
if (block.type === "tool_use" && block.name && block.input) {
|
|
2467
|
+
const toolName = block.name;
|
|
2468
|
+
const toolInput = block.input;
|
|
2469
|
+
const filePath = toolInput.file_path || toolInput.path || toolInput.pattern || "";
|
|
2470
|
+
const shortPath = filePath.split("/").slice(-2).join("/");
|
|
2471
|
+
const action = toolName === "Write" || toolName === "Edit" ? "Writing" : "Reading";
|
|
2472
|
+
if (shortPath) {
|
|
2473
|
+
spinner.text = `${action} ${shortPath}...`;
|
|
2474
|
+
} else {
|
|
2475
|
+
spinner.text = `Using ${toolName}...`;
|
|
2476
|
+
}
|
|
2477
|
+
}
|
|
2478
|
+
}
|
|
2479
|
+
}
|
|
2480
|
+
} catch {
|
|
2481
|
+
}
|
|
2482
|
+
}
|
|
2483
|
+
});
|
|
2484
|
+
child.stdin.on("error", () => {
|
|
2485
|
+
});
|
|
2151
2486
|
child.stdin.write(prompt);
|
|
2152
2487
|
child.stdin.end();
|
|
2488
|
+
let stderrOutput = "";
|
|
2489
|
+
child.stderr.on("data", (chunk) => {
|
|
2490
|
+
stderrOutput += chunk.toString();
|
|
2491
|
+
});
|
|
2153
2492
|
child.on("error", (err) => {
|
|
2154
2493
|
spinner.fail(`Failed to launch Claude CLI: ${err.message}`);
|
|
2155
2494
|
resolve(false);
|
|
@@ -2160,6 +2499,9 @@ function runClaudeAnalysis(projectDir, projectInfo) {
|
|
|
2160
2499
|
resolve(true);
|
|
2161
2500
|
} else {
|
|
2162
2501
|
spinner.fail(`Claude exited with code ${code}`);
|
|
2502
|
+
if (stderrOutput.trim()) {
|
|
2503
|
+
console.error(pc2.gray(stderrOutput.trim()));
|
|
2504
|
+
}
|
|
2163
2505
|
resolve(false);
|
|
2164
2506
|
}
|
|
2165
2507
|
});
|
|
@@ -2195,7 +2537,7 @@ async function main() {
|
|
|
2195
2537
|
}
|
|
2196
2538
|
showBanner();
|
|
2197
2539
|
const projectDir = process.cwd();
|
|
2198
|
-
console.log(
|
|
2540
|
+
console.log(pc2.gray("Analyzing repository..."));
|
|
2199
2541
|
console.log();
|
|
2200
2542
|
const projectInfo = analyzeRepository(projectDir);
|
|
2201
2543
|
showTechStack(projectInfo, args.verbose);
|
|
@@ -2223,62 +2565,77 @@ async function main() {
|
|
|
2223
2565
|
projectInfo.description = preferences.description;
|
|
2224
2566
|
}
|
|
2225
2567
|
} else {
|
|
2226
|
-
console.log(
|
|
2568
|
+
console.log(pc2.gray(`Existing project with ${projectInfo.fileCount} source files`));
|
|
2227
2569
|
console.log();
|
|
2228
2570
|
}
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2571
|
+
let claudeMdMode = "replace";
|
|
2572
|
+
let existingClaudeMd = null;
|
|
2573
|
+
const claudeMdPath = path5.join(projectDir, ".claude", "CLAUDE.md");
|
|
2574
|
+
if (fs5.existsSync(claudeMdPath)) {
|
|
2575
|
+
existingClaudeMd = fs5.readFileSync(claudeMdPath, "utf-8");
|
|
2576
|
+
if (args.force) {
|
|
2577
|
+
claudeMdMode = "replace";
|
|
2578
|
+
} else if (args.interactive) {
|
|
2579
|
+
console.log(pc2.yellow("Existing CLAUDE.md detected"));
|
|
2580
|
+
console.log();
|
|
2581
|
+
const { mode } = await prompts2({
|
|
2582
|
+
type: "select",
|
|
2583
|
+
name: "mode",
|
|
2584
|
+
message: "How should we handle the existing CLAUDE.md?",
|
|
2585
|
+
choices: [
|
|
2586
|
+
{ title: "Improve \u2014 scan and enhance the existing file", value: "improve" },
|
|
2587
|
+
{ title: "Replace \u2014 generate a new one from scratch", value: "replace" },
|
|
2588
|
+
{ title: "Keep \u2014 leave CLAUDE.md as-is, regenerate other files", value: "keep" }
|
|
2589
|
+
],
|
|
2590
|
+
initial: 0
|
|
2238
2591
|
});
|
|
2239
|
-
if (
|
|
2240
|
-
console.log(
|
|
2592
|
+
if (mode === void 0) {
|
|
2593
|
+
console.log(pc2.gray("Cancelled."));
|
|
2241
2594
|
process.exit(0);
|
|
2242
2595
|
}
|
|
2596
|
+
claudeMdMode = mode;
|
|
2243
2597
|
}
|
|
2244
2598
|
console.log();
|
|
2245
2599
|
}
|
|
2246
2600
|
if (!checkClaudeCli()) {
|
|
2247
|
-
console.error(
|
|
2248
|
-
console.error(
|
|
2601
|
+
console.error(pc2.red("Claude CLI is required but not found."));
|
|
2602
|
+
console.error(pc2.gray("Install it from: https://claude.ai/download"));
|
|
2249
2603
|
process.exit(1);
|
|
2250
2604
|
}
|
|
2251
|
-
console.log(
|
|
2605
|
+
console.log(pc2.gray("Setting up .claude/ directory structure..."));
|
|
2252
2606
|
console.log();
|
|
2253
2607
|
writeSettings(projectDir, projectInfo.techStack);
|
|
2254
2608
|
ensureDirectories(projectDir);
|
|
2255
|
-
console.log(
|
|
2256
|
-
console.log(
|
|
2609
|
+
console.log(pc2.green("Created:"));
|
|
2610
|
+
console.log(pc2.green(" + .claude/settings.json"));
|
|
2257
2611
|
console.log();
|
|
2258
|
-
const success = await runClaudeAnalysis(projectDir, projectInfo
|
|
2612
|
+
const success = await runClaudeAnalysis(projectDir, projectInfo, {
|
|
2613
|
+
claudeMdMode,
|
|
2614
|
+
existingClaudeMd: claudeMdMode === "improve" ? existingClaudeMd : null
|
|
2615
|
+
});
|
|
2259
2616
|
if (!success) {
|
|
2260
|
-
console.error(
|
|
2617
|
+
console.error(pc2.red("Claude analysis failed. Please try again."));
|
|
2261
2618
|
process.exit(1);
|
|
2262
2619
|
}
|
|
2263
2620
|
const validation = validateArtifacts(projectDir);
|
|
2264
2621
|
if (validation.duplicationsRemoved > 0) {
|
|
2265
2622
|
console.log(
|
|
2266
|
-
|
|
2623
|
+
pc2.gray(
|
|
2267
2624
|
` Deduplication: removed ${validation.duplicationsRemoved} redundancies from ${validation.filesModified} files`
|
|
2268
2625
|
)
|
|
2269
2626
|
);
|
|
2270
2627
|
}
|
|
2271
2628
|
const generatedFiles = getGeneratedFiles(projectDir);
|
|
2272
2629
|
console.log();
|
|
2273
|
-
console.log(
|
|
2630
|
+
console.log(pc2.green(`Done! (${generatedFiles.length} files)`));
|
|
2274
2631
|
console.log();
|
|
2275
|
-
console.log(
|
|
2632
|
+
console.log(pc2.bold("Generated for your stack:"));
|
|
2276
2633
|
const skills = generatedFiles.filter((f) => f.includes("/skills/"));
|
|
2277
2634
|
const agents = generatedFiles.filter((f) => f.includes("/agents/"));
|
|
2278
2635
|
const rules = generatedFiles.filter((f) => f.includes("/rules/"));
|
|
2279
2636
|
const commands = generatedFiles.filter((f) => f.includes("/commands/"));
|
|
2280
2637
|
if (generatedFiles.some((f) => f.endsWith("CLAUDE.md"))) {
|
|
2281
|
-
console.log(
|
|
2638
|
+
console.log(pc2.cyan(" CLAUDE.md (deep analysis by Claude)"));
|
|
2282
2639
|
}
|
|
2283
2640
|
if (skills.length > 0) {
|
|
2284
2641
|
console.log(
|
|
@@ -2299,23 +2656,13 @@ async function main() {
|
|
|
2299
2656
|
console.log();
|
|
2300
2657
|
if (args.interactive) {
|
|
2301
2658
|
console.log();
|
|
2302
|
-
|
|
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
|
-
}
|
|
2659
|
+
await promptExtras(projectDir);
|
|
2313
2660
|
}
|
|
2314
2661
|
console.log();
|
|
2315
|
-
console.log(`${
|
|
2662
|
+
console.log(`${pc2.cyan("Next step:")} Run ${pc2.bold("claude")} to start working!`);
|
|
2316
2663
|
console.log();
|
|
2317
2664
|
console.log(
|
|
2318
|
-
|
|
2665
|
+
pc2.gray(
|
|
2319
2666
|
"Your .claude/ files were generated by deep analysis - review them with: ls -la .claude/"
|
|
2320
2667
|
)
|
|
2321
2668
|
);
|
|
@@ -2324,7 +2671,7 @@ try {
|
|
|
2324
2671
|
const isMain = process.argv[1] && fs5.realpathSync(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
2325
2672
|
if (isMain) {
|
|
2326
2673
|
main().catch((err) => {
|
|
2327
|
-
console.error(
|
|
2674
|
+
console.error(pc2.red("Error:"), err.message);
|
|
2328
2675
|
if (process.env.DEBUG) {
|
|
2329
2676
|
console.error(err.stack);
|
|
2330
2677
|
}
|