claude-code-starter 0.16.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 +1106 -63
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
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
10
|
import pc2 from "picocolors";
|
|
@@ -33,9 +34,9 @@ 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
41
|
const isMonorepo = detectMonorepo(files, packageJson);
|
|
41
42
|
const hasDocker = files.includes("Dockerfile") || files.includes("docker-compose.yml") || files.includes("docker-compose.yaml");
|
|
@@ -275,7 +276,7 @@ function detectPackageManager(files) {
|
|
|
275
276
|
if (files.includes("build.gradle") || files.includes("build.gradle.kts")) return "gradle";
|
|
276
277
|
return null;
|
|
277
278
|
}
|
|
278
|
-
function detectTestingFramework(packageJson, files) {
|
|
279
|
+
function detectTestingFramework(packageJson, files, rootDir) {
|
|
279
280
|
if (packageJson) {
|
|
280
281
|
const allDeps = getAllDeps(packageJson);
|
|
281
282
|
if (allDeps.vitest) return "vitest";
|
|
@@ -290,7 +291,14 @@ function detectTestingFramework(packageJson, files) {
|
|
|
290
291
|
if (files.includes("go.mod")) return "go-test";
|
|
291
292
|
if (files.includes("Cargo.toml")) return "rust-test";
|
|
292
293
|
if (files.includes(".rspec")) return "rspec";
|
|
293
|
-
if (files.includes("Gemfile")
|
|
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
|
+
}
|
|
294
302
|
return null;
|
|
295
303
|
}
|
|
296
304
|
function detectLinter(packageJson, files) {
|
|
@@ -309,7 +317,7 @@ function detectLinter(packageJson, files) {
|
|
|
309
317
|
if (files.includes(".flake8") || files.includes("setup.cfg")) return "flake8";
|
|
310
318
|
return null;
|
|
311
319
|
}
|
|
312
|
-
function detectFormatter(packageJson, files) {
|
|
320
|
+
function detectFormatter(packageJson, files, rootDir) {
|
|
313
321
|
if (files.some(
|
|
314
322
|
(f) => f.startsWith(".prettierrc") || f === "prettier.config.js" || f === "prettier.config.mjs"
|
|
315
323
|
)) {
|
|
@@ -322,7 +330,19 @@ function detectFormatter(packageJson, files) {
|
|
|
322
330
|
if (allDeps["@biomejs/biome"]) return "biome";
|
|
323
331
|
}
|
|
324
332
|
if (files.includes("ruff.toml") || files.includes(".ruff.toml")) return "ruff";
|
|
325
|
-
if (files.includes("pyproject.toml"))
|
|
333
|
+
if (files.includes("pyproject.toml")) {
|
|
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;
|
|
345
|
+
}
|
|
326
346
|
return null;
|
|
327
347
|
}
|
|
328
348
|
function detectBundler(packageJson, files) {
|
|
@@ -723,13 +743,19 @@ function checkHookStatus(rootDir) {
|
|
|
723
743
|
};
|
|
724
744
|
if (fs2.existsSync(projectScriptPath)) {
|
|
725
745
|
result.projectInstalled = true;
|
|
726
|
-
|
|
727
|
-
|
|
746
|
+
try {
|
|
747
|
+
const content = fs2.readFileSync(projectScriptPath, "utf-8");
|
|
748
|
+
result.projectMatchesOurs = content.trim() === HOOK_SCRIPT.trim();
|
|
749
|
+
} catch {
|
|
750
|
+
}
|
|
728
751
|
}
|
|
729
752
|
if (fs2.existsSync(globalScriptPath)) {
|
|
730
753
|
result.globalInstalled = true;
|
|
731
|
-
|
|
732
|
-
|
|
754
|
+
try {
|
|
755
|
+
const content = fs2.readFileSync(globalScriptPath, "utf-8");
|
|
756
|
+
result.globalMatchesOurs = content.trim() === HOOK_SCRIPT.trim();
|
|
757
|
+
} catch {
|
|
758
|
+
}
|
|
733
759
|
}
|
|
734
760
|
return result;
|
|
735
761
|
}
|
|
@@ -925,6 +951,133 @@ function installStatuslineGlobal() {
|
|
|
925
951
|
statusLine: { type: "command", command: "bash ~/.claude/config/statusline-command.sh" }
|
|
926
952
|
});
|
|
927
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
|
+
}
|
|
928
1081
|
function patchSettings(settingsPath, patch) {
|
|
929
1082
|
try {
|
|
930
1083
|
const existing = fs2.existsSync(settingsPath) ? JSON.parse(fs2.readFileSync(settingsPath, "utf-8")) : {};
|
|
@@ -959,6 +1112,16 @@ var EXTRAS = [
|
|
|
959
1112
|
installGlobal: installStatuslineGlobal,
|
|
960
1113
|
projectPath: ".claude/config/statusline-command.sh",
|
|
961
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"
|
|
962
1125
|
}
|
|
963
1126
|
];
|
|
964
1127
|
async function promptExtras(projectDir) {
|
|
@@ -1086,19 +1249,446 @@ function generateSettings(stack) {
|
|
|
1086
1249
|
content: JSON.stringify(settings, null, 2)
|
|
1087
1250
|
};
|
|
1088
1251
|
}
|
|
1089
|
-
function writeSettings(rootDir, stack) {
|
|
1252
|
+
function writeSettings(rootDir, stack, force = false) {
|
|
1090
1253
|
const { path: settingsPath, content } = generateSettings(stack);
|
|
1091
1254
|
const fullPath = path3.join(rootDir, settingsPath);
|
|
1092
1255
|
const dir = path3.dirname(fullPath);
|
|
1093
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
|
+
}
|
|
1094
1277
|
fs3.writeFileSync(fullPath, content);
|
|
1095
1278
|
}
|
|
1096
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;
|
|
1635
|
+
}
|
|
1636
|
+
function loadTemplate(templatePath) {
|
|
1637
|
+
try {
|
|
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;
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
function findProjectTemplate(projectDir) {
|
|
1648
|
+
const templatePath = path5.join(projectDir, ".claude-template.json");
|
|
1649
|
+
if (fs5.existsSync(templatePath)) return templatePath;
|
|
1650
|
+
return null;
|
|
1651
|
+
}
|
|
1652
|
+
function readFileOrNull(filePath) {
|
|
1653
|
+
try {
|
|
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");
|
|
1675
|
+
}
|
|
1676
|
+
} catch {
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
} catch {
|
|
1680
|
+
}
|
|
1681
|
+
return result;
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1097
1684
|
// src/prompt.ts
|
|
1098
1685
|
function getAnalysisPrompt(projectInfo, options = { claudeMdMode: "replace", existingClaudeMd: null }) {
|
|
1099
1686
|
const context = buildContextSection(projectInfo);
|
|
1100
1687
|
const templateVars = buildTemplateVariables(projectInfo);
|
|
1101
1688
|
const claudeMdInstructions = buildClaudeMdInstructions(options);
|
|
1689
|
+
const memorySection = options.noMemory ? "" : `
|
|
1690
|
+
|
|
1691
|
+
${MEMORY_PROMPT}`;
|
|
1102
1692
|
return `${ANALYSIS_PROMPT}
|
|
1103
1693
|
|
|
1104
1694
|
${SKILLS_PROMPT}
|
|
@@ -1107,7 +1697,7 @@ ${AGENTS_PROMPT}
|
|
|
1107
1697
|
|
|
1108
1698
|
${RULES_PROMPT}
|
|
1109
1699
|
|
|
1110
|
-
${COMMANDS_PROMPT}
|
|
1700
|
+
${COMMANDS_PROMPT}${memorySection}
|
|
1111
1701
|
|
|
1112
1702
|
---
|
|
1113
1703
|
|
|
@@ -1134,11 +1724,12 @@ ${options.claudeMdMode === "keep" ? `3. Skip CLAUDE.md generation \u2014 the exi
|
|
|
1134
1724
|
4. Execute Phase 3 - verify quality before writing
|
|
1135
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`}
|
|
1136
1726
|
6. Execute Phase 4 - generate ALL skill files (4 core + framework-specific if detected)
|
|
1137
|
-
7. Execute Phase 5 - generate agent files
|
|
1727
|
+
7. Execute Phase 5 - generate ALL agent files (6 agents)
|
|
1138
1728
|
8. Execute Phase 6 - generate rule files
|
|
1139
|
-
9. Execute Phase 7 - generate command files (
|
|
1140
|
-
|
|
1141
|
-
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
|
|
1142
1733
|
|
|
1143
1734
|
Do NOT output file contents to stdout. Write all files to disk using the Write tool.
|
|
1144
1735
|
Generate ALL files in a single pass \u2014 do not stop after CLAUDE.md.`;
|
|
@@ -1588,7 +2179,7 @@ var SKILLS_PROMPT = `---
|
|
|
1588
2179
|
## Phase 4: Generate Skills
|
|
1589
2180
|
|
|
1590
2181
|
Write each skill file to \`.claude/skills/\` using the Write tool. Every skill must have
|
|
1591
|
-
YAML frontmatter with \`name\`, \`description\`, and
|
|
2182
|
+
YAML frontmatter with \`name\`, \`description\`, and \`globs\` for auto-triggering when relevant files are open.
|
|
1592
2183
|
|
|
1593
2184
|
**Tailor ALL skills to this specific project** \u2014 use the actual file patterns and
|
|
1594
2185
|
conventions discovered during Phase 1.
|
|
@@ -1650,12 +2241,34 @@ Generate the matching skill ONLY if the framework was detected in the tech stack
|
|
|
1650
2241
|
|
|
1651
2242
|
- **Rails detected** \u2192 Write \`.claude/skills/rails-patterns.md\` \u2014 MVC, ActiveRecord, concerns, service objects, jobs, mailers, strong parameters.
|
|
1652
2243
|
|
|
1653
|
-
- **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.*"]\`)`;
|
|
1654
2267
|
var AGENTS_PROMPT = `---
|
|
1655
2268
|
|
|
1656
2269
|
## Phase 5: Generate Agents
|
|
1657
2270
|
|
|
1658
|
-
Write
|
|
2271
|
+
Write 6 agent files to \`.claude/agents/\`.
|
|
1659
2272
|
|
|
1660
2273
|
### \`.claude/agents/code-reviewer.md\`
|
|
1661
2274
|
|
|
@@ -1710,7 +2323,121 @@ Body content \u2014 instructions for the test writer agent:
|
|
|
1710
2323
|
- Include edge cases: empty inputs, nulls, errors, boundaries
|
|
1711
2324
|
- Mock external dependencies following project patterns
|
|
1712
2325
|
- Run tests after writing to verify they pass
|
|
1713
|
-
- 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`;
|
|
1714
2441
|
var RULES_PROMPT = `---
|
|
1715
2442
|
|
|
1716
2443
|
## Phase 6: Generate Rules
|
|
@@ -1775,7 +2502,7 @@ var COMMANDS_PROMPT = `---
|
|
|
1775
2502
|
|
|
1776
2503
|
## Phase 7: Generate Commands
|
|
1777
2504
|
|
|
1778
|
-
Write
|
|
2505
|
+
Write 6 command files to \`.claude/commands/\`. Each needs YAML frontmatter with
|
|
1779
2506
|
\`allowed-tools\`, \`description\`, and optionally \`argument-hint\`.
|
|
1780
2507
|
|
|
1781
2508
|
Do NOT generate task management commands (\`task.md\`, \`status.md\`, \`done.md\`) \u2014
|
|
@@ -1803,11 +2530,138 @@ Body: This command delegates to the code-reviewer agent for thorough review.
|
|
|
1803
2530
|
1. Run \`git diff\` and \`git diff --cached\` to identify staged and unstaged changes
|
|
1804
2531
|
2. Spawn the \`code-reviewer\` agent to perform the full review
|
|
1805
2532
|
3. If the agent is unavailable, perform a lightweight review: run the linter and check for obvious issues
|
|
1806
|
-
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.`;
|
|
1807
2661
|
|
|
1808
2662
|
// src/validator.ts
|
|
1809
|
-
import
|
|
1810
|
-
import
|
|
2663
|
+
import fs6 from "fs";
|
|
2664
|
+
import path6 from "path";
|
|
1811
2665
|
function extractCommands(claudeMd) {
|
|
1812
2666
|
const commands = [];
|
|
1813
2667
|
const match = claudeMd.match(/## Common Commands[\s\S]*?```(?:bash)?\n([\s\S]*?)```/);
|
|
@@ -1867,7 +2721,7 @@ function separateFrontmatter(content) {
|
|
|
1867
2721
|
};
|
|
1868
2722
|
}
|
|
1869
2723
|
function processFile(filePath, commands, fingerprints) {
|
|
1870
|
-
const content =
|
|
2724
|
+
const content = fs6.readFileSync(filePath, "utf-8");
|
|
1871
2725
|
const { frontmatter, body } = separateFrontmatter(content);
|
|
1872
2726
|
const lines = body.split("\n");
|
|
1873
2727
|
const changes = [];
|
|
@@ -1897,18 +2751,18 @@ function processFile(filePath, commands, fingerprints) {
|
|
|
1897
2751
|
newLines.push(line);
|
|
1898
2752
|
}
|
|
1899
2753
|
if (changes.length > 0) {
|
|
1900
|
-
|
|
2754
|
+
fs6.writeFileSync(filePath, frontmatter + newLines.join("\n"));
|
|
1901
2755
|
}
|
|
1902
2756
|
return changes;
|
|
1903
2757
|
}
|
|
1904
|
-
function
|
|
2758
|
+
function walkMdFiles2(dir) {
|
|
1905
2759
|
const files = [];
|
|
1906
|
-
if (!
|
|
1907
|
-
const entries =
|
|
2760
|
+
if (!fs6.existsSync(dir)) return files;
|
|
2761
|
+
const entries = fs6.readdirSync(dir, { withFileTypes: true });
|
|
1908
2762
|
for (const entry of entries) {
|
|
1909
|
-
const fullPath =
|
|
2763
|
+
const fullPath = path6.join(dir, entry.name);
|
|
1910
2764
|
if (entry.isDirectory()) {
|
|
1911
|
-
files.push(...
|
|
2765
|
+
files.push(...walkMdFiles2(fullPath));
|
|
1912
2766
|
} else if (entry.name.endsWith(".md")) {
|
|
1913
2767
|
files.push(fullPath);
|
|
1914
2768
|
}
|
|
@@ -1922,15 +2776,15 @@ function validateArtifacts(rootDir) {
|
|
|
1922
2776
|
duplicationsRemoved: 0,
|
|
1923
2777
|
changes: []
|
|
1924
2778
|
};
|
|
1925
|
-
const claudeMdPath =
|
|
1926
|
-
if (!
|
|
2779
|
+
const claudeMdPath = path6.join(rootDir, ".claude", "CLAUDE.md");
|
|
2780
|
+
if (!fs6.existsSync(claudeMdPath)) return result;
|
|
1927
2781
|
try {
|
|
1928
|
-
const claudeMd =
|
|
2782
|
+
const claudeMd = fs6.readFileSync(claudeMdPath, "utf-8");
|
|
1929
2783
|
const commands = extractCommands(claudeMd);
|
|
1930
2784
|
const fingerprints = extractConventionFingerprints(claudeMd);
|
|
1931
2785
|
if (commands.length === 0 && fingerprints.length === 0) return result;
|
|
1932
|
-
const claudeDir =
|
|
1933
|
-
const files =
|
|
2786
|
+
const claudeDir = path6.join(rootDir, ".claude");
|
|
2787
|
+
const files = walkMdFiles2(claudeDir).filter((f) => !f.endsWith("CLAUDE.md"));
|
|
1934
2788
|
for (const filePath of files) {
|
|
1935
2789
|
result.filesChecked++;
|
|
1936
2790
|
try {
|
|
@@ -1939,7 +2793,7 @@ function validateArtifacts(rootDir) {
|
|
|
1939
2793
|
result.filesModified++;
|
|
1940
2794
|
result.duplicationsRemoved += changes.length;
|
|
1941
2795
|
for (const change of changes) {
|
|
1942
|
-
change.file =
|
|
2796
|
+
change.file = path6.relative(rootDir, filePath);
|
|
1943
2797
|
}
|
|
1944
2798
|
result.changes.push(...changes);
|
|
1945
2799
|
}
|
|
@@ -1953,22 +2807,72 @@ function validateArtifacts(rootDir) {
|
|
|
1953
2807
|
}
|
|
1954
2808
|
|
|
1955
2809
|
// src/cli.ts
|
|
1956
|
-
var
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
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
|
+
}
|
|
1960
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;
|
|
1961
2834
|
return {
|
|
1962
2835
|
help: args.includes("-h") || args.includes("--help"),
|
|
1963
2836
|
version: args.includes("-v") || args.includes("--version"),
|
|
1964
2837
|
force: args.includes("-f") || args.includes("--force"),
|
|
1965
|
-
interactive
|
|
1966
|
-
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
|
|
1967
2848
|
};
|
|
1968
2849
|
}
|
|
1969
2850
|
function getVersion() {
|
|
1970
2851
|
return VERSION;
|
|
1971
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
|
+
}
|
|
1972
2876
|
function showHelp() {
|
|
1973
2877
|
console.log(`
|
|
1974
2878
|
${pc2.cyan("Claude Code Starter")} v${VERSION}
|
|
@@ -1979,11 +2883,19 @@ ${pc2.bold("USAGE")}
|
|
|
1979
2883
|
npx claude-code-starter [OPTIONS]
|
|
1980
2884
|
|
|
1981
2885
|
${pc2.bold("OPTIONS")}
|
|
1982
|
-
-h, --help
|
|
1983
|
-
-v, --version
|
|
1984
|
-
-f, --force
|
|
1985
|
-
-y, --no-interactive
|
|
1986
|
-
-V, --verbose
|
|
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
|
|
1987
2899
|
|
|
1988
2900
|
${pc2.bold("WHAT IT DOES")}
|
|
1989
2901
|
1. Analyzes your repository's tech stack
|
|
@@ -2452,6 +3364,11 @@ function runClaudeAnalysis(projectDir, projectInfo, options = { claudeMdMode: "r
|
|
|
2452
3364
|
stdio: ["pipe", "pipe", "pipe"]
|
|
2453
3365
|
}
|
|
2454
3366
|
);
|
|
3367
|
+
const cleanup = () => {
|
|
3368
|
+
child.kill("SIGTERM");
|
|
3369
|
+
};
|
|
3370
|
+
process.on("SIGINT", cleanup);
|
|
3371
|
+
process.on("SIGTERM", cleanup);
|
|
2455
3372
|
let stdoutBuffer = "";
|
|
2456
3373
|
child.stdout.on("data", (chunk) => {
|
|
2457
3374
|
stdoutBuffer += chunk.toString();
|
|
@@ -2494,6 +3411,8 @@ function runClaudeAnalysis(projectDir, projectInfo, options = { claudeMdMode: "r
|
|
|
2494
3411
|
resolve(false);
|
|
2495
3412
|
});
|
|
2496
3413
|
child.on("close", (code) => {
|
|
3414
|
+
process.off("SIGINT", cleanup);
|
|
3415
|
+
process.off("SIGTERM", cleanup);
|
|
2497
3416
|
if (code === 0) {
|
|
2498
3417
|
spinner.succeed("Claude analysis complete!");
|
|
2499
3418
|
resolve(true);
|
|
@@ -2508,17 +3427,17 @@ function runClaudeAnalysis(projectDir, projectInfo, options = { claudeMdMode: "r
|
|
|
2508
3427
|
});
|
|
2509
3428
|
}
|
|
2510
3429
|
function getGeneratedFiles(projectDir) {
|
|
2511
|
-
const claudeDir =
|
|
3430
|
+
const claudeDir = path7.join(projectDir, ".claude");
|
|
2512
3431
|
const files = [];
|
|
2513
3432
|
function walk(dir) {
|
|
2514
|
-
if (!
|
|
2515
|
-
const entries =
|
|
3433
|
+
if (!fs7.existsSync(dir)) return;
|
|
3434
|
+
const entries = fs7.readdirSync(dir, { withFileTypes: true });
|
|
2516
3435
|
for (const entry of entries) {
|
|
2517
|
-
const fullPath =
|
|
3436
|
+
const fullPath = path7.join(dir, entry.name);
|
|
2518
3437
|
if (entry.isDirectory()) {
|
|
2519
3438
|
walk(fullPath);
|
|
2520
3439
|
} else {
|
|
2521
|
-
files.push(
|
|
3440
|
+
files.push(path7.relative(projectDir, fullPath));
|
|
2522
3441
|
}
|
|
2523
3442
|
}
|
|
2524
3443
|
}
|
|
@@ -2536,11 +3455,132 @@ async function main() {
|
|
|
2536
3455
|
process.exit(0);
|
|
2537
3456
|
}
|
|
2538
3457
|
showBanner();
|
|
3458
|
+
checkForUpdate();
|
|
2539
3459
|
const projectDir = process.cwd();
|
|
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
|
+
}
|
|
2540
3566
|
console.log(pc2.gray("Analyzing repository..."));
|
|
2541
3567
|
console.log();
|
|
2542
3568
|
const projectInfo = analyzeRepository(projectDir);
|
|
2543
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
|
+
}
|
|
2544
3584
|
let preferences = null;
|
|
2545
3585
|
if (!projectInfo.isExisting) {
|
|
2546
3586
|
preferences = await promptNewProject(args);
|
|
@@ -2570,9 +3610,9 @@ async function main() {
|
|
|
2570
3610
|
}
|
|
2571
3611
|
let claudeMdMode = "replace";
|
|
2572
3612
|
let existingClaudeMd = null;
|
|
2573
|
-
const claudeMdPath =
|
|
2574
|
-
if (
|
|
2575
|
-
existingClaudeMd =
|
|
3613
|
+
const claudeMdPath = path7.join(projectDir, ".claude", "CLAUDE.md");
|
|
3614
|
+
if (fs7.existsSync(claudeMdPath)) {
|
|
3615
|
+
existingClaudeMd = fs7.readFileSync(claudeMdPath, "utf-8");
|
|
2576
3616
|
if (args.force) {
|
|
2577
3617
|
claudeMdMode = "replace";
|
|
2578
3618
|
} else if (args.interactive) {
|
|
@@ -2604,14 +3644,15 @@ async function main() {
|
|
|
2604
3644
|
}
|
|
2605
3645
|
console.log(pc2.gray("Setting up .claude/ directory structure..."));
|
|
2606
3646
|
console.log();
|
|
2607
|
-
writeSettings(projectDir, projectInfo.techStack);
|
|
3647
|
+
writeSettings(projectDir, projectInfo.techStack, args.force);
|
|
2608
3648
|
ensureDirectories(projectDir);
|
|
2609
3649
|
console.log(pc2.green("Created:"));
|
|
2610
3650
|
console.log(pc2.green(" + .claude/settings.json"));
|
|
2611
3651
|
console.log();
|
|
2612
3652
|
const success = await runClaudeAnalysis(projectDir, projectInfo, {
|
|
2613
3653
|
claudeMdMode,
|
|
2614
|
-
existingClaudeMd: claudeMdMode === "improve" ? existingClaudeMd : null
|
|
3654
|
+
existingClaudeMd: claudeMdMode === "improve" ? existingClaudeMd : null,
|
|
3655
|
+
noMemory: args.noMemory
|
|
2615
3656
|
});
|
|
2616
3657
|
if (!success) {
|
|
2617
3658
|
console.error(pc2.red("Claude analysis failed. Please try again."));
|
|
@@ -2639,12 +3680,12 @@ async function main() {
|
|
|
2639
3680
|
}
|
|
2640
3681
|
if (skills.length > 0) {
|
|
2641
3682
|
console.log(
|
|
2642
|
-
` ${skills.length} skills (${skills.map((s) =>
|
|
3683
|
+
` ${skills.length} skills (${skills.map((s) => path7.basename(s, ".md")).join(", ")})`
|
|
2643
3684
|
);
|
|
2644
3685
|
}
|
|
2645
3686
|
if (agents.length > 0) {
|
|
2646
3687
|
console.log(
|
|
2647
|
-
` ${agents.length} agents (${agents.map((a) =>
|
|
3688
|
+
` ${agents.length} agents (${agents.map((a) => path7.basename(a, ".md")).join(", ")})`
|
|
2648
3689
|
);
|
|
2649
3690
|
}
|
|
2650
3691
|
if (rules.length > 0) {
|
|
@@ -2668,7 +3709,7 @@ async function main() {
|
|
|
2668
3709
|
);
|
|
2669
3710
|
}
|
|
2670
3711
|
try {
|
|
2671
|
-
const isMain = process.argv[1] &&
|
|
3712
|
+
const isMain = process.argv[1] && fs7.realpathSync(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
2672
3713
|
if (isMain) {
|
|
2673
3714
|
main().catch((err) => {
|
|
2674
3715
|
console.error(pc2.red("Error:"), err.message);
|
|
@@ -2682,9 +3723,11 @@ try {
|
|
|
2682
3723
|
}
|
|
2683
3724
|
export {
|
|
2684
3725
|
checkClaudeCli,
|
|
3726
|
+
checkForUpdate,
|
|
2685
3727
|
formatFramework,
|
|
2686
3728
|
formatLanguage,
|
|
2687
3729
|
getVersion,
|
|
3730
|
+
isNewerVersion,
|
|
2688
3731
|
mapFormatter,
|
|
2689
3732
|
parseArgs,
|
|
2690
3733
|
promptNewProject,
|