How It Works
A technical deep-dive into AI Kit’s architecture and design decisions.
Architecture Overview
┌─────────────────────────────────────────────────┐
│ CLI (Commander.js) │
│ init | update | reset │
└───────────┬──────────┬──────────┬───────────────┘
│ │ │
┌───────▼───┐ ┌────▼────┐ ┌──▼──────────┐
│ Scanner │ │Generator│ │ Copier │
│ Module │ │ Module │ │ Module │
└───────┬───┘ └────┬────┘ └──┬──────────┘
│ │ │
┌───────▼───┐ ┌────▼────┐ ┌──▼──────────┐
│ 7 Detect │ │Template │ │ Skills │
│ Functions │ │Assembler│ │ Commands │
│ │ │ │ │ Guides │
│ │ │ │ │ Docs │
└───────────┘ └─────────┘ └─────────────┘The Scanner
The scanner is the core intelligence. It reads project indicators and produces a ProjectScan object.
Scan Flow
1. Read package.json
2. Extract dependencies, devDependencies, scripts
3. Run 7 parallel detectors:
├── detectNextjs(projectPath, pkg)
├── detectSitecore(pkg)
├── detectStyling(projectPath, pkg)
├── detectTypescript(projectPath)
├── detectMonorepo(projectPath, pkg)
├── detectPackageManager(projectPath)
└── detectFigma(projectPath, pkg)
4. Merge results into ProjectScanDetection Strategy
Each detector follows a priority chain:
- Package dependencies — Most reliable signal. If
nextis independencies, it’s a Next.js project. - Config files — Second signal.
turbo.jsonmeans Turborepo,tsconfig.jsonmeans TypeScript. - Directory structure — Third signal.
app/directory means App Router. - File content — Last resort. Reading
globals.cssfor@themeto detect Tailwind v4.
Detectors never make assumptions. If something can’t be determined, it’s left as undefined and the CLI asks the user.
ProjectScan Type
interface ProjectScan {
framework: 'nextjs' | 'react' | 'unknown';
nextjsVersion?: string;
routerType?: 'app' | 'pages' | 'hybrid';
cms: 'sitecore-xmc' | 'sitecore-jss' | 'none';
sitecorejssVersion?: string;
styling: ('tailwind' | 'css-modules' | 'styled-components' | 'scss')[];
tailwindVersion?: string;
typescript: boolean;
typescriptStrict?: boolean;
monorepo: boolean;
monorepoTool?: 'turborepo' | 'nx' | 'lerna' | 'pnpm-workspaces';
figma: {
detected: boolean;
figmaMcp: boolean;
figmaCodeCli: boolean;
designTokens: boolean;
tokenFormat: 'tailwind-v4' | 'tailwind-v3' | 'css-variables' | 'none';
visualTests: boolean;
};
packageManager: 'npm' | 'pnpm' | 'yarn' | 'bun';
projectName: string;
projectPath: string;
scripts: Record<string, string>;
}The Generator
The generator turns a ProjectScan into output files using a template fragment system.
Fragment Selection
function selectFragments(scan: ProjectScan): string[] {
const fragments = ['base']; // Always included
// Conditional fragments based on scan results
if (scan.framework === 'nextjs' && scan.routerType === 'app')
fragments.push('nextjs-app-router');
if (scan.cms !== 'none')
fragments.push('sitecore-xmc');
if (scan.styling.includes('tailwind'))
fragments.push('tailwind');
// ... etc
return fragments;
}Template Assembly
1. Read header.md template
2. For each selected fragment, read templates/{subfolder}/{fragment}.md
3. Join fragments with --- separators
4. Replace {{placeholders}} with scan-derived values
5. Wrap in <!-- AI-KIT:START/END --> markers
6. Add <!-- Generated by ai-kit vX.X.X --> commentVariable Substitution
The assembler replaces {{variable}} placeholders:
function replacePlaceholders(content, variables) {
let result = content;
for (const [key, value] of Object.entries(variables)) {
result = result.replaceAll(`{{${key}}}`, value);
}
return result;
}Variables are built from the scan:
{{projectName}}— frompackage.jsonname{{techStack}}— concatenated detected technologies{{packageManager}}— detected package manager{{scripts}}— filtered standard scripts
The Copier
Simple file copy operations with smart behavior:
- Skills: Copy from
skills/→.claude/skills/[name]/SKILL.mdand.cursor/skills/[name]/SKILL.md. Always overwrites AI Kit-created skill directories. - Commands: Copy from
commands/→.claude/commands/. Always overwrites. Legacy format kept for backward compatibility. - Guides: Copy from
guides/→ai-kit/guides/. Always overwrites. - Doc scaffolds: Copy from
docs-scaffolds/→docs/. Never overwrites existing files.
The “never overwrite” behavior for docs is intentional — users add content to these files over time. Custom skill directories (those not created by AI Kit) are also never overwritten.
Safe Update Mechanism
The update flow preserves user edits:
1. Read existing file
2. Find <!-- AI-KIT:START --> and <!-- AI-KIT:END --> markers
3. If markers found:
- Keep everything BEFORE the start marker
- Replace content between markers with new generated content
- Keep everything AFTER the end marker
4. If no markers found:
- Replace the entire file (backward compatibility)function mergeWithMarkers(existing: string, newGenerated: string): string {
const startIdx = existing.indexOf('<!-- AI-KIT:START -->');
const endIdx = existing.indexOf('<!-- AI-KIT:END -->');
if (startIdx === -1 || endIdx === -1) return newGenerated;
const before = existing.substring(0, startIdx);
const after = existing.substring(endIdx + '<!-- AI-KIT:END -->'.length);
return `${before}${newGenerated}${after}`;
}Design Decisions
Why fragments instead of a single template?
A single large template with conditionals would be:
- Harder to maintain
- Harder to test
- Impossible to scope for Cursor
.mdcfiles
Fragments allow:
- Independent testing per technology
- Mix-and-match composition
- Per-fragment glob targeting in
.mdcfiles
Why detect from package.json instead of AST parsing?
Speed and reliability. package.json is always present, small, and standardized. AST parsing would be slower, fragile across file formats, and overkill for the detection signals we need.
Why markers instead of a separate “custom rules” file?
Developers expect CLAUDE.md to be one file. Splitting into CLAUDE.md + CLAUDE.custom.md would confuse the tool and complicate the setup. Markers are invisible to the AI tool but give ai-kit update a safe boundary.
Why both .cursorrules and .mdc files?
.cursorrules is the established format — all Cursor users can use it. .mdc files are the newer per-file-type format that’s more precise. Generating both ensures backward and forward compatibility.