@mikulgohil/ai-kit 1.0.0 → 1.1.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/index.js CHANGED
@@ -13,19 +13,31 @@ var TEMPLATES_DIR = path.join(PACKAGE_ROOT, "templates");
13
13
  var COMMANDS_DIR = path.join(PACKAGE_ROOT, "commands");
14
14
  var GUIDES_DIR = path.join(PACKAGE_ROOT, "guides");
15
15
  var DOCS_SCAFFOLDS_DIR = path.join(PACKAGE_ROOT, "docs-scaffolds");
16
- var VERSION = "1.0.0";
16
+ var VERSION = "1.1.0";
17
17
  var AI_KIT_CONFIG_FILE = "ai-kit.config.json";
18
18
  var GENERATED_FILES = {
19
19
  claudeMd: "CLAUDE.md",
20
20
  cursorRules: ".cursorrules",
21
21
  cursorMdcDir: ".cursor/rules",
22
22
  claudeSettings: ".claude/settings.json",
23
- claudeCommands: ".claude/commands"
23
+ claudeCommands: ".claude/commands",
24
+ claudeSkills: ".claude/skills",
25
+ cursorSkills: ".cursor/skills"
24
26
  };
27
+ var TEMPLATE_FRAGMENTS = [
28
+ "base",
29
+ "nextjs-app-router",
30
+ "nextjs-pages-router",
31
+ "sitecore-xmc",
32
+ "tailwind",
33
+ "typescript",
34
+ "monorepo",
35
+ "figma"
36
+ ];
25
37
 
26
38
  // src/commands/init.ts
27
39
  import path15 from "path";
28
- import fs5 from "fs-extra";
40
+ import fs6 from "fs-extra";
29
41
  import { select, confirm } from "@inquirer/prompts";
30
42
  import ora from "ora";
31
43
 
@@ -388,8 +400,7 @@ function detectMcpServers(projectPath) {
388
400
  figma: combined.includes("figma"),
389
401
  github: combined.includes("github"),
390
402
  context7: combined.includes("context7"),
391
- perplexity: combined.includes("perplexity"),
392
- notion: combined.includes("notion")
403
+ perplexity: combined.includes("perplexity")
393
404
  };
394
405
  }
395
406
 
