@mikulgohil/ai-kit 1.1.0 → 1.3.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/README.md +85 -12
- package/agents/architect.md +79 -0
- package/agents/build-resolver.md +54 -0
- package/agents/code-reviewer.md +60 -0
- package/agents/doc-updater.md +46 -0
- package/agents/e2e-runner.md +64 -0
- package/agents/planner.md +56 -0
- package/agents/refactor-cleaner.md +58 -0
- package/agents/security-reviewer.md +67 -0
- package/agents/sitecore-specialist.md +96 -0
- package/agents/tdd-guide.md +115 -0
- package/commands/checkpoint.md +78 -0
- package/commands/harness-audit.md +73 -0
- package/commands/middleware.md +80 -0
- package/commands/orchestrate.md +67 -0
- package/commands/quality-gate-check.md +109 -0
- package/commands/quality-gate.md +82 -0
- package/commands/resume-session.md +40 -0
- package/commands/save-session.md +65 -0
- package/commands/search-first.md +60 -0
- package/commands/server-action.md +93 -0
- package/commands/sitecore-debug.md +58 -0
- package/contexts/dev.md +35 -0
- package/contexts/research.md +56 -0
- package/contexts/review.md +49 -0
- package/dist/index.js +891 -119
- package/dist/index.js.map +1 -1
- package/guides/getting-started.md +74 -21
- package/guides/hooks-and-agents.md +124 -0
- package/package.json +9 -4
- package/templates/claude-md/nextjs-app-router.md +35 -1
- package/templates/claude-md/sitecore-xmc.md +99 -2
- package/templates/claude-md/typescript.md +79 -1
- package/templates/cursorrules/nextjs-app-router.md +6 -1
- package/templates/cursorrules/sitecore-xmc.md +5 -1
- package/templates/cursorrules/typescript.md +6 -1
- package/commands/ci-fix.md +0 -102
- package/commands/db-migrate.md +0 -138
- package/commands/dependency-graph.md +0 -138
- package/commands/docker-debug.md +0 -111
- package/commands/visual-diff.md +0 -131
package/dist/index.js
CHANGED
|
@@ -13,15 +13,20 @@ 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
|
|
16
|
+
var AGENTS_DIR = path.join(PACKAGE_ROOT, "agents");
|
|
17
|
+
var CONTEXTS_DIR = path.join(PACKAGE_ROOT, "contexts");
|
|
18
|
+
var VERSION = "1.3.0";
|
|
17
19
|
var AI_KIT_CONFIG_FILE = "ai-kit.config.json";
|
|
18
20
|
var GENERATED_FILES = {
|
|
19
21
|
claudeMd: "CLAUDE.md",
|
|
20
22
|
cursorRules: ".cursorrules",
|
|
21
23
|
cursorMdcDir: ".cursor/rules",
|
|
22
24
|
claudeSettings: ".claude/settings.json",
|
|
25
|
+
claudeSettingsLocal: ".claude/settings.local.json",
|
|
23
26
|
claudeCommands: ".claude/commands",
|
|
24
27
|
claudeSkills: ".claude/skills",
|
|
28
|
+
claudeAgents: ".claude/agents",
|
|
29
|
+
claudeContexts: ".claude/contexts",
|
|
25
30
|
cursorSkills: ".cursor/skills"
|
|
26
31
|
};
|
|
27
32
|
var TEMPLATE_FRAGMENTS = [
|
|
@@ -36,8 +41,8 @@ var TEMPLATE_FRAGMENTS = [
|
|
|
36
41
|
];
|
|
37
42
|
|
|
38
43
|
// src/commands/init.ts
|
|
39
|
-
import
|
|
40
|
-
import
|
|
44
|
+
import path17 from "path";
|
|
45
|
+
import fs8 from "fs-extra";
|
|
41
46
|
import { select, confirm } from "@inquirer/prompts";
|
|
42
47
|
import ora from "ora";
|
|
43
48
|
|
|
@@ -129,8 +134,15 @@ function detectSitecore(pkg) {
|
|
|
129
134
|
const jssNextjs = deps["@sitecore-jss/sitecore-jss-nextjs"];
|
|
130
135
|
const jssReact = deps["@sitecore-jss/sitecore-jss-react"];
|
|
131
136
|
const contentSdk = deps["@sitecore-content-sdk/nextjs"];
|
|
132
|
-
if (contentSdk
|
|
133
|
-
const version =
|
|
137
|
+
if (contentSdk) {
|
|
138
|
+
const version = contentSdk.replace(/[\^~>=<]/g, "");
|
|
139
|
+
return {
|
|
140
|
+
cms: "sitecore-xmc-v2",
|
|
141
|
+
sitecoreContentSdkVersion: version || void 0
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
if (jssNextjs) {
|
|
145
|
+
const version = jssNextjs.replace(/[\^~>=<]/g, "");
|
|
134
146
|
return { cms: "sitecore-xmc", sitecorejssVersion: version || void 0 };
|
|
135
147
|
}
|
|
136
148
|
if (jssReact) {
|
|
@@ -307,6 +319,7 @@ function detectTools(projectPath, pkg) {
|
|
|
307
319
|
storybook: detectStorybook(projectPath, deps),
|
|
308
320
|
eslint: detectEslint(projectPath, deps),
|
|
309
321
|
prettier: detectPrettier(projectPath, deps),
|
|
322
|
+
biome: detectBiome(projectPath, deps),
|
|
310
323
|
axeCore: detectAxeCore(deps),
|
|
311
324
|
snyk: detectSnyk(projectPath, deps),
|
|
312
325
|
knip: detectKnip(projectPath, deps),
|
|
@@ -362,6 +375,12 @@ function detectPrettier(projectPath, deps) {
|
|
|
362
375
|
}
|
|
363
376
|
return false;
|
|
364
377
|
}
|
|
378
|
+
function detectBiome(projectPath, deps) {
|
|
379
|
+
if ("@biomejs/biome" in deps) return true;
|
|
380
|
+
if (fileExists(path8.join(projectPath, "biome.json"))) return true;
|
|
381
|
+
if (fileExists(path8.join(projectPath, "biome.jsonc"))) return true;
|
|
382
|
+
return false;
|
|
383
|
+
}
|
|
365
384
|
function detectAxeCore(deps) {
|
|
366
385
|
return "@axe-core/playwright" in deps || "axe-core" in deps;
|
|
367
386
|
}
|
|
@@ -545,9 +564,17 @@ function buildVariables(scan) {
|
|
|
545
564
|
techStack.push(`Next.js ${scan.nextjsVersion || ""}`);
|
|
546
565
|
}
|
|
547
566
|
if (scan.cms !== "none") {
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
567
|
+
if (scan.cms === "sitecore-xmc-v2") {
|
|
568
|
+
techStack.push(
|
|
569
|
+
`Sitecore XM Cloud${scan.sitecoreContentSdkVersion ? ` (Content SDK ${scan.sitecoreContentSdkVersion})` : " (Content SDK v2)"}`
|
|
570
|
+
);
|
|
571
|
+
} else if (scan.cms === "sitecore-xmc") {
|
|
572
|
+
techStack.push(
|
|
573
|
+
`Sitecore XM Cloud${scan.sitecorejssVersion ? ` (JSS ${scan.sitecorejssVersion})` : ""}`
|
|
574
|
+
);
|
|
575
|
+
} else {
|
|
576
|
+
techStack.push("Sitecore JSS");
|
|
577
|
+
}
|
|
551
578
|
}
|
|
552
579
|
if (scan.typescript) techStack.push("TypeScript");
|
|
553
580
|
if (scan.styling.includes("tailwind"))
|
|
@@ -585,7 +612,9 @@ function buildCursorVariables(scan) {
|
|
|
585
612
|
techStack.push(`Next.js ${scan.nextjsVersion || ""}`);
|
|
586
613
|
}
|
|
587
614
|
if (scan.cms !== "none") {
|
|
588
|
-
techStack.push(
|
|
615
|
+
techStack.push(
|
|
616
|
+
scan.cms === "sitecore-xmc-v2" || scan.cms === "sitecore-xmc" ? "Sitecore XM Cloud" : "Sitecore JSS"
|
|
617
|
+
);
|
|
589
618
|
}
|
|
590
619
|
if (scan.typescript) techStack.push("TypeScript");
|
|
591
620
|
if (scan.styling.includes("tailwind")) techStack.push("Tailwind CSS");
|
|
@@ -675,11 +704,152 @@ function generateConfig(scan, templates, commands, guides, options) {
|
|
|
675
704
|
templates,
|
|
676
705
|
commands,
|
|
677
706
|
guides,
|
|
707
|
+
agents: options?.agents || [],
|
|
708
|
+
contexts: options?.contexts || [],
|
|
709
|
+
hooks: options?.hooks || false,
|
|
710
|
+
hookProfile: options?.hookProfile || "standard",
|
|
678
711
|
strictness: options?.strictness || "standard",
|
|
679
712
|
customFragments: options?.customFragments || []
|
|
680
713
|
};
|
|
681
714
|
}
|
|
682
715
|
|
|
716
|
+
// src/generator/hooks.ts
|
|
717
|
+
function generateHooks(scan, profile = "standard") {
|
|
718
|
+
const hooks = {};
|
|
719
|
+
const preToolUse = [];
|
|
720
|
+
const postToolUse = [];
|
|
721
|
+
const stop = [];
|
|
722
|
+
preToolUse.push({
|
|
723
|
+
matcher: "Bash(git push*)",
|
|
724
|
+
hooks: [
|
|
725
|
+
{
|
|
726
|
+
type: "command",
|
|
727
|
+
command: 'echo "\u26A0\uFE0F Review your changes before pushing. Run tests and type-check first."'
|
|
728
|
+
}
|
|
729
|
+
]
|
|
730
|
+
});
|
|
731
|
+
if (scan.tools.biome) {
|
|
732
|
+
postToolUse.push({
|
|
733
|
+
matcher: "Edit|Write",
|
|
734
|
+
hooks: [
|
|
735
|
+
{
|
|
736
|
+
type: "command",
|
|
737
|
+
command: `npx @biomejs/biome check --write --unsafe "$CLAUDE_FILE_PATH" 2>/dev/null || true`
|
|
738
|
+
}
|
|
739
|
+
]
|
|
740
|
+
});
|
|
741
|
+
} else if (scan.tools.prettier) {
|
|
742
|
+
postToolUse.push({
|
|
743
|
+
matcher: "Edit|Write",
|
|
744
|
+
hooks: [
|
|
745
|
+
{
|
|
746
|
+
type: "command",
|
|
747
|
+
command: `npx prettier --write "$CLAUDE_FILE_PATH" 2>/dev/null || true`
|
|
748
|
+
}
|
|
749
|
+
]
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
if (scan.typescript && profile !== "minimal") {
|
|
753
|
+
postToolUse.push({
|
|
754
|
+
matcher: "Edit|Write",
|
|
755
|
+
hooks: [
|
|
756
|
+
{
|
|
757
|
+
type: "command",
|
|
758
|
+
command: 'case "$CLAUDE_FILE_PATH" in *.ts|*.tsx) npx tsc --noEmit --pretty 2>&1 | head -20 ;; esac'
|
|
759
|
+
}
|
|
760
|
+
]
|
|
761
|
+
});
|
|
762
|
+
}
|
|
763
|
+
if (scan.tools.eslint && profile === "strict") {
|
|
764
|
+
postToolUse.push({
|
|
765
|
+
matcher: "Edit|Write",
|
|
766
|
+
hooks: [
|
|
767
|
+
{
|
|
768
|
+
type: "command",
|
|
769
|
+
command: 'case "$CLAUDE_FILE_PATH" in *.ts|*.tsx|*.js|*.jsx) npx eslint "$CLAUDE_FILE_PATH" --max-warnings 0 2>&1 | head -15 ;; esac'
|
|
770
|
+
}
|
|
771
|
+
]
|
|
772
|
+
});
|
|
773
|
+
}
|
|
774
|
+
if (profile !== "minimal") {
|
|
775
|
+
postToolUse.push({
|
|
776
|
+
matcher: "Edit|Write",
|
|
777
|
+
hooks: [
|
|
778
|
+
{
|
|
779
|
+
type: "command",
|
|
780
|
+
command: 'case "$CLAUDE_FILE_PATH" in *.ts|*.tsx|*.js|*.jsx) grep -n "console\\.log" "$CLAUDE_FILE_PATH" && echo "\u26A0\uFE0F console.log detected \u2014 remove before committing" || true ;; esac'
|
|
781
|
+
}
|
|
782
|
+
]
|
|
783
|
+
});
|
|
784
|
+
}
|
|
785
|
+
if (profile !== "minimal") {
|
|
786
|
+
postToolUse.push({
|
|
787
|
+
matcher: "Bash",
|
|
788
|
+
hooks: [
|
|
789
|
+
{
|
|
790
|
+
type: "command",
|
|
791
|
+
command: [
|
|
792
|
+
'if [ "$CLAUDE_TOOL_EXIT_CODE" != "0" ] && [ -n "$CLAUDE_TOOL_EXIT_CODE" ]; then',
|
|
793
|
+
' OUTPUT="$CLAUDE_TOOL_OUTPUT"',
|
|
794
|
+
" IS_BUILD_ERROR=false",
|
|
795
|
+
' case "$OUTPUT" in',
|
|
796
|
+
' *"error TS"*|*"tsc"*) IS_BUILD_ERROR=true ;;',
|
|
797
|
+
' *"ESLint"*|*"eslint"*|*"Lint error"*) IS_BUILD_ERROR=true ;;',
|
|
798
|
+
' *"Build error"*|*"build failed"*|*"ELIFECYCLE"*) IS_BUILD_ERROR=true ;;',
|
|
799
|
+
' *"Module not found"*|*"Cannot find module"*) IS_BUILD_ERROR=true ;;',
|
|
800
|
+
' *"SyntaxError"*|*"TypeError"*) IS_BUILD_ERROR=true ;;',
|
|
801
|
+
" esac",
|
|
802
|
+
' if [ "$IS_BUILD_ERROR" = "true" ]; then',
|
|
803
|
+
' LOG_FILE="docs/mistakes-log.md"',
|
|
804
|
+
' if [ -f "$LOG_FILE" ]; then',
|
|
805
|
+
' DATE=$(date +"%Y-%m-%d %H:%M")',
|
|
806
|
+
' ERROR_PREVIEW=$(echo "$OUTPUT" | grep -i "error" | head -3 | sed "s/^/ /")',
|
|
807
|
+
" {",
|
|
808
|
+
' echo ""',
|
|
809
|
+
' echo "### $DATE \u2014 Build/lint failure (auto-captured)"',
|
|
810
|
+
' echo "- **What happened**: Command exited with code $CLAUDE_TOOL_EXIT_CODE"',
|
|
811
|
+
' echo "- **Error preview**:"',
|
|
812
|
+
' echo "\\`\\`\\`"',
|
|
813
|
+
' echo "$ERROR_PREVIEW"',
|
|
814
|
+
' echo "\\`\\`\\`"',
|
|
815
|
+
' echo "- **Root cause**: <!-- TODO: Fill in after investigating -->"',
|
|
816
|
+
' echo "- **Fix**: <!-- TODO: How was it resolved? -->"',
|
|
817
|
+
' echo "- **Lesson**: <!-- TODO: What to do differently -->"',
|
|
818
|
+
' echo ""',
|
|
819
|
+
' echo "---"',
|
|
820
|
+
' } >> "$LOG_FILE"',
|
|
821
|
+
' echo "\u{1F4DD} Mistake auto-logged to docs/mistakes-log.md"',
|
|
822
|
+
" fi",
|
|
823
|
+
" fi",
|
|
824
|
+
"fi"
|
|
825
|
+
].join("\n")
|
|
826
|
+
}
|
|
827
|
+
]
|
|
828
|
+
});
|
|
829
|
+
}
|
|
830
|
+
if (profile === "strict") {
|
|
831
|
+
stop.push({
|
|
832
|
+
matcher: "",
|
|
833
|
+
hooks: [
|
|
834
|
+
{
|
|
835
|
+
type: "command",
|
|
836
|
+
command: 'git diff --name-only --diff-filter=M 2>/dev/null | grep -E "\\.(ts|tsx|js|jsx)$" | xargs grep -l "console\\.log" 2>/dev/null && echo "\u26A0\uFE0F console.log found in modified files" || true'
|
|
837
|
+
}
|
|
838
|
+
]
|
|
839
|
+
});
|
|
840
|
+
}
|
|
841
|
+
if (preToolUse.length > 0) hooks.PreToolUse = preToolUse;
|
|
842
|
+
if (postToolUse.length > 0) hooks.PostToolUse = postToolUse;
|
|
843
|
+
if (stop.length > 0) hooks.Stop = stop;
|
|
844
|
+
return hooks;
|
|
845
|
+
}
|
|
846
|
+
function generateSettingsLocal(scan, profile = "standard") {
|
|
847
|
+
const hooks = generateHooks(scan, profile);
|
|
848
|
+
return {
|
|
849
|
+
hooks
|
|
850
|
+
};
|
|
851
|
+
}
|
|
852
|
+
|
|
683
853
|
// src/copier/skills.ts
|
|
684
854
|
import path12 from "path";
|
|
685
855
|
import fs3 from "fs-extra";
|
|
@@ -715,14 +885,20 @@ var AVAILABLE_SKILLS = [
|
|
|
715
885
|
"bundle-check",
|
|
716
886
|
"i18n-check",
|
|
717
887
|
"schema-gen",
|
|
718
|
-
"docker-debug",
|
|
719
|
-
"ci-fix",
|
|
720
888
|
"changelog",
|
|
721
889
|
"release",
|
|
722
890
|
"storybook-gen",
|
|
723
|
-
|
|
724
|
-
"
|
|
725
|
-
"
|
|
891
|
+
// New skills (v1.2.0) — hooks, agents, sessions, orchestration
|
|
892
|
+
"search-first",
|
|
893
|
+
"quality-gate-check",
|
|
894
|
+
"server-action",
|
|
895
|
+
"middleware",
|
|
896
|
+
"save-session",
|
|
897
|
+
"resume-session",
|
|
898
|
+
"checkpoint",
|
|
899
|
+
"orchestrate",
|
|
900
|
+
"quality-gate",
|
|
901
|
+
"harness-audit"
|
|
726
902
|
];
|
|
727
903
|
var SKILL_DESCRIPTIONS = {
|
|
728
904
|
"prompt-help": "Help developers write effective AI prompts with structured context",
|
|
@@ -756,14 +932,20 @@ var SKILL_DESCRIPTIONS = {
|
|
|
756
932
|
"bundle-check": "Analyze bundle size, find heavy imports, suggest tree-shaking and code splitting",
|
|
757
933
|
"i18n-check": "Find hardcoded strings, missing translation keys, and internationalization gaps",
|
|
758
934
|
"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
935
|
"changelog": "Generate formatted changelogs from git history following Keep a Changelog format",
|
|
762
936
|
"release": "Guided release workflow with versioning, changelog, tagging, and release notes",
|
|
763
937
|
"storybook-gen": "Generate Storybook stories with controls, play functions, and visual tests",
|
|
764
|
-
|
|
765
|
-
"
|
|
766
|
-
"
|
|
938
|
+
// New skills (v1.2.0) — hooks, agents, sessions, orchestration
|
|
939
|
+
"search-first": "Research-before-coding \u2014 search docs, existing patterns, and APIs before writing code",
|
|
940
|
+
"quality-gate-check": "Post-implementation quality checklist \u2014 type safety, a11y, security, performance, Sitecore",
|
|
941
|
+
"server-action": "Scaffold Next.js Server Actions with Zod validation, error handling, and revalidation",
|
|
942
|
+
"middleware": "Create or update Next.js middleware for auth, redirects, i18n, or Sitecore preview mode",
|
|
943
|
+
"save-session": "Persist current session context, decisions, and pending work for later resumption",
|
|
944
|
+
"resume-session": "Restore context from a previous session and continue where you left off",
|
|
945
|
+
"checkpoint": "Create a verification snapshot \u2014 run all quality checks and record pass/fail status",
|
|
946
|
+
"orchestrate": "Multi-agent orchestration \u2014 break complex tasks into subtasks and delegate to agents",
|
|
947
|
+
"quality-gate": "Run comprehensive quality checks: types, lint, format, tests, bundle, a11y, security",
|
|
948
|
+
"harness-audit": "Audit AI agent configuration \u2014 check CLAUDE.md, hooks, agents, skills, MCP servers"
|
|
767
949
|
};
|
|
768
950
|
async function copySkills(targetDir) {
|
|
769
951
|
const copied = [];
|
|
@@ -802,7 +984,8 @@ var AVAILABLE_GUIDES = [
|
|
|
802
984
|
"prompt-playbook",
|
|
803
985
|
"when-to-use-ai",
|
|
804
986
|
"token-saving-tips",
|
|
805
|
-
"figma-workflow"
|
|
987
|
+
"figma-workflow",
|
|
988
|
+
"hooks-and-agents"
|
|
806
989
|
];
|
|
807
990
|
async function copyGuides(targetDir) {
|
|
808
991
|
const guidesTarget = path13.join(targetDir, "ai-kit", "guides");
|
|
@@ -841,12 +1024,81 @@ async function scaffoldDocs(targetDir) {
|
|
|
841
1024
|
return created;
|
|
842
1025
|
}
|
|
843
1026
|
|
|
1027
|
+
// src/copier/agents.ts
|
|
1028
|
+
import path15 from "path";
|
|
1029
|
+
import fs6 from "fs-extra";
|
|
1030
|
+
var UNIVERSAL_AGENTS = [
|
|
1031
|
+
"planner",
|
|
1032
|
+
"code-reviewer",
|
|
1033
|
+
"security-reviewer",
|
|
1034
|
+
"build-resolver",
|
|
1035
|
+
"doc-updater",
|
|
1036
|
+
"refactor-cleaner",
|
|
1037
|
+
"architect"
|
|
1038
|
+
];
|
|
1039
|
+
var CONDITIONAL_AGENTS = [
|
|
1040
|
+
{
|
|
1041
|
+
name: "e2e-runner",
|
|
1042
|
+
condition: (scan) => scan.tools.playwright
|
|
1043
|
+
},
|
|
1044
|
+
{
|
|
1045
|
+
name: "sitecore-specialist",
|
|
1046
|
+
condition: (scan) => scan.cms !== "none"
|
|
1047
|
+
},
|
|
1048
|
+
{
|
|
1049
|
+
name: "tdd-guide",
|
|
1050
|
+
condition: (scan) => scan.tools.playwright || !!scan.scripts["test"]
|
|
1051
|
+
}
|
|
1052
|
+
];
|
|
1053
|
+
async function copyAgents(targetDir, scan) {
|
|
1054
|
+
const agentsTarget = path15.join(targetDir, ".claude", "agents");
|
|
1055
|
+
await fs6.ensureDir(agentsTarget);
|
|
1056
|
+
const copied = [];
|
|
1057
|
+
for (const agent of UNIVERSAL_AGENTS) {
|
|
1058
|
+
const src = path15.join(AGENTS_DIR, `${agent}.md`);
|
|
1059
|
+
if (!await fs6.pathExists(src)) continue;
|
|
1060
|
+
await fs6.copy(src, path15.join(agentsTarget, `${agent}.md`), {
|
|
1061
|
+
overwrite: true
|
|
1062
|
+
});
|
|
1063
|
+
copied.push(agent);
|
|
1064
|
+
}
|
|
1065
|
+
for (const { name, condition } of CONDITIONAL_AGENTS) {
|
|
1066
|
+
if (!condition(scan)) continue;
|
|
1067
|
+
const src = path15.join(AGENTS_DIR, `${name}.md`);
|
|
1068
|
+
if (!await fs6.pathExists(src)) continue;
|
|
1069
|
+
await fs6.copy(src, path15.join(agentsTarget, `${name}.md`), {
|
|
1070
|
+
overwrite: true
|
|
1071
|
+
});
|
|
1072
|
+
copied.push(name);
|
|
1073
|
+
}
|
|
1074
|
+
return copied;
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
// src/copier/contexts.ts
|
|
1078
|
+
import path16 from "path";
|
|
1079
|
+
import fs7 from "fs-extra";
|
|
1080
|
+
var AVAILABLE_CONTEXTS = ["dev", "review", "research"];
|
|
1081
|
+
async function copyContexts(targetDir) {
|
|
1082
|
+
const contextsTarget = path16.join(targetDir, ".claude", "contexts");
|
|
1083
|
+
await fs7.ensureDir(contextsTarget);
|
|
1084
|
+
const copied = [];
|
|
1085
|
+
for (const context of AVAILABLE_CONTEXTS) {
|
|
1086
|
+
const src = path16.join(CONTEXTS_DIR, `${context}.md`);
|
|
1087
|
+
if (!await fs7.pathExists(src)) continue;
|
|
1088
|
+
await fs7.copy(src, path16.join(contextsTarget, `${context}.md`), {
|
|
1089
|
+
overwrite: true
|
|
1090
|
+
});
|
|
1091
|
+
copied.push(context);
|
|
1092
|
+
}
|
|
1093
|
+
return copied;
|
|
1094
|
+
}
|
|
1095
|
+
|
|
844
1096
|
// src/commands/init.ts
|
|
845
1097
|
async function initCommand(targetPath) {
|
|
846
|
-
const projectDir =
|
|
1098
|
+
const projectDir = path17.resolve(targetPath || process.cwd());
|
|
847
1099
|
logSection("AI Kit \u2014 Project Setup");
|
|
848
1100
|
logInfo(`Scanning: ${projectDir}`);
|
|
849
|
-
const configPath =
|
|
1101
|
+
const configPath = path17.join(projectDir, AI_KIT_CONFIG_FILE);
|
|
850
1102
|
if (fileExists(configPath)) {
|
|
851
1103
|
const overwrite = await confirm({
|
|
852
1104
|
message: "AI Kit is already configured in this project. Re-initialize?",
|
|
@@ -874,14 +1126,20 @@ async function initCommand(targetPath) {
|
|
|
874
1126
|
logInfo(`TypeScript: ${scan.typescript ? "Yes" : "No"}`);
|
|
875
1127
|
logInfo(`Monorepo: ${scan.monorepo ? `Yes (${scan.monorepoTool})` : "No"}`);
|
|
876
1128
|
logInfo(`Package Manager: ${scan.packageManager}`);
|
|
1129
|
+
logInfo(`Formatter: ${scan.tools.biome ? "Biome" : scan.tools.prettier ? "Prettier" : "None detected"}`);
|
|
877
1130
|
const clarifications = await askClarifications(scan);
|
|
878
1131
|
scan = applyClarifications(scan, clarifications);
|
|
879
1132
|
const tools = await selectTools();
|
|
880
1133
|
const strictness = await selectStrictness();
|
|
1134
|
+
const hookProfile = await selectHookProfile();
|
|
881
1135
|
const customFragments = loadCustomFragments(projectDir);
|
|
882
1136
|
const conflict = await selectConflictStrategy(projectDir);
|
|
883
1137
|
logSection("Generating Files");
|
|
884
|
-
const results = await generate(projectDir, scan, tools, conflict, {
|
|
1138
|
+
const results = await generate(projectDir, scan, tools, conflict, {
|
|
1139
|
+
strictness,
|
|
1140
|
+
customFragments,
|
|
1141
|
+
hookProfile
|
|
1142
|
+
});
|
|
885
1143
|
logSection("Setup Complete");
|
|
886
1144
|
if (results.claudeMd) logSuccess(`CLAUDE.md generated`);
|
|
887
1145
|
if (results.cursorRules) logSuccess(`.cursorrules generated`);
|
|
@@ -889,6 +1147,12 @@ async function initCommand(targetPath) {
|
|
|
889
1147
|
logSuccess(`${results.cursorMdcFiles} .cursor/rules/*.mdc files generated`);
|
|
890
1148
|
if (results.commands.length > 0)
|
|
891
1149
|
logSuccess(`${results.commands.length} skills generated (.claude/skills/ + .cursor/skills/)`);
|
|
1150
|
+
if (results.agents.length > 0)
|
|
1151
|
+
logSuccess(`${results.agents.length} agents generated (.claude/agents/)`);
|
|
1152
|
+
if (results.contexts.length > 0)
|
|
1153
|
+
logSuccess(`${results.contexts.length} context modes generated (.claude/contexts/)`);
|
|
1154
|
+
if (results.hooks)
|
|
1155
|
+
logSuccess(`Hooks configured (.claude/settings.local.json) \u2014 profile: ${hookProfile}`);
|
|
892
1156
|
if (results.guides.length > 0)
|
|
893
1157
|
logSuccess(`${results.guides.length} guides added to ai-kit/guides/`);
|
|
894
1158
|
if (results.docs.length > 0)
|
|
@@ -896,6 +1160,7 @@ async function initCommand(targetPath) {
|
|
|
896
1160
|
showRecommendations(scan);
|
|
897
1161
|
console.log("");
|
|
898
1162
|
logInfo("Run `ai-kit update` anytime to refresh configs after project changes.");
|
|
1163
|
+
logInfo("Run `ai-kit audit` to check your AI agent configuration health.");
|
|
899
1164
|
logInfo("Check ai-kit/guides/getting-started.md to get started.");
|
|
900
1165
|
}
|
|
901
1166
|
function formatFramework(scan) {
|
|
@@ -953,8 +1218,28 @@ async function selectStrictness() {
|
|
|
953
1218
|
default: "standard"
|
|
954
1219
|
});
|
|
955
1220
|
}
|
|
1221
|
+
async function selectHookProfile() {
|
|
1222
|
+
return select({
|
|
1223
|
+
message: "Hook automation profile (runs checks automatically as you code):",
|
|
1224
|
+
choices: [
|
|
1225
|
+
{
|
|
1226
|
+
name: "Standard \u2014 auto-format + typecheck + console.log warnings",
|
|
1227
|
+
value: "standard"
|
|
1228
|
+
},
|
|
1229
|
+
{
|
|
1230
|
+
name: "Strict \u2014 all standard hooks + ESLint + stop checks",
|
|
1231
|
+
value: "strict"
|
|
1232
|
+
},
|
|
1233
|
+
{
|
|
1234
|
+
name: "Minimal \u2014 auto-format + git push safety only",
|
|
1235
|
+
value: "minimal"
|
|
1236
|
+
}
|
|
1237
|
+
],
|
|
1238
|
+
default: "standard"
|
|
1239
|
+
});
|
|
1240
|
+
}
|
|
956
1241
|
async function selectConflictStrategy(projectDir) {
|
|
957
|
-
const hasExisting = fileExists(
|
|
1242
|
+
const hasExisting = fileExists(path17.join(projectDir, GENERATED_FILES.claudeMd)) || fileExists(path17.join(projectDir, GENERATED_FILES.cursorRules));
|
|
958
1243
|
if (!hasExisting) return "overwrite";
|
|
959
1244
|
return select({
|
|
960
1245
|
message: "Existing AI config files detected. How should we handle conflicts?",
|
|
@@ -977,34 +1262,45 @@ async function generate(projectDir, scan, tools, conflict, opts) {
|
|
|
977
1262
|
cursorRules: false,
|
|
978
1263
|
cursorMdcFiles: 0,
|
|
979
1264
|
commands: [],
|
|
1265
|
+
agents: [],
|
|
1266
|
+
contexts: [],
|
|
1267
|
+
hooks: false,
|
|
980
1268
|
guides: [],
|
|
981
1269
|
docs: []
|
|
982
1270
|
};
|
|
983
1271
|
if (tools.claude) {
|
|
984
|
-
const claudeMdPath =
|
|
1272
|
+
const claudeMdPath = path17.join(projectDir, GENERATED_FILES.claudeMd);
|
|
985
1273
|
if (conflict === "overwrite" || !fileExists(claudeMdPath)) {
|
|
986
1274
|
const content = generateClaudeMd(scan, { strictness: opts?.strictness, customFragments: opts?.customFragments });
|
|
987
|
-
await
|
|
1275
|
+
await fs8.writeFile(claudeMdPath, content, "utf-8");
|
|
988
1276
|
result.claudeMd = true;
|
|
989
1277
|
} else {
|
|
990
1278
|
logWarning("CLAUDE.md exists, skipping");
|
|
991
1279
|
}
|
|
992
1280
|
result.commands = await copySkills(projectDir);
|
|
1281
|
+
result.agents = await copyAgents(projectDir, scan);
|
|
1282
|
+
result.contexts = await copyContexts(projectDir);
|
|
1283
|
+
const hookProfile = opts?.hookProfile || "standard";
|
|
1284
|
+
const settingsLocalPath = path17.join(projectDir, GENERATED_FILES.claudeSettingsLocal);
|
|
1285
|
+
const settingsLocal = generateSettingsLocal(scan, hookProfile);
|
|
1286
|
+
await fs8.ensureDir(path17.dirname(settingsLocalPath));
|
|
1287
|
+
await fs8.writeJson(settingsLocalPath, settingsLocal, { spaces: 2 });
|
|
1288
|
+
result.hooks = true;
|
|
993
1289
|
}
|
|
994
1290
|
if (tools.cursor) {
|
|
995
|
-
const cursorPath =
|
|
1291
|
+
const cursorPath = path17.join(projectDir, GENERATED_FILES.cursorRules);
|
|
996
1292
|
if (conflict === "overwrite" || !fileExists(cursorPath)) {
|
|
997
1293
|
const content = generateCursorRules(scan, { strictness: opts?.strictness, customFragments: opts?.customFragments });
|
|
998
|
-
await
|
|
1294
|
+
await fs8.writeFile(cursorPath, content, "utf-8");
|
|
999
1295
|
result.cursorRules = true;
|
|
1000
1296
|
} else {
|
|
1001
1297
|
logWarning(".cursorrules exists, skipping");
|
|
1002
1298
|
}
|
|
1003
|
-
const mdcDir =
|
|
1004
|
-
await
|
|
1299
|
+
const mdcDir = path17.join(projectDir, GENERATED_FILES.cursorMdcDir);
|
|
1300
|
+
await fs8.ensureDir(mdcDir);
|
|
1005
1301
|
const mdcFiles = generateMdcFiles(scan);
|
|
1006
1302
|
for (const mdc of mdcFiles) {
|
|
1007
|
-
await
|
|
1303
|
+
await fs8.writeFile(path17.join(mdcDir, mdc.filename), mdc.content, "utf-8");
|
|
1008
1304
|
}
|
|
1009
1305
|
result.cursorMdcFiles = mdcFiles.length;
|
|
1010
1306
|
}
|
|
@@ -1013,9 +1309,16 @@ async function generate(projectDir, scan, tools, conflict, opts) {
|
|
|
1013
1309
|
const templates = [];
|
|
1014
1310
|
if (result.claudeMd) templates.push("CLAUDE.md");
|
|
1015
1311
|
if (result.cursorRules) templates.push(".cursorrules");
|
|
1016
|
-
const config = generateConfig(scan, templates, result.commands, result.guides, {
|
|
1017
|
-
|
|
1018
|
-
|
|
1312
|
+
const config = generateConfig(scan, templates, result.commands, result.guides, {
|
|
1313
|
+
strictness: opts?.strictness,
|
|
1314
|
+
customFragments: opts?.customFragments,
|
|
1315
|
+
agents: result.agents,
|
|
1316
|
+
contexts: result.contexts,
|
|
1317
|
+
hooks: result.hooks,
|
|
1318
|
+
hookProfile: opts?.hookProfile
|
|
1319
|
+
});
|
|
1320
|
+
await fs8.writeJson(
|
|
1321
|
+
path17.join(projectDir, AI_KIT_CONFIG_FILE),
|
|
1019
1322
|
config,
|
|
1020
1323
|
{ spaces: 2 }
|
|
1021
1324
|
);
|
|
@@ -1034,9 +1337,9 @@ function showRecommendations(scan) {
|
|
|
1034
1337
|
hint: " npm install -D eslint @typescript-eslint/eslint-plugin"
|
|
1035
1338
|
},
|
|
1036
1339
|
{
|
|
1037
|
-
check: scan.tools.prettier,
|
|
1038
|
-
label: "
|
|
1039
|
-
hint: " npm install -D prettier"
|
|
1340
|
+
check: scan.tools.prettier || scan.tools.biome,
|
|
1341
|
+
label: "No formatter detected \u2014 install Prettier or Biome for auto-formatting hooks:",
|
|
1342
|
+
hint: " npm install -D prettier (or) npm install -D @biomejs/biome"
|
|
1040
1343
|
},
|
|
1041
1344
|
{
|
|
1042
1345
|
check: scan.tools.axeCore,
|
|
@@ -1091,13 +1394,13 @@ function showRecommendations(scan) {
|
|
|
1091
1394
|
}
|
|
1092
1395
|
|
|
1093
1396
|
// src/commands/update.ts
|
|
1094
|
-
import
|
|
1095
|
-
import
|
|
1397
|
+
import path18 from "path";
|
|
1398
|
+
import fs9 from "fs-extra";
|
|
1096
1399
|
import ora2 from "ora";
|
|
1097
1400
|
import { confirm as confirm2 } from "@inquirer/prompts";
|
|
1098
1401
|
async function updateCommand(targetPath) {
|
|
1099
|
-
const projectDir =
|
|
1100
|
-
const configPath =
|
|
1402
|
+
const projectDir = path18.resolve(targetPath || process.cwd());
|
|
1403
|
+
const configPath = path18.join(projectDir, AI_KIT_CONFIG_FILE);
|
|
1101
1404
|
if (!fileExists(configPath)) {
|
|
1102
1405
|
logError("No ai-kit.config.json found. Run `ai-kit init` first.");
|
|
1103
1406
|
return;
|
|
@@ -1124,57 +1427,75 @@ async function updateCommand(targetPath) {
|
|
|
1124
1427
|
spinner.succeed("Project re-scanned");
|
|
1125
1428
|
logSection("Updating Files");
|
|
1126
1429
|
const strictness = existingConfig.strictness || "standard";
|
|
1430
|
+
const hookProfile = existingConfig.hookProfile || "standard";
|
|
1127
1431
|
const customFragments = loadCustomFragments(projectDir);
|
|
1128
1432
|
const genOpts = { strictness, customFragments };
|
|
1129
1433
|
const templates = [];
|
|
1130
|
-
if (existingConfig.templates.includes("CLAUDE.md") || fileExists(
|
|
1131
|
-
const claudeMdPath =
|
|
1434
|
+
if (existingConfig.templates.includes("CLAUDE.md") || fileExists(path18.join(projectDir, GENERATED_FILES.claudeMd))) {
|
|
1435
|
+
const claudeMdPath = path18.join(projectDir, GENERATED_FILES.claudeMd);
|
|
1132
1436
|
const newContent = generateClaudeMd(scan, genOpts);
|
|
1133
1437
|
const existing = readFileSafe(claudeMdPath);
|
|
1134
1438
|
if (existing) {
|
|
1135
|
-
await
|
|
1439
|
+
await fs9.writeFile(claudeMdPath, mergeWithMarkers(existing, newContent), "utf-8");
|
|
1136
1440
|
} else {
|
|
1137
|
-
await
|
|
1441
|
+
await fs9.writeFile(claudeMdPath, newContent, "utf-8");
|
|
1138
1442
|
}
|
|
1139
1443
|
templates.push("CLAUDE.md");
|
|
1140
1444
|
logSuccess("CLAUDE.md updated");
|
|
1141
1445
|
}
|
|
1142
|
-
if (existingConfig.templates.includes(".cursorrules") || fileExists(
|
|
1143
|
-
const cursorRulesPath =
|
|
1446
|
+
if (existingConfig.templates.includes(".cursorrules") || fileExists(path18.join(projectDir, GENERATED_FILES.cursorRules))) {
|
|
1447
|
+
const cursorRulesPath = path18.join(projectDir, GENERATED_FILES.cursorRules);
|
|
1144
1448
|
const newContent = generateCursorRules(scan, genOpts);
|
|
1145
1449
|
const existing = readFileSafe(cursorRulesPath);
|
|
1146
1450
|
if (existing) {
|
|
1147
|
-
await
|
|
1451
|
+
await fs9.writeFile(cursorRulesPath, mergeWithMarkers(existing, newContent), "utf-8");
|
|
1148
1452
|
} else {
|
|
1149
|
-
await
|
|
1453
|
+
await fs9.writeFile(cursorRulesPath, newContent, "utf-8");
|
|
1150
1454
|
}
|
|
1151
1455
|
templates.push(".cursorrules");
|
|
1152
1456
|
logSuccess(".cursorrules updated");
|
|
1153
|
-
const mdcDir =
|
|
1154
|
-
await
|
|
1457
|
+
const mdcDir = path18.join(projectDir, GENERATED_FILES.cursorMdcDir);
|
|
1458
|
+
await fs9.ensureDir(mdcDir);
|
|
1155
1459
|
const mdcFiles = generateMdcFiles(scan);
|
|
1156
1460
|
for (const mdc of mdcFiles) {
|
|
1157
|
-
await
|
|
1461
|
+
await fs9.writeFile(path18.join(mdcDir, mdc.filename), mdc.content, "utf-8");
|
|
1158
1462
|
}
|
|
1159
1463
|
logSuccess(`${mdcFiles.length} .cursor/rules/*.mdc files updated`);
|
|
1160
1464
|
}
|
|
1161
1465
|
const commands = await copySkills(projectDir);
|
|
1162
1466
|
logSuccess(`${commands.length} skills updated (.claude/skills/ + .cursor/skills/)`);
|
|
1467
|
+
const agents = await copyAgents(projectDir, scan);
|
|
1468
|
+
logSuccess(`${agents.length} agents updated (.claude/agents/)`);
|
|
1469
|
+
const contexts = await copyContexts(projectDir);
|
|
1470
|
+
logSuccess(`${contexts.length} context modes updated (.claude/contexts/)`);
|
|
1471
|
+
if (existingConfig.hooks !== false) {
|
|
1472
|
+
const settingsLocalPath = path18.join(projectDir, GENERATED_FILES.claudeSettingsLocal);
|
|
1473
|
+
const settingsLocal = generateSettingsLocal(scan, hookProfile);
|
|
1474
|
+
await fs9.ensureDir(path18.dirname(settingsLocalPath));
|
|
1475
|
+
await fs9.writeJson(settingsLocalPath, settingsLocal, { spaces: 2 });
|
|
1476
|
+
logSuccess(`Hooks updated (profile: ${hookProfile})`);
|
|
1477
|
+
}
|
|
1163
1478
|
const guides = await copyGuides(projectDir);
|
|
1164
1479
|
logSuccess(`${guides.length} guides updated`);
|
|
1165
|
-
const config = generateConfig(scan, templates, commands, guides,
|
|
1166
|
-
|
|
1480
|
+
const config = generateConfig(scan, templates, commands, guides, {
|
|
1481
|
+
...genOpts,
|
|
1482
|
+
agents,
|
|
1483
|
+
contexts,
|
|
1484
|
+
hooks: existingConfig.hooks !== false,
|
|
1485
|
+
hookProfile
|
|
1486
|
+
});
|
|
1487
|
+
await fs9.writeJson(configPath, config, { spaces: 2 });
|
|
1167
1488
|
logSuccess("ai-kit.config.json updated");
|
|
1168
1489
|
console.log("");
|
|
1169
1490
|
logInfo("All AI configs refreshed with latest project scan.");
|
|
1170
1491
|
}
|
|
1171
1492
|
|
|
1172
1493
|
// src/commands/reset.ts
|
|
1173
|
-
import
|
|
1174
|
-
import
|
|
1494
|
+
import path19 from "path";
|
|
1495
|
+
import fs10 from "fs-extra";
|
|
1175
1496
|
import { confirm as confirm3 } from "@inquirer/prompts";
|
|
1176
1497
|
async function resetCommand(targetPath) {
|
|
1177
|
-
const projectDir =
|
|
1498
|
+
const projectDir = path19.resolve(targetPath || process.cwd());
|
|
1178
1499
|
logSection("AI Kit \u2014 Reset");
|
|
1179
1500
|
logWarning("This will remove all AI Kit generated files:");
|
|
1180
1501
|
logInfo(` - ${GENERATED_FILES.claudeMd}`);
|
|
@@ -1195,44 +1516,44 @@ async function resetCommand(targetPath) {
|
|
|
1195
1516
|
return;
|
|
1196
1517
|
}
|
|
1197
1518
|
const removed = [];
|
|
1198
|
-
const claudeMdPath =
|
|
1519
|
+
const claudeMdPath = path19.join(projectDir, GENERATED_FILES.claudeMd);
|
|
1199
1520
|
if (fileExists(claudeMdPath)) {
|
|
1200
|
-
await
|
|
1521
|
+
await fs10.remove(claudeMdPath);
|
|
1201
1522
|
removed.push(GENERATED_FILES.claudeMd);
|
|
1202
1523
|
}
|
|
1203
|
-
const cursorPath =
|
|
1524
|
+
const cursorPath = path19.join(projectDir, GENERATED_FILES.cursorRules);
|
|
1204
1525
|
if (fileExists(cursorPath)) {
|
|
1205
|
-
await
|
|
1526
|
+
await fs10.remove(cursorPath);
|
|
1206
1527
|
removed.push(GENERATED_FILES.cursorRules);
|
|
1207
1528
|
}
|
|
1208
|
-
const cursorMdcDir =
|
|
1529
|
+
const cursorMdcDir = path19.join(projectDir, GENERATED_FILES.cursorMdcDir);
|
|
1209
1530
|
if (fileExists(cursorMdcDir)) {
|
|
1210
|
-
await
|
|
1531
|
+
await fs10.remove(cursorMdcDir);
|
|
1211
1532
|
removed.push(GENERATED_FILES.cursorMdcDir);
|
|
1212
1533
|
}
|
|
1213
|
-
const commandsDir =
|
|
1534
|
+
const commandsDir = path19.join(projectDir, GENERATED_FILES.claudeCommands);
|
|
1214
1535
|
if (fileExists(commandsDir)) {
|
|
1215
|
-
await
|
|
1536
|
+
await fs10.remove(commandsDir);
|
|
1216
1537
|
removed.push(GENERATED_FILES.claudeCommands);
|
|
1217
1538
|
}
|
|
1218
|
-
const claudeSkillsDir =
|
|
1539
|
+
const claudeSkillsDir = path19.join(projectDir, GENERATED_FILES.claudeSkills);
|
|
1219
1540
|
if (fileExists(claudeSkillsDir)) {
|
|
1220
|
-
await
|
|
1541
|
+
await fs10.remove(claudeSkillsDir);
|
|
1221
1542
|
removed.push(GENERATED_FILES.claudeSkills);
|
|
1222
1543
|
}
|
|
1223
|
-
const cursorSkillsDir =
|
|
1544
|
+
const cursorSkillsDir = path19.join(projectDir, GENERATED_FILES.cursorSkills);
|
|
1224
1545
|
if (fileExists(cursorSkillsDir)) {
|
|
1225
|
-
await
|
|
1546
|
+
await fs10.remove(cursorSkillsDir);
|
|
1226
1547
|
removed.push(GENERATED_FILES.cursorSkills);
|
|
1227
1548
|
}
|
|
1228
|
-
const aiKitDir =
|
|
1549
|
+
const aiKitDir = path19.join(projectDir, "ai-kit");
|
|
1229
1550
|
if (fileExists(aiKitDir)) {
|
|
1230
|
-
await
|
|
1551
|
+
await fs10.remove(aiKitDir);
|
|
1231
1552
|
removed.push("ai-kit/");
|
|
1232
1553
|
}
|
|
1233
|
-
const configPath =
|
|
1554
|
+
const configPath = path19.join(projectDir, AI_KIT_CONFIG_FILE);
|
|
1234
1555
|
if (fileExists(configPath)) {
|
|
1235
|
-
await
|
|
1556
|
+
await fs10.remove(configPath);
|
|
1236
1557
|
removed.push(AI_KIT_CONFIG_FILE);
|
|
1237
1558
|
}
|
|
1238
1559
|
logSection("Reset Complete");
|
|
@@ -1244,8 +1565,8 @@ async function resetCommand(targetPath) {
|
|
|
1244
1565
|
}
|
|
1245
1566
|
|
|
1246
1567
|
// src/commands/tokens.ts
|
|
1247
|
-
import
|
|
1248
|
-
import
|
|
1568
|
+
import path20 from "path";
|
|
1569
|
+
import fs11 from "fs-extra";
|
|
1249
1570
|
import chalk2 from "chalk";
|
|
1250
1571
|
import ora3 from "ora";
|
|
1251
1572
|
import os from "os";
|
|
@@ -1255,14 +1576,14 @@ var PRICING = {
|
|
|
1255
1576
|
};
|
|
1256
1577
|
var PLAN_BUDGET = 20;
|
|
1257
1578
|
function findSessionFiles() {
|
|
1258
|
-
const claudeDir =
|
|
1259
|
-
if (!
|
|
1579
|
+
const claudeDir = path20.join(os.homedir(), ".claude", "projects");
|
|
1580
|
+
if (!fs11.existsSync(claudeDir)) return [];
|
|
1260
1581
|
const files = [];
|
|
1261
1582
|
function walkDir(dir) {
|
|
1262
1583
|
try {
|
|
1263
|
-
const entries =
|
|
1584
|
+
const entries = fs11.readdirSync(dir, { withFileTypes: true });
|
|
1264
1585
|
for (const entry of entries) {
|
|
1265
|
-
const full =
|
|
1586
|
+
const full = path20.join(dir, entry.name);
|
|
1266
1587
|
if (entry.isDirectory()) {
|
|
1267
1588
|
walkDir(full);
|
|
1268
1589
|
} else if (entry.name.endsWith(".jsonl")) {
|
|
@@ -1277,7 +1598,7 @@ function findSessionFiles() {
|
|
|
1277
1598
|
}
|
|
1278
1599
|
function parseSessionFile(filePath) {
|
|
1279
1600
|
try {
|
|
1280
|
-
const content =
|
|
1601
|
+
const content = fs11.readFileSync(filePath, "utf-8");
|
|
1281
1602
|
const lines = content.split("\n").filter((l) => l.trim());
|
|
1282
1603
|
const usage = {
|
|
1283
1604
|
inputTokens: 0,
|
|
@@ -1314,11 +1635,11 @@ function parseSessionFile(filePath) {
|
|
|
1314
1635
|
}
|
|
1315
1636
|
if (usage.inputTokens === 0 && usage.outputTokens === 0) return null;
|
|
1316
1637
|
if (!sessionDate) {
|
|
1317
|
-
const stat =
|
|
1638
|
+
const stat = fs11.statSync(filePath);
|
|
1318
1639
|
sessionDate = stat.mtime.toISOString().slice(0, 10);
|
|
1319
1640
|
}
|
|
1320
|
-
const sessionId =
|
|
1321
|
-
const projectName =
|
|
1641
|
+
const sessionId = path20.basename(filePath, ".jsonl");
|
|
1642
|
+
const projectName = path20.basename(path20.dirname(filePath));
|
|
1322
1643
|
return {
|
|
1323
1644
|
sessionId,
|
|
1324
1645
|
filePath,
|
|
@@ -1547,13 +1868,13 @@ ${chalk2.dim("Tip: Use /understand before modifying unfamiliar code \u2014")}`
|
|
|
1547
1868
|
);
|
|
1548
1869
|
console.log("");
|
|
1549
1870
|
if (options.csv) {
|
|
1550
|
-
const csvPath =
|
|
1871
|
+
const csvPath = path20.join(process.cwd(), "token-usage.csv");
|
|
1551
1872
|
const csvHeader = "Date,Sessions,Input Tokens,Output Tokens,Cache Tokens,Cost\n";
|
|
1552
1873
|
const daily = aggregateByDate(sessions);
|
|
1553
1874
|
const csvRows = daily.map(
|
|
1554
1875
|
(d) => `${d.date},${d.sessions},${d.usage.inputTokens},${d.usage.outputTokens},${d.usage.cacheReadTokens},${d.cost.toFixed(2)}`
|
|
1555
1876
|
).join("\n");
|
|
1556
|
-
await
|
|
1877
|
+
await fs11.writeFile(csvPath, csvHeader + csvRows, "utf-8");
|
|
1557
1878
|
logSuccess(`CSV exported to ${csvPath}`);
|
|
1558
1879
|
}
|
|
1559
1880
|
if (options.export) {
|
|
@@ -1590,13 +1911,13 @@ async function exportDashboard(allSessions, todaySessions, weekSessions, monthSe
|
|
|
1590
1911
|
}
|
|
1591
1912
|
};
|
|
1592
1913
|
const outputDir = process.cwd();
|
|
1593
|
-
const dataPath =
|
|
1594
|
-
const dashboardSrc =
|
|
1595
|
-
const dashboardDest =
|
|
1596
|
-
await
|
|
1914
|
+
const dataPath = path20.join(outputDir, "token-data.json");
|
|
1915
|
+
const dashboardSrc = path20.join(PACKAGE_ROOT, "templates", "token-dashboard.html");
|
|
1916
|
+
const dashboardDest = path20.join(outputDir, "token-dashboard.html");
|
|
1917
|
+
await fs11.writeJson(dataPath, exportData, { spaces: 2 });
|
|
1597
1918
|
logInfo(`Token data written to ${dataPath}`);
|
|
1598
|
-
if (await
|
|
1599
|
-
await
|
|
1919
|
+
if (await fs11.pathExists(dashboardSrc)) {
|
|
1920
|
+
await fs11.copy(dashboardSrc, dashboardDest, { overwrite: true });
|
|
1600
1921
|
logInfo(`Dashboard copied to ${dashboardDest}`);
|
|
1601
1922
|
} else {
|
|
1602
1923
|
logWarning("Dashboard template not found. Skipping HTML export.");
|
|
@@ -1613,12 +1934,12 @@ async function exportDashboard(allSessions, todaySessions, weekSessions, monthSe
|
|
|
1613
1934
|
}
|
|
1614
1935
|
|
|
1615
1936
|
// src/commands/doctor.ts
|
|
1616
|
-
import
|
|
1937
|
+
import path21 from "path";
|
|
1617
1938
|
import chalk3 from "chalk";
|
|
1618
1939
|
import ora4 from "ora";
|
|
1619
1940
|
async function doctorCommand(targetPath) {
|
|
1620
|
-
const projectDir =
|
|
1621
|
-
const configPath =
|
|
1941
|
+
const projectDir = path21.resolve(targetPath || process.cwd());
|
|
1942
|
+
const configPath = path21.join(projectDir, AI_KIT_CONFIG_FILE);
|
|
1622
1943
|
let passed = 0;
|
|
1623
1944
|
let warnings = 0;
|
|
1624
1945
|
let issues = 0;
|
|
@@ -1650,7 +1971,7 @@ async function doctorCommand(targetPath) {
|
|
|
1650
1971
|
}
|
|
1651
1972
|
for (const template of config.templates) {
|
|
1652
1973
|
const templateFile = template === "CLAUDE.md" ? GENERATED_FILES.claudeMd : template === ".cursorrules" ? GENERATED_FILES.cursorRules : template;
|
|
1653
|
-
const templatePath =
|
|
1974
|
+
const templatePath = path21.join(projectDir, templateFile);
|
|
1654
1975
|
if (fileExists(templatePath)) {
|
|
1655
1976
|
logSuccess(`${template} exists and in sync`);
|
|
1656
1977
|
passed++;
|
|
@@ -1661,8 +1982,8 @@ async function doctorCommand(targetPath) {
|
|
|
1661
1982
|
}
|
|
1662
1983
|
const missingSkills = [];
|
|
1663
1984
|
for (const skill of config.commands) {
|
|
1664
|
-
const claudeSkillPath =
|
|
1665
|
-
const cursorSkillPath =
|
|
1985
|
+
const claudeSkillPath = path21.join(projectDir, GENERATED_FILES.claudeSkills, skill);
|
|
1986
|
+
const cursorSkillPath = path21.join(projectDir, GENERATED_FILES.cursorSkills, skill);
|
|
1666
1987
|
if (!fileExists(claudeSkillPath) && !fileExists(cursorSkillPath)) {
|
|
1667
1988
|
missingSkills.push(skill);
|
|
1668
1989
|
}
|
|
@@ -1676,10 +1997,10 @@ async function doctorCommand(targetPath) {
|
|
|
1676
1997
|
);
|
|
1677
1998
|
issues++;
|
|
1678
1999
|
}
|
|
1679
|
-
const guidesDir =
|
|
2000
|
+
const guidesDir = path21.join(projectDir, "ai-kit", "guides");
|
|
1680
2001
|
const missingGuides = [];
|
|
1681
2002
|
for (const guide of config.guides) {
|
|
1682
|
-
const guidePath =
|
|
2003
|
+
const guidePath = path21.join(guidesDir, guide);
|
|
1683
2004
|
if (!fileExists(guidePath)) {
|
|
1684
2005
|
missingGuides.push(guide);
|
|
1685
2006
|
}
|
|
@@ -1817,13 +2138,13 @@ function compareScanResults(previous, current) {
|
|
|
1817
2138
|
}
|
|
1818
2139
|
|
|
1819
2140
|
// src/commands/diff.ts
|
|
1820
|
-
import
|
|
1821
|
-
import
|
|
2141
|
+
import path22 from "path";
|
|
2142
|
+
import fs12 from "fs-extra";
|
|
1822
2143
|
import chalk4 from "chalk";
|
|
1823
2144
|
import ora5 from "ora";
|
|
1824
2145
|
async function diffCommand(targetPath) {
|
|
1825
|
-
const projectDir =
|
|
1826
|
-
const configPath =
|
|
2146
|
+
const projectDir = path22.resolve(targetPath || process.cwd());
|
|
2147
|
+
const configPath = path22.join(projectDir, AI_KIT_CONFIG_FILE);
|
|
1827
2148
|
console.log(chalk4.bold("AI Kit \u2014 Diff (dry run)\n"));
|
|
1828
2149
|
if (!fileExists(configPath)) {
|
|
1829
2150
|
logError("No ai-kit.config.json found. Run `ai-kit init` first.");
|
|
@@ -1890,11 +2211,11 @@ async function diffCommand(targetPath) {
|
|
|
1890
2211
|
if (cursorRulesStatus.status === "modified") modified++;
|
|
1891
2212
|
else if (cursorRulesStatus.status === "added") added++;
|
|
1892
2213
|
else unchanged++;
|
|
1893
|
-
const skillsDir =
|
|
2214
|
+
const skillsDir = path22.join(projectDir, GENERATED_FILES.claudeSkills);
|
|
1894
2215
|
const skillCount = countFilesInDir(skillsDir);
|
|
1895
2216
|
console.log(chalk4.dim(` unchanged ${GENERATED_FILES.claudeSkills}/ (${skillCount} skills)`));
|
|
1896
2217
|
unchanged++;
|
|
1897
|
-
const guidesDir =
|
|
2218
|
+
const guidesDir = path22.join(projectDir, AI_KIT_FOLDER_NAME, "guides");
|
|
1898
2219
|
const guideCount = existingConfig.guides?.length || 0;
|
|
1899
2220
|
console.log(chalk4.dim(` unchanged ai-kit/guides/ (${guideCount} guides)`));
|
|
1900
2221
|
unchanged++;
|
|
@@ -2003,7 +2324,7 @@ function diffStack(oldScan, newScan) {
|
|
|
2003
2324
|
return changes;
|
|
2004
2325
|
}
|
|
2005
2326
|
function diffGeneratedFile(projectDir, filename, config, generate2, oldFragments, newFragments) {
|
|
2006
|
-
const filePath =
|
|
2327
|
+
const filePath = path22.join(projectDir, filename);
|
|
2007
2328
|
const currentContent = readFileSafe(filePath);
|
|
2008
2329
|
const newContent = generate2();
|
|
2009
2330
|
if (!currentContent) {
|
|
@@ -2040,8 +2361,8 @@ function logFileChange(result) {
|
|
|
2040
2361
|
}
|
|
2041
2362
|
function countFilesInDir(dirPath) {
|
|
2042
2363
|
try {
|
|
2043
|
-
if (!
|
|
2044
|
-
const entries =
|
|
2364
|
+
if (!fs12.existsSync(dirPath)) return 0;
|
|
2365
|
+
const entries = fs12.readdirSync(dirPath);
|
|
2045
2366
|
return entries.filter((e) => !e.startsWith(".")).length;
|
|
2046
2367
|
} catch {
|
|
2047
2368
|
return 0;
|
|
@@ -2049,8 +2370,8 @@ function countFilesInDir(dirPath) {
|
|
|
2049
2370
|
}
|
|
2050
2371
|
|
|
2051
2372
|
// src/commands/export.ts
|
|
2052
|
-
import
|
|
2053
|
-
import
|
|
2373
|
+
import path23 from "path";
|
|
2374
|
+
import fs13 from "fs-extra";
|
|
2054
2375
|
import ora6 from "ora";
|
|
2055
2376
|
import { select as select2 } from "@inquirer/prompts";
|
|
2056
2377
|
var EXPORT_TARGETS = {
|
|
@@ -2086,9 +2407,9 @@ function toCline(content) {
|
|
|
2086
2407
|
${stripped}`;
|
|
2087
2408
|
}
|
|
2088
2409
|
async function exportCommand(targetPath, options) {
|
|
2089
|
-
const projectDir =
|
|
2090
|
-
const configPath =
|
|
2091
|
-
const claudeMdPath =
|
|
2410
|
+
const projectDir = path23.resolve(targetPath || process.cwd());
|
|
2411
|
+
const configPath = path23.join(projectDir, AI_KIT_CONFIG_FILE);
|
|
2412
|
+
const claudeMdPath = path23.join(projectDir, GENERATED_FILES.claudeMd);
|
|
2092
2413
|
logSection("AI Kit \u2014 Export");
|
|
2093
2414
|
if (!fileExists(configPath) && !fileExists(claudeMdPath)) {
|
|
2094
2415
|
logError("No ai-kit.config.json or CLAUDE.md found. Run `ai-kit init` first.");
|
|
@@ -2130,9 +2451,9 @@ async function exportCommand(targetPath, options) {
|
|
|
2130
2451
|
for (const fmt2 of formats) {
|
|
2131
2452
|
const target = EXPORT_TARGETS[fmt2];
|
|
2132
2453
|
const transformer = transformers[fmt2];
|
|
2133
|
-
const outputPath =
|
|
2454
|
+
const outputPath = path23.join(projectDir, target.file);
|
|
2134
2455
|
const transformed = transformer(claudeContent);
|
|
2135
|
-
await
|
|
2456
|
+
await fs13.writeFile(outputPath, transformed, "utf-8");
|
|
2136
2457
|
exported++;
|
|
2137
2458
|
}
|
|
2138
2459
|
spinner.succeed("Export complete");
|
|
@@ -2148,7 +2469,7 @@ async function exportCommand(targetPath, options) {
|
|
|
2148
2469
|
}
|
|
2149
2470
|
|
|
2150
2471
|
// src/commands/stats.ts
|
|
2151
|
-
import
|
|
2472
|
+
import path24 from "path";
|
|
2152
2473
|
import chalk5 from "chalk";
|
|
2153
2474
|
var SKILL_CATEGORIES = {
|
|
2154
2475
|
"Getting Started": ["prompt-help", "understand"],
|
|
@@ -2196,7 +2517,7 @@ function calculateComplexity(config) {
|
|
|
2196
2517
|
items.push({ label: frameworkLabel.trim(), active: hasFramework });
|
|
2197
2518
|
const hasCms = scan.cms !== "none";
|
|
2198
2519
|
if (hasCms) score += 2;
|
|
2199
|
-
const cmsLabel = scan.cms === "sitecore-xmc" ? "Sitecore XM Cloud" : scan.cms === "sitecore-jss" ? "Sitecore JSS" : "CMS";
|
|
2520
|
+
const cmsLabel = scan.cms === "sitecore-xmc-v2" ? "Sitecore XM Cloud (Content SDK v2)" : scan.cms === "sitecore-xmc" ? "Sitecore XM Cloud" : scan.cms === "sitecore-jss" ? "Sitecore JSS" : "CMS";
|
|
2200
2521
|
items.push({ label: cmsLabel, active: hasCms });
|
|
2201
2522
|
if (scan.typescript) score += 1;
|
|
2202
2523
|
items.push({
|
|
@@ -2251,8 +2572,8 @@ var MCP_DISPLAY_NAMES = {
|
|
|
2251
2572
|
perplexity: "Perplexity"
|
|
2252
2573
|
};
|
|
2253
2574
|
async function statsCommand(targetPath) {
|
|
2254
|
-
const projectDir =
|
|
2255
|
-
const configPath =
|
|
2575
|
+
const projectDir = path24.resolve(targetPath || process.cwd());
|
|
2576
|
+
const configPath = path24.join(projectDir, AI_KIT_CONFIG_FILE);
|
|
2256
2577
|
logSection("AI Kit \u2014 Project Stats");
|
|
2257
2578
|
console.log("");
|
|
2258
2579
|
if (!fileExists(configPath)) {
|
|
@@ -2345,6 +2666,435 @@ async function statsCommand(targetPath) {
|
|
|
2345
2666
|
console.log("");
|
|
2346
2667
|
}
|
|
2347
2668
|
|
|
2669
|
+
// src/commands/audit.ts
|
|
2670
|
+
import path25 from "path";
|
|
2671
|
+
import fs14 from "fs-extra";
|
|
2672
|
+
import chalk6 from "chalk";
|
|
2673
|
+
async function auditCommand(targetPath) {
|
|
2674
|
+
const projectDir = path25.resolve(targetPath || process.cwd());
|
|
2675
|
+
logSection("AI Kit \u2014 Security & Configuration Audit");
|
|
2676
|
+
logInfo(`Auditing: ${projectDir}`);
|
|
2677
|
+
const checks = [];
|
|
2678
|
+
const configPath = path25.join(projectDir, AI_KIT_CONFIG_FILE);
|
|
2679
|
+
if (fileExists(configPath)) {
|
|
2680
|
+
checks.push({ name: "Config file", status: "pass", message: "ai-kit.config.json found" });
|
|
2681
|
+
} else {
|
|
2682
|
+
checks.push({ name: "Config file", status: "fail", message: "ai-kit.config.json missing \u2014 run `ai-kit init`" });
|
|
2683
|
+
}
|
|
2684
|
+
const claudeMdPath = path25.join(projectDir, GENERATED_FILES.claudeMd);
|
|
2685
|
+
const claudeMd = readFileSafe(claudeMdPath);
|
|
2686
|
+
if (claudeMd) {
|
|
2687
|
+
if (claudeMd.includes("AI-KIT:START") && claudeMd.includes("AI-KIT:END")) {
|
|
2688
|
+
checks.push({ name: "CLAUDE.md", status: "pass", message: "CLAUDE.md has AI-KIT markers" });
|
|
2689
|
+
} else {
|
|
2690
|
+
checks.push({ name: "CLAUDE.md", status: "warn", message: "CLAUDE.md exists but missing AI-KIT markers \u2014 may not update correctly" });
|
|
2691
|
+
}
|
|
2692
|
+
} else {
|
|
2693
|
+
checks.push({ name: "CLAUDE.md", status: "warn", message: "CLAUDE.md not found" });
|
|
2694
|
+
}
|
|
2695
|
+
if (claudeMd) {
|
|
2696
|
+
const secretPatterns = [
|
|
2697
|
+
/(?:api[_-]?key|secret|token|password|credential)\s*[:=]\s*['"][^'"]+['"]/i,
|
|
2698
|
+
/(?:sk|pk|rk)[-_][a-zA-Z0-9]{20,}/,
|
|
2699
|
+
/ghp_[a-zA-Z0-9]{36}/,
|
|
2700
|
+
/xox[bpoas]-[a-zA-Z0-9-]+/
|
|
2701
|
+
];
|
|
2702
|
+
const hasSecrets = secretPatterns.some((p) => p.test(claudeMd));
|
|
2703
|
+
if (hasSecrets) {
|
|
2704
|
+
checks.push({ name: "Secrets in CLAUDE.md", status: "fail", message: "Potential secrets detected in CLAUDE.md \u2014 remove immediately" });
|
|
2705
|
+
} else {
|
|
2706
|
+
checks.push({ name: "Secrets in CLAUDE.md", status: "pass", message: "No secrets detected" });
|
|
2707
|
+
}
|
|
2708
|
+
}
|
|
2709
|
+
const settingsLocalPath = path25.join(projectDir, GENERATED_FILES.claudeSettingsLocal);
|
|
2710
|
+
if (fileExists(settingsLocalPath)) {
|
|
2711
|
+
const settings2 = readJsonSafe(settingsLocalPath);
|
|
2712
|
+
if (settings2?.hooks) {
|
|
2713
|
+
checks.push({ name: "Hooks", status: "pass", message: "Hooks configured in settings.local.json" });
|
|
2714
|
+
} else {
|
|
2715
|
+
checks.push({ name: "Hooks", status: "warn", message: "settings.local.json exists but no hooks defined" });
|
|
2716
|
+
}
|
|
2717
|
+
} else {
|
|
2718
|
+
checks.push({ name: "Hooks", status: "warn", message: "No hooks configured \u2014 run `ai-kit init` to generate" });
|
|
2719
|
+
}
|
|
2720
|
+
const agentsDir = path25.join(projectDir, GENERATED_FILES.claudeAgents);
|
|
2721
|
+
if (await fs14.pathExists(agentsDir)) {
|
|
2722
|
+
const agentFiles = (await fs14.readdir(agentsDir)).filter((f) => f.endsWith(".md"));
|
|
2723
|
+
if (agentFiles.length > 0) {
|
|
2724
|
+
checks.push({ name: "Agents", status: "pass", message: `${agentFiles.length} agent(s) configured` });
|
|
2725
|
+
let invalidAgents = 0;
|
|
2726
|
+
for (const file of agentFiles) {
|
|
2727
|
+
const content = readFileSafe(path25.join(agentsDir, file));
|
|
2728
|
+
if (content && !content.startsWith("---")) {
|
|
2729
|
+
invalidAgents++;
|
|
2730
|
+
}
|
|
2731
|
+
}
|
|
2732
|
+
if (invalidAgents > 0) {
|
|
2733
|
+
checks.push({ name: "Agent frontmatter", status: "warn", message: `${invalidAgents} agent(s) missing YAML frontmatter` });
|
|
2734
|
+
}
|
|
2735
|
+
} else {
|
|
2736
|
+
checks.push({ name: "Agents", status: "warn", message: "Agents directory empty" });
|
|
2737
|
+
}
|
|
2738
|
+
} else {
|
|
2739
|
+
checks.push({ name: "Agents", status: "warn", message: "No agents directory \u2014 run `ai-kit init` to generate" });
|
|
2740
|
+
}
|
|
2741
|
+
const contextsDir = path25.join(projectDir, GENERATED_FILES.claudeContexts);
|
|
2742
|
+
if (await fs14.pathExists(contextsDir)) {
|
|
2743
|
+
const contextFiles = (await fs14.readdir(contextsDir)).filter((f) => f.endsWith(".md"));
|
|
2744
|
+
checks.push({ name: "Contexts", status: contextFiles.length > 0 ? "pass" : "warn", message: `${contextFiles.length} context mode(s) available` });
|
|
2745
|
+
} else {
|
|
2746
|
+
checks.push({ name: "Contexts", status: "warn", message: "No contexts directory" });
|
|
2747
|
+
}
|
|
2748
|
+
const skillsDir = path25.join(projectDir, GENERATED_FILES.claudeSkills);
|
|
2749
|
+
if (await fs14.pathExists(skillsDir)) {
|
|
2750
|
+
const skillDirs = (await fs14.readdir(skillsDir)).filter(async (f) => {
|
|
2751
|
+
try {
|
|
2752
|
+
return (await fs14.stat(path25.join(skillsDir, f))).isDirectory();
|
|
2753
|
+
} catch {
|
|
2754
|
+
return false;
|
|
2755
|
+
}
|
|
2756
|
+
});
|
|
2757
|
+
checks.push({ name: "Skills", status: "pass", message: `${skillDirs.length} skill(s) installed` });
|
|
2758
|
+
} else {
|
|
2759
|
+
checks.push({ name: "Skills", status: "warn", message: "No skills directory" });
|
|
2760
|
+
}
|
|
2761
|
+
const gitignorePath = path25.join(projectDir, ".gitignore");
|
|
2762
|
+
const gitignore = readFileSafe(gitignorePath);
|
|
2763
|
+
if (gitignore) {
|
|
2764
|
+
const envIgnored = gitignore.includes(".env") || gitignore.includes(".env.local");
|
|
2765
|
+
if (envIgnored) {
|
|
2766
|
+
checks.push({ name: ".env gitignore", status: "pass", message: ".env files are gitignored" });
|
|
2767
|
+
} else {
|
|
2768
|
+
checks.push({ name: ".env gitignore", status: "fail", message: ".env files are NOT gitignored \u2014 add to .gitignore immediately" });
|
|
2769
|
+
}
|
|
2770
|
+
}
|
|
2771
|
+
if (gitignore) {
|
|
2772
|
+
const settingsIgnored = gitignore.includes("settings.local.json");
|
|
2773
|
+
if (!settingsIgnored) {
|
|
2774
|
+
checks.push({ name: "Settings gitignore", status: "warn", message: "settings.local.json not in .gitignore \u2014 may leak local config" });
|
|
2775
|
+
}
|
|
2776
|
+
}
|
|
2777
|
+
const settingsPath = path25.join(projectDir, ".claude", "settings.json");
|
|
2778
|
+
const settings = readFileSafe(settingsPath);
|
|
2779
|
+
if (settings) {
|
|
2780
|
+
const hasEnvVarsInSettings = /(?:api[_-]?key|token|secret|password)\s*[:=]\s*"[^"]+"/i.test(settings);
|
|
2781
|
+
if (hasEnvVarsInSettings) {
|
|
2782
|
+
checks.push({ name: "MCP secrets", status: "fail", message: "Potential secrets in .claude/settings.json \u2014 use environment variables instead" });
|
|
2783
|
+
} else {
|
|
2784
|
+
checks.push({ name: "MCP secrets", status: "pass", message: "No hardcoded secrets in settings" });
|
|
2785
|
+
}
|
|
2786
|
+
}
|
|
2787
|
+
logSection("Audit Results");
|
|
2788
|
+
const passed = checks.filter((c) => c.status === "pass");
|
|
2789
|
+
const warned = checks.filter((c) => c.status === "warn");
|
|
2790
|
+
const failed = checks.filter((c) => c.status === "fail");
|
|
2791
|
+
for (const check of checks) {
|
|
2792
|
+
const icon = check.status === "pass" ? chalk6.green("\u2713") : check.status === "warn" ? chalk6.yellow("\u26A0") : chalk6.red("\u2717");
|
|
2793
|
+
console.log(` ${icon} ${check.name}: ${check.message}`);
|
|
2794
|
+
}
|
|
2795
|
+
const total = checks.length;
|
|
2796
|
+
const score = Math.round((passed.length + warned.length * 0.5) / total * 100);
|
|
2797
|
+
const grade = score >= 90 ? "A" : score >= 80 ? "B" : score >= 70 ? "C" : score >= 60 ? "D" : "F";
|
|
2798
|
+
console.log("");
|
|
2799
|
+
logSection("Score");
|
|
2800
|
+
const gradeColor2 = grade <= "B" ? chalk6.green : grade <= "C" ? chalk6.yellow : chalk6.red;
|
|
2801
|
+
console.log(` ${gradeColor2.bold(grade)} (${score}/100)`);
|
|
2802
|
+
console.log(` ${chalk6.green(String(passed.length))} passed \xB7 ${chalk6.yellow(String(warned.length))} warnings \xB7 ${chalk6.red(String(failed.length))} failures`);
|
|
2803
|
+
if (failed.length > 0) {
|
|
2804
|
+
console.log("");
|
|
2805
|
+
logError("Fix the failures above before proceeding.");
|
|
2806
|
+
}
|
|
2807
|
+
}
|
|
2808
|
+
|
|
2809
|
+
// src/commands/health.ts
|
|
2810
|
+
import path26 from "path";
|
|
2811
|
+
import fs15 from "fs-extra";
|
|
2812
|
+
import chalk7 from "chalk";
|
|
2813
|
+
import ora7 from "ora";
|
|
2814
|
+
function gradeFromScore(score) {
|
|
2815
|
+
if (score >= 90) return "A";
|
|
2816
|
+
if (score >= 80) return "B";
|
|
2817
|
+
if (score >= 70) return "C";
|
|
2818
|
+
if (score >= 60) return "D";
|
|
2819
|
+
return "F";
|
|
2820
|
+
}
|
|
2821
|
+
function gradeColor(grade) {
|
|
2822
|
+
if (grade <= "B") return chalk7.green;
|
|
2823
|
+
if (grade <= "C") return chalk7.yellow;
|
|
2824
|
+
return chalk7.red;
|
|
2825
|
+
}
|
|
2826
|
+
function statusIcon(status) {
|
|
2827
|
+
switch (status) {
|
|
2828
|
+
case "pass":
|
|
2829
|
+
return chalk7.green("\u2713");
|
|
2830
|
+
case "warn":
|
|
2831
|
+
return chalk7.yellow("\u26A0");
|
|
2832
|
+
case "fail":
|
|
2833
|
+
return chalk7.red("\u2717");
|
|
2834
|
+
}
|
|
2835
|
+
}
|
|
2836
|
+
function progressBar2(percent, width = 20) {
|
|
2837
|
+
const filled = Math.round(percent / 100 * width);
|
|
2838
|
+
const empty = width - filled;
|
|
2839
|
+
return chalk7.green("\u2588".repeat(filled)) + chalk7.gray("\u2591".repeat(empty));
|
|
2840
|
+
}
|
|
2841
|
+
function checkSetup(projectDir, config) {
|
|
2842
|
+
const checks = [];
|
|
2843
|
+
if (config.version === VERSION) {
|
|
2844
|
+
checks.push({ name: "Version", status: "pass", detail: `v${VERSION}` });
|
|
2845
|
+
} else {
|
|
2846
|
+
checks.push({
|
|
2847
|
+
name: "Version",
|
|
2848
|
+
status: "warn",
|
|
2849
|
+
detail: `config v${config.version} \u2260 CLI v${VERSION} \u2014 run \`ai-kit update\``
|
|
2850
|
+
});
|
|
2851
|
+
}
|
|
2852
|
+
const claudeMd = readFileSafe(path26.join(projectDir, GENERATED_FILES.claudeMd));
|
|
2853
|
+
if (claudeMd && claudeMd.includes("AI-KIT:START")) {
|
|
2854
|
+
checks.push({ name: "CLAUDE.md", status: "pass", detail: "Present with markers" });
|
|
2855
|
+
} else if (claudeMd) {
|
|
2856
|
+
checks.push({ name: "CLAUDE.md", status: "warn", detail: "Missing AI-KIT markers" });
|
|
2857
|
+
} else {
|
|
2858
|
+
checks.push({ name: "CLAUDE.md", status: "fail", detail: "Not found" });
|
|
2859
|
+
}
|
|
2860
|
+
if (fileExists(path26.join(projectDir, GENERATED_FILES.cursorRules))) {
|
|
2861
|
+
checks.push({ name: ".cursorrules", status: "pass", detail: "Present" });
|
|
2862
|
+
} else {
|
|
2863
|
+
checks.push({ name: ".cursorrules", status: "warn", detail: "Not generated" });
|
|
2864
|
+
}
|
|
2865
|
+
const skillsDir = path26.join(projectDir, GENERATED_FILES.claudeSkills);
|
|
2866
|
+
if (dirExists(skillsDir)) {
|
|
2867
|
+
const count = config.commands.length;
|
|
2868
|
+
checks.push({ name: "Skills", status: "pass", detail: `${count} installed` });
|
|
2869
|
+
} else {
|
|
2870
|
+
checks.push({ name: "Skills", status: "warn", detail: "No skills directory" });
|
|
2871
|
+
}
|
|
2872
|
+
const agentsDir = path26.join(projectDir, GENERATED_FILES.claudeAgents);
|
|
2873
|
+
if (dirExists(agentsDir)) {
|
|
2874
|
+
try {
|
|
2875
|
+
const agentFiles = fs15.readdirSync(agentsDir).filter((f) => f.endsWith(".md"));
|
|
2876
|
+
checks.push({ name: "Agents", status: "pass", detail: `${agentFiles.length} configured` });
|
|
2877
|
+
} catch {
|
|
2878
|
+
checks.push({ name: "Agents", status: "warn", detail: "Could not read agents" });
|
|
2879
|
+
}
|
|
2880
|
+
} else {
|
|
2881
|
+
checks.push({ name: "Agents", status: "warn", detail: "Not configured" });
|
|
2882
|
+
}
|
|
2883
|
+
const settingsLocal = readFileSafe(path26.join(projectDir, GENERATED_FILES.claudeSettingsLocal));
|
|
2884
|
+
if (settingsLocal && settingsLocal.includes('"hooks"')) {
|
|
2885
|
+
checks.push({ name: "Hooks", status: "pass", detail: "Configured" });
|
|
2886
|
+
} else {
|
|
2887
|
+
checks.push({ name: "Hooks", status: "warn", detail: "Not configured" });
|
|
2888
|
+
}
|
|
2889
|
+
return { title: "Setup Integrity", checks };
|
|
2890
|
+
}
|
|
2891
|
+
function checkSecurity(projectDir) {
|
|
2892
|
+
const checks = [];
|
|
2893
|
+
const claudeMd = readFileSafe(path26.join(projectDir, GENERATED_FILES.claudeMd));
|
|
2894
|
+
if (claudeMd) {
|
|
2895
|
+
const secretPatterns = [
|
|
2896
|
+
/(?:api[_-]?key|secret|token|password|credential)\s*[:=]\s*['"][^'"]+['"]/i,
|
|
2897
|
+
/(?:sk|pk|rk)[-_][a-zA-Z0-9]{20,}/,
|
|
2898
|
+
/ghp_[a-zA-Z0-9]{36}/,
|
|
2899
|
+
/xox[bpoas]-[a-zA-Z0-9-]+/
|
|
2900
|
+
];
|
|
2901
|
+
const hasSecrets = secretPatterns.some((p) => p.test(claudeMd));
|
|
2902
|
+
checks.push({
|
|
2903
|
+
name: "Secrets in CLAUDE.md",
|
|
2904
|
+
status: hasSecrets ? "fail" : "pass",
|
|
2905
|
+
detail: hasSecrets ? "Potential secrets detected \u2014 remove immediately" : "Clean"
|
|
2906
|
+
});
|
|
2907
|
+
}
|
|
2908
|
+
const gitignore = readFileSafe(path26.join(projectDir, ".gitignore"));
|
|
2909
|
+
if (gitignore) {
|
|
2910
|
+
const envIgnored = gitignore.includes(".env") || gitignore.includes(".env.local");
|
|
2911
|
+
checks.push({
|
|
2912
|
+
name: ".env gitignore",
|
|
2913
|
+
status: envIgnored ? "pass" : "fail",
|
|
2914
|
+
detail: envIgnored ? "Protected" : "NOT gitignored \u2014 add .env to .gitignore"
|
|
2915
|
+
});
|
|
2916
|
+
}
|
|
2917
|
+
const settingsJson = readFileSafe(path26.join(projectDir, ".claude", "settings.json"));
|
|
2918
|
+
if (settingsJson) {
|
|
2919
|
+
const hasHardcoded = /(?:api[_-]?key|token|secret|password)\s*[:=]\s*"[^"]+"/i.test(settingsJson);
|
|
2920
|
+
checks.push({
|
|
2921
|
+
name: "MCP config",
|
|
2922
|
+
status: hasHardcoded ? "fail" : "pass",
|
|
2923
|
+
detail: hasHardcoded ? "Hardcoded secrets in settings.json" : "No hardcoded secrets"
|
|
2924
|
+
});
|
|
2925
|
+
}
|
|
2926
|
+
return { title: "Security", checks };
|
|
2927
|
+
}
|
|
2928
|
+
function checkStack(config) {
|
|
2929
|
+
const checks = [];
|
|
2930
|
+
const scan = config.scanResult;
|
|
2931
|
+
if (scan.framework !== "unknown") {
|
|
2932
|
+
const label = scan.framework === "nextjs" ? `Next.js ${scan.nextjsVersion || ""} (${scan.routerType === "app" ? "App Router" : scan.routerType === "pages" ? "Pages Router" : "Hybrid"})` : scan.framework;
|
|
2933
|
+
checks.push({ name: "Framework", status: "pass", detail: label.trim() });
|
|
2934
|
+
} else {
|
|
2935
|
+
checks.push({ name: "Framework", status: "warn", detail: "Not detected" });
|
|
2936
|
+
}
|
|
2937
|
+
if (scan.cms !== "none") {
|
|
2938
|
+
const label = scan.cms === "sitecore-xmc-v2" ? "Sitecore XM Cloud (Content SDK v2)" : scan.cms === "sitecore-xmc" ? "Sitecore XM Cloud" : scan.cms;
|
|
2939
|
+
checks.push({ name: "CMS", status: "pass", detail: label });
|
|
2940
|
+
}
|
|
2941
|
+
checks.push({
|
|
2942
|
+
name: "TypeScript",
|
|
2943
|
+
status: scan.typescript ? "pass" : "warn",
|
|
2944
|
+
detail: scan.typescript ? `Enabled${scan.typescriptStrict ? " (strict)" : ""}` : "Not detected"
|
|
2945
|
+
});
|
|
2946
|
+
if (scan.styling.length > 0) {
|
|
2947
|
+
checks.push({ name: "Styling", status: "pass", detail: scan.styling.join(", ") });
|
|
2948
|
+
}
|
|
2949
|
+
if (scan.monorepo) {
|
|
2950
|
+
checks.push({
|
|
2951
|
+
name: "Monorepo",
|
|
2952
|
+
status: "pass",
|
|
2953
|
+
detail: scan.monorepoTool || "Detected"
|
|
2954
|
+
});
|
|
2955
|
+
}
|
|
2956
|
+
return { title: "Stack Detection", checks };
|
|
2957
|
+
}
|
|
2958
|
+
function checkTools(scan) {
|
|
2959
|
+
const checks = [];
|
|
2960
|
+
const toolMap = {
|
|
2961
|
+
Playwright: { key: "playwright", hint: "npm i -D @playwright/test" },
|
|
2962
|
+
ESLint: { key: "eslint", hint: "npm i -D eslint" },
|
|
2963
|
+
Prettier: { key: "prettier", hint: "npm i -D prettier" },
|
|
2964
|
+
"axe-core": { key: "axeCore", hint: "npm i -D @axe-core/playwright" },
|
|
2965
|
+
Knip: { key: "knip", hint: "npm i -D knip" },
|
|
2966
|
+
"Bundle Analyzer": { key: "bundleAnalyzer", hint: "npm i -D @next/bundle-analyzer" },
|
|
2967
|
+
Storybook: { key: "storybook", hint: "npx storybook@latest init" }
|
|
2968
|
+
};
|
|
2969
|
+
for (const [name, { key, hint }] of Object.entries(toolMap)) {
|
|
2970
|
+
checks.push({
|
|
2971
|
+
name,
|
|
2972
|
+
status: scan.tools[key] ? "pass" : "warn",
|
|
2973
|
+
detail: scan.tools[key] ? "Detected" : `Missing \u2014 ${hint}`
|
|
2974
|
+
});
|
|
2975
|
+
}
|
|
2976
|
+
const mcpMap = {
|
|
2977
|
+
"GitHub MCP": "github",
|
|
2978
|
+
"Figma MCP": "figma",
|
|
2979
|
+
"Context7 MCP": "context7",
|
|
2980
|
+
"Perplexity MCP": "perplexity"
|
|
2981
|
+
};
|
|
2982
|
+
for (const [name, key] of Object.entries(mcpMap)) {
|
|
2983
|
+
checks.push({
|
|
2984
|
+
name,
|
|
2985
|
+
status: scan.mcpServers[key] ? "pass" : "warn",
|
|
2986
|
+
detail: scan.mcpServers[key] ? "Configured" : "Not configured"
|
|
2987
|
+
});
|
|
2988
|
+
}
|
|
2989
|
+
return { title: "Tools & MCP", checks };
|
|
2990
|
+
}
|
|
2991
|
+
function checkDocs(projectDir) {
|
|
2992
|
+
const checks = [];
|
|
2993
|
+
const docsToCheck = [
|
|
2994
|
+
{ name: "Mistakes Log", path: "docs/mistakes-log.md" },
|
|
2995
|
+
{ name: "Decisions Log", path: "docs/decisions-log.md" },
|
|
2996
|
+
{ name: "Time Log", path: "docs/time-log.md" }
|
|
2997
|
+
];
|
|
2998
|
+
for (const doc of docsToCheck) {
|
|
2999
|
+
const content = readFileSafe(path26.join(projectDir, doc.path));
|
|
3000
|
+
if (content) {
|
|
3001
|
+
const hasEntries = content.includes("## 20") || content.split("---").length > 2;
|
|
3002
|
+
checks.push({
|
|
3003
|
+
name: doc.name,
|
|
3004
|
+
status: hasEntries ? "pass" : "warn",
|
|
3005
|
+
detail: hasEntries ? "Has entries" : "Scaffolded but empty"
|
|
3006
|
+
});
|
|
3007
|
+
} else {
|
|
3008
|
+
checks.push({ name: doc.name, status: "warn", detail: "Not found" });
|
|
3009
|
+
}
|
|
3010
|
+
}
|
|
3011
|
+
return { title: "Documentation", checks };
|
|
3012
|
+
}
|
|
3013
|
+
async function healthCommand(targetPath) {
|
|
3014
|
+
const projectDir = path26.resolve(targetPath || process.cwd());
|
|
3015
|
+
const configPath = path26.join(projectDir, AI_KIT_CONFIG_FILE);
|
|
3016
|
+
console.log("");
|
|
3017
|
+
logSection("AI Kit \u2014 Project Health");
|
|
3018
|
+
console.log(chalk7.dim(` ${projectDir}`));
|
|
3019
|
+
console.log("");
|
|
3020
|
+
if (!fileExists(configPath)) {
|
|
3021
|
+
logError("ai-kit.config.json not found. Run `ai-kit init` first.");
|
|
3022
|
+
return;
|
|
3023
|
+
}
|
|
3024
|
+
const config = readJsonSafe(configPath);
|
|
3025
|
+
if (!config) {
|
|
3026
|
+
logError("ai-kit.config.json is corrupted. Run `ai-kit init` to re-initialize.");
|
|
3027
|
+
return;
|
|
3028
|
+
}
|
|
3029
|
+
const spinner = ora7("Scanning project...").start();
|
|
3030
|
+
let freshScan;
|
|
3031
|
+
try {
|
|
3032
|
+
freshScan = await scanProject(projectDir);
|
|
3033
|
+
spinner.succeed("Project scanned");
|
|
3034
|
+
} catch {
|
|
3035
|
+
spinner.fail("Scan failed \u2014 using cached data");
|
|
3036
|
+
freshScan = config.scanResult;
|
|
3037
|
+
}
|
|
3038
|
+
const sections = [
|
|
3039
|
+
checkSetup(projectDir, config),
|
|
3040
|
+
checkSecurity(projectDir),
|
|
3041
|
+
checkStack(config),
|
|
3042
|
+
checkTools(freshScan),
|
|
3043
|
+
checkDocs(projectDir)
|
|
3044
|
+
];
|
|
3045
|
+
let totalPassed = 0;
|
|
3046
|
+
let totalWarnings = 0;
|
|
3047
|
+
let totalFailed = 0;
|
|
3048
|
+
for (const section of sections) {
|
|
3049
|
+
console.log(`
|
|
3050
|
+
${chalk7.bold(section.title)}`);
|
|
3051
|
+
for (const check of section.checks) {
|
|
3052
|
+
const icon = statusIcon(check.status);
|
|
3053
|
+
console.log(` ${icon} ${chalk7.white(check.name)}: ${chalk7.dim(check.detail)}`);
|
|
3054
|
+
switch (check.status) {
|
|
3055
|
+
case "pass":
|
|
3056
|
+
totalPassed++;
|
|
3057
|
+
break;
|
|
3058
|
+
case "warn":
|
|
3059
|
+
totalWarnings++;
|
|
3060
|
+
break;
|
|
3061
|
+
case "fail":
|
|
3062
|
+
totalFailed++;
|
|
3063
|
+
break;
|
|
3064
|
+
}
|
|
3065
|
+
}
|
|
3066
|
+
}
|
|
3067
|
+
const total = totalPassed + totalWarnings + totalFailed;
|
|
3068
|
+
const score = total > 0 ? Math.round((totalPassed + totalWarnings * 0.5) / total * 100) : 0;
|
|
3069
|
+
const grade = gradeFromScore(score);
|
|
3070
|
+
const colorFn = gradeColor(grade);
|
|
3071
|
+
console.log("");
|
|
3072
|
+
console.log(
|
|
3073
|
+
` ${chalk7.bold("Overall:")} ${colorFn.bold(grade)} ${chalk7.dim(`(${score}/100)`)}`
|
|
3074
|
+
);
|
|
3075
|
+
console.log(
|
|
3076
|
+
` ${progressBar2(score)} ${chalk7.green(String(totalPassed))} passed \xB7 ${chalk7.yellow(String(totalWarnings))} warnings \xB7 ${chalk7.red(String(totalFailed))} failures`
|
|
3077
|
+
);
|
|
3078
|
+
if (totalFailed > 0 || totalWarnings > 2) {
|
|
3079
|
+
console.log("");
|
|
3080
|
+
console.log(chalk7.bold(" Recommendations:"));
|
|
3081
|
+
if (totalFailed > 0) {
|
|
3082
|
+
const failures = sections.flatMap((s) => s.checks.filter((c) => c.status === "fail"));
|
|
3083
|
+
for (const f of failures.slice(0, 3)) {
|
|
3084
|
+
console.log(` ${chalk7.red("\u2192")} Fix: ${f.name} \u2014 ${f.detail}`);
|
|
3085
|
+
}
|
|
3086
|
+
}
|
|
3087
|
+
const warnings = sections.flatMap((s) => s.checks.filter((c) => c.status === "warn"));
|
|
3088
|
+
for (const w of warnings.slice(0, 3)) {
|
|
3089
|
+
console.log(` ${chalk7.yellow("\u2192")} Improve: ${w.name} \u2014 ${w.detail}`);
|
|
3090
|
+
}
|
|
3091
|
+
}
|
|
3092
|
+
const scan = config.scanResult;
|
|
3093
|
+
console.log("");
|
|
3094
|
+
console.log(chalk7.dim(` v${config.version} \xB7 ${scan.projectName} \xB7 ${config.commands.length} skills \xB7 ${config.guides.length} guides \xB7 Generated ${config.generatedAt}`));
|
|
3095
|
+
console.log("");
|
|
3096
|
+
}
|
|
3097
|
+
|
|
2348
3098
|
// src/index.ts
|
|
2349
3099
|
var program = new Command();
|
|
2350
3100
|
program.name("ai-kit").description(
|
|
@@ -2438,5 +3188,27 @@ program.command("stats").description("Show project setup statistics and complexi
|
|
|
2438
3188
|
process.exit(1);
|
|
2439
3189
|
}
|
|
2440
3190
|
});
|
|
3191
|
+
program.command("audit").description("Security and configuration audit for AI agent setup").argument("[path]", "Project directory (defaults to current directory)").action(async (targetPath) => {
|
|
3192
|
+
try {
|
|
3193
|
+
await auditCommand(targetPath);
|
|
3194
|
+
} catch (err) {
|
|
3195
|
+
if (err.name === "ExitPromptError") {
|
|
3196
|
+
process.exit(0);
|
|
3197
|
+
}
|
|
3198
|
+
console.error(err);
|
|
3199
|
+
process.exit(1);
|
|
3200
|
+
}
|
|
3201
|
+
});
|
|
3202
|
+
program.command("health").description("One-glance project health \u2014 setup, security, stack, tools, and docs").argument("[path]", "Project directory (defaults to current directory)").action(async (targetPath) => {
|
|
3203
|
+
try {
|
|
3204
|
+
await healthCommand(targetPath);
|
|
3205
|
+
} catch (err) {
|
|
3206
|
+
if (err.name === "ExitPromptError") {
|
|
3207
|
+
process.exit(0);
|
|
3208
|
+
}
|
|
3209
|
+
console.error(err);
|
|
3210
|
+
process.exit(1);
|
|
3211
|
+
}
|
|
3212
|
+
});
|
|
2441
3213
|
program.parse();
|
|
2442
3214
|
//# sourceMappingURL=index.js.map
|