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.
Files changed (2) hide show
  1. package/dist/cli.js +1106 -63
  2. 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 fs5 from "fs";
6
- import path5 from "path";
5
+ import fs7 from "fs";
6
+ import os2 from "os";
7
+ import path7 from "path";
7
8
  import { fileURLToPath } from "url";
8
9
  import ora from "ora";
9
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") && files.includes("spec")) 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
+ }
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")) return "black";
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
- const content = fs2.readFileSync(projectScriptPath, "utf-8");
727
- result.projectMatchesOurs = content.trim() === HOOK_SCRIPT.trim();
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
- const content = fs2.readFileSync(globalScriptPath, "utf-8");
732
- result.globalMatchesOurs = content.trim() === HOOK_SCRIPT.trim();
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 (2 commands: analyze, code-review)
1140
- 10. Run the Anti-Redundancy Enforcement checks one final time across ALL generated files \u2014 if any convention is restated, any command is duplicated, or any rule lacks a \`paths:\` filter, fix it before proceeding
1141
- 11. Output a brief summary of what was generated and any gaps found
1729
+ 9. Execute Phase 7 - generate ALL command files (6 commands)
1730
+ ${options.noMemory ? `10. Skip memory seeding (--no-memory flag)` : `10. Execute Phase 8 - seed initial memory files`}
1731
+ 11. Run the Anti-Redundancy Enforcement checks one final time across ALL generated files \u2014 if any convention is restated, any command is duplicated, or any rule lacks a \`paths:\` filter, fix it before proceeding
1732
+ 12. Output a brief summary of what was generated and any gaps found
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 optionally \`globs\` for file matching.
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 2 agent files to \`.claude/agents/\`.
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 2 command files to \`.claude/commands/\`. Each needs YAML frontmatter with
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 fs4 from "fs";
1810
- import path4 from "path";
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 = fs4.readFileSync(filePath, "utf-8");
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
- fs4.writeFileSync(filePath, frontmatter + newLines.join("\n"));
2754
+ fs6.writeFileSync(filePath, frontmatter + newLines.join("\n"));
1901
2755
  }
1902
2756
  return changes;
1903
2757
  }
1904
- function walkMdFiles(dir) {
2758
+ function walkMdFiles2(dir) {
1905
2759
  const files = [];
1906
- if (!fs4.existsSync(dir)) return files;
1907
- const entries = fs4.readdirSync(dir, { withFileTypes: true });
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 = path4.join(dir, entry.name);
2763
+ const fullPath = path6.join(dir, entry.name);
1910
2764
  if (entry.isDirectory()) {
1911
- files.push(...walkMdFiles(fullPath));
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 = path4.join(rootDir, ".claude", "CLAUDE.md");
1926
- if (!fs4.existsSync(claudeMdPath)) return result;
2779
+ const claudeMdPath = path6.join(rootDir, ".claude", "CLAUDE.md");
2780
+ if (!fs6.existsSync(claudeMdPath)) return result;
1927
2781
  try {
1928
- const claudeMd = fs4.readFileSync(claudeMdPath, "utf-8");
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 = path4.join(rootDir, ".claude");
1933
- const files = walkMdFiles(claudeDir).filter((f) => !f.endsWith("CLAUDE.md"));
2786
+ const claudeDir = path6.join(rootDir, ".claude");
2787
+ const files = walkMdFiles2(claudeDir).filter((f) => !f.endsWith("CLAUDE.md"));
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 = path4.relative(rootDir, filePath);
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 __dirname2 = path5.dirname(fileURLToPath(import.meta.url));
1957
- var VERSION = JSON.parse(
1958
- fs5.readFileSync(path5.join(__dirname2, "..", "package.json"), "utf-8")
1959
- ).version;
2810
+ var VERSION;
2811
+ if (true) {
2812
+ VERSION = "0.17.0";
2813
+ } else {
2814
+ try {
2815
+ const __dirname2 = path7.dirname(fileURLToPath(import.meta.url));
2816
+ VERSION = JSON.parse(
2817
+ fs7.readFileSync(path7.join(__dirname2, "..", "package.json"), "utf-8")
2818
+ ).version;
2819
+ } catch {
2820
+ VERSION = "unknown";
2821
+ }
2822
+ }
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: !args.includes("--no-interactive") && !args.includes("-y"),
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 Show this help message
1983
- -v, --version Show version number
1984
- -f, --force Force overwrite existing .claude files
1985
- -y, --no-interactive Skip interactive prompts (use defaults)
1986
- -V, --verbose Show detailed output
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 = path5.join(projectDir, ".claude");
3430
+ const claudeDir = path7.join(projectDir, ".claude");
2512
3431
  const files = [];
2513
3432
  function walk(dir) {
2514
- if (!fs5.existsSync(dir)) return;
2515
- const entries = fs5.readdirSync(dir, { withFileTypes: true });
3433
+ if (!fs7.existsSync(dir)) return;
3434
+ const entries = fs7.readdirSync(dir, { withFileTypes: true });
2516
3435
  for (const entry of entries) {
2517
- const fullPath = path5.join(dir, entry.name);
3436
+ const fullPath = path7.join(dir, entry.name);
2518
3437
  if (entry.isDirectory()) {
2519
3438
  walk(fullPath);
2520
3439
  } else {
2521
- files.push(path5.relative(projectDir, fullPath));
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 = path5.join(projectDir, ".claude", "CLAUDE.md");
2574
- if (fs5.existsSync(claudeMdPath)) {
2575
- existingClaudeMd = fs5.readFileSync(claudeMdPath, "utf-8");
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) => path5.basename(s, ".md")).join(", ")})`
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) => path5.basename(a, ".md")).join(", ")})`
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] && fs5.realpathSync(process.argv[1]) === fileURLToPath(import.meta.url);
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,