@@ -430,6 +441,7 @@ async function scanProject(projectPath) {
430
441
 
431
442
  // src/generator/assembler.ts
432
443
  import path11 from "path";
444
+ import fs2 from "fs-extra";
433
445
  function readTemplate(relativePath) {
434
446
  const fullPath = path11.join(TEMPLATES_DIR, relativePath);
435
447
  const content = readFileSafe(fullPath);
@@ -438,21 +450,51 @@ function readTemplate(relativePath) {
438
450
  }
439
451
  return content.trim();
440
452
  }
441
- function assembleTemplate(subfolder, fragments, variables) {
453
+ function loadCustomFragments(projectPath) {
454
+ const customDir = path11.join(projectPath, ".ai-kit", "fragments");
455
+ if (!fs2.existsSync(customDir)) return [];
456
+ try {
457
+ const files = fs2.readdirSync(customDir).filter((f) => f.endsWith(".md"));
458
+ return files.map((f) => {
459
+ const content = fs2.readFileSync(path11.join(customDir, f), "utf-8");
460
+ return content.trim();
461
+ }).filter(Boolean);
462
+ } catch {
463
+ return [];
464
+ }
465
+ }
466
+ function assembleTemplate(subfolder, fragments, variables, options) {
442
467
  const versionComment = `<!-- Generated by ai-kit v${VERSION} -->`;
443
468
  const header = readTemplate("header.md");
444
469
  const body = fragments.map((name) => readTemplate(`${subfolder}/${name}.md`)).join("\n\n---\n\n");
470
+ const strictnessNote = options?.strictness && options.strictness !== "standard" ? `
471
+
472
+ ---
473
+
474
+ > **Strictness Level: ${options.strictness}**
475
+ > ${getStrictnessDescription(options.strictness)}` : "";
476
+ const customSection = options?.customFragments?.length ? "\n\n---\n\n" + options.customFragments.join("\n\n---\n\n") : "";
445
477
  const full = `${versionComment}
446
478
  ${header}
447
479
 
448
480
  ---
449
481
 
450
- ${body}`;
482
+ ${body}${strictnessNote}${customSection}`;
451
483
  const replaced = replacePlaceholders(full, variables);
452
484
  return `<!-- AI-KIT:START -->
453
485
  ${replaced}
454
486
  <!-- AI-KIT:END -->`;
455
487
  }
488
+ function getStrictnessDescription(level) {
489
+ switch (level) {
490
+ case "strict":
491
+ return "Enforce all rules strictly. No exceptions without explicit approval. All PRs must pass every check. Zero tolerance for `any` types, missing tests, or accessibility gaps.";
492
+ case "relaxed":
493
+ return "Rules are guidelines, not hard requirements. Prioritize shipping speed over perfection. Skip non-critical checks when under time pressure. Use best judgment.";
494
+ default:
495
+ return "Standard enforcement. Follow rules by default, use judgment for edge cases.";
496
+ }
497
+ }
456
498
  function replacePlaceholders(content, variables) {
457
499
  let result = content;
458
500
  for (const [key, value] of Object.entries(variables)) {
@@ -489,10 +531,13 @@ function selectFragments(scan) {
489
531
  }
490
532
  return fragments;
491
533
  }
492
- function generateClaudeMd(scan) {
534
+ function generateClaudeMd(scan, options) {
493
535
  const fragments = selectFragments(scan);
494
536
  const variables = buildVariables(scan);
495
- return assembleTemplate("claude-md", fragments, variables);
537
+ return assembleTemplate("claude-md", fragments, variables, {
538
+ customFragments: options?.customFragments,
539
+ strictness: options?.strictness
540
+ });
496
541
  }
497
542
  function buildVariables(scan) {
498
543
  const techStack = [];
@@ -526,10 +571,13 @@ function buildVariables(scan) {
526
571
  }
527
572
 
528
573
  // src/generator/cursorrules.ts
529
- function generateCursorRules(scan) {
574
+ function generateCursorRules(scan, options) {
530
575
  const fragments = selectFragments(scan);
531
576
  const variables = buildCursorVariables(scan);
532
- return assembleTemplate("cursorrules", fragments, variables);
577
+ return assembleTemplate("cursorrules", fragments, variables, {
578
+ customFragments: options?.customFragments,
579
+ strictness: options?.strictness
580
+ });
533
581
  }
534
582
  function buildCursorVariables(scan) {
535
583
  const techStack = [];
@@ -619,21 +667,23 @@ ${replaced}`
619
667
  }
620
668
 
621
669
  // src/generator/config.ts
622
- function generateConfig(scan, templates, commands, guides) {
670
+ function generateConfig(scan, templates, commands, guides, options) {
623
671
  return {
624
672
  version: VERSION,
625
673
  scanResult: scan,
626
674
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
627
675
  templates,
628
676
  commands,
629
- guides
677
+ guides,
678
+ strictness: options?.strictness || "standard",
679
+ customFragments: options?.customFragments || []
630
680
  };
631
681
  }
632
682
 
633
- // src/copier/commands.ts
683
+ // src/copier/skills.ts
634
684
  import path12 from "path";
635
- import fs2 from "fs-extra";
636
- var AVAILABLE_COMMANDS = [
685
+ import fs3 from "fs-extra";
686
+ var AVAILABLE_SKILLS = [
637
687
  "prompt-help",
638
688
  "review",
639
689
  "fix-bug",
@@ -659,26 +709,94 @@ var AVAILABLE_COMMANDS = [
659
709
  "sitecore-debug",
660
710
  "responsive-check",
661
711
  "document",
662
- "token-tips"
712
+ "token-tips",
713
+ // New skills (v1.1.0)
714
+ "perf-audit",
715
+ "bundle-check",
716
+ "i18n-check",
717
+ "schema-gen",
718
+ "docker-debug",
719
+ "ci-fix",
720
+ "changelog",
721
+ "release",
722
+ "storybook-gen",
723
+ "visual-diff",
724
+ "db-migrate",
725
+ "dependency-graph"
663
726
  ];
664
- async function copyCommands(targetDir) {
665
- const commandsTarget = path12.join(targetDir, ".claude", "commands");
666
- await fs2.ensureDir(commandsTarget);
727
+ var SKILL_DESCRIPTIONS = {
728
+ "prompt-help": "Help developers write effective AI prompts with structured context",
729
+ "review": "Deep code review following project coding standards",
730
+ "fix-bug": "Systematic debugging workflow with root cause analysis and regression testing",
731
+ "new-component": "Scaffold new React components with types, tests, and documentation",
732
+ "new-page": "Scaffold new Next.js pages/routes with proper file structure",
733
+ "understand": "Explain code architecture, data flow, and design decisions",
734
+ "test": "Generate unit and integration tests with React Testing Library",
735
+ "optimize": "Analyze and fix performance issues in components and pages",
736
+ "figma-to-code": "Implement Figma designs using project design tokens",
737
+ "design-tokens": "Audit and manage design token systems",
738
+ "accessibility-audit": "WCAG 2.1 AA accessibility compliance audit",
739
+ "security-check": "Scan for XSS, injection, secrets, and OWASP Top 10 vulnerabilities",
740
+ "refactor": "Restructure code to improve readability without changing behavior",
741
+ "api-route": "Scaffold Next.js API routes with validation, typing, and error handling",
742
+ "pre-pr": "Pre-pull-request checklist covering types, a11y, security, tests, and more",
743
+ "migrate": "Guide framework and library migrations step by step",
744
+ "error-boundary": "Generate error boundaries, loading states, and fallback UI",
745
+ "type-fix": "Fix TypeScript issues \u2014 replace any types, add null checks, tighten types",
746
+ "extract-hook": "Extract component logic into reusable custom React hooks",
747
+ "dep-check": "Audit dependencies for unused, outdated, vulnerable, and bloated packages",
748
+ "env-setup": "Generate .env.example, validate environment variables, check for leaked secrets",
749
+ "commit-msg": "Generate conventional commit messages from staged git changes",
750
+ "sitecore-debug": "Debug Sitecore XM Cloud integration issues",
751
+ "responsive-check": "Audit responsive design \u2014 breakpoints, touch targets, overflow",
752
+ "document": "Generate documentation for existing components and utilities",
753
+ "token-tips": "Token usage optimization strategies for AI coding assistants",
754
+ // New skills (v1.1.0)
755
+ "perf-audit": "Lighthouse-style performance audit covering Core Web Vitals, resource loading, and caching",
756
+ "bundle-check": "Analyze bundle size, find heavy imports, suggest tree-shaking and code splitting",
757
+ "i18n-check": "Find hardcoded strings, missing translation keys, and internationalization gaps",
758
+ "schema-gen": "Generate TypeScript types and Zod schemas from API responses, JSON, or GraphQL",
759
+ "docker-debug": "Analyze Dockerfiles for security, efficiency, and best practice violations",
760
+ "ci-fix": "Debug CI/CD pipeline failures, optimize caching, and improve build performance",
761
+ "changelog": "Generate formatted changelogs from git history following Keep a Changelog format",
762
+ "release": "Guided release workflow with versioning, changelog, tagging, and release notes",
763
+ "storybook-gen": "Generate Storybook stories with controls, play functions, and visual tests",
764
+ "visual-diff": "Visual regression testing plan for changed components and pages",
765
+ "db-migrate": "Create safe database migrations with rollback strategies for Prisma, Drizzle, or SQL",
766
+ "dependency-graph": "Map module dependencies, find circular imports, and analyze coupling metrics"
767
+ };
768
+ async function copySkills(targetDir) {
667
769
  const copied = [];
668
- for (const cmd of AVAILABLE_COMMANDS) {
669
- const src = path12.join(COMMANDS_DIR, `${cmd}.md`);
670
- const dest = path12.join(commandsTarget, `${cmd}.md`);
671
- if (await fs2.pathExists(src)) {
672
- await fs2.copy(src, dest, { overwrite: true });
673
- copied.push(cmd);
674
- }
770
+ for (const skill of AVAILABLE_SKILLS) {
771
+ const src = path12.join(COMMANDS_DIR, `${skill}.md`);
772
+ if (!await fs3.pathExists(src)) continue;
773
+ const content = await fs3.readFile(src, "utf-8");
774
+ const description = SKILL_DESCRIPTIONS[skill] || skill;
775
+ const claudeSkillDir = path12.join(targetDir, ".claude", "skills", skill);
776
+ await fs3.ensureDir(claudeSkillDir);
777
+ await fs3.writeFile(
778
+ path12.join(claudeSkillDir, "SKILL.md"),
779
+ content,
780
+ "utf-8"
781
+ );
782
+ const cursorSkillDir = path12.join(targetDir, ".cursor", "skills", skill);
783
+ await fs3.ensureDir(cursorSkillDir);
784
+ await fs3.writeFile(
785
+ path12.join(cursorSkillDir, "SKILL.md"),
786
+ content,
787
+ "utf-8"
788
+ );
789
+ const legacyDir = path12.join(targetDir, ".claude", "commands");
790
+ await fs3.ensureDir(legacyDir);
791
+ await fs3.copy(src, path12.join(legacyDir, `${skill}.md`), { overwrite: true });
792
+ copied.push(skill);
675
793
  }
676
794
  return copied;
677
795
  }
678
796
 
679
797
  // src/copier/guides.ts
680
798
  import path13 from "path";
681
- import fs3 from "fs-extra";
799
+ import fs4 from "fs-extra";
682
800
  var AVAILABLE_GUIDES = [
683
801
  "getting-started",
684
802
  "prompt-playbook",
@@ -688,13 +806,13 @@ var AVAILABLE_GUIDES = [
688
806
  ];
689
807
  async function copyGuides(targetDir) {
690
808
  const guidesTarget = path13.join(targetDir, "ai-kit", "guides");
691
- await fs3.ensureDir(guidesTarget);
809
+ await fs4.ensureDir(guidesTarget);
692
810
  const copied = [];
693
811
  for (const guide of AVAILABLE_GUIDES) {
694
812
  const src = path13.join(GUIDES_DIR, `${guide}.md`);
695
813
  const dest = path13.join(guidesTarget, `${guide}.md`);
696
- if (await fs3.pathExists(src)) {
697
- await fs3.copy(src, dest, { overwrite: true });
814
+ if (await fs4.pathExists(src)) {
815
+ await fs4.copy(src, dest, { overwrite: true });
698
816
  copied.push(guide);
699
817
  }
700
818
  }
@@ -703,20 +821,20 @@ async function copyGuides(targetDir) {
703
821
 
704
822
  // src/copier/docs.ts
705
823
  import path14 from "path";
706
- import fs4 from "fs-extra";
824
+ import fs5 from "fs-extra";
707
825
  var DOC_SCAFFOLDS = ["mistakes-log", "decisions-log", "time-log"];
708
826
  async function scaffoldDocs(targetDir) {
709
827
  const docsTarget = path14.join(targetDir, "docs");
710
- await fs4.ensureDir(docsTarget);
828
+ await fs5.ensureDir(docsTarget);
711
829
  const created = [];
712
830
  for (const doc of DOC_SCAFFOLDS) {
713
831
  const src = path14.join(DOCS_SCAFFOLDS_DIR, `${doc}.md`);
714
832
  const dest = path14.join(docsTarget, `${doc}.md`);
715
- if (await fs4.pathExists(dest)) {
833
+ if (await fs5.pathExists(dest)) {
716
834
  continue;
717
835
  }
718
- if (await fs4.pathExists(src)) {
719
- await fs4.copy(src, dest);
836
+ if (await fs5.pathExists(src)) {
837
+ await fs5.copy(src, dest);
720
838
  created.push(doc);
721
839
  }
722
840
  }
@@ -759,16 +877,18 @@ async function initCommand(targetPath) {
759
877
  const clarifications = await askClarifications(scan);
760
878
  scan = applyClarifications(scan, clarifications);
761
879
  const tools = await selectTools();
880
+ const strictness = await selectStrictness();
881
+ const customFragments = loadCustomFragments(projectDir);
762
882
  const conflict = await selectConflictStrategy(projectDir);
763
883
  logSection("Generating Files");
764
- const results = await generate(projectDir, scan, tools, conflict);
884
+ const results = await generate(projectDir, scan, tools, conflict, { strictness, customFragments });
765
885
  logSection("Setup Complete");
766
886
  if (results.claudeMd) logSuccess(`CLAUDE.md generated`);
767
887
  if (results.cursorRules) logSuccess(`.cursorrules generated`);
768
888
  if (results.cursorMdcFiles > 0)
769
889
  logSuccess(`${results.cursorMdcFiles} .cursor/rules/*.mdc files generated`);
770
890
  if (results.commands.length > 0)
771
- logSuccess(`${results.commands.length} slash commands copied`);
891
+ logSuccess(`${results.commands.length} skills generated (.claude/skills/ + .cursor/skills/)`);
772
892
  if (results.guides.length > 0)
773
893
  logSuccess(`${results.guides.length} guides added to ai-kit/guides/`);
774
894
  if (results.docs.length > 0)
@@ -822,6 +942,17 @@ async function selectTools() {
822
942
  cursor: tool === "both" || tool === "cursor"
823
943
  };
824
944
  }
945
+ async function selectStrictness() {
946
+ return select({
947
+ message: "How strictly should AI enforce these rules?",
948
+ choices: [
949
+ { name: "Standard \u2014 follow rules by default, use judgment for edge cases", value: "standard" },
950
+ { name: "Strict \u2014 enforce all rules, no exceptions without approval", value: "strict" },
951
+ { name: "Relaxed \u2014 rules are guidelines, prioritize shipping speed", value: "relaxed" }
952
+ ],
953
+ default: "standard"
954
+ });
955
+ }
825
956
  async function selectConflictStrategy(projectDir) {
826
957
  const hasExisting = fileExists(path15.join(projectDir, GENERATED_FILES.claudeMd)) || fileExists(path15.join(projectDir, GENERATED_FILES.cursorRules));
827
958
  if (!hasExisting) return "overwrite";
@@ -840,7 +971,7 @@ async function selectConflictStrategy(projectDir) {
840
971
  default: "overwrite"
841
972
  });
842
973
  }
843
- async function generate(projectDir, scan, tools, conflict) {
974
+ async function generate(projectDir, scan, tools, conflict, opts) {
844
975
  const result = {
845
976
  claudeMd: false,
846
977
  cursorRules: false,
@@ -852,29 +983,28 @@ async function generate(projectDir, scan, tools, conflict) {
852
983
  if (tools.claude) {
853
984
  const claudeMdPath = path15.join(projectDir, GENERATED_FILES.claudeMd);
854
985
  if (conflict === "overwrite" || !fileExists(claudeMdPath)) {
855
- const content = generateClaudeMd(scan);
856
- await fs5.writeFile(claudeMdPath, content, "utf-8");
986
+ const content = generateClaudeMd(scan, { strictness: opts?.strictness, customFragments: opts?.customFragments });
987
+ await fs6.writeFile(claudeMdPath, content, "utf-8");
857
988
  result.claudeMd = true;
858
989
  } else {
859
990
  logWarning("CLAUDE.md exists, skipping");
860
991
  }
861
- result.commands = await copyCommands(projectDir);
862
- await fs5.ensureDir(path15.join(projectDir, ".claude", "commands"));
992
+ result.commands = await copySkills(projectDir);
863
993
  }
864
994
  if (tools.cursor) {
865
995
  const cursorPath = path15.join(projectDir, GENERATED_FILES.cursorRules);
866
996
  if (conflict === "overwrite" || !fileExists(cursorPath)) {
867
- const content = generateCursorRules(scan);
868
- await fs5.writeFile(cursorPath, content, "utf-8");
997
+ const content = generateCursorRules(scan, { strictness: opts?.strictness, customFragments: opts?.customFragments });
998
+ await fs6.writeFile(cursorPath, content, "utf-8");
869
999
  result.cursorRules = true;
870
1000
  } else {
871
1001
  logWarning(".cursorrules exists, skipping");
872
1002
  }
873
1003
  const mdcDir = path15.join(projectDir, GENERATED_FILES.cursorMdcDir);
874
- await fs5.ensureDir(mdcDir);
1004
+ await fs6.ensureDir(mdcDir);
875
1005
  const mdcFiles = generateMdcFiles(scan);
876
1006
  for (const mdc of mdcFiles) {
877
- await fs5.writeFile(path15.join(mdcDir, mdc.filename), mdc.content, "utf-8");
1007
+ await fs6.writeFile(path15.join(mdcDir, mdc.filename), mdc.content, "utf-8");
878
1008
  }
879
1009
  result.cursorMdcFiles = mdcFiles.length;
880
1010
  }
@@ -883,8 +1013,8 @@ async function generate(projectDir, scan, tools, conflict) {
883
1013
  const templates = [];
884
1014
  if (result.claudeMd) templates.push("CLAUDE.md");
885
1015
  if (result.cursorRules) templates.push(".cursorrules");
886
- const config = generateConfig(scan, templates, result.commands, result.guides);
887
- await fs5.writeJson(
1016
+ const config = generateConfig(scan, templates, result.commands, result.guides, { strictness: opts?.strictness, customFragments: opts?.customFragments });
1017
+ await fs6.writeJson(
888
1018
  path15.join(projectDir, AI_KIT_CONFIG_FILE),
889
1019
  config,
890
1020
  { spaces: 2 }
@@ -962,7 +1092,7 @@ function showRecommendations(scan) {
962
1092
 
963
1093
  // src/commands/update.ts
964
1094
  import path16 from "path";
965
- import fs6 from "fs-extra";
1095
+ import fs7 from "fs-extra";
966
1096
  import ora2 from "ora";
967
1097
  import { confirm as confirm2 } from "@inquirer/prompts";
968
1098
  async function updateCommand(targetPath) {
@@ -993,44 +1123,47 @@ async function updateCommand(targetPath) {
993
1123
  const scan = await scanProject(projectDir);
994
1124
  spinner.succeed("Project re-scanned");
995
1125
  logSection("Updating Files");
1126
+ const strictness = existingConfig.strictness || "standard";
1127
+ const customFragments = loadCustomFragments(projectDir);
1128
+ const genOpts = { strictness, customFragments };
996
1129
  const templates = [];
997
1130
  if (existingConfig.templates.includes("CLAUDE.md") || fileExists(path16.join(projectDir, GENERATED_FILES.claudeMd))) {
998
1131
  const claudeMdPath = path16.join(projectDir, GENERATED_FILES.claudeMd);
999
- const newContent = generateClaudeMd(scan);
1132
+ const newContent = generateClaudeMd(scan, genOpts);
1000
1133
  const existing = readFileSafe(claudeMdPath);
1001
1134
  if (existing) {
1002
- await fs6.writeFile(claudeMdPath, mergeWithMarkers(existing, newContent), "utf-8");
1135
+ await fs7.writeFile(claudeMdPath, mergeWithMarkers(existing, newContent), "utf-8");
1003
1136
  } else {
1004
- await fs6.writeFile(claudeMdPath, newContent, "utf-8");
1137
+ await fs7.writeFile(claudeMdPath, newContent, "utf-8");
1005
1138
  }
1006
1139
  templates.push("CLAUDE.md");
1007
1140
  logSuccess("CLAUDE.md updated");
1008
1141
  }
1009
1142
  if (existingConfig.templates.includes(".cursorrules") || fileExists(path16.join(projectDir, GENERATED_FILES.cursorRules))) {
1010
1143
  const cursorRulesPath = path16.join(projectDir, GENERATED_FILES.cursorRules);
1011
- const newContent = generateCursorRules(scan);
1144
+ const newContent = generateCursorRules(scan, genOpts);
1012
1145
  const existing = readFileSafe(cursorRulesPath);
1013
1146
  if (existing) {
1014
- await fs6.writeFile(cursorRulesPath, mergeWithMarkers(existing, newContent), "utf-8");
1147
+ await fs7.writeFile(cursorRulesPath, mergeWithMarkers(existing, newContent), "utf-8");
1015
1148
  } else {
1016
- await fs6.writeFile(cursorRulesPath, newContent, "utf-8");
1149
+ await fs7.writeFile(cursorRulesPath, newContent, "utf-8");
1017
1150
  }
1018
1151
  templates.push(".cursorrules");
1019
1152
  logSuccess(".cursorrules updated");
1020
1153
  const mdcDir = path16.join(projectDir, GENERATED_FILES.cursorMdcDir);
1021
- await fs6.ensureDir(mdcDir);
1154
+ await fs7.ensureDir(mdcDir);
1022
1155
  const mdcFiles = generateMdcFiles(scan);
1023
1156
  for (const mdc of mdcFiles) {
1024
- await fs6.writeFile(path16.join(mdcDir, mdc.filename), mdc.content, "utf-8");
1157
+ await fs7.writeFile(path16.join(mdcDir, mdc.filename), mdc.content, "utf-8");
1025
1158
  }
1026
1159
  logSuccess(`${mdcFiles.length} .cursor/rules/*.mdc files updated`);
1027
1160
  }
1028
- const commands = await copyCommands(projectDir);
1029
- logSuccess(`${commands.length} slash commands updated`);
1161
+ const commands = await copySkills(projectDir);
1162
+ logSuccess(`${commands.length} skills updated (.claude/skills/ + .cursor/skills/)`);
1030
1163
  const guides = await copyGuides(projectDir);
1031
1164
  logSuccess(`${guides.length} guides updated`);
1032
- const config = generateConfig(scan, templates, commands, guides);
1033
- await fs6.writeJson(configPath, config, { spaces: 2 });
1165
+ const config = generateConfig(scan, templates, commands, guides, genOpts);
1166
+ await fs7.writeJson(configPath, config, { spaces: 2 });
1034
1167
  logSuccess("ai-kit.config.json updated");
1035
1168
  console.log("");
1036
1169
  logInfo("All AI configs refreshed with latest project scan.");
@@ -1038,7 +1171,7 @@ async function updateCommand(targetPath) {
1038
1171
 
1039
1172
  // src/commands/reset.ts
1040
1173
  import path17 from "path";
1041
- import fs7 from "fs-extra";
1174
+ import fs8 from "fs-extra";
1042
1175
  import { confirm as confirm3 } from "@inquirer/prompts";
1043
1176
  async function resetCommand(targetPath) {
1044
1177
  const projectDir = path17.resolve(targetPath || process.cwd());
@@ -1048,6 +1181,8 @@ async function resetCommand(targetPath) {
1048
1181
  logInfo(` - ${GENERATED_FILES.cursorRules}`);
1049
1182
  logInfo(` - ${GENERATED_FILES.cursorMdcDir}/`);
1050
1183
  logInfo(` - ${GENERATED_FILES.claudeCommands}/`);
1184
+ logInfo(` - ${GENERATED_FILES.claudeSkills}/`);
1185
+ logInfo(` - ${GENERATED_FILES.cursorSkills}/`);
1051
1186
  logInfo(` - ai-kit/`);
1052
1187
  logInfo(` - ${AI_KIT_CONFIG_FILE}`);
1053
1188
  console.log("");
@@ -1062,32 +1197,42 @@ async function resetCommand(targetPath) {
1062
1197
  const removed = [];
1063
1198
  const claudeMdPath = path17.join(projectDir, GENERATED_FILES.claudeMd);
1064
1199
  if (fileExists(claudeMdPath)) {
1065
- await fs7.remove(claudeMdPath);
1200
+ await fs8.remove(claudeMdPath);
1066
1201
  removed.push(GENERATED_FILES.claudeMd);
1067
1202
  }
1068
1203
  const cursorPath = path17.join(projectDir, GENERATED_FILES.cursorRules);
1069
1204
  if (fileExists(cursorPath)) {
1070
- await fs7.remove(cursorPath);
1205
+ await fs8.remove(cursorPath);
1071
1206
  removed.push(GENERATED_FILES.cursorRules);
1072
1207
  }
1073
1208
  const cursorMdcDir = path17.join(projectDir, GENERATED_FILES.cursorMdcDir);
1074
1209
  if (fileExists(cursorMdcDir)) {
1075
- await fs7.remove(cursorMdcDir);
1210
+ await fs8.remove(cursorMdcDir);
1076
1211
  removed.push(GENERATED_FILES.cursorMdcDir);
1077
1212
  }
1078
1213
  const commandsDir = path17.join(projectDir, GENERATED_FILES.claudeCommands);
1079
1214
  if (fileExists(commandsDir)) {
1080
- await fs7.remove(commandsDir);
1215
+ await fs8.remove(commandsDir);
1081
1216
  removed.push(GENERATED_FILES.claudeCommands);
1082
1217
  }
1218
+ const claudeSkillsDir = path17.join(projectDir, GENERATED_FILES.claudeSkills);
1219
+ if (fileExists(claudeSkillsDir)) {
1220
+ await fs8.remove(claudeSkillsDir);
1221
+ removed.push(GENERATED_FILES.claudeSkills);
1222
+ }
1223
+ const cursorSkillsDir = path17.join(projectDir, GENERATED_FILES.cursorSkills);
1224
+ if (fileExists(cursorSkillsDir)) {
1225
+ await fs8.remove(cursorSkillsDir);
1226
+ removed.push(GENERATED_FILES.cursorSkills);
1227
+ }
1083
1228
  const aiKitDir = path17.join(projectDir, "ai-kit");
1084
1229
  if (fileExists(aiKitDir)) {
1085
- await fs7.remove(aiKitDir);
1230
+ await fs8.remove(aiKitDir);
1086
1231
  removed.push("ai-kit/");
1087
1232
  }
1088
1233
  const configPath = path17.join(projectDir, AI_KIT_CONFIG_FILE);
1089
1234
  if (fileExists(configPath)) {
1090
- await fs7.remove(configPath);
1235
+ await fs8.remove(configPath);
1091
1236
  removed.push(AI_KIT_CONFIG_FILE);
1092
1237
  }
1093
1238
  logSection("Reset Complete");
@@ -1100,7 +1245,7 @@ async function resetCommand(targetPath) {
1100
1245
 
1101
1246
  // src/commands/tokens.ts
1102
1247
  import path18 from "path";
1103
- import fs8 from "fs-extra";
1248
+ import fs9 from "fs-extra";
1104
1249
  import chalk2 from "chalk";
1105
1250
  import ora3 from "ora";
1106
1251
  import os from "os";
@@ -1111,11 +1256,11 @@ var PRICING = {
1111
1256
  var PLAN_BUDGET = 20;
1112
1257
  function findSessionFiles() {
1113
1258
  const claudeDir = path18.join(os.homedir(), ".claude", "projects");
1114
- if (!fs8.existsSync(claudeDir)) return [];
1259
+ if (!fs9.existsSync(claudeDir)) return [];
1115
1260
  const files = [];
1116
1261
  function walkDir(dir) {
1117
1262
  try {
1118
- const entries = fs8.readdirSync(dir, { withFileTypes: true });
1263
+ const entries = fs9.readdirSync(dir, { withFileTypes: true });
1119
1264
  for (const entry of entries) {
1120
1265
  const full = path18.join(dir, entry.name);
1121
1266
  if (entry.isDirectory()) {
@@ -1132,7 +1277,7 @@ function findSessionFiles() {
1132
1277
  }
1133
1278
  function parseSessionFile(filePath) {
1134
1279
  try {
1135
- const content = fs8.readFileSync(filePath, "utf-8");
1280
+ const content = fs9.readFileSync(filePath, "utf-8");
1136
1281
  const lines = content.split("\n").filter((l) => l.trim());
1137
1282
  const usage = {
1138
1283
  inputTokens: 0,
@@ -1169,17 +1314,19 @@ function parseSessionFile(filePath) {
1169
1314
  }
1170
1315
  if (usage.inputTokens === 0 && usage.outputTokens === 0) return null;
1171
1316
  if (!sessionDate) {
1172
- const stat = fs8.statSync(filePath);
1317
+ const stat = fs9.statSync(filePath);
1173
1318
  sessionDate = stat.mtime.toISOString().slice(0, 10);
1174
1319
  }
1175
1320
  const sessionId = path18.basename(filePath, ".jsonl");
1321
+ const projectName = path18.basename(path18.dirname(filePath));
1176
1322
  return {
1177
1323
  sessionId,
1178
1324
  filePath,
1179
1325
  date: sessionDate,
1180
1326
  usage,
1181
1327
  model,
1182
- messageCount
1328
+ messageCount,
1329
+ projectName
1183
1330
  };
1184
1331
  } catch {
1185
1332
  return null;
@@ -1255,8 +1402,70 @@ ${chalk2.bold(label)}`);
1255
1402
  console.log(` Cache read: ${chalk2.white(fmt(usage.cacheReadTokens))}`);
1256
1403
  console.log(` Estimated cost: ${chalk2.yellow("~" + fmtCost(cost))}`);
1257
1404
  }
1405
+ function printProjectBreakdown(sessions) {
1406
+ const projectMap = /* @__PURE__ */ new Map();
1407
+ for (const s of sessions) {
1408
+ const existing = projectMap.get(s.projectName) || { sessions: 0, cost: 0 };
1409
+ existing.sessions++;
1410
+ existing.cost += calculateCost(s.usage, s.model);
1411
+ projectMap.set(s.projectName, existing);
1412
+ }
1413
+ const sorted = Array.from(projectMap.entries()).sort((a, b) => b[1].cost - a[1].cost);
1414
+ console.log(`
1415
+ ${chalk2.bold("Per-Project Breakdown")}`);
1416
+ for (const [name, data] of sorted.slice(0, 10)) {
1417
+ const bar = progressBar(data.cost / sumCost(sessions) * 100, 15);
1418
+ console.log(` ${bar} ${chalk2.yellow(fmtCost(data.cost))} ${chalk2.dim(`(${data.sessions} sessions)`)} ${name}`);
1419
+ }
1420
+ }
1421
+ function printTrendAnalysis(sessions) {
1422
+ const today = /* @__PURE__ */ new Date();
1423
+ const thisWeekStart = new Date(today);
1424
+ thisWeekStart.setDate(today.getDate() - today.getDay() + (today.getDay() === 0 ? -6 : 1));
1425
+ const thisWeekStr = thisWeekStart.toISOString().slice(0, 10);
1426
+ const lastWeekStart = new Date(thisWeekStart);
1427
+ lastWeekStart.setDate(lastWeekStart.getDate() - 7);
1428
+ const lastWeekEnd = new Date(thisWeekStart);
1429
+ lastWeekEnd.setDate(lastWeekEnd.getDate() - 1);
1430
+ const lastWeekStartStr = lastWeekStart.toISOString().slice(0, 10);
1431
+ const lastWeekEndStr = lastWeekEnd.toISOString().slice(0, 10);
1432
+ const todayStr = today.toISOString().slice(0, 10);
1433
+ const thisWeekSessions = filterDateRange(sessions, thisWeekStr, todayStr);
1434
+ const lastWeekSessions = filterDateRange(sessions, lastWeekStartStr, lastWeekEndStr);
1435
+ const thisWeekCost = sumCost(thisWeekSessions);
1436
+ const lastWeekCost = sumCost(lastWeekSessions);
1437
+ if (lastWeekCost > 0) {
1438
+ const change = (thisWeekCost - lastWeekCost) / lastWeekCost * 100;
1439
+ const arrow = change > 0 ? chalk2.red("\u2191") : change < 0 ? chalk2.green("\u2193") : chalk2.dim("\u2192");
1440
+ const changeStr = change > 0 ? `+${change.toFixed(0)}%` : `${change.toFixed(0)}%`;
1441
+ console.log(`
1442
+ ${chalk2.bold("Week-over-Week Trend")}`);
1443
+ console.log(` Last week: ${chalk2.yellow(fmtCost(lastWeekCost))} (${lastWeekSessions.length} sessions)`);
1444
+ console.log(` This week: ${chalk2.yellow(fmtCost(thisWeekCost))} (${thisWeekSessions.length} sessions)`);
1445
+ console.log(` Change: ${arrow} ${changeStr}`);
1446
+ }
1447
+ }
1448
+ function printRoiEstimate(sessions, monthCost) {
1449
+ const AVG_MINUTES_SAVED_PER_SESSION = 15;
1450
+ const DEVELOPER_HOURLY_RATE = 75;
1451
+ const totalSessions = sessions.length;
1452
+ const minutesSaved = totalSessions * AVG_MINUTES_SAVED_PER_SESSION;
1453
+ const hoursSaved = minutesSaved / 60;
1454
+ const valueSaved = hoursSaved * DEVELOPER_HOURLY_RATE;
1455
+ const roi = monthCost > 0 ? (valueSaved - monthCost) / monthCost * 100 : 0;
1456
+ console.log(`
1457
+ ${chalk2.bold("ROI Estimate")} ${chalk2.dim("(based on ~15 min saved per session)")}`);
1458
+ console.log(` Sessions this month: ${chalk2.cyan(String(totalSessions))}`);
1459
+ console.log(` Estimated time saved: ${chalk2.cyan(`${hoursSaved.toFixed(1)} hours`)} (${minutesSaved} min)`);
1460
+ console.log(` Estimated value saved: ${chalk2.green(fmtCost(valueSaved))} (at $${DEVELOPER_HOURLY_RATE}/hr)`);
1461
+ console.log(` AI cost: ${chalk2.yellow(fmtCost(monthCost))}`);
1462
+ if (roi > 0) {
1463
+ console.log(` ROI: ${chalk2.green(`${roi.toFixed(0)}%`)}`);
1464
+ }
1465
+ }
1258
1466
  async function tokensCommand(options = {}) {
1259
1467
  logSection("AI Kit \u2014 Token Usage");
1468
+ const budget = options.budget || PLAN_BUDGET;
1260
1469
  const spinner = ora3("Scanning Claude Code session logs...").start();
1261
1470
  const sessionFiles = findSessionFiles();
1262
1471
  if (sessionFiles.length === 0) {
@@ -1289,7 +1498,7 @@ async function tokensCommand(options = {}) {
1289
1498
  printPeriodSummary("This Week", weekSessions);
1290
1499
  printPeriodSummary("This Month", monthSessions);
1291
1500
  const monthCost = sumCost(monthSessions);
1292
- const budgetPercent = Math.min(monthCost / PLAN_BUDGET * 100, 100);
1501
+ const budgetPercent = Math.min(monthCost / budget * 100, 100);
1293
1502
  const daysInMonth = new Date(
1294
1503
  (/* @__PURE__ */ new Date()).getFullYear(),
1295
1504
  (/* @__PURE__ */ new Date()).getMonth() + 1,
@@ -1298,16 +1507,37 @@ async function tokensCommand(options = {}) {
1298
1507
  const dayOfMonth = (/* @__PURE__ */ new Date()).getDate();
1299
1508
  const daysRemaining = daysInMonth - dayOfMonth;
1300
1509
  const dailyAvg = dayOfMonth > 0 ? monthCost / dayOfMonth : 0;
1301
- const estimatedDaysLeft = dailyAvg > 0 ? Math.floor((PLAN_BUDGET - monthCost) / dailyAvg) : daysRemaining;
1510
+ const estimatedDaysLeft = dailyAvg > 0 ? Math.floor((budget - monthCost) / dailyAvg) : daysRemaining;
1302
1511
  console.log(`
1303
- ${chalk2.bold(`$${PLAN_BUDGET} Plan Budget`)}`);
1512
+ ${chalk2.bold(`$${budget} Plan Budget`)}`);
1304
1513
  console.log(
1305
- ` ${progressBar(budgetPercent)} ${Math.round(budgetPercent)}% used (~${fmtCost(monthCost)} of ${fmtCost(PLAN_BUDGET)})`
1514
+ ` ${progressBar(budgetPercent)} ${Math.round(budgetPercent)}% used (~${fmtCost(monthCost)} of ${fmtCost(budget)})`
1306
1515
  );
1307
1516
  console.log(
1308
1517
  ` Estimated days remaining at this rate: ${chalk2.cyan(String(Math.max(estimatedDaysLeft, 0)))} days`
1309
1518
  );
1310
1519
  console.log(` Daily average: ${chalk2.yellow(fmtCost(dailyAvg))}`);
1520
+ if (budgetPercent >= 90) {
1521
+ console.log(chalk2.red.bold("\n \u26A0 ALERT: Over 90% of monthly budget used!"));
1522
+ } else if (budgetPercent >= 75) {
1523
+ console.log(chalk2.yellow("\n \u26A0 Warning: Over 75% of monthly budget used."));
1524
+ } else if (budgetPercent >= 50) {
1525
+ console.log(chalk2.blue("\n \u2139 Note: Over 50% of monthly budget used."));
1526
+ }
1527
+ const opusSessions = monthSessions.filter((s) => s.model === "opus");
1528
+ const sonnetSessions = monthSessions.filter((s) => s.model === "sonnet");
1529
+ const opusCost = sumCost(opusSessions);
1530
+ const sonnetCost = sumCost(sonnetSessions);
1531
+ if (opusCost > sonnetCost * 2 && opusSessions.length > 5) {
1532
+ console.log(`
1533
+ ${chalk2.bold("Model Recommendation")}`);
1534
+ console.log(` Opus usage: ${chalk2.yellow(fmtCost(opusCost))} (${opusSessions.length} sessions)`);
1535
+ console.log(` Sonnet usage: ${chalk2.yellow(fmtCost(sonnetCost))} (${sonnetSessions.length} sessions)`);
1536
+ console.log(chalk2.dim(" Tip: Use Sonnet for routine tasks (reviews, tests, docs) and Opus for complex tasks (architecture, debugging)."));
1537
+ }
1538
+ printProjectBreakdown(monthSessions);
1539
+ printTrendAnalysis(sessions);
1540
+ printRoiEstimate(monthSessions, monthCost);
1311
1541
  console.log(
1312
1542
  `
1313
1543
  ${chalk2.dim("Tip: Use /understand before modifying unfamiliar code \u2014")}`
@@ -1316,6 +1546,16 @@ ${chalk2.dim("Tip: Use /understand before modifying unfamiliar code \u2014")}`
1316
1546
  chalk2.dim(" it's cheaper than a failed implementation attempt.")
1317
1547
  );
1318
1548
  console.log("");
1549
+ if (options.csv) {
1550
+ const csvPath = path18.join(process.cwd(), "token-usage.csv");
1551
+ const csvHeader = "Date,Sessions,Input Tokens,Output Tokens,Cache Tokens,Cost\n";
1552
+ const daily = aggregateByDate(sessions);
1553
+ const csvRows = daily.map(
1554
+ (d) => `${d.date},${d.sessions},${d.usage.inputTokens},${d.usage.outputTokens},${d.usage.cacheReadTokens},${d.cost.toFixed(2)}`
1555
+ ).join("\n");
1556
+ await fs9.writeFile(csvPath, csvHeader + csvRows, "utf-8");
1557
+ logSuccess(`CSV exported to ${csvPath}`);
1558
+ }
1319
1559
  if (options.export) {
1320
1560
  await exportDashboard(sessions, todaySessions, weekSessions, monthSessions);
1321
1561
  }
@@ -1353,10 +1593,10 @@ async function exportDashboard(allSessions, todaySessions, weekSessions, monthSe
1353
1593
  const dataPath = path18.join(outputDir, "token-data.json");
1354
1594
  const dashboardSrc = path18.join(PACKAGE_ROOT, "templates", "token-dashboard.html");
1355
1595
  const dashboardDest = path18.join(outputDir, "token-dashboard.html");
1356
- await fs8.writeJson(dataPath, exportData, { spaces: 2 });
1596
+ await fs9.writeJson(dataPath, exportData, { spaces: 2 });
1357
1597
  logInfo(`Token data written to ${dataPath}`);
1358
- if (await fs8.pathExists(dashboardSrc)) {
1359
- await fs8.copy(dashboardSrc, dashboardDest, { overwrite: true });
1598
+ if (await fs9.pathExists(dashboardSrc)) {
1599
+ await fs9.copy(dashboardSrc, dashboardDest, { overwrite: true });
1360
1600
  logInfo(`Dashboard copied to ${dashboardDest}`);
1361
1601
  } else {
1362
1602
  logWarning("Dashboard template not found. Skipping HTML export.");
@@ -1372,6 +1612,739 @@ async function exportDashboard(allSessions, todaySessions, weekSessions, monthSe
1372
1612
  }
1373
1613
  }
1374
1614
 
1615
+ // src/commands/doctor.ts
1616
+ import path19 from "path";
1617
+ import chalk3 from "chalk";
1618
+ import ora4 from "ora";
1619
+ async function doctorCommand(targetPath) {
1620
+ const projectDir = path19.resolve(targetPath || process.cwd());
1621
+ const configPath = path19.join(projectDir, AI_KIT_CONFIG_FILE);
1622
+ let passed = 0;
1623
+ let warnings = 0;
1624
+ let issues = 0;
1625
+ logSection("AI Kit \u2014 Doctor");
1626
+ console.log("");
1627
+ if (!fileExists(configPath)) {
1628
+ logError("ai-kit.config.json not found. Run `ai-kit init` first.");
1629
+ issues++;
1630
+ showSummary(passed, warnings, issues);
1631
+ return;
1632
+ }
1633
+ const config = readJsonSafe(configPath);
1634
+ if (!config) {
1635
+ logError("ai-kit.config.json is corrupted or unreadable. Run `ai-kit init` to re-initialize.");
1636
+ issues++;
1637
+ showSummary(passed, warnings, issues);
1638
+ return;
1639
+ }
1640
+ logSuccess(`ai-kit.config.json found (v${config.version})`);
1641
+ passed++;
1642
+ if (config.version !== VERSION) {
1643
+ logWarning(
1644
+ `Config version mismatch \u2014 config is v${config.version}, CLI is v${VERSION}. Run \`ai-kit update\` to sync.`
1645
+ );
1646
+ warnings++;
1647
+ } else {
1648
+ logSuccess(`Version matches CLI (v${VERSION})`);
1649
+ passed++;
1650
+ }
1651
+ for (const template of config.templates) {
1652
+ const templateFile = template === "CLAUDE.md" ? GENERATED_FILES.claudeMd : template === ".cursorrules" ? GENERATED_FILES.cursorRules : template;
1653
+ const templatePath = path19.join(projectDir, templateFile);
1654
+ if (fileExists(templatePath)) {
1655
+ logSuccess(`${template} exists and in sync`);
1656
+ passed++;
1657
+ } else {
1658
+ logError(`${template} is listed in config but missing from disk`);
1659
+ issues++;
1660
+ }
1661
+ }
1662
+ const missingSkills = [];
1663
+ for (const skill of config.commands) {
1664
+ const claudeSkillPath = path19.join(projectDir, GENERATED_FILES.claudeSkills, skill);
1665
+ const cursorSkillPath = path19.join(projectDir, GENERATED_FILES.cursorSkills, skill);
1666
+ if (!fileExists(claudeSkillPath) && !fileExists(cursorSkillPath)) {
1667
+ missingSkills.push(skill);
1668
+ }
1669
+ }
1670
+ if (missingSkills.length === 0) {
1671
+ logSuccess(`${config.commands.length}/${config.commands.length} skills present`);
1672
+ passed++;
1673
+ } else {
1674
+ logError(
1675
+ `${config.commands.length - missingSkills.length}/${config.commands.length} skills present \u2014 missing: ${missingSkills.join(", ")}`
1676
+ );
1677
+ issues++;
1678
+ }
1679
+ const guidesDir = path19.join(projectDir, "ai-kit", "guides");
1680
+ const missingGuides = [];
1681
+ for (const guide of config.guides) {
1682
+ const guidePath = path19.join(guidesDir, guide);
1683
+ if (!fileExists(guidePath)) {
1684
+ missingGuides.push(guide);
1685
+ }
1686
+ }
1687
+ if (missingGuides.length === 0) {
1688
+ logSuccess(`${config.guides.length}/${config.guides.length} guides present`);
1689
+ passed++;
1690
+ } else {
1691
+ logError(
1692
+ `${config.guides.length - missingGuides.length}/${config.guides.length} guides present \u2014 missing: ${missingGuides.join(", ")}`
1693
+ );
1694
+ issues++;
1695
+ }
1696
+ const spinner = ora4("Running fresh project scan...").start();
1697
+ let freshScan;
1698
+ try {
1699
+ freshScan = await scanProject(projectDir);
1700
+ spinner.succeed("Project re-scanned");
1701
+ } catch (err) {
1702
+ spinner.fail("Failed to scan project");
1703
+ logWarning(`Could not run staleness check: ${String(err)}`);
1704
+ warnings++;
1705
+ freshScan = config.scanResult;
1706
+ }
1707
+ const stalenessWarnings = compareScanResults(config.scanResult, freshScan);
1708
+ if (stalenessWarnings.length === 0) {
1709
+ logSuccess("Config is up to date with project state");
1710
+ passed++;
1711
+ } else {
1712
+ for (const warning of stalenessWarnings) {
1713
+ logWarning(warning);
1714
+ warnings++;
1715
+ }
1716
+ }
1717
+ const mcpChecks = [
1718
+ { name: "Playwright MCP", key: "playwright" },
1719
+ { name: "Context7 MCP", key: "context7" },
1720
+ { name: "GitHub MCP", key: "github" },
1721
+ { name: "Perplexity MCP", key: "perplexity" },
1722
+ { name: "Figma MCP", key: "figma" }
1723
+ ];
1724
+ for (const mcp of mcpChecks) {
1725
+ if (freshScan.mcpServers[mcp.key]) {
1726
+ logSuccess(`${mcp.name} configured`);
1727
+ passed++;
1728
+ } else {
1729
+ logWarning(`${mcp.name} not configured`);
1730
+ warnings++;
1731
+ }
1732
+ }
1733
+ const toolChecks = [
1734
+ { name: "Playwright", key: "playwright", hint: "npm install -D @playwright/test" },
1735
+ { name: "ESLint", key: "eslint", hint: "npm install -D eslint" },
1736
+ { name: "Prettier", key: "prettier", hint: "npm install -D prettier" },
1737
+ { name: "axe-core", key: "axeCore", hint: "npm install -D @axe-core/playwright" },
1738
+ { name: "Knip", key: "knip", hint: "npm install -D knip" },
1739
+ { name: "Bundle Analyzer", key: "bundleAnalyzer", hint: "npm install -D @next/bundle-analyzer" }
1740
+ ];
1741
+ for (const tool of toolChecks) {
1742
+ if (freshScan.tools[tool.key]) {
1743
+ logSuccess(`${tool.name} detected`);
1744
+ passed++;
1745
+ } else {
1746
+ logError(`${tool.name} not found \u2014 recommend installing: ${tool.hint}`);
1747
+ issues++;
1748
+ }
1749
+ }
1750
+ console.log("");
1751
+ showSummary(passed, warnings, issues);
1752
+ }
1753
+ function showSummary(passed, warnings, issues) {
1754
+ const parts = [];
1755
+ parts.push(chalk3.green(`${passed} passed`));
1756
+ if (warnings > 0) parts.push(chalk3.yellow(`${warnings} warnings`));
1757
+ if (issues > 0) parts.push(chalk3.red(`${issues} issues`));
1758
+ console.log(chalk3.bold(`Summary: ${parts.join(", ")}`));
1759
+ }
1760
+ function compareScanResults(previous, current) {
1761
+ const warnings = [];
1762
+ if (previous.framework !== current.framework) {
1763
+ warnings.push(
1764
+ `Stack may have changed \u2014 framework was ${previous.framework}, now ${current.framework}`
1765
+ );
1766
+ }
1767
+ if (previous.nextjsVersion && current.nextjsVersion && previous.nextjsVersion !== current.nextjsVersion) {
1768
+ warnings.push(
1769
+ `Next.js version changed: ${previous.nextjsVersion} \u2192 ${current.nextjsVersion}`
1770
+ );
1771
+ }
1772
+ if (previous.routerType && current.routerType && previous.routerType !== current.routerType) {
1773
+ warnings.push(
1774
+ `Router type changed: ${previous.routerType} \u2192 ${current.routerType}`
1775
+ );
1776
+ }
1777
+ const prevStyles = new Set(previous.styling);
1778
+ const currStyles = new Set(current.styling);
1779
+ for (const style of currStyles) {
1780
+ if (!prevStyles.has(style)) {
1781
+ warnings.push(`Stack may have changed \u2014 detected new styling: ${style}`);
1782
+ }
1783
+ }
1784
+ for (const style of prevStyles) {
1785
+ if (!currStyles.has(style)) {
1786
+ warnings.push(`Stack may have changed \u2014 styling removed: ${style}`);
1787
+ }
1788
+ }
1789
+ if (previous.typescript !== current.typescript) {
1790
+ warnings.push(
1791
+ `TypeScript ${current.typescript ? "detected (was not before)" : "no longer detected"}`
1792
+ );
1793
+ }
1794
+ if (previous.monorepo !== current.monorepo) {
1795
+ warnings.push(
1796
+ `Monorepo ${current.monorepo ? "detected (was not before)" : "no longer detected"}`
1797
+ );
1798
+ }
1799
+ if (previous.packageManager !== current.packageManager) {
1800
+ warnings.push(
1801
+ `Package manager changed: ${previous.packageManager} \u2192 ${current.packageManager}`
1802
+ );
1803
+ }
1804
+ const toolKeys = Object.keys(current.tools);
1805
+ for (const key of toolKeys) {
1806
+ if (current.tools[key] && !previous.tools[key]) {
1807
+ warnings.push(`Stack may have changed \u2014 detected new tool: ${key}`);
1808
+ }
1809
+ if (!current.tools[key] && previous.tools[key]) {
1810
+ warnings.push(`Stack may have changed \u2014 tool removed: ${key}`);
1811
+ }
1812
+ }
1813
+ if (previous.cms !== current.cms) {
1814
+ warnings.push(`CMS changed: ${previous.cms} \u2192 ${current.cms}`);
1815
+ }
1816
+ return warnings;
1817
+ }
1818
+
1819
+ // src/commands/diff.ts
1820
+ import path20 from "path";
1821
+ import fs10 from "fs-extra";
1822
+ import chalk4 from "chalk";
1823
+ import ora5 from "ora";
1824
+ async function diffCommand(targetPath) {
1825
+ const projectDir = path20.resolve(targetPath || process.cwd());
1826
+ const configPath = path20.join(projectDir, AI_KIT_CONFIG_FILE);
1827
+ console.log(chalk4.bold("AI Kit \u2014 Diff (dry run)\n"));
1828
+ if (!fileExists(configPath)) {
1829
+ logError("No ai-kit.config.json found. Run `ai-kit init` first.");
1830
+ return;
1831
+ }
1832
+ const existingConfig = readJsonSafe(configPath);
1833
+ if (!existingConfig) {
1834
+ logError("Could not read ai-kit.config.json. Run `ai-kit init` to re-initialize.");
1835
+ return;
1836
+ }
1837
+ const existingMajor = parseInt(existingConfig.version.split(".")[0], 10);
1838
+ const currentMajor = parseInt(VERSION.split(".")[0], 10);
1839
+ if (existingMajor !== currentMajor) {
1840
+ logWarning(
1841
+ `Config was generated with ai-kit v${existingConfig.version}, but you're running v${VERSION}. Consider running \`ai-kit init\` to re-initialize.`
1842
+ );
1843
+ }
1844
+ const spinner = ora5("Scanning project...").start();
1845
+ const newScan = await scanProject(projectDir);
1846
+ spinner.succeed("Project scanned");
1847
+ const oldScan = existingConfig.scanResult;
1848
+ logSection("Stack Changes");
1849
+ const stackChanges = diffStack(oldScan, newScan);
1850
+ if (stackChanges.length === 0) {
1851
+ console.log(chalk4.dim(" No stack changes detected"));
1852
+ } else {
1853
+ for (const change of stackChanges) {
1854
+ if (change.type === "added") {
1855
+ console.log(chalk4.green(` + ${change.label}`));
1856
+ } else if (change.type === "modified") {
1857
+ console.log(chalk4.yellow(` ~ ${change.label}`));
1858
+ } else {
1859
+ console.log(chalk4.red(` - ${change.label}`));
1860
+ }
1861
+ }
1862
+ }
1863
+ logSection("File Changes");
1864
+ const oldFragments = selectFragments(oldScan);
1865
+ const newFragments = selectFragments(newScan);
1866
+ let modified = 0;
1867
+ let added = 0;
1868
+ let unchanged = 0;
1869
+ const claudeMdStatus = diffGeneratedFile(
1870
+ projectDir,
1871
+ GENERATED_FILES.claudeMd,
1872
+ existingConfig,
1873
+ () => generateClaudeMd(newScan),
1874
+ oldFragments,
1875
+ newFragments
1876
+ );
1877
+ logFileChange(claudeMdStatus);
1878
+ if (claudeMdStatus.status === "modified") modified++;
1879
+ else if (claudeMdStatus.status === "added") added++;
1880
+ else unchanged++;
1881
+ const cursorRulesStatus = diffGeneratedFile(
1882
+ projectDir,
1883
+ GENERATED_FILES.cursorRules,
1884
+ existingConfig,
1885
+ () => generateCursorRules(newScan),
1886
+ oldFragments,
1887
+ newFragments
1888
+ );
1889
+ logFileChange(cursorRulesStatus);
1890
+ if (cursorRulesStatus.status === "modified") modified++;
1891
+ else if (cursorRulesStatus.status === "added") added++;
1892
+ else unchanged++;
1893
+ const skillsDir = path20.join(projectDir, GENERATED_FILES.claudeSkills);
1894
+ const skillCount = countFilesInDir(skillsDir);
1895
+ console.log(chalk4.dim(` unchanged ${GENERATED_FILES.claudeSkills}/ (${skillCount} skills)`));
1896
+ unchanged++;
1897
+ const guidesDir = path20.join(projectDir, AI_KIT_FOLDER_NAME, "guides");
1898
+ const guideCount = existingConfig.guides?.length || 0;
1899
+ console.log(chalk4.dim(` unchanged ai-kit/guides/ (${guideCount} guides)`));
1900
+ unchanged++;
1901
+ console.log("");
1902
+ const parts = [];
1903
+ if (modified > 0) parts.push(`${modified} files would be modified`);
1904
+ if (added > 0) parts.push(`${added} added`);
1905
+ const removed = 0;
1906
+ parts.push(`${removed} removed`);
1907
+ logInfo(`Summary: ${parts.join(", ")}`);
1908
+ logInfo("Run `ai-kit update` to apply these changes.");
1909
+ }
1910
+ var AI_KIT_FOLDER_NAME = "ai-kit";
1911
+ function diffStack(oldScan, newScan) {
1912
+ const changes = [];
1913
+ if (oldScan.framework === newScan.framework && newScan.framework === "nextjs") {
1914
+ if (oldScan.nextjsVersion !== newScan.nextjsVersion) {
1915
+ changes.push({
1916
+ type: "modified",
1917
+ label: `Next.js version: ${oldScan.nextjsVersion || "unknown"} \u2192 ${newScan.nextjsVersion || "unknown"}`
1918
+ });
1919
+ }
1920
+ } else if (oldScan.framework !== newScan.framework) {
1921
+ if (oldScan.framework !== "unknown") {
1922
+ changes.push({ type: "removed", label: `${oldScan.framework} no longer detected` });
1923
+ }
1924
+ if (newScan.framework !== "unknown") {
1925
+ changes.push({ type: "added", label: `${newScan.framework} detected` });
1926
+ }
1927
+ }
1928
+ if (oldScan.routerType !== newScan.routerType && newScan.framework === "nextjs") {
1929
+ changes.push({
1930
+ type: "modified",
1931
+ label: `Router type: ${oldScan.routerType || "unknown"} \u2192 ${newScan.routerType || "unknown"}`
1932
+ });
1933
+ }
1934
+ if (!oldScan.typescript && newScan.typescript) {
1935
+ changes.push({ type: "added", label: "TypeScript detected" });
1936
+ } else if (oldScan.typescript && !newScan.typescript) {
1937
+ changes.push({ type: "removed", label: "TypeScript no longer detected" });
1938
+ }
1939
+ const oldStyles = new Set(oldScan.styling);
1940
+ const newStyles = new Set(newScan.styling);
1941
+ for (const style of newStyles) {
1942
+ if (!oldStyles.has(style)) {
1943
+ changes.push({ type: "added", label: `${style} detected (styling)` });
1944
+ }
1945
+ }
1946
+ for (const style of oldStyles) {
1947
+ if (!newStyles.has(style)) {
1948
+ changes.push({ type: "removed", label: `${style} no longer detected` });
1949
+ }
1950
+ }
1951
+ if (oldScan.styling.includes("tailwind") && newScan.styling.includes("tailwind") && oldScan.tailwindVersion !== newScan.tailwindVersion) {
1952
+ changes.push({
1953
+ type: "modified",
1954
+ label: `Tailwind version: ${oldScan.tailwindVersion || "unknown"} \u2192 ${newScan.tailwindVersion || "unknown"}`
1955
+ });
1956
+ }
1957
+ if (!oldScan.monorepo && newScan.monorepo) {
1958
+ changes.push({
1959
+ type: "added",
1960
+ label: `monorepo detected${newScan.monorepoTool ? ` (${newScan.monorepoTool})` : ""}`
1961
+ });
1962
+ } else if (oldScan.monorepo && !newScan.monorepo) {
1963
+ changes.push({ type: "removed", label: "monorepo no longer detected" });
1964
+ } else if (oldScan.monorepoTool !== newScan.monorepoTool && newScan.monorepo) {
1965
+ changes.push({
1966
+ type: "modified",
1967
+ label: `monorepo tool: ${oldScan.monorepoTool || "unknown"} \u2192 ${newScan.monorepoTool || "unknown"}`
1968
+ });
1969
+ }
1970
+ if (oldScan.cms !== newScan.cms) {
1971
+ if (oldScan.cms !== "none") {
1972
+ changes.push({ type: "removed", label: `${oldScan.cms} no longer detected` });
1973
+ }
1974
+ if (newScan.cms !== "none") {
1975
+ changes.push({ type: "added", label: `${newScan.cms} detected (CMS)` });
1976
+ }
1977
+ }
1978
+ if (oldScan.packageManager !== newScan.packageManager) {
1979
+ changes.push({
1980
+ type: "modified",
1981
+ label: `Package manager: ${oldScan.packageManager} \u2192 ${newScan.packageManager}`
1982
+ });
1983
+ }
1984
+ if (!oldScan.figma?.detected && newScan.figma?.detected) {
1985
+ changes.push({ type: "added", label: "Figma integration detected" });
1986
+ } else if (oldScan.figma?.detected && !newScan.figma?.detected) {
1987
+ changes.push({ type: "removed", label: "Figma integration no longer detected" });
1988
+ }
1989
+ if (oldScan.tools && newScan.tools) {
1990
+ const toolNames = Object.keys(newScan.tools);
1991
+ for (const tool of toolNames) {
1992
+ if (!oldScan.tools[tool] && newScan.tools[tool]) {
1993
+ changes.push({ type: "added", label: `${tool} detected (tooling)` });
1994
+ }
1995
+ }
1996
+ const oldToolNames = Object.keys(oldScan.tools);
1997
+ for (const tool of oldToolNames) {
1998
+ if (oldScan.tools[tool] && !newScan.tools[tool]) {
1999
+ changes.push({ type: "removed", label: `${tool} no longer detected` });
2000
+ }
2001
+ }
2002
+ }
2003
+ return changes;
2004
+ }
2005
+ function diffGeneratedFile(projectDir, filename, config, generate2, oldFragments, newFragments) {
2006
+ const filePath = path20.join(projectDir, filename);
2007
+ const currentContent = readFileSafe(filePath);
2008
+ const newContent = generate2();
2009
+ if (!currentContent) {
2010
+ if (config.templates.includes(filename)) {
2011
+ return { filename, status: "added", detail: "file missing, would be created" };
2012
+ }
2013
+ return { filename, status: "unchanged", detail: "not configured" };
2014
+ }
2015
+ if (currentContent.trim() === newContent.trim()) {
2016
+ return { filename, status: "unchanged" };
2017
+ }
2018
+ const addedFragments = newFragments.filter((f) => !oldFragments.includes(f));
2019
+ const removedFragments = oldFragments.filter((f) => !newFragments.includes(f));
2020
+ const fragmentDetails = [];
2021
+ if (addedFragments.length > 0) {
2022
+ fragmentDetails.push(`+${addedFragments.join(", +")}`);
2023
+ }
2024
+ if (removedFragments.length > 0) {
2025
+ fragmentDetails.push(`-${removedFragments.join(", -")}`);
2026
+ }
2027
+ const detail = fragmentDetails.length > 0 ? `template fragments changed: ${fragmentDetails.join(", ")}` : "content changed";
2028
+ return { filename, status: "modified", detail };
2029
+ }
2030
+ function logFileChange(result) {
2031
+ if (result.status === "added") {
2032
+ console.log(chalk4.green(` added ${result.filename}`) + (result.detail ? chalk4.dim(` (${result.detail})`) : ""));
2033
+ } else if (result.status === "modified") {
2034
+ console.log(
2035
+ chalk4.yellow(` modified ${result.filename}`) + (result.detail ? chalk4.dim(` (${result.detail})`) : "")
2036
+ );
2037
+ } else {
2038
+ console.log(chalk4.dim(` unchanged ${result.filename}`));
2039
+ }
2040
+ }
2041
+ function countFilesInDir(dirPath) {
2042
+ try {
2043
+ if (!fs10.existsSync(dirPath)) return 0;
2044
+ const entries = fs10.readdirSync(dirPath);
2045
+ return entries.filter((e) => !e.startsWith(".")).length;
2046
+ } catch {
2047
+ return 0;
2048
+ }
2049
+ }
2050
+
2051
+ // src/commands/export.ts
2052
+ import path21 from "path";
2053
+ import fs11 from "fs-extra";
2054
+ import ora6 from "ora";
2055
+ import { select as select2 } from "@inquirer/prompts";
2056
+ var EXPORT_TARGETS = {
2057
+ windsurf: { file: ".windsurfrules", label: "Windsurf" },
2058
+ aider: { file: ".aider.conf.yml", label: "Aider" },
2059
+ cline: { file: ".clinerules", label: "Cline" }
2060
+ };
2061
+ function stripMarkers(content) {
2062
+ return content.replace("<!-- AI-KIT:START -->\n", "").replace("\n<!-- AI-KIT:END -->", "").replace(/<!-- Generated by ai-kit v[\d.]+ -->\n?/, "");
2063
+ }
2064
+ function toWindsurf(content) {
2065
+ const stripped = stripMarkers(content);
2066
+ return `# Windsurf Rules
2067
+ # Exported from ai-kit CLAUDE.md
2068
+
2069
+ ${stripped}`;
2070
+ }
2071
+ function toAider(content) {
2072
+ const stripped = stripMarkers(content);
2073
+ const indented = stripped.split("\n").map((line) => line.length > 0 ? ` ${line}` : "").join("\n");
2074
+ return `# Aider configuration
2075
+ # Exported from ai-kit CLAUDE.md
2076
+
2077
+ conventions: |
2078
+ ${indented}
2079
+ `;
2080
+ }
2081
+ function toCline(content) {
2082
+ const stripped = stripMarkers(content);
2083
+ return `# Cline Rules
2084
+ # Exported from ai-kit CLAUDE.md
2085
+
2086
+ ${stripped}`;
2087
+ }
2088
+ async function exportCommand(targetPath, options) {
2089
+ const projectDir = path21.resolve(targetPath || process.cwd());
2090
+ const configPath = path21.join(projectDir, AI_KIT_CONFIG_FILE);
2091
+ const claudeMdPath = path21.join(projectDir, GENERATED_FILES.claudeMd);
2092
+ logSection("AI Kit \u2014 Export");
2093
+ if (!fileExists(configPath) && !fileExists(claudeMdPath)) {
2094
+ logError("No ai-kit.config.json or CLAUDE.md found. Run `ai-kit init` first.");
2095
+ return;
2096
+ }
2097
+ const claudeContent = readFileSafe(claudeMdPath);
2098
+ if (!claudeContent) {
2099
+ logError("Could not read CLAUDE.md. Run `ai-kit init` or `ai-kit update` first.");
2100
+ return;
2101
+ }
2102
+ let format;
2103
+ if (options?.format) {
2104
+ const f = options.format.toLowerCase();
2105
+ if (f === "windsurf" || f === "aider" || f === "cline" || f === "all") {
2106
+ format = f;
2107
+ } else {
2108
+ logError(`Unknown format "${options.format}". Use: windsurf, aider, cline, or all.`);
2109
+ return;
2110
+ }
2111
+ } else {
2112
+ format = await select2({
2113
+ message: "Which format do you want to export to?",
2114
+ choices: [
2115
+ { name: "Windsurf (.windsurfrules)", value: "windsurf" },
2116
+ { name: "Aider (.aider.conf.yml)", value: "aider" },
2117
+ { name: "Cline (.clinerules)", value: "cline" },
2118
+ { name: "All of the above", value: "all" }
2119
+ ]
2120
+ });
2121
+ }
2122
+ const formats = format === "all" ? ["windsurf", "aider", "cline"] : [format];
2123
+ const spinner = ora6("Exporting rules to other AI tools...").start();
2124
+ const transformers = {
2125
+ windsurf: toWindsurf,
2126
+ aider: toAider,
2127
+ cline: toCline
2128
+ };
2129
+ let exported = 0;
2130
+ for (const fmt2 of formats) {
2131
+ const target = EXPORT_TARGETS[fmt2];
2132
+ const transformer = transformers[fmt2];
2133
+ const outputPath = path21.join(projectDir, target.file);
2134
+ const transformed = transformer(claudeContent);
2135
+ await fs11.writeFile(outputPath, transformed, "utf-8");
2136
+ exported++;
2137
+ }
2138
+ spinner.succeed("Export complete");
2139
+ console.log("");
2140
+ for (const fmt2 of formats) {
2141
+ const target = EXPORT_TARGETS[fmt2];
2142
+ logSuccess(`${target.file} generated (${target.label})`);
2143
+ }
2144
+ console.log("");
2145
+ logInfo(
2146
+ `${exported} file${exported !== 1 ? "s" : ""} exported. These contain the same rules as your CLAUDE.md.`
2147
+ );
2148
+ }
2149
+
2150
+ // src/commands/stats.ts
2151
+ import path22 from "path";
2152
+ import chalk5 from "chalk";
2153
+ var SKILL_CATEGORIES = {
2154
+ "Getting Started": ["prompt-help", "understand"],
2155
+ "Building": [
2156
+ "new-component",
2157
+ "new-page",
2158
+ "api-route",
2159
+ "error-boundary",
2160
+ "extract-hook",
2161
+ "figma-to-code",
2162
+ "design-tokens"
2163
+ ],
2164
+ "Quality & Review": [
2165
+ "review",
2166
+ "test",
2167
+ "accessibility-audit",
2168
+ "security-check",
2169
+ "responsive-check",
2170
+ "type-fix",
2171
+ "pre-pr"
2172
+ ],
2173
+ "Maintenance": [
2174
+ "optimize",
2175
+ "refactor",
2176
+ "dep-check",
2177
+ "bundle-check",
2178
+ "migrate",
2179
+ "env-setup"
2180
+ ],
2181
+ "Workflow": ["fix-bug", "commit-msg", "document", "token-tips"]
2182
+ };
2183
+ function getComplexityLabel(score) {
2184
+ if (score >= 10) return "Enterprise";
2185
+ if (score >= 7) return "Complex";
2186
+ if (score >= 4) return "Moderate";
2187
+ return "Simple";
2188
+ }
2189
+ function calculateComplexity(config) {
2190
+ const scan = config.scanResult;
2191
+ let score = 0;
2192
+ const items = [];
2193
+ const hasFramework = scan.framework !== "unknown";
2194
+ if (hasFramework) score += 1;
2195
+ const frameworkLabel = scan.framework === "nextjs" ? `Next.js ${scan.nextjsVersion || ""} (${scan.routerType === "app" ? "App Router" : scan.routerType === "pages" ? "Pages Router" : "Hybrid"})` : scan.framework === "react" ? "React" : "Unknown";
2196
+ items.push({ label: frameworkLabel.trim(), active: hasFramework });
2197
+ const hasCms = scan.cms !== "none";
2198
+ if (hasCms) score += 2;
2199
+ const cmsLabel = scan.cms === "sitecore-xmc" ? "Sitecore XM Cloud" : scan.cms === "sitecore-jss" ? "Sitecore JSS" : "CMS";
2200
+ items.push({ label: cmsLabel, active: hasCms });
2201
+ if (scan.typescript) score += 1;
2202
+ items.push({
2203
+ label: `TypeScript${scan.typescriptStrict ? " (strict)" : ""}`,
2204
+ active: scan.typescript
2205
+ });
2206
+ for (const style of scan.styling) {
2207
+ score += 1;
2208
+ const styleLabel = style === "tailwind" ? `Tailwind CSS${scan.tailwindVersion ? ` v${scan.tailwindVersion}` : ""}` : style === "css-modules" ? "CSS Modules" : style === "styled-components" ? "Styled Components" : style === "scss" ? "SCSS" : style;
2209
+ items.push({ label: styleLabel, active: true });
2210
+ }
2211
+ items.push({
2212
+ label: scan.monorepo ? `Monorepo (${scan.monorepoTool || "detected"})` : "Monorepo",
2213
+ active: scan.monorepo
2214
+ });
2215
+ if (scan.monorepo) score += 2;
2216
+ items.push({ label: "Figma", active: scan.figma.detected });
2217
+ if (scan.figma.detected) score += 1;
2218
+ return { score, items };
2219
+ }
2220
+ function categorizeSkills(skills) {
2221
+ const result = {};
2222
+ const categorized = /* @__PURE__ */ new Set();
2223
+ for (const [category, categorySkills] of Object.entries(SKILL_CATEGORIES)) {
2224
+ const matched = skills.filter((s) => categorySkills.includes(s));
2225
+ if (matched.length > 0) {
2226
+ result[category] = matched;
2227
+ matched.forEach((s) => categorized.add(s));
2228
+ }
2229
+ }
2230
+ const uncategorized = skills.filter((s) => !categorized.has(s));
2231
+ if (uncategorized.length > 0) {
2232
+ result["Other"] = uncategorized;
2233
+ }
2234
+ return result;
2235
+ }
2236
+ var TOOL_DISPLAY_NAMES = {
2237
+ playwright: "Playwright",
2238
+ storybook: "Storybook",
2239
+ eslint: "ESLint",
2240
+ prettier: "Prettier",
2241
+ axeCore: "axe-core",
2242
+ snyk: "Snyk",
2243
+ knip: "Knip",
2244
+ bundleAnalyzer: "Bundle Analyzer"
2245
+ };
2246
+ var MCP_DISPLAY_NAMES = {
2247
+ playwright: "Playwright",
2248
+ figma: "Figma",
2249
+ github: "GitHub",
2250
+ context7: "Context7",
2251
+ perplexity: "Perplexity"
2252
+ };
2253
+ async function statsCommand(targetPath) {
2254
+ const projectDir = path22.resolve(targetPath || process.cwd());
2255
+ const configPath = path22.join(projectDir, AI_KIT_CONFIG_FILE);
2256
+ logSection("AI Kit \u2014 Project Stats");
2257
+ console.log("");
2258
+ if (!fileExists(configPath)) {
2259
+ logWarning(
2260
+ "ai-kit.config.json not found. Run `ai-kit init` first."
2261
+ );
2262
+ return;
2263
+ }
2264
+ const config = readJsonSafe(configPath);
2265
+ if (!config) {
2266
+ logWarning(
2267
+ "ai-kit.config.json is corrupted or unreadable. Run `ai-kit init` to re-initialize."
2268
+ );
2269
+ return;
2270
+ }
2271
+ const scan = config.scanResult;
2272
+ console.log(` Project: ${chalk5.bold(scan.projectName)}`);
2273
+ console.log(` Version: ai-kit v${config.version}`);
2274
+ console.log(` Generated: ${config.generatedAt}`);
2275
+ console.log(` Package Manager: ${scan.packageManager}`);
2276
+ console.log("");
2277
+ const { score, items } = calculateComplexity(config);
2278
+ const label = getComplexityLabel(score);
2279
+ const maxScore = 10;
2280
+ console.log(
2281
+ ` ${chalk5.bold("Stack Complexity:")} ${score}/${maxScore} (${label})`
2282
+ );
2283
+ for (const item of items) {
2284
+ if (item.active) {
2285
+ console.log(` ${chalk5.green("\u2713")} ${item.label}`);
2286
+ } else {
2287
+ console.log(` ${chalk5.gray("\u2717")} ${chalk5.gray(item.label)}`);
2288
+ }
2289
+ }
2290
+ console.log("");
2291
+ const activeFragments = (config.templates || []).filter(
2292
+ (t) => TEMPLATE_FRAGMENTS.includes(t)
2293
+ );
2294
+ const fragmentCount = activeFragments.length;
2295
+ console.log(
2296
+ ` ${chalk5.bold("Active Rules:")} ${fragmentCount} template fragment${fragmentCount !== 1 ? "s" : ""}`
2297
+ );
2298
+ if (fragmentCount > 0) {
2299
+ console.log(` ${activeFragments.join(", ")}`);
2300
+ }
2301
+ console.log("");
2302
+ const skills = config.commands || [];
2303
+ const totalSkills = skills.length;
2304
+ const categorized = categorizeSkills(skills);
2305
+ console.log(` ${chalk5.bold("Skills:")} ${totalSkills} total`);
2306
+ for (const [category, categorySkills] of Object.entries(categorized)) {
2307
+ console.log(` ${category}: ${categorySkills.length}`);
2308
+ }
2309
+ console.log("");
2310
+ console.log(` ${chalk5.bold("Tools:")}`);
2311
+ const toolEntries = Object.entries(scan.tools);
2312
+ const detectedTools = [];
2313
+ const missingTools = [];
2314
+ for (const [key, detected] of toolEntries) {
2315
+ const displayName = TOOL_DISPLAY_NAMES[key] || key;
2316
+ if (detected) {
2317
+ detectedTools.push(`${chalk5.green("\u2713")} ${displayName}`);
2318
+ } else {
2319
+ missingTools.push(`${chalk5.yellow("\u2717")} ${displayName}`);
2320
+ }
2321
+ }
2322
+ const allToolItems = [...detectedTools, ...missingTools];
2323
+ for (let i = 0; i < allToolItems.length; i += 3) {
2324
+ const row = allToolItems.slice(i, i + 3).map((item) => item.padEnd(22)).join("");
2325
+ console.log(` ${row}`);
2326
+ }
2327
+ console.log("");
2328
+ console.log(` ${chalk5.bold("MCP Servers:")}`);
2329
+ const mcpEntries = Object.entries(scan.mcpServers);
2330
+ const configuredMcps = [];
2331
+ const missingMcps = [];
2332
+ for (const [key, configured] of mcpEntries) {
2333
+ const displayName = MCP_DISPLAY_NAMES[key] || key;
2334
+ if (configured) {
2335
+ configuredMcps.push(`${chalk5.green("\u2713")} ${displayName}`);
2336
+ } else {
2337
+ missingMcps.push(`${chalk5.yellow("\u2717")} ${displayName}`);
2338
+ }
2339
+ }
2340
+ const allMcpItems = [...configuredMcps, ...missingMcps];
2341
+ for (let i = 0; i < allMcpItems.length; i += 3) {
2342
+ const row = allMcpItems.slice(i, i + 3).map((item) => item.padEnd(22)).join("");
2343
+ console.log(` ${row}`);
2344
+ }
2345
+ console.log("");
2346
+ }
2347
+
1375
2348
  // src/index.ts
1376
2349
  var program = new Command();
1377
2350
  program.name("ai-kit").description(
@@ -1410,7 +2383,7 @@ program.command("reset").description("Remove all AI Kit generated files").argume
1410
2383
  process.exit(1);
1411
2384
  }
1412
2385
  });
1413
- program.command("tokens").description("Show token usage summary and cost estimates").option("--export", "Export data and open HTML dashboard").action(async (opts) => {
2386
+ program.command("tokens").description("Show token usage summary and cost estimates").option("--export", "Export data and open HTML dashboard").option("--csv", "Export daily usage to CSV file").option("--budget <amount>", "Monthly budget in USD (default: $20)", parseFloat).action(async (opts) => {
1414
2387
  try {
1415
2388
  await tokensCommand(opts);
1416
2389
  } catch (err) {
@@ -1421,5 +2394,49 @@ program.command("tokens").description("Show token usage summary and cost estimat
1421
2394
  process.exit(1);
1422
2395
  }
1423
2396
  });
2397
+ program.command("doctor").description("Diagnose AI Kit setup and check for issues").argument("[path]", "Project directory (defaults to current directory)").action(async (targetPath) => {
2398
+ try {
2399
+ await doctorCommand(targetPath);
2400
+ } catch (err) {
2401
+ if (err.name === "ExitPromptError") {
2402
+ process.exit(0);
2403
+ }
2404
+ console.error(err);
2405
+ process.exit(1);
2406
+ }
2407
+ });
2408
+ program.command("diff").description("Show what would change on update (dry run)").argument("[path]", "Project directory (defaults to current directory)").action(async (targetPath) => {
2409
+ try {
2410
+ await diffCommand(targetPath);
2411
+ } catch (err) {
2412
+ if (err.name === "ExitPromptError") {
2413
+ process.exit(0);
2414
+ }
2415
+ console.error(err);
2416
+ process.exit(1);
2417
+ }
2418
+ });
2419
+ program.command("export").description("Export rules to other AI tools (Windsurf, Aider, Cline)").argument("[path]", "Project directory (defaults to current directory)").option("--format <format>", "Export format: windsurf, aider, cline, or all").action(async (targetPath, opts) => {
2420
+ try {
2421
+ await exportCommand(targetPath, opts);
2422
+ } catch (err) {
2423
+ if (err.name === "ExitPromptError") {
2424
+ process.exit(0);
2425
+ }
2426
+ console.error(err);
2427
+ process.exit(1);
2428
+ }
2429
+ });
2430
+ program.command("stats").description("Show project setup statistics and complexity").argument("[path]", "Project directory (defaults to current directory)").action(async (targetPath) => {
2431
+ try {
2432
+ await statsCommand(targetPath);
2433
+ } catch (err) {
2434
+ if (err.name === "ExitPromptError") {
2435
+ process.exit(0);
2436
+ }
2437
+ console.error(err);
2438
+ process.exit(1);
2439
+ }
2440
+ });
1424
2441
  program.parse();
1425
2442
  //# sourceMappingURL=index.js.map