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.
- package/dist/cli.js +1694 -304
- 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
|
|
6
|
-
import
|
|
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
|
|
10
|
-
import
|
|
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
|
|
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(
|
|
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("
|
|
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
|
-
|
|
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(
|
|
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
|
|
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/
|
|
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
|
-
}
|
|
525
|
+
// src/extras.ts
|
|
526
|
+
import pc from "picocolors";
|
|
527
|
+
import prompts from "prompts";
|
|
601
528
|
|
|
602
529
|
// src/hooks.ts
|
|
603
|
-
import
|
|
604
|
-
import
|
|
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
|
-
|
|
777
|
-
const
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
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
|
-
|
|
802
|
-
|
|
803
|
-
|
|
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
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
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
|
-
|
|
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 (
|
|
875
|
-
|
|
876
|
-
11.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1510
|
-
import
|
|
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 =
|
|
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
|
-
|
|
2754
|
+
fs6.writeFileSync(filePath, frontmatter + newLines.join("\n"));
|
|
1601
2755
|
}
|
|
1602
2756
|
return changes;
|
|
1603
2757
|
}
|
|
1604
|
-
function
|
|
2758
|
+
function walkMdFiles2(dir) {
|
|
1605
2759
|
const files = [];
|
|
1606
|
-
if (!
|
|
1607
|
-
const entries =
|
|
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 =
|
|
2763
|
+
const fullPath = path6.join(dir, entry.name);
|
|
1610
2764
|
if (entry.isDirectory()) {
|
|
1611
|
-
files.push(...
|
|
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 =
|
|
1626
|
-
if (!
|
|
2779
|
+
const claudeMdPath = path6.join(rootDir, ".claude", "CLAUDE.md");
|
|
2780
|
+
if (!fs6.existsSync(claudeMdPath)) return result;
|
|
1627
2781
|
try {
|
|
1628
|
-
const claudeMd =
|
|
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 =
|
|
1633
|
-
const files =
|
|
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
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
change
|
|
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
|
-
|
|
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
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
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
|
|
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
|
-
${
|
|
2878
|
+
${pc2.cyan("Claude Code Starter")} v${VERSION}
|
|
1672
2879
|
|
|
1673
2880
|
Bootstrap intelligent Claude Code configurations for any repository.
|
|
1674
2881
|
|
|
1675
|
-
${
|
|
2882
|
+
${pc2.bold("USAGE")}
|
|
1676
2883
|
npx claude-code-starter [OPTIONS]
|
|
1677
2884
|
|
|
1678
|
-
${
|
|
1679
|
-
-h, --help
|
|
1680
|
-
-v, --version
|
|
1681
|
-
-f, --force
|
|
1682
|
-
-y, --no-interactive
|
|
1683
|
-
-V, --verbose
|
|
1684
|
-
|
|
1685
|
-
|
|
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
|
-
${
|
|
2910
|
+
${pc2.bold("REQUIREMENTS")}
|
|
1696
2911
|
Claude CLI must be installed: https://claude.ai/download
|
|
1697
2912
|
|
|
1698
|
-
${
|
|
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(
|
|
1705
|
-
console.log(
|
|
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(
|
|
2925
|
+
console.log(pc2.bold("Tech Stack"));
|
|
1711
2926
|
console.log();
|
|
1712
2927
|
if (techStack.primaryLanguage) {
|
|
1713
|
-
console.log(` ${
|
|
2928
|
+
console.log(` ${pc2.bold("Language:")} ${formatLanguage(techStack.primaryLanguage)}`);
|
|
1714
2929
|
}
|
|
1715
2930
|
if (techStack.primaryFramework) {
|
|
1716
|
-
console.log(` ${
|
|
2931
|
+
console.log(` ${pc2.bold("Framework:")} ${formatFramework(techStack.primaryFramework)}`);
|
|
1717
2932
|
}
|
|
1718
2933
|
if (techStack.packageManager) {
|
|
1719
|
-
console.log(` ${
|
|
2934
|
+
console.log(` ${pc2.bold("Package Manager:")} ${techStack.packageManager}`);
|
|
1720
2935
|
}
|
|
1721
2936
|
if (techStack.testingFramework) {
|
|
1722
|
-
console.log(` ${
|
|
2937
|
+
console.log(` ${pc2.bold("Testing:")} ${techStack.testingFramework}`);
|
|
1723
2938
|
}
|
|
1724
2939
|
if (verbose) {
|
|
1725
2940
|
if (techStack.linter) {
|
|
1726
|
-
console.log(` ${
|
|
2941
|
+
console.log(` ${pc2.bold("Linter:")} ${techStack.linter}`);
|
|
1727
2942
|
}
|
|
1728
2943
|
if (techStack.formatter) {
|
|
1729
|
-
console.log(` ${
|
|
2944
|
+
console.log(` ${pc2.bold("Formatter:")} ${techStack.formatter}`);
|
|
1730
2945
|
}
|
|
1731
2946
|
if (techStack.bundler) {
|
|
1732
|
-
console.log(` ${
|
|
2947
|
+
console.log(` ${pc2.bold("Bundler:")} ${techStack.bundler}`);
|
|
1733
2948
|
}
|
|
1734
2949
|
if (techStack.isMonorepo) {
|
|
1735
|
-
console.log(` ${
|
|
2950
|
+
console.log(` ${pc2.bold("Monorepo:")} yes`);
|
|
1736
2951
|
}
|
|
1737
2952
|
if (techStack.hasDocker) {
|
|
1738
|
-
console.log(` ${
|
|
2953
|
+
console.log(` ${pc2.bold("Docker:")} yes`);
|
|
1739
2954
|
}
|
|
1740
2955
|
if (techStack.hasCICD) {
|
|
1741
|
-
console.log(` ${
|
|
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(
|
|
3032
|
+
console.log(pc2.yellow("New project detected - let's set it up!"));
|
|
1818
3033
|
console.log();
|
|
1819
|
-
const descResponse = await
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
3331
|
+
const prompt = getAnalysisPrompt(projectInfo, options);
|
|
3332
|
+
console.log(pc2.cyan("Launching Claude for deep project analysis..."));
|
|
2119
3333
|
console.log(
|
|
2120
|
-
|
|
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 =
|
|
3430
|
+
const claudeDir = path7.join(projectDir, ".claude");
|
|
2170
3431
|
const files = [];
|
|
2171
3432
|
function walk(dir) {
|
|
2172
|
-
if (!
|
|
2173
|
-
const entries =
|
|
3433
|
+
if (!fs7.existsSync(dir)) return;
|
|
3434
|
+
const entries = fs7.readdirSync(dir, { withFileTypes: true });
|
|
2174
3435
|
for (const entry of entries) {
|
|
2175
|
-
const fullPath =
|
|
3436
|
+
const fullPath = path7.join(dir, entry.name);
|
|
2176
3437
|
if (entry.isDirectory()) {
|
|
2177
3438
|
walk(fullPath);
|
|
2178
3439
|
} else {
|
|
2179
|
-
files.push(
|
|
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
|
-
|
|
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(
|
|
3608
|
+
console.log(pc2.gray(`Existing project with ${projectInfo.fileCount} source files`));
|
|
2227
3609
|
console.log();
|
|
2228
3610
|
}
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
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 (
|
|
2240
|
-
console.log(
|
|
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(
|
|
2248
|
-
console.error(
|
|
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(
|
|
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(
|
|
2256
|
-
console.log(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
3671
|
+
console.log(pc2.green(`Done! (${generatedFiles.length} files)`));
|
|
2274
3672
|
console.log();
|
|
2275
|
-
console.log(
|
|
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(
|
|
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) =>
|
|
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) =>
|
|
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
|
-
|
|
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(`${
|
|
3703
|
+
console.log(`${pc2.cyan("Next step:")} Run ${pc2.bold("claude")} to start working!`);
|
|
2316
3704
|
console.log();
|
|
2317
3705
|
console.log(
|
|
2318
|
-
|
|
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] &&
|
|
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(
|
|
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,
|