Compare commits
63 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 211827cd78 | |||
| e9f1e63029 | |||
| afcc1b74e3 | |||
| 28ba988e65 | |||
| b2aec11d1b | |||
| 0921d22788 | |||
| 6ad506cb0d | |||
| d18a0cc111 | |||
| 8573073e8b | |||
| c08e692868 | |||
| 2bd78047cd | |||
| 5d210ab5b4 | |||
| afa124f533 | |||
| 12182232a8 | |||
| 4b57f1238f | |||
| e9453c3956 | |||
| 19e37682e7 | |||
| 90422f4206 | |||
| d5796a6c26 | |||
| b6267050d5 | |||
| 67210c8d78 | |||
| 87253bbd52 | |||
| 243354f565 | |||
| ea8e70a0ca | |||
| 4f5a0aa443 | |||
| 3a88e03664 | |||
| 827021fad0 | |||
| 6d4fa82b4b | |||
| 7783ebd18e | |||
| 5dc6e1a900 | |||
| d5a0ff6eea | |||
| 8fc4bf39d7 | |||
| cb2ffb03cc | |||
| 179b457d4e | |||
| 1d86b267ae | |||
| 3a345b72e2 | |||
| b7ea702c75 | |||
| a35e139861 | |||
| 8196b6bdb8 | |||
| 63d02eadeb | |||
| 6691009de4 | |||
| ab340d028b | |||
| 29634912a5 | |||
| 26f5a42682 | |||
| def8d756a0 | |||
| e5ab4f7e3e | |||
| 1da9aa1170 | |||
| 60ad917ebd | |||
| 012d6e65cb | |||
| 22a60ae6b4 | |||
| 39448d277b | |||
| a0ffe2a602 | |||
| 5f347e77d2 | |||
| cb59fc4794 | |||
| ef026a6a31 | |||
| 85a94a9579 | |||
| 1c850fc2f7 | |||
| 31d40739a7 | |||
| f12e1ef585 | |||
| 577d79d92b | |||
| 263e56d4da | |||
| 97f8ba85e3 | |||
| bf865cfbeb |
44
.ai-context-config.json
Normal file
44
.ai-context-config.json
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"includes": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "type": "string" },
|
||||||
|
"default": ["**/*.ts", "**/*.js", "**/*.py", "**/*.go", "**/*.rs", "**/*.java"]
|
||||||
|
},
|
||||||
|
"excludes": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "type": "string" },
|
||||||
|
"default": ["node_modules/**", "dist/**", "build/**", ".git/**"]
|
||||||
|
},
|
||||||
|
"outputFormat": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["json", "yaml"],
|
||||||
|
"default": "json"
|
||||||
|
},
|
||||||
|
"template": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["cursor", "copilot", "generic", "default"],
|
||||||
|
"default": "default"
|
||||||
|
},
|
||||||
|
"outputFile": {
|
||||||
|
"type": "string",
|
||||||
|
"default": "ai-context.json"
|
||||||
|
},
|
||||||
|
"analyzeConventions": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": true
|
||||||
|
},
|
||||||
|
"includeDevDependencies": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"respectGitignore": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["includes", "excludes"],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
29
.ai-context-generator-cli/.gitea/workflows/ci.yml
Normal file
29
.ai-context-generator-cli/.gitea/workflows/ci.yml
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
paths:
|
||||||
|
- 'ai-context-generator-cli/**'
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
paths:
|
||||||
|
- 'ai-context-generator-cli/**'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: ./ai-context-generator-cli
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '20'
|
||||||
|
cache: 'npm'
|
||||||
|
cache-dependency-path: ai-context-generator-cli/package-lock.json
|
||||||
|
- run: npm ci
|
||||||
|
- run: npm run build
|
||||||
|
- run: npm test
|
||||||
|
- run: npm run lint
|
||||||
235
.ai-context-generator-cli/CLI.md
Normal file
235
.ai-context-generator-cli/CLI.md
Normal file
@@ -0,0 +1,235 @@
|
|||||||
|
# CLI Command Reference
|
||||||
|
|
||||||
|
## Quick Reference
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ai-context [options]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Global Options
|
||||||
|
|
||||||
|
| Option | Type | Default | Description |
|
||||||
|
|--------|------|---------|-------------|
|
||||||
|
| `--dir` | string | `process.cwd()` | Project directory to analyze |
|
||||||
|
| `--output` | string | `ai-context` | Output file path (without extension) |
|
||||||
|
| `--format` | string | `json` | Output format: `json` or `yaml` |
|
||||||
|
| `--template` | string | `default` | Template: `default`, `cursor`, `copilot`, `generic` |
|
||||||
|
| `--config` | string | auto-detected | Config file path |
|
||||||
|
| `--verbose` | boolean | `false` | Enable verbose output |
|
||||||
|
| `--no-conventions` | boolean | `false` | Skip convention analysis |
|
||||||
|
| `--include-dev` | boolean | `false` | Include dev dependencies |
|
||||||
|
| `--no-gitignore` | boolean | `false` | Do not respect .gitignore |
|
||||||
|
|
||||||
|
## Detailed Options
|
||||||
|
|
||||||
|
### `--dir`, `-d`
|
||||||
|
|
||||||
|
Specifies the project directory to analyze.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ai-context --dir /path/to/project
|
||||||
|
ai-context -d ./my-project
|
||||||
|
```
|
||||||
|
|
||||||
|
If not specified, uses the current working directory.
|
||||||
|
|
||||||
|
### `--output`, `-o`
|
||||||
|
|
||||||
|
Specifies the output file path. The file extension is automatically added based on the format.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ai-context --output my-context
|
||||||
|
# Creates my-context.json or my-context.yaml
|
||||||
|
|
||||||
|
ai-context --output /absolute/path/output
|
||||||
|
# Creates /absolute/path/output.json
|
||||||
|
```
|
||||||
|
|
||||||
|
### `--format`, `-f`
|
||||||
|
|
||||||
|
Specifies the output format.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# JSON output (default)
|
||||||
|
ai-context --format json
|
||||||
|
|
||||||
|
# YAML output
|
||||||
|
ai-context --format yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
### `--template`, `-t`
|
||||||
|
|
||||||
|
Specifies the template for context output.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Default template
|
||||||
|
ai-context --template default
|
||||||
|
|
||||||
|
# Cursor-optimized template
|
||||||
|
ai-context --template cursor
|
||||||
|
|
||||||
|
# Copilot-optimized template
|
||||||
|
ai-context --template copilot
|
||||||
|
|
||||||
|
# Generic template
|
||||||
|
ai-context --template generic
|
||||||
|
```
|
||||||
|
|
||||||
|
### `--config`, `-c`
|
||||||
|
|
||||||
|
Specifies a custom config file path.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ai-context --config /path/to/config.json
|
||||||
|
ai-context -c ./custom-config.json
|
||||||
|
```
|
||||||
|
|
||||||
|
If not specified, automatically looks for `.ai-context-config.json` in the project directory.
|
||||||
|
|
||||||
|
### `--verbose`, `-v`
|
||||||
|
|
||||||
|
Enables verbose output for debugging.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ai-context --verbose
|
||||||
|
ai-context -v
|
||||||
|
```
|
||||||
|
|
||||||
|
Output includes:
|
||||||
|
- Analyzed directory
|
||||||
|
- Output file path
|
||||||
|
- Format and template selection
|
||||||
|
- Analysis progress
|
||||||
|
|
||||||
|
### `--no-conventions`
|
||||||
|
|
||||||
|
Skips convention analysis for faster processing.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ai-context --no-conventions
|
||||||
|
```
|
||||||
|
|
||||||
|
### `--include-dev`
|
||||||
|
|
||||||
|
Includes development dependencies in the analysis.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ai-context --include-dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### `--no-gitignore`
|
||||||
|
|
||||||
|
Disables `.gitignore` pattern matching.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ai-context --no-gitignore
|
||||||
|
```
|
||||||
|
|
||||||
|
## Exit Codes
|
||||||
|
|
||||||
|
| Code | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `0` | Success |
|
||||||
|
| `1` | Error (see error message for details) |
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Basic Analysis
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Analyze current directory
|
||||||
|
ai-context
|
||||||
|
|
||||||
|
# Analyze specific directory with verbose output
|
||||||
|
ai-context --dir ./my-project --verbose
|
||||||
|
|
||||||
|
# Generate YAML output
|
||||||
|
ai-context --format yaml --output project-context
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom Output
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Custom output file name
|
||||||
|
ai-context --output my-ai-context
|
||||||
|
|
||||||
|
# Different format
|
||||||
|
ai-context --format yaml --output context
|
||||||
|
|
||||||
|
# Use specific template
|
||||||
|
ai-context --template cursor --output cursor-context
|
||||||
|
```
|
||||||
|
|
||||||
|
### Advanced Options
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Full analysis with all options
|
||||||
|
ai-context --dir ./project --verbose --include-dev
|
||||||
|
|
||||||
|
# Skip conventions for speed
|
||||||
|
ai-context --no-conventions --output quick-context
|
||||||
|
|
||||||
|
# Ignore gitignore
|
||||||
|
ai-context --no-gitignore --output all-files-context
|
||||||
|
```
|
||||||
|
|
||||||
|
### Combined Options
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Analyze Python project with conventions
|
||||||
|
ai-context --dir ./python-app --format yaml --no-conventions
|
||||||
|
|
||||||
|
# Analyze with dev dependencies
|
||||||
|
ai-context --dir ./node-app --include-dev --template copilot
|
||||||
|
|
||||||
|
# Full verbose analysis
|
||||||
|
ai-context --dir ./full-stack-app --verbose --format json --include-dev --template cursor
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Command Not Found
|
||||||
|
|
||||||
|
If `ai-context` is not found after installation:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Re-link the CLI
|
||||||
|
npm unlink
|
||||||
|
npm link
|
||||||
|
|
||||||
|
# Or use npx
|
||||||
|
npx ai-context-generator-cli --dir ./project
|
||||||
|
```
|
||||||
|
|
||||||
|
### Permission Denied
|
||||||
|
|
||||||
|
On Unix systems, you may need to make the script executable:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chmod +x dist/index.js
|
||||||
|
```
|
||||||
|
|
||||||
|
Or run with node directly:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node dist/index.js --dir ./project
|
||||||
|
```
|
||||||
|
|
||||||
|
### Slow Analysis
|
||||||
|
|
||||||
|
For large projects, use:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Skip conventions
|
||||||
|
ai-context --no-conventions
|
||||||
|
|
||||||
|
# Skip dev dependencies
|
||||||
|
ai-context --no-include-dev
|
||||||
|
|
||||||
|
# Limit file patterns
|
||||||
|
ai-context --dir ./project --format json
|
||||||
|
```
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
Currently, the CLI does not use environment variables. All configuration is done via CLI options or config files.
|
||||||
280
.ai-context-generator-cli/README.md
Normal file
280
.ai-context-generator-cli/README.md
Normal file
@@ -0,0 +1,280 @@
|
|||||||
|
# AI Context Generator CLI
|
||||||
|
|
||||||
|
A CLI tool that generates comprehensive context files for AI coding assistants by analyzing project structure, dependencies, and coding patterns. Outputs structured JSON/YAML context files optimized for different AI tools.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Automatic Project Detection**: Detects project type (Node.js, Python, Go, Rust, Java, etc.) and frameworks (React, Django, FastAPI, etc.)
|
||||||
|
- **Dependency Analysis**: Extracts dependencies from package.json, requirements.txt, go.mod, Cargo.toml, and more
|
||||||
|
- **Convention Extraction**: Analyzes coding patterns, naming conventions, import styles, and testing frameworks
|
||||||
|
- **Multi-format Output**: Generates context files in JSON or YAML format
|
||||||
|
- **AI-specific Templates**: Predefined templates for Cursor, Copilot, and generic AI assistants
|
||||||
|
- **Configurable**: Customize analysis scope and output via `.ai-context-config.json`
|
||||||
|
- **Ignore Pattern Support**: Respects `.gitignore` and custom ignore rules
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- Node.js 16+
|
||||||
|
- npm or yarn
|
||||||
|
|
||||||
|
### Install from Source
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/yourusername/ai-context-generator-cli.git
|
||||||
|
cd ai-context-generator-cli
|
||||||
|
npm install
|
||||||
|
npm run build
|
||||||
|
npm link
|
||||||
|
```
|
||||||
|
|
||||||
|
### Install Globally
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install -g ai-context-generator-cli
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Basic Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Analyze current directory and generate context
|
||||||
|
ai-context
|
||||||
|
|
||||||
|
# Analyze specific directory
|
||||||
|
ai-context --dir /path/to/project
|
||||||
|
|
||||||
|
# Save to specific output file
|
||||||
|
ai-context --output my-context
|
||||||
|
|
||||||
|
# Generate YAML instead of JSON
|
||||||
|
ai-context --format yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
### Command Options
|
||||||
|
|
||||||
|
| Option | Short | Description | Default |
|
||||||
|
|--------|-------|-------------|---------|
|
||||||
|
| `--dir` | `-d` | Project directory to analyze | Current directory |
|
||||||
|
| `--output` | `-o` | Output file path | `ai-context` |
|
||||||
|
| `--format` | `-f` | Output format: `json` or `yaml` | `json` |
|
||||||
|
| `--template` | `-t` | Template: `default`, `cursor`, `copilot`, `generic` | `default` |
|
||||||
|
| `--config` | `-c` | Config file path | Auto-detected |
|
||||||
|
| `--verbose` | `-v` | Enable verbose output | `false` |
|
||||||
|
| `--no-conventions` | | Skip convention analysis | `false` |
|
||||||
|
| `--include-dev` | | Include dev dependencies | `false` |
|
||||||
|
| `--no-gitignore` | | Do not respect .gitignore | `false` |
|
||||||
|
|
||||||
|
### Examples
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Generate context with all analysis
|
||||||
|
ai-context --dir ./my-project --verbose
|
||||||
|
|
||||||
|
# Generate YAML output for documentation
|
||||||
|
ai-context --format yaml --output project-context
|
||||||
|
|
||||||
|
# Use Cursor-optimized template
|
||||||
|
ai-context --template cursor --output cursor-context
|
||||||
|
|
||||||
|
# Quick analysis without conventions
|
||||||
|
ai-context --no-conventions
|
||||||
|
|
||||||
|
# Include development dependencies
|
||||||
|
ai-context --include-dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Create a `.ai-context-config.json` file in your project root:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"includes": [
|
||||||
|
"**/*.ts",
|
||||||
|
"**/*.js",
|
||||||
|
"**/*.py",
|
||||||
|
"**/*.go"
|
||||||
|
],
|
||||||
|
"excludes": [
|
||||||
|
"node_modules/**",
|
||||||
|
"dist/**",
|
||||||
|
".git/**"
|
||||||
|
],
|
||||||
|
"outputFormat": "json",
|
||||||
|
"template": "default",
|
||||||
|
"outputFile": "ai-context.json",
|
||||||
|
"analyzeConventions": true,
|
||||||
|
"includeDevDependencies": false,
|
||||||
|
"respectGitignore": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Configuration Options
|
||||||
|
|
||||||
|
| Option | Type | Description | Default |
|
||||||
|
|--------|------|-------------|---------|
|
||||||
|
| `includes` | `string[]` | File patterns to include | All common source files |
|
||||||
|
| `excludes` | `string[]` | File patterns to exclude | Common ignore patterns |
|
||||||
|
| `outputFormat` | `json | yaml` | Output format | `json` |
|
||||||
|
| `template` | `string` | Template name | `default` |
|
||||||
|
| `outputFile` | `string` | Output filename | `ai-context.json` |
|
||||||
|
| `analyzeConventions` | `boolean` | Extract coding conventions | `true` |
|
||||||
|
| `includeDevDependencies` | `boolean` | Include dev dependencies | `false` |
|
||||||
|
| `respectGitignore` | `boolean` | Respect .gitignore | `true` |
|
||||||
|
|
||||||
|
## Templates
|
||||||
|
|
||||||
|
### Default Template
|
||||||
|
Generates structured JSON/YAML with full project analysis.
|
||||||
|
|
||||||
|
### Cursor Template
|
||||||
|
Optimized for Cursor AI, focusing on:
|
||||||
|
- Key dependencies
|
||||||
|
- Coding conventions
|
||||||
|
- File structure overview
|
||||||
|
|
||||||
|
### Copilot Template
|
||||||
|
Tailored for GitHub Copilot, featuring:
|
||||||
|
- Concise project summary
|
||||||
|
- Dependency overview
|
||||||
|
- Style guidelines
|
||||||
|
|
||||||
|
### Generic Template
|
||||||
|
Simple format suitable for any AI assistant:
|
||||||
|
- Project information
|
||||||
|
- Dependency list
|
||||||
|
- Conventions summary
|
||||||
|
|
||||||
|
## Output Structure
|
||||||
|
|
||||||
|
### JSON Output
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"projectInfo": {
|
||||||
|
"projectType": {
|
||||||
|
"primaryLanguage": "TypeScript",
|
||||||
|
"languages": ["TypeScript", "JavaScript"],
|
||||||
|
"frameworks": ["React", "Next.js"],
|
||||||
|
"buildTools": ["npm"]
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"direct": [...],
|
||||||
|
"dev": [...],
|
||||||
|
"total": 50
|
||||||
|
},
|
||||||
|
"conventions": {
|
||||||
|
"namingConvention": {
|
||||||
|
"files": "kebab-case",
|
||||||
|
"variables": "camelCase",
|
||||||
|
"functions": "camelCase",
|
||||||
|
"classes": "PascalCase"
|
||||||
|
},
|
||||||
|
"importStyle": {
|
||||||
|
"style": "ESM",
|
||||||
|
"aliasPrefix": "@/"
|
||||||
|
},
|
||||||
|
"testingFramework": "Jest",
|
||||||
|
"codeStyle": {
|
||||||
|
"indentSize": 2,
|
||||||
|
"indentType": "spaces",
|
||||||
|
"lineEndings": "LF",
|
||||||
|
"quoteStyle": "single"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fileCount": 150,
|
||||||
|
"analysisDate": "2024-01-15T10:30:00.000Z"
|
||||||
|
},
|
||||||
|
"files": [...],
|
||||||
|
"config": {...},
|
||||||
|
"generatedAt": "2024-01-15T10:30:00.000Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Supported Languages
|
||||||
|
|
||||||
|
| Language | Detected By | Dependency Files |
|
||||||
|
|----------|-------------|------------------|
|
||||||
|
| TypeScript | `.ts`, `.tsx`, `tsconfig.json` | `package.json` |
|
||||||
|
| JavaScript | `.js`, `.jsx`, `package.json` | `package.json` |
|
||||||
|
| Python | `.py`, `requirements.txt` | `requirements.txt`, `pyproject.toml`, `Pipfile` |
|
||||||
|
| Go | `.go`, `go.mod` | `go.mod` |
|
||||||
|
| Rust | `.rs`, `Cargo.toml` | `Cargo.toml` |
|
||||||
|
| Java | `.java`, `pom.xml` | `pom.xml`, `build.gradle` |
|
||||||
|
| C/C++ | `.c`, `.cpp`, `CMakeLists.txt` | `CMakeLists.txt` |
|
||||||
|
| Ruby | `.rb`, `Gemfile` | `Gemfile` |
|
||||||
|
| PHP | `.php`, `composer.json` | `composer.json` |
|
||||||
|
|
||||||
|
## Supported Frameworks
|
||||||
|
|
||||||
|
- **Frontend**: React, Vue, Next.js, NestJS
|
||||||
|
- **Backend**: Express, Django, FastAPI, Flask, Gin, Echo
|
||||||
|
- **Testing**: Jest, Mocha, Pytest, Go testing, JUnit
|
||||||
|
|
||||||
|
## Programmatic Usage
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { ContextGenerator } from 'ai-context-generator-cli';
|
||||||
|
|
||||||
|
const generator = new ContextGenerator();
|
||||||
|
|
||||||
|
// Generate context
|
||||||
|
const projectInfo = await generator.generate('/path/to/project');
|
||||||
|
|
||||||
|
// Generate JSON output
|
||||||
|
const jsonOutput = await generator.generateJson('/path/to/project');
|
||||||
|
|
||||||
|
// Generate YAML output
|
||||||
|
const yamlOutput = await generator.generateYaml('/path/to/project');
|
||||||
|
|
||||||
|
// Save to file
|
||||||
|
await generator.saveContext('/path/to/project', 'output', 'json');
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
### Common Errors
|
||||||
|
|
||||||
|
| Error | Solution |
|
||||||
|
|-------|----------|
|
||||||
|
| `No package.json found` | Check if running in project root or use `--dir` flag |
|
||||||
|
| `Invalid config file format` | Validate JSON syntax and required fields |
|
||||||
|
| `Permission denied on output file` | Check write permissions in target directory |
|
||||||
|
| `Directory not found` | Verify the directory path exists |
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install dependencies
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# Run tests
|
||||||
|
npm test
|
||||||
|
|
||||||
|
# Build
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
# Run locally
|
||||||
|
npm run dev -- --dir ./my-project
|
||||||
|
|
||||||
|
# Lint
|
||||||
|
npm run lint
|
||||||
|
|
||||||
|
# Lint with fix
|
||||||
|
npm run lint:fix
|
||||||
|
```
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
1. Fork the repository
|
||||||
|
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
|
||||||
|
3. Commit your changes (`git commit -m 'Add amazing feature'`)
|
||||||
|
4. Push to the branch (`git push origin feature/amazing-feature`)
|
||||||
|
5. Open a Pull Request
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT License - see [LICENSE](LICENSE) for details.
|
||||||
13
.ai-context-generator-cli/jest.config.js
Normal file
13
.ai-context-generator-cli/jest.config.js
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
module.exports = {
|
||||||
|
preset: 'ts-jest',
|
||||||
|
testEnvironment: 'node',
|
||||||
|
roots: ['<rootDir>/tests'],
|
||||||
|
testMatch: ['**/*.test.ts'],
|
||||||
|
moduleFileExtensions: ['ts', 'js', 'json'],
|
||||||
|
collectCoverageFrom: [
|
||||||
|
'src/**/*.ts',
|
||||||
|
'!src/**/*.d.ts',
|
||||||
|
],
|
||||||
|
coverageDirectory: 'coverage',
|
||||||
|
verbose: true,
|
||||||
|
};
|
||||||
42
.ai-context-generator-cli/package.json
Normal file
42
.ai-context-generator-cli/package.json
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
{
|
||||||
|
"name": "ai-context-generator-cli",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "A CLI tool that generates comprehensive context files for AI coding assistants",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"bin": {
|
||||||
|
"ai-context": "./dist/index.js"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc",
|
||||||
|
"test": "jest",
|
||||||
|
"test:coverage": "jest --coverage",
|
||||||
|
"lint": "eslint src/**/*.ts",
|
||||||
|
"lint:fix": "eslint src/**/*.ts --fix",
|
||||||
|
"dev": "ts-node src/index.ts",
|
||||||
|
"start": "node dist/index.js"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"cli",
|
||||||
|
"ai",
|
||||||
|
"context",
|
||||||
|
"coding-assistant",
|
||||||
|
"typescript"
|
||||||
|
],
|
||||||
|
"author": "",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"commander": "^12.0.0",
|
||||||
|
"glob": "^10.3.10",
|
||||||
|
"ignore": "^5.3.0",
|
||||||
|
"js-yaml": "^4.1.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/glob": "^8.1.0",
|
||||||
|
"@types/jest": "^29.5.11",
|
||||||
|
"@types/js-yaml": "^4.0.9",
|
||||||
|
"@types/node": "^20.10.6",
|
||||||
|
"jest": "^29.7.0",
|
||||||
|
"ts-jest": "^29.1.1",
|
||||||
|
"typescript": "^5.3.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
312
.ai-context-generator-cli/src/analyzers/conventionExtractor.ts
Normal file
312
.ai-context-generator-cli/src/analyzers/conventionExtractor.ts
Normal file
@@ -0,0 +1,312 @@
|
|||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import { ConventionInfo, NamingConvention, ImportStyle, CodeStyle } from '../types';
|
||||||
|
|
||||||
|
interface NamingPattern {
|
||||||
|
regex: RegExp;
|
||||||
|
type: 'camelCase' | 'snake_case' | 'kebab-case' | 'PascalCase';
|
||||||
|
}
|
||||||
|
|
||||||
|
const NAMING_PATTERNS: NamingPattern[] = [
|
||||||
|
{ regex: /^[a-z][a-zA-Z0-9]*$/, type: 'camelCase' },
|
||||||
|
{ regex: /^[a-z]+_[a-z0-9_]+$/, type: 'snake_case' },
|
||||||
|
{ regex: /^[a-z]+-[a-z0-9-]+$/, type: 'kebab-case' },
|
||||||
|
{ regex: /^[A-Z][a-zA-Z0-9]*$/, type: 'PascalCase' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const TEST_FRAMEWORK_PATTERNS = [
|
||||||
|
{ name: 'Jest', indicators: ['jest', '@types/jest'] },
|
||||||
|
{ name: 'Mocha', indicators: ['mocha'] },
|
||||||
|
{ name: 'Vitest', indicators: ['vitest'] },
|
||||||
|
{ name: 'Pytest', indicators: ['pytest'] },
|
||||||
|
{ name: 'unittest', indicators: ['unittest', 'unittest.mock'] },
|
||||||
|
{ name: 'Go testing', indicators: ['testing'] },
|
||||||
|
{ name: 'JUnit', indicators: ['junit', '@junit'] },
|
||||||
|
{ name: 'pytest', indicators: ['pytest'] },
|
||||||
|
];
|
||||||
|
|
||||||
|
export class ConventionExtractor {
|
||||||
|
async extract(dir: string, files: string[]): Promise<ConventionInfo> {
|
||||||
|
const namingConvention = await this.extractNamingConvention(files);
|
||||||
|
const importStyle = await this.extractImportStyle(dir, files);
|
||||||
|
const testingFramework = await this.detectTestingFramework(dir);
|
||||||
|
const codeStyle = await this.extractCodeStyle(files);
|
||||||
|
|
||||||
|
return {
|
||||||
|
namingConvention,
|
||||||
|
importStyle,
|
||||||
|
testingFramework,
|
||||||
|
codeStyle,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async extractNamingConvention(
|
||||||
|
files: string[]
|
||||||
|
): Promise<NamingConvention> {
|
||||||
|
const fileNames = files.map(f => path.basename(f));
|
||||||
|
const fileNameScores = this.scoreNamingPatterns(fileNames);
|
||||||
|
|
||||||
|
const allNames: string[] = [];
|
||||||
|
for (const file of files) {
|
||||||
|
try {
|
||||||
|
const content = await fs.promises.readFile(file, 'utf-8');
|
||||||
|
const identifiers = this.extractIdentifiers(content);
|
||||||
|
allNames.push(...identifiers);
|
||||||
|
} catch {
|
||||||
|
// Skip files that can't be read
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const variableNames = allNames.filter(n =>
|
||||||
|
/^[a-z]/.test(n) && !n.includes('_') && !n.contains('-')
|
||||||
|
);
|
||||||
|
const functionNames = allNames.filter(n =>
|
||||||
|
/^[a-z]/.test(n) && !n.includes('_') && !n.contains('-')
|
||||||
|
);
|
||||||
|
const classNames = allNames.filter(n =>
|
||||||
|
/^[A-Z]/.test(n)
|
||||||
|
);
|
||||||
|
|
||||||
|
const variableScores = this.scoreNamingPatterns(variableNames.slice(0, 100));
|
||||||
|
const functionScores = this.scoreNamingPatterns(functionNames.slice(0, 100));
|
||||||
|
const classScores = this.scoreNamingPatterns(classNames.slice(0, 50));
|
||||||
|
|
||||||
|
return {
|
||||||
|
files: this.getBestType(fileNameScores),
|
||||||
|
variables: this.getBestType(variableScores),
|
||||||
|
functions: this.getBestType(functionScores),
|
||||||
|
classes: this.getBestType(classScores),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private scoreNamingPatterns(names: string[]): Record<string, number> {
|
||||||
|
const scores: Record<string, number> = {
|
||||||
|
camelCase: 0,
|
||||||
|
snake_case: 0,
|
||||||
|
'kebab-case': 0,
|
||||||
|
PascalCase: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const name of names) {
|
||||||
|
for (const pattern of NAMING_PATTERNS) {
|
||||||
|
if (pattern.regex.test(name)) {
|
||||||
|
scores[pattern.type]++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return scores;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getBestType(
|
||||||
|
scores: Record<string, number>
|
||||||
|
): 'camelCase' | 'snake_case' | 'kebab-case' | 'PascalCase' {
|
||||||
|
let maxScore = 0;
|
||||||
|
let bestType: 'camelCase' | 'snake_case' | 'kebab-case' | 'PascalCase' =
|
||||||
|
'camelCase';
|
||||||
|
|
||||||
|
for (const [type, score] of Object.entries(scores)) {
|
||||||
|
if (score > maxScore) {
|
||||||
|
maxScore = score;
|
||||||
|
bestType = type as typeof bestType;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return bestType;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async extractImportStyle(
|
||||||
|
dir: string,
|
||||||
|
files: string[]
|
||||||
|
): Promise<ImportStyle> {
|
||||||
|
let hasESMImports = false;
|
||||||
|
let hasCommonJSImports = false;
|
||||||
|
let hasCommonJSRequires = false;
|
||||||
|
let hasAliasImports = false;
|
||||||
|
const commonPatterns: string[] = [];
|
||||||
|
|
||||||
|
const aliasPatterns = [
|
||||||
|
/^@\//,
|
||||||
|
/^~/,
|
||||||
|
/^src\/,
|
||||||
|
/^components\/,
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
try {
|
||||||
|
const content = await fs.promises.readFile(file, 'utf-8');
|
||||||
|
|
||||||
|
if (/import\s+.*\s+from\s+['"]/.test(content)) {
|
||||||
|
hasESMImports = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/require\s*\(/.test(content)) {
|
||||||
|
hasCommonJSRequires = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/export\s+(const|function|class|interface|type)/.test(content)) {
|
||||||
|
hasESMImports = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const pattern of aliasPatterns) {
|
||||||
|
if (pattern.test(content.replace(/import\s+.*\s+from\s+/, ''))) {
|
||||||
|
hasAliasImports = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const importMatches = content.match(/import\s+.*\s+from\s+['"]([^'"]+)['"]/g);
|
||||||
|
if (importMatches) {
|
||||||
|
for (const match of importMatches) {
|
||||||
|
const modMatch = match.match(/['"]([^'"]+)['"]$/);
|
||||||
|
if (modMatch) {
|
||||||
|
const module = modMatch[1];
|
||||||
|
if (!commonPatterns.includes(module)) {
|
||||||
|
commonPatterns.push(module);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Skip unreadable files
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hasCommonJSImports = hasCommonJSRequires;
|
||||||
|
|
||||||
|
let style: 'ESM' | 'CommonJS' | 'mixed' = 'CommonJS';
|
||||||
|
if (hasESMImports && hasCommonJSImports) {
|
||||||
|
style = 'mixed';
|
||||||
|
} else if (hasESMImports) {
|
||||||
|
style = 'ESM';
|
||||||
|
}
|
||||||
|
|
||||||
|
const aliasPrefix = hasAliasImports ? '@/' : null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
style,
|
||||||
|
aliasPrefix,
|
||||||
|
commonPatterns: commonPatterns.slice(0, 10),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async detectTestingFramework(dir: string): Promise<string | null> {
|
||||||
|
const packageJsonPath = path.join(dir, 'package.json');
|
||||||
|
if (await this.fileExists(packageJsonPath)) {
|
||||||
|
const content = await fs.promises.readFile(packageJsonPath, 'utf-8');
|
||||||
|
const packageJson = JSON.parse(content);
|
||||||
|
const allDeps = {
|
||||||
|
...packageJson.dependencies,
|
||||||
|
...packageJson.devDependencies,
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const framework of TEST_FRAMEWORK_PATTERNS) {
|
||||||
|
for (const indicator of framework.indicators) {
|
||||||
|
if (Object.keys(allDeps).some(dep => dep.includes(indicator))) {
|
||||||
|
return framework.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const requirementsPath = path.join(dir, 'requirements.txt');
|
||||||
|
if (await this.fileExists(requirementsPath)) {
|
||||||
|
const content = await fs.promises.readFile(requirementsPath, 'utf-8');
|
||||||
|
for (const framework of TEST_FRAMEWORK_PATTERNS) {
|
||||||
|
for (const indicator of framework.indicators) {
|
||||||
|
if (content.toLowerCase().includes(indicator.toLowerCase())) {
|
||||||
|
return framework.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const file of await fs.promises.readdir(dir)) {
|
||||||
|
if (file.endsWith('.test.ts') || file.endsWith('.spec.ts')) {
|
||||||
|
return 'Jest';
|
||||||
|
}
|
||||||
|
if (file.endsWith('.test.js') || file.endsWith('.spec.js')) {
|
||||||
|
return 'Jest';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async extractCodeStyle(files: string[]): Promise<CodeStyle> {
|
||||||
|
let spaceIndentCount = 0;
|
||||||
|
let tabIndentCount = 0;
|
||||||
|
let singleQuoteCount = 0;
|
||||||
|
let doubleQuoteCount = 0;
|
||||||
|
let lfLineEndings = 0;
|
||||||
|
let crlfLineEndings = 0;
|
||||||
|
|
||||||
|
const maxSamples = 50;
|
||||||
|
|
||||||
|
for (const file of files.slice(0, maxSamples)) {
|
||||||
|
try {
|
||||||
|
const content = await fs.promises.readFile(file, 'utf-8');
|
||||||
|
const lines = content.split('\n').slice(0, 100);
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
if (/^\s+ /.test(line)) spaceIndentCount += 2;
|
||||||
|
if (/^\s+\t/.test(line)) tabIndentCount++;
|
||||||
|
if (/'[^']*'/.test(line) && !/\\'/.test(line)) singleQuoteCount++;
|
||||||
|
if (/"[^"]*"/.test(line) && !/\\"/.test(line)) doubleQuoteCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (content.includes('\r\n')) crlfLineEndings++;
|
||||||
|
if (!content.includes('\r\n') && content.includes('\n')) lfLineEndings++;
|
||||||
|
} catch {
|
||||||
|
// Skip unreadable files
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const indentType = spaceIndentCount > tabIndentCount ? 'spaces' : 'tabs';
|
||||||
|
const quoteStyle = singleQuoteCount > doubleQuoteCount ? 'single' : 'double';
|
||||||
|
const lineEndings = lfLineEndings > crlfLineEndings ? 'LF' : 'CRLF';
|
||||||
|
|
||||||
|
return {
|
||||||
|
indentSize: 2,
|
||||||
|
indentType,
|
||||||
|
lineEndings,
|
||||||
|
quoteStyle,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private extractIdentifiers(content: string): string[] {
|
||||||
|
const identifiers: string[] = [];
|
||||||
|
|
||||||
|
const varPattern = /\b(const|let|var)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/g;
|
||||||
|
let match;
|
||||||
|
while ((match = varPattern.exec(content)) !== null) {
|
||||||
|
identifiers.push(match[2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const funcPattern = /function\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/g;
|
||||||
|
while ((match = funcPattern.exec(content)) !== null) {
|
||||||
|
identifiers.push(match[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const arrowFuncPattern = /const\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=/g;
|
||||||
|
while ((match = arrowFuncPattern.exec(content)) !== null) {
|
||||||
|
identifiers.push(match[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const classPattern = /class\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/g;
|
||||||
|
while ((match = classPattern.exec(content)) !== null) {
|
||||||
|
identifiers.push(match[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...new Set(identifiers)];
|
||||||
|
}
|
||||||
|
|
||||||
|
private async fileExists(filePath: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
await fs.promises.access(filePath, fs.constants.F_OK);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
375
.ai-context-generator-cli/src/analyzers/dependencyAnalyzer.ts
Normal file
375
.ai-context-generator-cli/src/analyzers/dependencyAnalyzer.ts
Normal file
@@ -0,0 +1,375 @@
|
|||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import { DependencyInfo, Dependency } from '../types';
|
||||||
|
|
||||||
|
interface DependencyFile {
|
||||||
|
path: string;
|
||||||
|
parser: (content: string) => Dependency[];
|
||||||
|
type: 'prod' | 'dev' | 'both';
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DependencyAnalyzer {
|
||||||
|
private dependencyFiles: DependencyFile[] = [
|
||||||
|
{
|
||||||
|
path: 'package.json',
|
||||||
|
parser: this.parsePackageJson.bind(this),
|
||||||
|
type: 'both',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'requirements.txt',
|
||||||
|
parser: this.parseRequirementsTxt.bind(this),
|
||||||
|
type: 'prod',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'pyproject.toml',
|
||||||
|
parser: this.parsePyprojectToml.bind(this),
|
||||||
|
type: 'both',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'go.mod',
|
||||||
|
parser: this.parseGoMod.bind(this),
|
||||||
|
type: 'prod',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'Cargo.toml',
|
||||||
|
parser: this.parseCargoToml.bind(this),
|
||||||
|
type: 'both',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'Pipfile',
|
||||||
|
parser: this.parsePipfile.bind(this),
|
||||||
|
type: 'both',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'Gemfile',
|
||||||
|
parser: this.parseGemfile.bind(this),
|
||||||
|
type: 'both',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'composer.json',
|
||||||
|
parser: this.parseComposerJson.bind(this),
|
||||||
|
type: 'both',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
async analyze(
|
||||||
|
dir: string,
|
||||||
|
includeDev: boolean = false
|
||||||
|
): Promise<DependencyInfo> {
|
||||||
|
const allDependencies: Dependency[] = [];
|
||||||
|
|
||||||
|
for (const depFile of this.dependencyFiles) {
|
||||||
|
const filePath = path.join(dir, depFile.path);
|
||||||
|
if (await this.fileExists(filePath)) {
|
||||||
|
try {
|
||||||
|
const content = await fs.promises.readFile(filePath, 'utf-8');
|
||||||
|
const deps = depFile.parser(content);
|
||||||
|
|
||||||
|
if (depFile.type === 'both') {
|
||||||
|
const filteredDeps = includeDev
|
||||||
|
? deps
|
||||||
|
: deps.filter(d => d.type === 'prod');
|
||||||
|
allDependencies.push(...filteredDeps);
|
||||||
|
} else if (depFile.type === 'prod' || includeDev) {
|
||||||
|
allDependencies.push(...deps);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`Failed to parse ${depFile.path}: ${error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const direct = allDependencies.filter(d => d.type === 'prod');
|
||||||
|
const dev = allDependencies.filter(d => d.type === 'dev');
|
||||||
|
|
||||||
|
return {
|
||||||
|
direct,
|
||||||
|
dev,
|
||||||
|
total: direct.length + dev.length,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private parsePackageJson(content: string): Dependency[] {
|
||||||
|
const packageJson = JSON.parse(content);
|
||||||
|
const dependencies: Dependency[] = [];
|
||||||
|
|
||||||
|
const parseDeps = (
|
||||||
|
deps: Record<string, string> | undefined,
|
||||||
|
type: 'prod' | 'dev'
|
||||||
|
) => {
|
||||||
|
if (!deps) return;
|
||||||
|
for (const [name, version] of Object.entries(deps)) {
|
||||||
|
dependencies.push({
|
||||||
|
name,
|
||||||
|
version: version as string,
|
||||||
|
type,
|
||||||
|
isLocal: name.startsWith('.') || name.startsWith('/') || name.startsWith('@'),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
parseDeps(packageJson.dependencies, 'prod');
|
||||||
|
parseDeps(packageJson.devDependencies, 'dev');
|
||||||
|
|
||||||
|
return dependencies;
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseRequirementsTxt(content: string): Dependency[] {
|
||||||
|
const lines = content.split('\n');
|
||||||
|
const dependencies: Dependency[] = [];
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
const trimmed = line.trim();
|
||||||
|
if (!trimmed || trimmed.startsWith('#') || trimmed.startsWith('-')) continue;
|
||||||
|
|
||||||
|
const match = trimmed.match(/^([a-zA-Z0-9_-]+)([<>=!~]+)(.+)$/);
|
||||||
|
if (match) {
|
||||||
|
dependencies.push({
|
||||||
|
name: match[1],
|
||||||
|
version: match[2] + match[3],
|
||||||
|
type: 'prod',
|
||||||
|
isLocal: false,
|
||||||
|
});
|
||||||
|
} else if (/^[a-zA-Z0-9_-]+$/.test(trimmed)) {
|
||||||
|
dependencies.push({
|
||||||
|
name: trimmed,
|
||||||
|
version: '*',
|
||||||
|
type: 'prod',
|
||||||
|
isLocal: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return dependencies;
|
||||||
|
}
|
||||||
|
|
||||||
|
private parsePyprojectToml(content: string): Dependency[] {
|
||||||
|
const dependencies: Dependency[] = [];
|
||||||
|
const lines = content.split('\n');
|
||||||
|
let inDependencies = false;
|
||||||
|
let inDevDependencies = false;
|
||||||
|
let currentSection: 'prod' | 'dev' | null = null;
|
||||||
|
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
const line = lines[i];
|
||||||
|
const trimmed = line.trim();
|
||||||
|
|
||||||
|
if (trimmed.startsWith('[') && trimmed.endsWith(']')) {
|
||||||
|
const section = trimmed.slice(1, -1).toLowerCase();
|
||||||
|
if (section === 'project') {
|
||||||
|
currentSection = 'prod';
|
||||||
|
inDependencies = true;
|
||||||
|
inDevDependencies = false;
|
||||||
|
} else if (section === 'project.optional-dependencies') {
|
||||||
|
currentSection = 'dev';
|
||||||
|
inDevDependencies = true;
|
||||||
|
inDependencies = false;
|
||||||
|
} else if (section === 'dependencies' || section === 'tool.poetry.dependencies') {
|
||||||
|
currentSection = 'prod';
|
||||||
|
inDependencies = true;
|
||||||
|
inDevDependencies = false;
|
||||||
|
} else if (section === 'dev-dependencies' || section === 'tool.poetry.dev-dependencies') {
|
||||||
|
currentSection = 'dev';
|
||||||
|
inDevDependencies = true;
|
||||||
|
inDependencies = false;
|
||||||
|
} else {
|
||||||
|
inDependencies = false;
|
||||||
|
inDevDependencies = false;
|
||||||
|
currentSection = null;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!inDependencies && !inDevDependencies) continue;
|
||||||
|
if (trimmed.startsWith('#') || !trimmed) continue;
|
||||||
|
|
||||||
|
const cleanLine = trimmed.replace(/^["']|["',]/g, '');
|
||||||
|
const match = cleanLine.match(/^([a-zA-Z0-9_-]+)([<>=!~]+)(.+)$/);
|
||||||
|
if (match) {
|
||||||
|
dependencies.push({
|
||||||
|
name: match[1],
|
||||||
|
version: match[2] + match[3],
|
||||||
|
type: currentSection || 'prod',
|
||||||
|
isLocal: false,
|
||||||
|
});
|
||||||
|
} else if (/^[a-zA-Z0-9_-]+$/.test(cleanLine)) {
|
||||||
|
dependencies.push({
|
||||||
|
name: trimmed,
|
||||||
|
version: '*',
|
||||||
|
type: currentSection || 'prod',
|
||||||
|
isLocal: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return dependencies;
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseGoMod(content: string): Dependency[] {
|
||||||
|
const dependencies: Dependency[] = [];
|
||||||
|
const lines = content.split('\n');
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
const trimmed = line.trim();
|
||||||
|
if (!trimmed || trimmed.startsWith('module') || trimmed.startsWith('go')) continue;
|
||||||
|
|
||||||
|
const match = trimmed.match(/^([a-zA-Z0-9./_-]+)\s+v?([0-9.]+)/);
|
||||||
|
if (match) {
|
||||||
|
dependencies.push({
|
||||||
|
name: match[1],
|
||||||
|
version: match[2],
|
||||||
|
type: 'prod',
|
||||||
|
isLocal: match[1].startsWith('./') || match[1].startsWith('../'),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return dependencies;
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseCargoToml(content: string): Dependency[] {
|
||||||
|
const dependencies: Dependency[] = [];
|
||||||
|
const lines = content.split('\n');
|
||||||
|
let currentSection: 'prod' | 'dev' | null = null;
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
const trimmed = line.trim();
|
||||||
|
|
||||||
|
if (trimmed.startsWith('[') && trimmed.endsWith(']')) {
|
||||||
|
const section = trimmed.slice(1, -1).toLowerCase();
|
||||||
|
if (section === 'dependencies') {
|
||||||
|
currentSection = 'prod';
|
||||||
|
} else if (section === 'dev-dependencies') {
|
||||||
|
currentSection = 'dev';
|
||||||
|
} else {
|
||||||
|
currentSection = null;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!currentSection) continue;
|
||||||
|
if (trimmed.startsWith('#') || !trimmed) continue;
|
||||||
|
|
||||||
|
const match = trimmed.match(/^([a-zA-Z0-9_-]+)\s*=\s*["\']?(.+?)["\']?\s*,?$/);
|
||||||
|
if (match) {
|
||||||
|
dependencies.push({
|
||||||
|
name: match[1],
|
||||||
|
version: match[2].replace(/["']/g, ''),
|
||||||
|
type: currentSection,
|
||||||
|
isLocal: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return dependencies;
|
||||||
|
}
|
||||||
|
|
||||||
|
private parsePipfile(content: string): Dependency[] {
|
||||||
|
const dependencies: Dependency[] = [];
|
||||||
|
const lines = content.split('\n');
|
||||||
|
let currentSection: 'prod' | 'dev' | null = null;
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
const trimmed = line.trim();
|
||||||
|
|
||||||
|
if (trimmed.startsWith('[') && trimmed.endsWith(']')) {
|
||||||
|
const section = trimmed.slice(1, -1).toLowerCase();
|
||||||
|
if (section === 'packages') {
|
||||||
|
currentSection = 'prod';
|
||||||
|
} else if (section === 'dev-packages') {
|
||||||
|
currentSection = 'dev';
|
||||||
|
} else {
|
||||||
|
currentSection = null;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!currentSection) continue;
|
||||||
|
if (trimmed.startsWith('#') || !trimmed) continue;
|
||||||
|
|
||||||
|
const match = trimmed.match(/^([a-zA-Z0-9_-]+)\s*=\s*\{?\s*version\s*=\s*["']([^"']+)["']/);
|
||||||
|
if (match) {
|
||||||
|
dependencies.push({
|
||||||
|
name: match[1],
|
||||||
|
version: match[2],
|
||||||
|
type: currentSection,
|
||||||
|
isLocal: false,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const simpleMatch = trimmed.match(/^([a-zA-Z0-9_-]+)/);
|
||||||
|
if (simpleMatch) {
|
||||||
|
dependencies.push({
|
||||||
|
name: simpleMatch[1],
|
||||||
|
version: '*',
|
||||||
|
type: currentSection,
|
||||||
|
isLocal: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return dependencies;
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseGemfile(content: string): Dependency[] {
|
||||||
|
const dependencies: Dependency[] = [];
|
||||||
|
const lines = content.split('\n');
|
||||||
|
let currentSection: 'prod' | 'dev' = 'prod';
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
const trimmed = line.trim();
|
||||||
|
if (!trimmed || trimmed.startsWith('#')) continue;
|
||||||
|
|
||||||
|
if (trimmed.startsWith('group') && trimmed.includes(':development') || trimmed.includes(':test')) {
|
||||||
|
currentSection = 'dev';
|
||||||
|
continue;
|
||||||
|
} else if (trimmed.startsWith('group') || trimmed.startsWith('source')) {
|
||||||
|
currentSection = 'prod';
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const match = trimmed.match(/^gem\s+["']([^"']+)["'](?:\s*,\s*version:\s*["']([^"']+)["'])?/);
|
||||||
|
if (match) {
|
||||||
|
dependencies.push({
|
||||||
|
name: match[1],
|
||||||
|
version: match[2] || '*',
|
||||||
|
type: currentSection,
|
||||||
|
isLocal: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return dependencies;
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseComposerJson(content: string): Dependency[] {
|
||||||
|
const composerJson = JSON.parse(content);
|
||||||
|
const dependencies: Dependency[] = [];
|
||||||
|
|
||||||
|
const parseDeps = (deps: Record<string, string> | undefined, type: 'prod' | 'dev') => {
|
||||||
|
if (!deps) return;
|
||||||
|
for (const [name, version] of Object.entries(deps)) {
|
||||||
|
dependencies.push({
|
||||||
|
name,
|
||||||
|
version: version as string,
|
||||||
|
type,
|
||||||
|
isLocal: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
parseDeps(composerJson.require, 'prod');
|
||||||
|
parseDeps(composerJson['require-dev'], 'dev');
|
||||||
|
|
||||||
|
return dependencies;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async fileExists(filePath: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
await fs.promises.access(filePath, fs.constants.F_OK);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
326
.ai-context-generator-cli/src/analyzers/projectTypeDetector.ts
Normal file
326
.ai-context-generator-cli/src/analyzers/projectTypeDetector.ts
Normal file
@@ -0,0 +1,326 @@
|
|||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import { ProjectType } from '../types';
|
||||||
|
|
||||||
|
interface FrameworkPattern {
|
||||||
|
name: string;
|
||||||
|
indicators: string[];
|
||||||
|
language: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LanguagePattern {
|
||||||
|
extensions: string[];
|
||||||
|
configFiles: string[];
|
||||||
|
language: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ProjectTypeDetector {
|
||||||
|
private languagePatterns: LanguagePattern[] = [
|
||||||
|
{
|
||||||
|
extensions: ['.ts', '.tsx'],
|
||||||
|
language: 'TypeScript',
|
||||||
|
configFiles: ['tsconfig.json'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
extensions: ['.js', '.jsx'],
|
||||||
|
language: 'JavaScript',
|
||||||
|
configFiles: ['package.json', 'jsconfig.json'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
extensions: ['.py'],
|
||||||
|
language: 'Python',
|
||||||
|
configFiles: ['pyproject.toml', 'setup.py', 'requirements.txt'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
extensions: ['.go'],
|
||||||
|
language: 'Go',
|
||||||
|
configFiles: ['go.mod', 'go.sum'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
extensions: ['.rs'],
|
||||||
|
language: 'Rust',
|
||||||
|
configFiles: ['Cargo.toml', 'Cargo.lock'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
extensions: ['.java'],
|
||||||
|
language: 'Java',
|
||||||
|
configFiles: ['pom.xml', 'build.gradle', 'build.gradle.kts'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
extensions: ['.c', '.h'],
|
||||||
|
language: 'C',
|
||||||
|
configFiles: ['CMakeLists.txt', 'Makefile'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
extensions: ['.cpp', '.cc', '.cxx', '.hpp'],
|
||||||
|
language: 'C++',
|
||||||
|
configFiles: ['CMakeLists.txt', 'Makefile'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
extensions: ['.cs'],
|
||||||
|
language: 'C#',
|
||||||
|
configFiles: ['*.csproj', '*.sln'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
extensions: ['.rb'],
|
||||||
|
language: 'Ruby',
|
||||||
|
configFiles: ['Gemfile', 'Rakefile'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
extensions: ['.php'],
|
||||||
|
language: 'PHP',
|
||||||
|
configFiles: ['composer.json', 'phpunit.xml'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
extensions: ['.swift'],
|
||||||
|
language: 'Swift',
|
||||||
|
configFiles: ['Package.swift', '*.xcodeproj'],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
private frameworkPatterns: FrameworkPattern[] = [
|
||||||
|
{
|
||||||
|
name: 'React',
|
||||||
|
indicators: ['react', 'react-dom', 'react-scripts'],
|
||||||
|
language: 'TypeScript',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Vue',
|
||||||
|
indicators: ['vue', 'vue-loader', '@vue/'],
|
||||||
|
language: 'TypeScript',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Express',
|
||||||
|
indicators: ['express'],
|
||||||
|
language: 'JavaScript',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Next.js',
|
||||||
|
indicators: ['next'],
|
||||||
|
language: 'TypeScript',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'NestJS',
|
||||||
|
indicators: ['@nestjs/'],
|
||||||
|
language: 'TypeScript',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Django',
|
||||||
|
indicators: ['django'],
|
||||||
|
language: 'Python',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'FastAPI',
|
||||||
|
indicators: ['fastapi', 'uvicorn'],
|
||||||
|
language: 'Python',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Flask',
|
||||||
|
indicators: ['flask'],
|
||||||
|
language: 'Python',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Gin',
|
||||||
|
indicators: ['gin-gonic/gin'],
|
||||||
|
language: 'Go',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Echo',
|
||||||
|
indicators: ['labstack/echo'],
|
||||||
|
language: 'Go',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Actix-web',
|
||||||
|
indicators: ['actix-web'],
|
||||||
|
language: 'Rust',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Rocket',
|
||||||
|
indicators: ['rocket'],
|
||||||
|
language: 'Rust',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Spring Boot',
|
||||||
|
indicators: ['spring-boot'],
|
||||||
|
language: 'Java',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
async detect(dir: string): Promise<ProjectType> {
|
||||||
|
const languages = await this.detectLanguages(dir);
|
||||||
|
const primaryLanguage = await this.determinePrimaryLanguage(languages);
|
||||||
|
const frameworks = await this.detectFrameworks(dir, primaryLanguage);
|
||||||
|
const buildTools = await this.detectBuildTools(dir, languages);
|
||||||
|
|
||||||
|
return {
|
||||||
|
primaryLanguage,
|
||||||
|
languages,
|
||||||
|
frameworks,
|
||||||
|
buildTools,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async determinePrimaryLanguage(
|
||||||
|
languages: string[]
|
||||||
|
): Promise<string> {
|
||||||
|
if (languages.length === 0) return 'Unknown';
|
||||||
|
return languages[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
private async detectLanguages(dir: string): Promise<string[]> {
|
||||||
|
const detectedLanguages: string[] = [];
|
||||||
|
const contents = await fs.promises.readdir(dir, { withFileTypes: true });
|
||||||
|
|
||||||
|
const fileExtensions = new Set<string>();
|
||||||
|
for (const item of contents) {
|
||||||
|
if (item.isFile()) {
|
||||||
|
const ext = path.extname(item.name).toLowerCase();
|
||||||
|
fileExtensions.add(ext);
|
||||||
|
} else if (item.isDirectory()) {
|
||||||
|
const subDirContents = await fs.promises.readdir(
|
||||||
|
path.join(dir, item.name),
|
||||||
|
{ withFileTypes: true }
|
||||||
|
);
|
||||||
|
for (const subItem of subDirContents) {
|
||||||
|
if (subItem.isFile()) {
|
||||||
|
const ext = path.extname(subItem.name).toLowerCase();
|
||||||
|
fileExtensions.add(ext);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const pattern of this.languagePatterns) {
|
||||||
|
for (const ext of pattern.extensions) {
|
||||||
|
if (fileExtensions.has(ext)) {
|
||||||
|
if (!detectedLanguages.includes(pattern.language)) {
|
||||||
|
detectedLanguages.push(pattern.language);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const pattern of this.languagePatterns) {
|
||||||
|
for (const configFile of pattern.configFiles) {
|
||||||
|
const fullPath = path.join(dir, configFile);
|
||||||
|
if (await this.fileExists(fullPath)) {
|
||||||
|
if (!detectedLanguages.includes(pattern.language)) {
|
||||||
|
detectedLanguages.push(pattern.language);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return detectedLanguages;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async detectFrameworks(
|
||||||
|
dir: string,
|
||||||
|
primaryLanguage: string
|
||||||
|
): Promise<string[]> {
|
||||||
|
const detectedFrameworks: string[] = [];
|
||||||
|
|
||||||
|
for (const item of this.frameworkPatterns) {
|
||||||
|
if (item.language !== primaryLanguage) continue;
|
||||||
|
|
||||||
|
const packageJsonPath = path.join(dir, 'package.json');
|
||||||
|
if (await this.fileExists(packageJsonPath)) {
|
||||||
|
const content = await fs.promises.readFile(packageJsonPath, 'utf-8');
|
||||||
|
const packageJson = JSON.parse(content);
|
||||||
|
const allDeps = {
|
||||||
|
...packageJson.dependencies,
|
||||||
|
...packageJson.devDependencies,
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const indicator of item.indicators) {
|
||||||
|
if (Object.keys(allDeps).some(dep => dep.startsWith(indicator))) {
|
||||||
|
if (!detectedFrameworks.includes(item.name)) {
|
||||||
|
detectedFrameworks.push(item.name);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.language === 'Python') {
|
||||||
|
const requirementsPath = path.join(dir, 'requirements.txt');
|
||||||
|
const pyprojectPath = path.join(dir, 'pyproject.toml');
|
||||||
|
|
||||||
|
const filesToCheck = [requirementsPath, pyprojectPath];
|
||||||
|
for (const filePath of filesToCheck) {
|
||||||
|
if (await this.fileExists(filePath)) {
|
||||||
|
const content = await fs.promises.readFile(filePath, 'utf-8');
|
||||||
|
for (const indicator of item.indicators) {
|
||||||
|
if (content.toLowerCase().includes(indicator.toLowerCase())) {
|
||||||
|
if (!detectedFrameworks.includes(item.name)) {
|
||||||
|
detectedFrameworks.push(item.name);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.language === 'Go') {
|
||||||
|
const goModPath = path.join(dir, 'go.mod');
|
||||||
|
if (await this.fileExists(goModPath)) {
|
||||||
|
const content = await fs.promises.readFile(goModPath, 'utf-8');
|
||||||
|
for (const indicator of item.indicators) {
|
||||||
|
if (content.includes(indicator)) {
|
||||||
|
if (!detectedFrameworks.includes(item.name)) {
|
||||||
|
detectedFrameworks.push(item.name);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return detectedFrameworks;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async detectBuildTools(
|
||||||
|
dir: string,
|
||||||
|
_languages: string[]
|
||||||
|
): Promise<string[]> {
|
||||||
|
const buildTools: string[] = [];
|
||||||
|
|
||||||
|
const buildToolFiles: Record<string, string[]> = {
|
||||||
|
npm: ['package.json'],
|
||||||
|
yarn: ['yarn.lock', '.yarnrc'],
|
||||||
|
pnpm: ['pnpm-lock.yaml'],
|
||||||
|
make: ['Makefile'],
|
||||||
|
cmake: ['CMakeLists.txt'],
|
||||||
|
gradle: ['build.gradle', 'build.gradle.kts'],
|
||||||
|
maven: ['pom.xml'],
|
||||||
|
cargo: ['Cargo.toml', 'Cargo.lock'],
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const [tool, files] of Object.entries(buildToolFiles)) {
|
||||||
|
for (const file of files) {
|
||||||
|
const filePath = path.join(dir, file);
|
||||||
|
if (await this.fileExists(filePath)) {
|
||||||
|
if (!buildTools.includes(tool)) {
|
||||||
|
buildTools.push(tool);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return buildTools;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async fileExists(filePath: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
await fs.promises.access(filePath, fs.constants.F_OK);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
99
.ai-context-generator-cli/src/config/configLoader.ts
Normal file
99
.ai-context-generator-cli/src/config/configLoader.ts
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import { ContextConfig } from '../types';
|
||||||
|
|
||||||
|
export class ConfigLoader {
|
||||||
|
private static defaultConfig: ContextConfig = {
|
||||||
|
includes: [
|
||||||
|
'**/*.ts',
|
||||||
|
'**/*.js',
|
||||||
|
'**/*.py',
|
||||||
|
'**/*.go',
|
||||||
|
'**/*.rs',
|
||||||
|
'**/*.java',
|
||||||
|
'**/*.c',
|
||||||
|
'**/*.cpp',
|
||||||
|
'**/*.h',
|
||||||
|
'**/*.hpp',
|
||||||
|
],
|
||||||
|
excludes: [
|
||||||
|
'node_modules/**',
|
||||||
|
'dist/**',
|
||||||
|
'build/**',
|
||||||
|
'.git/**',
|
||||||
|
'.DS_Store',
|
||||||
|
'*.log',
|
||||||
|
],
|
||||||
|
outputFormat: 'json',
|
||||||
|
template: 'default',
|
||||||
|
outputFile: 'ai-context.json',
|
||||||
|
analyzeConventions: true,
|
||||||
|
includeDevDependencies: false,
|
||||||
|
respectGitignore: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
static async load(
|
||||||
|
configPath?: string
|
||||||
|
): Promise<ContextConfig> {
|
||||||
|
if (!configPath) {
|
||||||
|
const localConfig = path.join(process.cwd(), '.ai-context-config.json');
|
||||||
|
if (await this.fileExists(localConfig)) {
|
||||||
|
return this.loadFromFile(localConfig);
|
||||||
|
}
|
||||||
|
return { ...this.defaultConfig };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (await this.fileExists(configPath)) {
|
||||||
|
return this.loadFromFile(configPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Config file not found: ${configPath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async loadFromFile(
|
||||||
|
configPath: string
|
||||||
|
): Promise<ContextConfig> {
|
||||||
|
const content = await fs.promises.readFile(configPath, 'utf-8');
|
||||||
|
const userConfig = JSON.parse(content);
|
||||||
|
|
||||||
|
return this.mergeConfig(userConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static mergeConfig(
|
||||||
|
userConfig: Partial<ContextConfig>
|
||||||
|
): ContextConfig {
|
||||||
|
return {
|
||||||
|
includes: userConfig.includes ?? this.defaultConfig.includes,
|
||||||
|
excludes: userConfig.excludes ?? this.defaultConfig.excludes,
|
||||||
|
outputFormat:
|
||||||
|
userConfig.outputFormat ?? this.defaultConfig.outputFormat,
|
||||||
|
template: userConfig.template ?? this.defaultConfig.template,
|
||||||
|
outputFile:
|
||||||
|
userConfig.outputFile ?? this.defaultConfig.outputFile,
|
||||||
|
analyzeConventions:
|
||||||
|
userConfig.analyzeConventions ?? this.defaultConfig.analyzeConventions,
|
||||||
|
includeDevDependencies:
|
||||||
|
userConfig.includeDevDependencies ??
|
||||||
|
this.defaultConfig.includeDevDependencies,
|
||||||
|
respectGitignore:
|
||||||
|
userConfig.respectGitignore ?? this.defaultConfig.respectGitignore,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async fileExists(filePath: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
await fs.promises.access(filePath, fs.constants.F_OK);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static save(
|
||||||
|
config: ContextConfig,
|
||||||
|
outputPath: string
|
||||||
|
): void {
|
||||||
|
const content = JSON.stringify(config, null, 2);
|
||||||
|
fs.writeFileSync(outputPath, content, 'utf-8');
|
||||||
|
}
|
||||||
|
}
|
||||||
164
.ai-context-generator-cli/src/generators/contextGenerator.ts
Normal file
164
.ai-context-generator-cli/src/generators/contextGenerator.ts
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
import * as path from 'path';
|
||||||
|
import yaml from 'js-yaml';
|
||||||
|
import {
|
||||||
|
ProjectInfo,
|
||||||
|
ContextConfig,
|
||||||
|
TemplateData,
|
||||||
|
FileInfo,
|
||||||
|
ConventionInfo,
|
||||||
|
} from '../types';
|
||||||
|
import { ProjectTypeDetector } from '../analyzers/projectTypeDetector';
|
||||||
|
import { DependencyAnalyzer } from '../analyzers/dependencyAnalyzer';
|
||||||
|
import { ConventionExtractor } from '../analyzers/conventionExtractor';
|
||||||
|
import { FileUtils } from '../utils/fileUtils';
|
||||||
|
import { ConfigLoader } from '../config/configLoader';
|
||||||
|
|
||||||
|
export class ContextGenerator {
|
||||||
|
private projectTypeDetector: ProjectTypeDetector;
|
||||||
|
private dependencyAnalyzer: DependencyAnalyzer;
|
||||||
|
private conventionExtractor: ConventionExtractor;
|
||||||
|
private fileUtils: FileUtils;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.projectTypeDetector = new ProjectTypeDetector();
|
||||||
|
this.dependencyAnalyzer = new DependencyAnalyzer();
|
||||||
|
this.conventionExtractor = new ConventionExtractor();
|
||||||
|
this.fileUtils = FileUtils.getInstance();
|
||||||
|
}
|
||||||
|
|
||||||
|
async generate(
|
||||||
|
dir: string,
|
||||||
|
config?: ContextConfig
|
||||||
|
): Promise<ProjectInfo> {
|
||||||
|
const resolvedDir = this.fileUtils.resolveDirectory(dir);
|
||||||
|
|
||||||
|
const contextConfig = config ?? await ConfigLoader.load();
|
||||||
|
|
||||||
|
if (contextConfig.respectGitignore) {
|
||||||
|
await this.fileUtils.loadGitignore(resolvedDir);
|
||||||
|
this.fileUtils.addCustomPatterns(contextConfig.excludes);
|
||||||
|
}
|
||||||
|
|
||||||
|
const files = await this.fileUtils.getFiles(
|
||||||
|
resolvedDir,
|
||||||
|
contextConfig.includes,
|
||||||
|
contextConfig.excludes
|
||||||
|
);
|
||||||
|
|
||||||
|
const projectType = await this.projectTypeDetector.detect(resolvedDir);
|
||||||
|
const dependencies = await this.dependencyAnalyzer.analyze(
|
||||||
|
resolvedDir,
|
||||||
|
contextConfig.includeDevDependencies
|
||||||
|
);
|
||||||
|
|
||||||
|
let conventions: ConventionInfo | null = null;
|
||||||
|
if (contextConfig.analyzeConventions) {
|
||||||
|
conventions = await this.conventionExtractor.extract(resolvedDir, files) ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
projectType,
|
||||||
|
language: projectType.primaryLanguage,
|
||||||
|
framework: projectType.frameworks[0] || null,
|
||||||
|
dependencies,
|
||||||
|
conventions,
|
||||||
|
fileCount: files.length,
|
||||||
|
analysisDate: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async generateWithFiles(
|
||||||
|
dir: string,
|
||||||
|
config?: ContextConfig
|
||||||
|
): Promise<TemplateData> {
|
||||||
|
const projectInfo = await this.generate(dir, config);
|
||||||
|
const contextConfig = config ?? await ConfigLoader.load();
|
||||||
|
const resolvedDir = this.fileUtils.resolveDirectory(dir);
|
||||||
|
|
||||||
|
const files = await this.fileUtils.getFiles(
|
||||||
|
resolvedDir,
|
||||||
|
contextConfig.includes,
|
||||||
|
contextConfig.excludes
|
||||||
|
);
|
||||||
|
|
||||||
|
const fileInfos: FileInfo[] = [];
|
||||||
|
for (const file of files) {
|
||||||
|
try {
|
||||||
|
const size = await this.fileUtils.getFileSize(file);
|
||||||
|
fileInfos.push({
|
||||||
|
path: path.relative(resolvedDir, file),
|
||||||
|
size,
|
||||||
|
type: this.fileUtils.getFileExtension(file),
|
||||||
|
language: this.detectLanguage(file),
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
// Skip files that can't be read
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
projectInfo,
|
||||||
|
files: fileInfos,
|
||||||
|
config: contextConfig,
|
||||||
|
generatedAt: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async generateJson(dir: string, config?: ContextConfig): Promise<string> {
|
||||||
|
const data = await this.generateWithFiles(dir, config);
|
||||||
|
return JSON.stringify(data, null, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
async generateYaml(dir: string, config?: ContextConfig): Promise<string> {
|
||||||
|
const data = await this.generateWithFiles(dir, config);
|
||||||
|
return yaml.dump(data, { indent: 2, lineWidth: -1 });
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveContext(
|
||||||
|
dir: string,
|
||||||
|
outputPath: string,
|
||||||
|
format: 'json' | 'yaml',
|
||||||
|
config?: ContextConfig
|
||||||
|
): Promise<void> {
|
||||||
|
let content: string;
|
||||||
|
let finalPath = outputPath;
|
||||||
|
|
||||||
|
if (format === 'json') {
|
||||||
|
content = await this.generateJson(dir, config);
|
||||||
|
if (!finalPath.endsWith('.json')) {
|
||||||
|
finalPath = `${finalPath}.json`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
content = await this.generateYaml(dir, config);
|
||||||
|
if (!finalPath.endsWith('.yaml') && !finalPath.endsWith('.yml')) {
|
||||||
|
finalPath = `${finalPath}.yaml`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.fileUtils.writeFile(finalPath, content);
|
||||||
|
}
|
||||||
|
|
||||||
|
private detectLanguage(filePath: string): string {
|
||||||
|
const ext = this.fileUtils.getFileExtension(filePath);
|
||||||
|
const languageMap: Record<string, string> = {
|
||||||
|
'.ts': 'TypeScript',
|
||||||
|
'.tsx': 'TypeScript',
|
||||||
|
'.js': 'JavaScript',
|
||||||
|
'.jsx': 'JavaScript',
|
||||||
|
'.py': 'Python',
|
||||||
|
'.go': 'Go',
|
||||||
|
'.rs': 'Rust',
|
||||||
|
'.java': 'Java',
|
||||||
|
'.c': 'C',
|
||||||
|
'.cpp': 'C++',
|
||||||
|
'.h': 'C',
|
||||||
|
'.hpp': 'C++',
|
||||||
|
'.cs': 'C#',
|
||||||
|
'.rb': 'Ruby',
|
||||||
|
'.php': 'PHP',
|
||||||
|
'.swift': 'Swift',
|
||||||
|
};
|
||||||
|
|
||||||
|
return languageMap[ext] || 'Unknown';
|
||||||
|
}
|
||||||
|
}
|
||||||
40
.ai-context-generator-cli/src/generators/jsonGenerator.ts
Normal file
40
.ai-context-generator-cli/src/generators/jsonGenerator.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { TemplateData } from '../types';
|
||||||
|
|
||||||
|
export class JsonGenerator {
|
||||||
|
static generate(data: TemplateData): string {
|
||||||
|
return JSON.stringify(data, null, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
static generateSummary(data: TemplateData): object {
|
||||||
|
const { projectInfo, files, config, generatedAt } = data;
|
||||||
|
|
||||||
|
return {
|
||||||
|
generatedAt,
|
||||||
|
project: {
|
||||||
|
type: projectInfo.projectType.primaryLanguage,
|
||||||
|
languages: projectInfo.projectType.languages,
|
||||||
|
frameworks: projectInfo.projectType.frameworks,
|
||||||
|
buildTools: projectInfo.projectType.buildTools,
|
||||||
|
fileCount: projectInfo.fileCount,
|
||||||
|
},
|
||||||
|
dependencies: {
|
||||||
|
total: projectInfo.dependencies.total,
|
||||||
|
production: projectInfo.dependencies.direct.length,
|
||||||
|
development: projectInfo.dependencies.dev.length,
|
||||||
|
topDependencies: projectInfo.dependencies.direct
|
||||||
|
.slice(0, 10)
|
||||||
|
.map(d => d.name),
|
||||||
|
},
|
||||||
|
conventions: projectInfo.conventions
|
||||||
|
? {
|
||||||
|
naming: projectInfo.conventions.namingConvention,
|
||||||
|
importStyle: projectInfo.conventions.importStyle.style,
|
||||||
|
testingFramework: projectInfo.conventions.testingFramework,
|
||||||
|
codeStyle: projectInfo.conventions.codeStyle,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
config,
|
||||||
|
sampleFiles: files.slice(0, 20),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
54
.ai-context-generator-cli/src/generators/yamlGenerator.ts
Normal file
54
.ai-context-generator-cli/src/generators/yamlGenerator.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import yaml from 'js-yaml';
|
||||||
|
import { TemplateData } from '../types';
|
||||||
|
|
||||||
|
export class YamlGenerator {
|
||||||
|
static generate(data: TemplateData): string {
|
||||||
|
return yaml.dump(data, {
|
||||||
|
indent: 2,
|
||||||
|
lineWidth: -1,
|
||||||
|
noRefs: true,
|
||||||
|
sortKeys: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static generateSummary(data: TemplateData): string {
|
||||||
|
const summary = {
|
||||||
|
generated_at: data.generatedAt,
|
||||||
|
project: {
|
||||||
|
type: data.projectInfo.projectType.primaryLanguage,
|
||||||
|
languages: data.projectInfo.projectType.languages,
|
||||||
|
frameworks: data.projectInfo.projectType.frameworks,
|
||||||
|
build_tools: data.projectInfo.projectType.buildTools,
|
||||||
|
file_count: data.projectInfo.fileCount,
|
||||||
|
},
|
||||||
|
dependencies: {
|
||||||
|
total: data.projectInfo.dependencies.total,
|
||||||
|
production: data.projectInfo.dependencies.direct.length,
|
||||||
|
development: data.projectInfo.dependencies.dev.length,
|
||||||
|
top_dependencies: data.projectInfo.dependencies.direct
|
||||||
|
.slice(0, 10)
|
||||||
|
.map(d => d.name),
|
||||||
|
},
|
||||||
|
conventions: data.projectInfo.conventions
|
||||||
|
? {
|
||||||
|
naming: {
|
||||||
|
files: data.projectInfo.conventions.namingConvention.files,
|
||||||
|
variables: data.projectInfo.conventions.namingConvention.variables,
|
||||||
|
functions: data.projectInfo.conventions.namingConvention.functions,
|
||||||
|
classes: data.projectInfo.conventions.namingConvention.classes,
|
||||||
|
},
|
||||||
|
import_style: data.projectInfo.conventions.importStyle.style,
|
||||||
|
testing_framework: data.projectInfo.conventions.testingFramework,
|
||||||
|
code_style: {
|
||||||
|
indent_size: data.projectInfo.conventions.codeStyle.indentSize,
|
||||||
|
indent_type: data.projectInfo.conventions.codeStyle.indentType,
|
||||||
|
line_endings: data.projectInfo.conventions.codeStyle.lineEndings,
|
||||||
|
quote_style: data.projectInfo.conventions.codeStyle.quoteStyle,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
return yaml.dump(summary, { indent: 2, lineWidth: -1 });
|
||||||
|
}
|
||||||
|
}
|
||||||
102
.ai-context-generator-cli/src/index.ts
Normal file
102
.ai-context-generator-cli/src/index.ts
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import { Command } from 'commander';
|
||||||
|
import * as path from 'path';
|
||||||
|
import { ContextGenerator } from './generators/contextGenerator';
|
||||||
|
import { TemplateLoader } from './templates/templateLoader';
|
||||||
|
import { ConfigLoader } from './config/configLoader';
|
||||||
|
import { CLIUtils } from './utils/cli';
|
||||||
|
import { FileUtils } from './utils/fileUtils';
|
||||||
|
import { ContextConfig } from './types';
|
||||||
|
|
||||||
|
const packageJson = {
|
||||||
|
name: 'ai-context-generator-cli',
|
||||||
|
version: '1.0.0',
|
||||||
|
description: 'A CLI tool that generates comprehensive context files for AI coding assistants',
|
||||||
|
};
|
||||||
|
|
||||||
|
async function main(): Promise<void> {
|
||||||
|
const program = new Command();
|
||||||
|
|
||||||
|
program
|
||||||
|
.name('ai-context')
|
||||||
|
.description('Generate comprehensive context files for AI coding assistants')
|
||||||
|
.version(packageJson.version);
|
||||||
|
|
||||||
|
program
|
||||||
|
.option('-d, --dir <directory>', 'Project directory to analyze', process.cwd())
|
||||||
|
.option('-o, --output <file>', 'Output file path', 'ai-context')
|
||||||
|
.option('-f, --format <format>', 'Output format (json or yaml)', 'json')
|
||||||
|
.option('-t, --template <template>', 'Template to use (default, cursor, copilot, generic)', 'default')
|
||||||
|
.option('-c, --config <file>', 'Config file path', '')
|
||||||
|
.option('-v, --verbose', 'Enable verbose output', false)
|
||||||
|
.option('--no-conventions', 'Skip convention analysis')
|
||||||
|
.option('--include-dev', 'Include dev dependencies')
|
||||||
|
.option('--no-gitignore', 'Do not respect .gitignore patterns');
|
||||||
|
|
||||||
|
program.parse(process.argv);
|
||||||
|
|
||||||
|
const options = program.opts();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const dir = CLIUtils.resolveDirectory(options.dir);
|
||||||
|
const outputPath = CLIUtils.resolveOutputPath(options.output, options.format as 'json' | 'yaml');
|
||||||
|
|
||||||
|
const fileUtils = FileUtils.getInstance();
|
||||||
|
if (!(await fileUtils.fileExists(dir))) {
|
||||||
|
console.error(`Error: Directory not found: ${dir}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
let config: ContextConfig | undefined;
|
||||||
|
if (options.config) {
|
||||||
|
config = await ConfigLoader.load(options.config);
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
config = await ConfigLoader.load();
|
||||||
|
} catch {
|
||||||
|
config = await ConfigLoader.load();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
config.analyzeConventions = options.conventions !== false;
|
||||||
|
config.includeDevDependencies = options.includeDev === true;
|
||||||
|
config.respectGitignore = options.gitignore !== false;
|
||||||
|
config.outputFormat = options.format as 'json' | 'yaml';
|
||||||
|
config.template = options.template as 'default' | 'cursor' | 'copilot' | 'generic';
|
||||||
|
config.outputFile = options.output;
|
||||||
|
|
||||||
|
if (options.verbose) {
|
||||||
|
console.log('Analyzing directory:', dir);
|
||||||
|
console.log('Output file:', outputPath);
|
||||||
|
console.log('Format:', config.outputFormat);
|
||||||
|
console.log('Template:', config.template);
|
||||||
|
console.log('Analyzing conventions:', config.analyzeConventions);
|
||||||
|
console.log('Including dev deps:', config.includeDevDependencies);
|
||||||
|
console.log('Respecting .gitignore:', config.respectGitignore);
|
||||||
|
}
|
||||||
|
|
||||||
|
const generator = new ContextGenerator();
|
||||||
|
|
||||||
|
if (options.verbose) {
|
||||||
|
console.log('\nGenerating context...');
|
||||||
|
}
|
||||||
|
|
||||||
|
await generator.saveContext(dir, outputPath, config.outputFormat, config);
|
||||||
|
|
||||||
|
if (options.verbose) {
|
||||||
|
console.log(`\nContext saved to: ${outputPath}`);
|
||||||
|
} else {
|
||||||
|
console.log(`Context saved to: ${outputPath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error generating context:', error instanceof Error ? error.message : error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(error => {
|
||||||
|
console.error('Fatal error:', error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
20
.ai-context-generator-cli/src/templates/copilot.template.md
Normal file
20
.ai-context-generator-cli/src/templates/copilot.template.md
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# Project Context for GitHub Copilot
|
||||||
|
|
||||||
|
## Project Details
|
||||||
|
```
|
||||||
|
Language: {{project.primaryLanguage}}
|
||||||
|
Frameworks: {{project.frameworks}}
|
||||||
|
Build Tools: {{project.buildTools}}
|
||||||
|
Files: {{project.fileCount}}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
```
|
||||||
|
{{dependencies}}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Coding Conventions
|
||||||
|
{{conventions}}
|
||||||
|
|
||||||
|
---
|
||||||
|
*Generated by AI Context Generator*
|
||||||
21
.ai-context-generator-cli/src/templates/cursor.template.md
Normal file
21
.ai-context-generator-cli/src/templates/cursor.template.md
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# Project Context for Cursor AI
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
- **Language**: {{project.primaryLanguage}}
|
||||||
|
- **Languages**: {{project.languages}}
|
||||||
|
- **Frameworks**: {{project.frameworks}}
|
||||||
|
- **Build Tools**: {{project.buildTools}}
|
||||||
|
- **Files Analyzed**: {{project.fileCount}}
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
{{dependencies}}
|
||||||
|
|
||||||
|
## Conventions
|
||||||
|
{{conventions}}
|
||||||
|
|
||||||
|
## Key Files
|
||||||
|
{{files}}
|
||||||
|
|
||||||
|
---
|
||||||
|
*Generated by AI Context Generator*
|
||||||
|
*Date: {{generatedAt}}*
|
||||||
28
.ai-context-generator-cli/src/templates/generic.template.md
Normal file
28
.ai-context-generator-cli/src/templates/generic.template.md
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# Project Context - AI Assistant Guide
|
||||||
|
|
||||||
|
## Project Information
|
||||||
|
- **Primary Language**: {{project.primaryLanguage}}
|
||||||
|
- **All Languages**: {{project.languages}}
|
||||||
|
- **Frameworks**: {{project.frameworks}}
|
||||||
|
- **Build Tools**: {{project.buildTools}}
|
||||||
|
|
||||||
|
## Dependency Overview
|
||||||
|
- **Total Dependencies**: {{dependencies.count}}
|
||||||
|
{{dependencies}}
|
||||||
|
|
||||||
|
## Code Analysis
|
||||||
|
### File Statistics
|
||||||
|
- **Total Files**: {{files.count}}
|
||||||
|
- **Naming Convention**: {{conventions.naming}}
|
||||||
|
- **Import Style**: {{conventions.importStyle}}
|
||||||
|
- **Testing Framework**: {{conventions.testing}}
|
||||||
|
|
||||||
|
### Code Style
|
||||||
|
- **Indent**: {{conventions.indent}}
|
||||||
|
- **Quotes**: {{conventions.quotes}}
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
{{files}}
|
||||||
|
|
||||||
|
---
|
||||||
|
*Generated at {{generatedAt}}*
|
||||||
238
.ai-context-generator-cli/src/templates/templateLoader.ts
Normal file
238
.ai-context-generator-cli/src/templates/templateLoader.ts
Normal file
@@ -0,0 +1,238 @@
|
|||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import { TemplateData } from '../types';
|
||||||
|
|
||||||
|
export class TemplateLoader {
|
||||||
|
private templates: Map<string, (data: TemplateData) => string>;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.templates = new Map();
|
||||||
|
this.registerDefaultTemplates();
|
||||||
|
}
|
||||||
|
|
||||||
|
private registerDefaultTemplates(): void {
|
||||||
|
this.templates.set('default', this.renderDefaultTemplate.bind(this));
|
||||||
|
this.templates.set('cursor', this.renderCursorTemplate.bind(this));
|
||||||
|
this.templates.set('copilot', this.renderCopilotTemplate.bind(this));
|
||||||
|
this.templates.set('generic', this.renderGenericTemplate.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadTemplate(name: string): Promise<(data: TemplateData) => string> {
|
||||||
|
const builtInTemplates = ['default', 'cursor', 'copilot', 'generic'];
|
||||||
|
|
||||||
|
if (builtInTemplates.includes(name)) {
|
||||||
|
return this.templates.get(name)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
const templatePath = path.resolve(name);
|
||||||
|
if (await this.fileExists(templatePath)) {
|
||||||
|
const content = await fs.promises.readFile(templatePath, 'utf-8');
|
||||||
|
return this.compileTemplate(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
const customTemplateDir = path.join(process.cwd(), 'templates');
|
||||||
|
const customTemplatePath = path.join(customTemplateDir, `${name}.template`);
|
||||||
|
if (await this.fileExists(customTemplatePath)) {
|
||||||
|
const content = await fs.promises.readFile(customTemplatePath, 'utf-8');
|
||||||
|
return this.compileTemplate(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Template not found: ${name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
compileTemplate(templateContent: string): (data: TemplateData) => string {
|
||||||
|
return (data: TemplateData): string => {
|
||||||
|
let result = templateContent;
|
||||||
|
|
||||||
|
result = result.replace(/\{\{\s*project\.type\s*\}\}/g,
|
||||||
|
data.projectInfo.projectType.primaryLanguage);
|
||||||
|
result = result.replace(/\{\{\s*project\.languages\s*\}\}/g,
|
||||||
|
data.projectInfo.projectType.languages.join(', '));
|
||||||
|
result = result.replace(/\{\{\s*project\.frameworks\s*\}\}/g,
|
||||||
|
data.projectInfo.projectType.frameworks.join(', '));
|
||||||
|
result = result.replace(/\{\{\s*project\.fileCount\s*\}\}/g,
|
||||||
|
String(data.projectInfo.fileCount));
|
||||||
|
result = result.replace(/\{\{\s*dependencies\.total\s*\}\}/g,
|
||||||
|
String(data.projectInfo.dependencies.total));
|
||||||
|
result = result.replace(/\{\{\s*generatedAt\s*\}\}/g,
|
||||||
|
data.generatedAt);
|
||||||
|
|
||||||
|
result = this.renderDependencies(result, data);
|
||||||
|
result = this.renderConventions(result, data);
|
||||||
|
result = this.renderFileList(result, data);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderDefaultTemplate(data: TemplateData): string {
|
||||||
|
const jsonOutput = JSON.stringify({
|
||||||
|
project: data.projectInfo,
|
||||||
|
files: data.files,
|
||||||
|
generatedAt: data.generatedAt,
|
||||||
|
}, null, 2);
|
||||||
|
|
||||||
|
return `## AI Context
|
||||||
|
|
||||||
|
\`\`\`json
|
||||||
|
${jsonOutput}
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
- **Language**: ${data.projectInfo.projectType.primaryLanguage}
|
||||||
|
- **Frameworks**: ${data.projectInfo.projectType.frameworks.join(', ') || 'None detected'}
|
||||||
|
- **Dependencies**: ${data.projectInfo.dependencies.total}
|
||||||
|
- **Files Analyzed**: ${data.projectInfo.fileCount}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderCursorTemplate(data: TemplateData): string {
|
||||||
|
const topDeps = data.projectInfo.dependencies.direct
|
||||||
|
.slice(0, 15)
|
||||||
|
.map(d => ` - ${d.name}@${d.version}`)
|
||||||
|
.join('\n');
|
||||||
|
|
||||||
|
return `## Project Context
|
||||||
|
|
||||||
|
**Language**: ${data.projectInfo.projectType.primaryLanguage}
|
||||||
|
**Frameworks**: ${data.projectInfo.projectType.frameworks.join(', ') || 'None'}
|
||||||
|
**Build Tools**: ${data.projectInfo.projectType.buildTools.join(', ') || 'None'}
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
${topDeps || ' No dependencies detected'}
|
||||||
|
|
||||||
|
### Conventions
|
||||||
|
${data.projectInfo.conventions ? `
|
||||||
|
- **File Naming**: ${data.projectInfo.conventions.namingConvention.files}
|
||||||
|
- **Import Style**: ${data.projectInfo.conventions.importStyle.style}
|
||||||
|
- **Testing Framework**: ${data.projectInfo.conventions.testingFramework || 'None'}
|
||||||
|
- **Code Style**:
|
||||||
|
- Indent: ${data.projectInfo.conventions.codeStyle.indentSize} ${data.projectInfo.conventions.codeStyle.indentType}
|
||||||
|
- Quotes: ${data.projectInfo.conventions.codeStyle.quoteStyle}
|
||||||
|
` : ' Not analyzed'}
|
||||||
|
|
||||||
|
### Key Files
|
||||||
|
${data.files.slice(0, 10).map(f => `- \`${f.path}\``).join('\n')}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderCopilotTemplate(data: TemplateData): string {
|
||||||
|
const deps = data.projectInfo.dependencies.direct
|
||||||
|
.map(d => ` "${d.name}": "${d.version}"`)
|
||||||
|
.join(',\n');
|
||||||
|
|
||||||
|
return `/* Project Context */
|
||||||
|
Language: ${data.projectInfo.projectType.primaryLanguage}
|
||||||
|
Frameworks: ${data.projectInfo.projectType.frameworks.join(', ') || 'None'}
|
||||||
|
Dependencies: ${data.projectInfo.dependencies.total}
|
||||||
|
|
||||||
|
/* Dependencies */
|
||||||
|
{
|
||||||
|
${deps}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Conventions */
|
||||||
|
File Naming: ${data.projectInfo.conventions?.namingConvention.files || 'Unknown'}
|
||||||
|
Import Style: ${data.projectInfo.conventions?.importStyle.style || 'Unknown'}
|
||||||
|
Testing: ${data.projectInfo.conventions?.testingFramework || 'None'}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderGenericTemplate(data: TemplateData): string {
|
||||||
|
return `=== PROJECT CONTEXT ===
|
||||||
|
|
||||||
|
Project Type: ${data.projectInfo.projectType.primaryLanguage}
|
||||||
|
Languages: ${data.projectInfo.projectType.languages.join(', ')}
|
||||||
|
Frameworks: ${data.projectInfo.projectType.frameworks.join(', ') || 'None'}
|
||||||
|
Build Tools: ${data.projectInfo.projectType.buildTools.join(', ') || 'None'}
|
||||||
|
|
||||||
|
=== DEPENDENCIES ===
|
||||||
|
Total: ${data.projectInfo.dependencies.total}
|
||||||
|
Production: ${data.projectInfo.dependencies.direct.length}
|
||||||
|
Development: ${data.projectInfo.dependencies.dev.length}
|
||||||
|
|
||||||
|
Top Dependencies:
|
||||||
|
${data.projectInfo.dependencies.direct.slice(0, 10).map(d => ` - ${d.name} (${d.version})`).join('\n')}
|
||||||
|
|
||||||
|
=== CONVENTIONS ===
|
||||||
|
${data.projectInfo.conventions ? `
|
||||||
|
Naming:
|
||||||
|
Files: ${data.projectInfo.conventions.namingConvention.files}
|
||||||
|
Variables: ${data.projectInfo.conventions.namingConvention.variables}
|
||||||
|
Functions: ${data.projectInfo.conventions.namingConvention.functions}
|
||||||
|
Classes: ${data.projectInfo.conventions.namingConvention.classes}
|
||||||
|
|
||||||
|
Import Style: ${data.projectInfo.conventions.importStyle.style}
|
||||||
|
${data.projectInfo.conventions.importStyle.aliasPrefix ? `Alias Prefix: ${data.projectInfo.conventions.importStyle.aliasPrefix}` : ''}
|
||||||
|
Testing Framework: ${data.projectInfo.conventions.testingFramework || 'None'}
|
||||||
|
|
||||||
|
Code Style:
|
||||||
|
Indent: ${data.projectInfo.conventions.codeStyle.indentSize} ${data.projectInfo.conventions.codeStyle.indentType}
|
||||||
|
Line Endings: ${data.projectInfo.conventions.codeStyle.lineEndings}
|
||||||
|
Quote Style: ${data.projectInfo.conventions.codeStyle.quoteStyle}
|
||||||
|
` : ' Not analyzed'}
|
||||||
|
|
||||||
|
=== FILES ===
|
||||||
|
Total Files: ${data.projectInfo.fileCount}
|
||||||
|
${data.files.slice(0, 20).map(f => ` - ${f.path}`).join('\n')}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderDependencies(
|
||||||
|
template: string,
|
||||||
|
data: TemplateData
|
||||||
|
): string {
|
||||||
|
const deps = data.projectInfo.dependencies.direct
|
||||||
|
.map(d => ` - ${d.name}@${d.version}`)
|
||||||
|
.join('\n');
|
||||||
|
|
||||||
|
return template
|
||||||
|
.replace(/\{\{\s*dependencies\s*\}\}/g, deps)
|
||||||
|
.replace(/\{\{\s*dependencies\.count\s*\}\}/g,
|
||||||
|
String(data.projectInfo.dependencies.total));
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderConventions(
|
||||||
|
template: string,
|
||||||
|
data: TemplateData
|
||||||
|
): string {
|
||||||
|
if (!data.projectInfo.conventions) {
|
||||||
|
return template;
|
||||||
|
}
|
||||||
|
|
||||||
|
const conventions = data.projectInfo.conventions;
|
||||||
|
|
||||||
|
return template
|
||||||
|
.replace(/\{\{\s*conventions\.naming\s*\}\}/g,
|
||||||
|
conventions.namingConvention.files)
|
||||||
|
.replace(/\{\{\s*conventions\.importStyle\s*\}\}/g,
|
||||||
|
conventions.importStyle.style)
|
||||||
|
.replace(/\{\{\s*conventions\.testing\s*\}\}/g,
|
||||||
|
conventions.testingFramework || 'None');
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderFileList(
|
||||||
|
template: string,
|
||||||
|
data: TemplateData
|
||||||
|
): string {
|
||||||
|
const fileList = data.files
|
||||||
|
.slice(0, 30)
|
||||||
|
.map(f => ` - ${f.path}`)
|
||||||
|
.join('\n');
|
||||||
|
|
||||||
|
return template
|
||||||
|
.replace(/\{\{\s*files\s*\}\}/g, fileList)
|
||||||
|
.replace(/\{\{\s*files\.count\s*\}\}/g,
|
||||||
|
String(data.projectInfo.fileCount));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async fileExists(filePath: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
await fs.promises.access(filePath, fs.constants.F_OK);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
90
.ai-context-generator-cli/src/types.ts
Normal file
90
.ai-context-generator-cli/src/types.ts
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
export interface CLIOptions {
|
||||||
|
dir: string;
|
||||||
|
output: string;
|
||||||
|
format: 'json' | 'yaml';
|
||||||
|
template: string;
|
||||||
|
config: string;
|
||||||
|
verbose: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProjectInfo {
|
||||||
|
projectType: ProjectType;
|
||||||
|
language: string;
|
||||||
|
framework: string | null;
|
||||||
|
dependencies: DependencyInfo;
|
||||||
|
conventions: ConventionInfo | null;
|
||||||
|
fileCount: number;
|
||||||
|
analysisDate: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProjectType {
|
||||||
|
primaryLanguage: string;
|
||||||
|
languages: string[];
|
||||||
|
frameworks: string[];
|
||||||
|
buildTools: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DependencyInfo {
|
||||||
|
direct: Dependency[];
|
||||||
|
dev: Dependency[];
|
||||||
|
total: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Dependency {
|
||||||
|
name: string;
|
||||||
|
version: string;
|
||||||
|
type: 'prod' | 'dev';
|
||||||
|
isLocal: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ConventionInfo {
|
||||||
|
namingConvention: NamingConvention;
|
||||||
|
importStyle: ImportStyle;
|
||||||
|
testingFramework: string | null;
|
||||||
|
codeStyle: CodeStyle;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NamingConvention {
|
||||||
|
files: 'camelCase' | 'snake_case' | 'kebab-case' | 'PascalCase';
|
||||||
|
variables: 'camelCase' | 'snake_case' | 'kebab-case' | 'PascalCase';
|
||||||
|
functions: 'camelCase' | 'snake_case' | 'kebab-case' | 'PascalCase';
|
||||||
|
classes: 'PascalCase' | 'camelCase' | 'snake_case' | 'kebab-case';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ImportStyle {
|
||||||
|
style: 'ESM' | 'CommonJS' | 'mixed';
|
||||||
|
aliasPrefix: string | null;
|
||||||
|
commonPatterns: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CodeStyle {
|
||||||
|
indentSize: number;
|
||||||
|
indentType: 'spaces' | 'tabs';
|
||||||
|
lineEndings: 'LF' | 'CRLF';
|
||||||
|
quoteStyle: 'single' | 'double';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ContextConfig {
|
||||||
|
includes: string[];
|
||||||
|
excludes: string[];
|
||||||
|
outputFormat: 'json' | 'yaml';
|
||||||
|
template: 'cursor' | 'copilot' | 'generic' | 'default';
|
||||||
|
outputFile: string;
|
||||||
|
analyzeConventions: boolean;
|
||||||
|
includeDevDependencies: boolean;
|
||||||
|
respectGitignore: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TemplateData {
|
||||||
|
projectInfo: ProjectInfo;
|
||||||
|
files: FileInfo[];
|
||||||
|
config: ContextConfig;
|
||||||
|
generatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FileInfo {
|
||||||
|
path: string;
|
||||||
|
size: number;
|
||||||
|
type: string;
|
||||||
|
language: string;
|
||||||
|
}
|
||||||
39
.ai-context-generator-cli/src/utils/cli.ts
Normal file
39
.ai-context-generator-cli/src/utils/cli.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import * as path from 'path';
|
||||||
|
|
||||||
|
export class CLIUtils {
|
||||||
|
static resolveDirectory(dir: string): string {
|
||||||
|
if (path.isAbsolute(dir)) {
|
||||||
|
return dir;
|
||||||
|
}
|
||||||
|
return path.resolve(process.cwd(), dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
static resolveOutputPath(
|
||||||
|
output: string,
|
||||||
|
format: 'json' | 'yaml'
|
||||||
|
): string {
|
||||||
|
if (path.isAbsolute(output)) {
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!output.endsWith(`.${format}`)) {
|
||||||
|
return `${output}.${format}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
static formatBytes(bytes: number): string {
|
||||||
|
if (bytes === 0) return '0 B';
|
||||||
|
const k = 1024;
|
||||||
|
const sizes = ['B', 'KB', 'MB', 'GB'];
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
|
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static sanitizePattern(pattern: string): string {
|
||||||
|
return pattern
|
||||||
|
.replace(/\*/g, '.*')
|
||||||
|
.replace(/\?/g, '.');
|
||||||
|
}
|
||||||
|
}
|
||||||
125
.ai-context-generator-cli/src/utils/fileUtils.ts
Normal file
125
.ai-context-generator-cli/src/utils/fileUtils.ts
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import ignore from 'ignore';
|
||||||
|
import { glob } from 'glob';
|
||||||
|
|
||||||
|
export class FileUtils {
|
||||||
|
private static instance: FileUtils;
|
||||||
|
private ig: ReturnType<typeof ignore>;
|
||||||
|
|
||||||
|
private constructor() {
|
||||||
|
this.ig = ignore();
|
||||||
|
}
|
||||||
|
|
||||||
|
static getInstance(): FileUtils {
|
||||||
|
if (!FileUtils.instance) {
|
||||||
|
FileUtils.instance = new FileUtils();
|
||||||
|
}
|
||||||
|
return FileUtils.instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadGitignore(dir: string): Promise<void> {
|
||||||
|
const gitignorePath = path.join(dir, '.gitignore');
|
||||||
|
if (await this.fileExists(gitignorePath)) {
|
||||||
|
const content = await this.readFile(gitignorePath);
|
||||||
|
const patterns = content
|
||||||
|
.split('\n')
|
||||||
|
.filter(line => line.trim() && !line.trim().startsWith('#'));
|
||||||
|
this.ig.add(patterns);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addCustomPatterns(patterns: string[]): void {
|
||||||
|
this.ig.add(patterns);
|
||||||
|
}
|
||||||
|
|
||||||
|
isIgnored(filePath: string): boolean {
|
||||||
|
const relativePath = path.relative(process.cwd(), filePath);
|
||||||
|
return this.ig.ignores(relativePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
resolveDirectory(dir: string): string {
|
||||||
|
if (path.isAbsolute(dir)) {
|
||||||
|
return dir;
|
||||||
|
}
|
||||||
|
return path.resolve(process.cwd(), dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getFiles(
|
||||||
|
dir: string,
|
||||||
|
includes: string[],
|
||||||
|
excludes: string[]
|
||||||
|
): Promise<string[]> {
|
||||||
|
const allFiles: string[] = [];
|
||||||
|
|
||||||
|
for (const pattern of includes) {
|
||||||
|
const files = await glob(pattern, {
|
||||||
|
cwd: dir,
|
||||||
|
ignore: excludes,
|
||||||
|
absolute: true,
|
||||||
|
});
|
||||||
|
allFiles.push(...files);
|
||||||
|
}
|
||||||
|
|
||||||
|
const uniqueFiles = [...new Set(allFiles)].filter(
|
||||||
|
file => !this.isIgnored(file)
|
||||||
|
);
|
||||||
|
|
||||||
|
return uniqueFiles.sort();
|
||||||
|
}
|
||||||
|
|
||||||
|
async fileExists(filePath: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
await fs.promises.access(filePath, fs.constants.F_OK);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async readFile(filePath: string): Promise<string> {
|
||||||
|
return fs.promises.readFile(filePath, 'utf-8');
|
||||||
|
}
|
||||||
|
|
||||||
|
async writeFile(
|
||||||
|
filePath: string,
|
||||||
|
content: string
|
||||||
|
): Promise<void> {
|
||||||
|
const dir = path.dirname(filePath);
|
||||||
|
if (!(await this.fileExists(dir))) {
|
||||||
|
await fs.promises.mkdir(dir, { recursive: true });
|
||||||
|
}
|
||||||
|
await fs.promises.writeFile(filePath, content, 'utf-8');
|
||||||
|
}
|
||||||
|
|
||||||
|
async getFileSize(filePath: string): Promise<number> {
|
||||||
|
const stats = await fs.promises.stat(filePath);
|
||||||
|
return stats.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getDirectoryContents(
|
||||||
|
dir: string
|
||||||
|
): Promise<string[]> {
|
||||||
|
try {
|
||||||
|
const entries = await fs.promises.readdir(dir, {
|
||||||
|
withFileTypes: true,
|
||||||
|
});
|
||||||
|
return entries.map(entry => entry.name);
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getFileExtension(filePath: string): string {
|
||||||
|
return path.extname(filePath).toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
isTextFile(filePath: string): boolean {
|
||||||
|
const textExtensions = [
|
||||||
|
'.ts', '.js', '.py', '.go', '.rs', '.java', '.c', '.cpp',
|
||||||
|
'.h', '.hpp', '.json', '.yaml', '.yml', '.xml', '.html',
|
||||||
|
'.css', '.scss', '.md', '.txt', '.sql', '.sh', '.bash',
|
||||||
|
];
|
||||||
|
return textExtensions.includes(this.getFileExtension(filePath));
|
||||||
|
}
|
||||||
|
}
|
||||||
181
.ai-context-generator-cli/tests/cli.test.ts
Normal file
181
.ai-context-generator-cli/tests/cli.test.ts
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import { execSync } from 'child_process';
|
||||||
|
|
||||||
|
describe('CLI', () => {
|
||||||
|
let testDir: string;
|
||||||
|
const projectPath = path.join(__dirname, '..');
|
||||||
|
const cliPath = path.join(projectPath, 'dist', 'index.js');
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
if (!fs.existsSync(cliPath)) {
|
||||||
|
throw new Error('CLI not built. Run npm run build first.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
testDir = path.join(__dirname, 'test-projects', `test-${Date.now()}`);
|
||||||
|
await fs.promises.mkdir(testDir, { recursive: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
if (await fs.promises.stat(testDir).catch(() => null)) {
|
||||||
|
await fs.promises.rm(testDir, { recursive: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('--version', () => {
|
||||||
|
it('should display version information', () => {
|
||||||
|
const output = execSync(`node ${cliPath} --version`, { encoding: 'utf-8' });
|
||||||
|
expect(output).toContain('1.0.0');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('--help', () => {
|
||||||
|
it('should display help information', () => {
|
||||||
|
const output = execSync(`node ${cliPath} --help`, { encoding: 'utf-8' });
|
||||||
|
expect(output).toContain('Usage:');
|
||||||
|
expect(output).toContain('Options:');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('--dir option', () => {
|
||||||
|
it('should analyze specified directory', async () => {
|
||||||
|
await fs.promises.writeFile(
|
||||||
|
path.join(testDir, 'tsconfig.json'),
|
||||||
|
JSON.stringify({})
|
||||||
|
);
|
||||||
|
await fs.promises.writeFile(
|
||||||
|
path.join(testDir, 'index.ts'),
|
||||||
|
'const x = 1;'
|
||||||
|
);
|
||||||
|
|
||||||
|
const outputPath = path.join(testDir, 'output.json');
|
||||||
|
execSync(
|
||||||
|
`node ${cliPath} --dir "${testDir}" --output "${outputPath}" --no-conventions`,
|
||||||
|
{ encoding: 'utf-8' }
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(fs.existsSync(outputPath)).toBe(true);
|
||||||
|
const content = fs.readFileSync(outputPath, 'utf-8');
|
||||||
|
expect(() => JSON.parse(content)).not.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('--output option', () => {
|
||||||
|
it('should save to specified file path', async () => {
|
||||||
|
await fs.promises.writeFile(path.join(testDir, 'tsconfig.json'), '{}');
|
||||||
|
|
||||||
|
const customPath = path.join(testDir, 'custom-context');
|
||||||
|
execSync(
|
||||||
|
`node ${cliPath} --dir "${testDir}" --output "${customPath}" --format json --no-conventions`,
|
||||||
|
{ encoding: 'utf-8' }
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(fs.existsSync(`${customPath}.json`)).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('--format option', () => {
|
||||||
|
it('should generate JSON output', async () => {
|
||||||
|
await fs.promises.writeFile(path.join(testDir, 'tsconfig.json'), '{}');
|
||||||
|
|
||||||
|
const outputPath = path.join(testDir, 'output');
|
||||||
|
execSync(
|
||||||
|
`node ${cliPath} --dir "${testDir}" --output "${outputPath}" --format json --no-conventions`,
|
||||||
|
{ encoding: 'utf-8' }
|
||||||
|
);
|
||||||
|
|
||||||
|
const content = fs.readFileSync(`${outputPath}.json`, 'utf-8');
|
||||||
|
expect(() => JSON.parse(content)).not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should generate YAML output', async () => {
|
||||||
|
await fs.promises.writeFile(path.join(testDir, 'tsconfig.json'), '{}');
|
||||||
|
|
||||||
|
const outputPath = path.join(testDir, 'output');
|
||||||
|
execSync(
|
||||||
|
`node ${cliPath} --dir "${testDir}" --output "${outputPath}" --format yaml --no-conventions`,
|
||||||
|
{ encoding: 'utf-8' }
|
||||||
|
);
|
||||||
|
|
||||||
|
const content = fs.readFileSync(`${outputPath}.yaml`, 'utf-8');
|
||||||
|
expect(content).toContain('projectInfo:');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('--template option', () => {
|
||||||
|
it('should accept default template', async () => {
|
||||||
|
await fs.promises.writeFile(path.join(testDir, 'tsconfig.json'), '{}');
|
||||||
|
|
||||||
|
const outputPath = path.join(testDir, 'output');
|
||||||
|
execSync(
|
||||||
|
`node ${cliPath} --dir "${testDir}" --output "${outputPath}" --template default --no-conventions`,
|
||||||
|
{ encoding: 'utf-8' }
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(fs.existsSync(`${outputPath}.json`)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should accept cursor template', async () => {
|
||||||
|
await fs.promises.writeFile(path.join(testDir, 'tsconfig.json'), '{}');
|
||||||
|
|
||||||
|
const outputPath = path.join(testDir, 'output');
|
||||||
|
execSync(
|
||||||
|
`node ${cliPath} --dir "${testDir}" --output "${outputPath}" --template cursor --no-conventions`,
|
||||||
|
{ encoding: 'utf-8' }
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(fs.existsSync(`${outputPath}.json`)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should accept copilot template', async () => {
|
||||||
|
await fs.promises.writeFile(path.join(testDir, 'tsconfig.json'), '{}');
|
||||||
|
|
||||||
|
const outputPath = path.join(testDir, 'output');
|
||||||
|
execSync(
|
||||||
|
`node ${cliPath} --dir "${testDir}" --output "${outputPath}" --template copilot --no-conventions`,
|
||||||
|
{ encoding: 'utf-8' }
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(fs.existsSync(`${outputPath}.json`)).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('--no-conventions option', () => {
|
||||||
|
it('should skip convention analysis', async () => {
|
||||||
|
await fs.promises.writeFile(path.join(testDir, 'tsconfig.json'), '{}');
|
||||||
|
|
||||||
|
const outputPath = path.join(testDir, 'output');
|
||||||
|
execSync(
|
||||||
|
`node ${cliPath} --dir "${testDir}" --output "${outputPath}" --no-conventions`,
|
||||||
|
{ encoding: 'utf-8' }
|
||||||
|
);
|
||||||
|
|
||||||
|
const content = fs.readFileSync(`${outputPath}.json`, 'utf-8');
|
||||||
|
const parsed = JSON.parse(content);
|
||||||
|
expect(parsed.projectInfo.conventions).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('--verbose option', () => {
|
||||||
|
it('should output verbose information', () => {
|
||||||
|
const output = execSync(
|
||||||
|
`node ${cliPath} --dir . --output /dev/null --format json --verbose --no-conventions 2>&1 || true`,
|
||||||
|
{ encoding: 'utf-8' }
|
||||||
|
);
|
||||||
|
expect(output).toContain('Analyzing directory');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('error handling', () => {
|
||||||
|
it('should handle non-existent directory', () => {
|
||||||
|
expect(() => {
|
||||||
|
execSync(
|
||||||
|
`node ${cliPath} --dir "/non/existent/path" --output /dev/null --no-conventions`,
|
||||||
|
{ encoding: 'utf-8' }
|
||||||
|
);
|
||||||
|
}).toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
162
.ai-context-generator-cli/tests/contextGenerator.test.ts
Normal file
162
.ai-context-generator-cli/tests/contextGenerator.test.ts
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import { ContextGenerator } from '../src/generators/contextGenerator';
|
||||||
|
import { ContextConfig } from '../src/types';
|
||||||
|
|
||||||
|
describe('ContextGenerator', () => {
|
||||||
|
let generator: ContextGenerator;
|
||||||
|
let testDir: string;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
generator = new ContextGenerator();
|
||||||
|
testDir = path.join(__dirname, 'test-projects', `test-${Date.now()}`);
|
||||||
|
await fs.promises.mkdir(testDir, { recursive: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
if (await fs.promises.stat(testDir).catch(() => null)) {
|
||||||
|
await fs.promises.rm(testDir, { recursive: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('generate', () => {
|
||||||
|
it('should generate project info for TypeScript project', async () => {
|
||||||
|
await fs.promises.writeFile(
|
||||||
|
path.join(testDir, 'tsconfig.json'),
|
||||||
|
JSON.stringify({ compilerOptions: { target: 'ES2020' } })
|
||||||
|
);
|
||||||
|
await fs.promises.writeFile(
|
||||||
|
path.join(testDir, 'package.json'),
|
||||||
|
JSON.stringify({
|
||||||
|
dependencies: { express: '^4.18.0' },
|
||||||
|
devDependencies: { jest: '^29.0.0' },
|
||||||
|
})
|
||||||
|
);
|
||||||
|
await fs.promises.writeFile(
|
||||||
|
path.join(testDir, 'index.ts'),
|
||||||
|
'const x: string = "hello";\nexport { x };'
|
||||||
|
);
|
||||||
|
|
||||||
|
const config: ContextConfig = {
|
||||||
|
includes: ['**/*.ts', '**/*.json'],
|
||||||
|
excludes: [],
|
||||||
|
outputFormat: 'json',
|
||||||
|
template: 'default',
|
||||||
|
outputFile: 'test.json',
|
||||||
|
analyzeConventions: false,
|
||||||
|
includeDevDependencies: false,
|
||||||
|
respectGitignore: false,
|
||||||
|
};
|
||||||
|
const result = await generator.generate(testDir, config);
|
||||||
|
|
||||||
|
expect(result.projectType.primaryLanguage).toBe('TypeScript');
|
||||||
|
expect(result.fileCount).toBe(3);
|
||||||
|
expect(result.analysisDate).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should include conventions when enabled', async () => {
|
||||||
|
await fs.promises.writeFile(
|
||||||
|
path.join(testDir, 'tsconfig.json'),
|
||||||
|
JSON.stringify({})
|
||||||
|
);
|
||||||
|
await fs.promises.writeFile(
|
||||||
|
path.join(testDir, 'index.ts'),
|
||||||
|
`const myVariable = "test";
|
||||||
|
function myFunction() {
|
||||||
|
return myVariable;
|
||||||
|
}
|
||||||
|
export class MyClass {}`
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await generator.generate(testDir);
|
||||||
|
|
||||||
|
expect(result.conventions).toBeDefined();
|
||||||
|
expect(result.conventions?.namingConvention.files).toBeDefined();
|
||||||
|
expect(result.conventions?.importStyle).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should analyze file count correctly', async () => {
|
||||||
|
await fs.promises.writeFile(path.join(testDir, 'file1.ts'), '// file 1');
|
||||||
|
await fs.promises.writeFile(path.join(testDir, 'file2.ts'), '// file 2');
|
||||||
|
await fs.promises.writeFile(path.join(testDir, 'file3.ts'), '// file 3');
|
||||||
|
await fs.promises.writeFile(path.join(testDir, 'tsconfig.json'), '{}');
|
||||||
|
|
||||||
|
const config: ContextConfig = {
|
||||||
|
includes: ['**/*.ts', '**/*.json'],
|
||||||
|
excludes: [],
|
||||||
|
outputFormat: 'json',
|
||||||
|
template: 'default',
|
||||||
|
outputFile: 'test.json',
|
||||||
|
analyzeConventions: false,
|
||||||
|
includeDevDependencies: false,
|
||||||
|
respectGitignore: false,
|
||||||
|
};
|
||||||
|
const result = await generator.generate(testDir, config);
|
||||||
|
|
||||||
|
expect(result.fileCount).toBe(4);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('generateJson', () => {
|
||||||
|
it('should generate valid JSON output', async () => {
|
||||||
|
await fs.promises.writeFile(path.join(testDir, 'tsconfig.json'), '{}');
|
||||||
|
await fs.promises.writeFile(path.join(testDir, 'index.ts'), 'const x = 1;');
|
||||||
|
|
||||||
|
const result = await generator.generateJson(testDir);
|
||||||
|
|
||||||
|
expect(() => JSON.parse(result)).not.toThrow();
|
||||||
|
const parsed = JSON.parse(result);
|
||||||
|
expect(parsed.projectInfo).toBeDefined();
|
||||||
|
expect(parsed.files).toBeDefined();
|
||||||
|
expect(parsed.generatedAt).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('generateYaml', () => {
|
||||||
|
it('should generate valid YAML output', async () => {
|
||||||
|
await fs.promises.writeFile(path.join(testDir, 'tsconfig.json'), '{}');
|
||||||
|
await fs.promises.writeFile(path.join(testDir, 'index.ts'), 'const x = 1;');
|
||||||
|
|
||||||
|
const result = await generator.generateYaml(testDir);
|
||||||
|
|
||||||
|
expect(result).toContain('projectInfo:');
|
||||||
|
expect(result).toContain('files:');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('saveContext', () => {
|
||||||
|
it('should save JSON file correctly', async () => {
|
||||||
|
const outputPath = path.join(testDir, 'output');
|
||||||
|
await fs.promises.writeFile(path.join(testDir, 'tsconfig.json'), '{}');
|
||||||
|
await fs.promises.writeFile(path.join(testDir, 'index.ts'), 'const x = 1;');
|
||||||
|
|
||||||
|
await generator.saveContext(testDir, outputPath, 'json');
|
||||||
|
|
||||||
|
const filePath = outputPath.endsWith('.json') ? outputPath : `${outputPath}.json`;
|
||||||
|
const content = await fs.promises.readFile(filePath, 'utf-8');
|
||||||
|
expect(() => JSON.parse(content)).not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should save YAML file correctly', async () => {
|
||||||
|
const outputPath = path.join(testDir, 'output');
|
||||||
|
await fs.promises.writeFile(path.join(testDir, 'tsconfig.json'), '{}');
|
||||||
|
await fs.promises.writeFile(path.join(testDir, 'index.ts'), 'const x = 1;');
|
||||||
|
|
||||||
|
await generator.saveContext(testDir, outputPath, 'yaml');
|
||||||
|
|
||||||
|
const filePath = outputPath.endsWith('.yaml') ? outputPath : `${outputPath}.yaml`;
|
||||||
|
const content = await fs.promises.readFile(filePath, 'utf-8');
|
||||||
|
expect(content).toContain('projectInfo:');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should auto-add extension when missing', async () => {
|
||||||
|
const outputPath = path.join(testDir, 'output');
|
||||||
|
await fs.promises.writeFile(path.join(testDir, 'tsconfig.json'), '{}');
|
||||||
|
|
||||||
|
await generator.saveContext(testDir, outputPath, 'json');
|
||||||
|
|
||||||
|
const exists = await fs.promises.access(`${outputPath}.json`).then(() => true).catch(() => false);
|
||||||
|
expect(exists).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
225
.ai-context-generator-cli/tests/dependencyAnalyzer.test.ts
Normal file
225
.ai-context-generator-cli/tests/dependencyAnalyzer.test.ts
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import { DependencyAnalyzer } from '../src/analyzers/dependencyAnalyzer';
|
||||||
|
|
||||||
|
describe('DependencyAnalyzer', () => {
|
||||||
|
let analyzer: DependencyAnalyzer;
|
||||||
|
let testDir: string;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
analyzer = new DependencyAnalyzer();
|
||||||
|
testDir = path.join('/tmp', `ai-context-test-${Date.now()}`);
|
||||||
|
await fs.promises.mkdir(testDir, { recursive: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
if (await fs.promises.stat(testDir).catch(() => null)) {
|
||||||
|
await fs.promises.rm(testDir, { recursive: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('analyze', () => {
|
||||||
|
it('should parse package.json dependencies correctly', async () => {
|
||||||
|
const packageJson = {
|
||||||
|
dependencies: {
|
||||||
|
express: '^4.18.0',
|
||||||
|
lodash: '^4.17.21',
|
||||||
|
},
|
||||||
|
devDependencies: {
|
||||||
|
jest: '^29.0.0',
|
||||||
|
typescript: '^5.0.0',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
await fs.promises.writeFile(
|
||||||
|
path.join(testDir, 'package.json'),
|
||||||
|
JSON.stringify(packageJson)
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await analyzer.analyze(testDir, false);
|
||||||
|
|
||||||
|
expect(result.total).toBe(2);
|
||||||
|
expect(result.direct.length).toBe(2);
|
||||||
|
expect(result.direct.find(d => d.name === 'express')?.version).toBe('^4.18.0');
|
||||||
|
expect(result.dev.length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should include dev dependencies when requested', async () => {
|
||||||
|
const packageJson = {
|
||||||
|
dependencies: { express: '^4.18.0' },
|
||||||
|
devDependencies: { jest: '^29.0.0' },
|
||||||
|
};
|
||||||
|
await fs.promises.writeFile(
|
||||||
|
path.join(testDir, 'package.json'),
|
||||||
|
JSON.stringify(packageJson)
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await analyzer.analyze(testDir, true);
|
||||||
|
|
||||||
|
expect(result.total).toBe(2);
|
||||||
|
expect(result.dev.length).toBe(1);
|
||||||
|
expect(result.dev[0].name).toBe('jest');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse requirements.txt dependencies', async () => {
|
||||||
|
await fs.promises.writeFile(
|
||||||
|
path.join(testDir, 'requirements.txt'),
|
||||||
|
`django>=4.0.0
|
||||||
|
flask>=2.0.0
|
||||||
|
requests==2.28.0
|
||||||
|
numpy~=1.24.0`
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await analyzer.analyze(testDir, false);
|
||||||
|
|
||||||
|
expect(result.total).toBeGreaterThan(0);
|
||||||
|
const deps = result.direct.map(d => d.name);
|
||||||
|
expect(deps).toContain('django');
|
||||||
|
expect(deps).toContain('flask');
|
||||||
|
expect(deps).toContain('requests');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse go.mod dependencies', async () => {
|
||||||
|
await fs.promises.writeFile(
|
||||||
|
path.join(testDir, 'go.mod'),
|
||||||
|
`module example.com/mymodule
|
||||||
|
|
||||||
|
go 1.21
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/gin-gonic/gin v1.9.0
|
||||||
|
github.com/stretchr/testify v1.8.0
|
||||||
|
)`
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await analyzer.analyze(testDir, false);
|
||||||
|
|
||||||
|
expect(result.total).toBeGreaterThan(0);
|
||||||
|
const deps = result.direct.map(d => d.name);
|
||||||
|
expect(deps).toContain('github.com/gin-gonic/gin');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse Cargo.toml dependencies', async () => {
|
||||||
|
await fs.promises.writeFile(
|
||||||
|
path.join(testDir, 'Cargo.toml'),
|
||||||
|
`[package]
|
||||||
|
name = "myproject"
|
||||||
|
version = "0.1.0"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
tokio = { version = "1.0", features = ["full"] }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
assertions = "0.3"`
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await analyzer.analyze(testDir, true);
|
||||||
|
|
||||||
|
expect(result.total).toBe(3);
|
||||||
|
expect(result.direct.length).toBe(2);
|
||||||
|
expect(result.dev.length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse pyproject.toml dependencies', async () => {
|
||||||
|
await fs.promises.writeFile(
|
||||||
|
path.join(testDir, 'pyproject.toml'),
|
||||||
|
`[project]
|
||||||
|
name = "myproject"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"requests>=2.28.0",
|
||||||
|
"flask>=2.0.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.optional-dependencies]
|
||||||
|
dev = [
|
||||||
|
"pytest>=7.0.0",
|
||||||
|
"black>=23.0.0",
|
||||||
|
]`
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await analyzer.analyze(testDir, false);
|
||||||
|
|
||||||
|
expect(result.total).toBe(2);
|
||||||
|
expect(result.direct.length).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse composer.json dependencies', async () => {
|
||||||
|
await fs.promises.writeFile(
|
||||||
|
path.join(testDir, 'composer.json'),
|
||||||
|
JSON.stringify({
|
||||||
|
require: {
|
||||||
|
php: '^8.0',
|
||||||
|
'laravel/framework': '^10.0',
|
||||||
|
},
|
||||||
|
'require-dev': {
|
||||||
|
'phpunit/phpunit': '^10.0',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await analyzer.analyze(testDir, true);
|
||||||
|
|
||||||
|
expect(result.total).toBe(3);
|
||||||
|
expect(result.dev.length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse Gemfile dependencies', async () => {
|
||||||
|
await fs.promises.writeFile(
|
||||||
|
path.join(testDir, 'Gemfile'),
|
||||||
|
`source 'https://rubygems.org'
|
||||||
|
|
||||||
|
gem 'rails', '~> 7.0.0'
|
||||||
|
gem 'nokogiri', '>= 1.13'
|
||||||
|
|
||||||
|
group :development, :test do
|
||||||
|
gem 'rspec-rails', '~> 6.0'
|
||||||
|
end`
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await analyzer.analyze(testDir, true);
|
||||||
|
|
||||||
|
expect(result.total).toBe(3);
|
||||||
|
expect(result.dev.length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle empty project gracefully', async () => {
|
||||||
|
const result = await analyzer.analyze(testDir, false);
|
||||||
|
|
||||||
|
expect(result.total).toBe(0);
|
||||||
|
expect(result.direct).toHaveLength(0);
|
||||||
|
expect(result.dev).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle invalid JSON gracefully', async () => {
|
||||||
|
await fs.promises.writeFile(
|
||||||
|
path.join(testDir, 'package.json'),
|
||||||
|
'invalid json {'
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await analyzer.analyze(testDir, false);
|
||||||
|
|
||||||
|
expect(result.total).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should mark local dependencies correctly', async () => {
|
||||||
|
const packageJson = {
|
||||||
|
dependencies: {
|
||||||
|
'./local-module': '*',
|
||||||
|
'@local/package': '*',
|
||||||
|
express: '^4.18.0',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
await fs.promises.writeFile(
|
||||||
|
path.join(testDir, 'package.json'),
|
||||||
|
JSON.stringify(packageJson)
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await analyzer.analyze(testDir, false);
|
||||||
|
|
||||||
|
expect(result.direct.find(d => d.name === './local-module')?.isLocal).toBe(true);
|
||||||
|
expect(result.direct.find(d => d.name === '@local/package')?.isLocal).toBe(true);
|
||||||
|
expect(result.direct.find(d => d.name === 'express')?.isLocal).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
205
.ai-context-generator-cli/tests/projectTypeDetector.test.ts
Normal file
205
.ai-context-generator-cli/tests/projectTypeDetector.test.ts
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import { ProjectTypeDetector } from '../src/analyzers/projectTypeDetector';
|
||||||
|
|
||||||
|
describe('ProjectTypeDetector', () => {
|
||||||
|
let detector: ProjectTypeDetector;
|
||||||
|
let testDir: string;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
detector = new ProjectTypeDetector();
|
||||||
|
testDir = path.join(__dirname, 'test-projects', `test-${Date.now()}`);
|
||||||
|
await fs.promises.mkdir(testDir, { recursive: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
if (await fs.promises.stat(testDir).catch(() => null)) {
|
||||||
|
await fs.promises.rm(testDir, { recursive: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('detect', () => {
|
||||||
|
it('should detect TypeScript project with tsconfig.json', async () => {
|
||||||
|
const tsconfig = { compilerOptions: { target: 'ES2020' } };
|
||||||
|
await fs.promises.writeFile(
|
||||||
|
path.join(testDir, 'tsconfig.json'),
|
||||||
|
JSON.stringify(tsconfig)
|
||||||
|
);
|
||||||
|
await fs.promises.writeFile(
|
||||||
|
path.join(testDir, 'index.ts'),
|
||||||
|
'const x: string = "hello";'
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await detector.detect(testDir);
|
||||||
|
|
||||||
|
expect(result.languages).toContain('TypeScript');
|
||||||
|
expect(result.primaryLanguage).toBe('TypeScript');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect JavaScript project with package.json', async () => {
|
||||||
|
const packageJson = { dependencies: { express: '^4.0.0' } };
|
||||||
|
await fs.promises.writeFile(
|
||||||
|
path.join(testDir, 'package.json'),
|
||||||
|
JSON.stringify(packageJson)
|
||||||
|
);
|
||||||
|
await fs.promises.writeFile(
|
||||||
|
path.join(testDir, 'index.js'),
|
||||||
|
'const express = require("express");'
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await detector.detect(testDir);
|
||||||
|
|
||||||
|
expect(result.languages).toContain('JavaScript');
|
||||||
|
expect(result.primaryLanguage).toBe('JavaScript');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect Python project with requirements.txt', async () => {
|
||||||
|
await fs.promises.writeFile(
|
||||||
|
path.join(testDir, 'requirements.txt'),
|
||||||
|
'django>=3.0.0\nflask>=1.0.0'
|
||||||
|
);
|
||||||
|
await fs.promises.writeFile(
|
||||||
|
path.join(testDir, 'main.py'),
|
||||||
|
'import django\nfrom flask import Flask'
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await detector.detect(testDir);
|
||||||
|
|
||||||
|
expect(result.languages).toContain('Python');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect Go project with go.mod', async () => {
|
||||||
|
await fs.promises.writeFile(
|
||||||
|
path.join(testDir, 'go.mod'),
|
||||||
|
'module example.com/mymodule\n\ngo 1.21\n\nrequire github.com/gin-gonic/gin v1.9.0'
|
||||||
|
);
|
||||||
|
await fs.promises.writeFile(
|
||||||
|
path.join(testDir, 'main.go'),
|
||||||
|
'package main\nimport "fmt"'
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await detector.detect(testDir);
|
||||||
|
|
||||||
|
expect(result.languages).toContain('Go');
|
||||||
|
expect(result.frameworks).toContain('Gin');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect Rust project with Cargo.toml', async () => {
|
||||||
|
await fs.promises.writeFile(
|
||||||
|
path.join(testDir, 'Cargo.toml'),
|
||||||
|
'[package]\nname = "myproject"\nversion = "0.1.0"\n\n[dependencies]\nserde = { version = "1.0", features = ["derive"] }'
|
||||||
|
);
|
||||||
|
await fs.promises.writeFile(
|
||||||
|
path.join(testDir, 'main.rs'),
|
||||||
|
'fn main() { println!("Hello"); }'
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await detector.detect(testDir);
|
||||||
|
|
||||||
|
expect(result.languages).toContain('Rust');
|
||||||
|
expect(result.buildTools).toContain('cargo');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect React framework from package.json', async () => {
|
||||||
|
const packageJson = {
|
||||||
|
dependencies: { react: '^18.0.0', 'react-dom': '^18.0.0' },
|
||||||
|
devDependencies: { '@types/react': '^18.0.0' }
|
||||||
|
};
|
||||||
|
await fs.promises.writeFile(
|
||||||
|
path.join(testDir, 'package.json'),
|
||||||
|
JSON.stringify(packageJson)
|
||||||
|
);
|
||||||
|
await fs.promises.writeFile(
|
||||||
|
path.join(testDir, 'App.tsx'),
|
||||||
|
'import React from "react";'
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await detector.detect(testDir);
|
||||||
|
|
||||||
|
expect(result.languages).toContain('TypeScript');
|
||||||
|
expect(result.frameworks).toContain('React');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect Next.js framework', async () => {
|
||||||
|
const packageJson = {
|
||||||
|
dependencies: {
|
||||||
|
next: '^14.0.0',
|
||||||
|
react: '^18.0.0',
|
||||||
|
'react-dom': '^18.0.0'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
await fs.promises.writeFile(
|
||||||
|
path.join(testDir, 'package.json'),
|
||||||
|
JSON.stringify(packageJson)
|
||||||
|
);
|
||||||
|
await fs.promises.writeFile(
|
||||||
|
path.join(testDir, 'next.config.js'),
|
||||||
|
'module.exports = {}'
|
||||||
|
);
|
||||||
|
await fs.promises.mkdir(path.join(testDir, 'pages'), { recursive: true });
|
||||||
|
await fs.promises.writeFile(
|
||||||
|
path.join(testDir, 'pages', 'index.tsx'),
|
||||||
|
'export default function() { return null; }'
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await detector.detect(testDir);
|
||||||
|
|
||||||
|
expect(result.frameworks).toContain('Next.js');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect Django framework', async () => {
|
||||||
|
await fs.promises.writeFile(
|
||||||
|
path.join(testDir, 'requirements.txt'),
|
||||||
|
'django>=4.0.0\ndjangorestframework>=3.0.0'
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await detector.detect(testDir);
|
||||||
|
|
||||||
|
expect(result.frameworks).toContain('Django');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect FastAPI framework', async () => {
|
||||||
|
await fs.promises.writeFile(
|
||||||
|
path.join(testDir, 'requirements.txt'),
|
||||||
|
'fastapi>=0.100.0\nuvicorn>=0.23.0'
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await detector.detect(testDir);
|
||||||
|
|
||||||
|
expect(result.frameworks).toContain('FastAPI');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect build tools from config files', async () => {
|
||||||
|
await fs.promises.writeFile(
|
||||||
|
path.join(testDir, 'package.json'),
|
||||||
|
JSON.stringify({ dependencies: {} })
|
||||||
|
);
|
||||||
|
await fs.promises.writeFile(path.join(testDir, 'Makefile'), 'all:\n\techo "build"');
|
||||||
|
|
||||||
|
const result = await detector.detect(testDir);
|
||||||
|
|
||||||
|
expect(result.buildTools).toContain('npm');
|
||||||
|
expect(result.buildTools).toContain('make');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect multiple languages in same project', async () => {
|
||||||
|
await fs.promises.writeFile(
|
||||||
|
path.join(testDir, 'package.json'),
|
||||||
|
JSON.stringify({ dependencies: {} })
|
||||||
|
);
|
||||||
|
await fs.promises.writeFile(path.join(testDir, 'index.js'), 'console.log("js");');
|
||||||
|
await fs.promises.writeFile(path.join(testDir, 'main.py'), 'print("python")');
|
||||||
|
|
||||||
|
const result = await detector.detect(testDir);
|
||||||
|
|
||||||
|
expect(result.languages.length).toBeGreaterThan(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return Unknown for empty directory', async () => {
|
||||||
|
const result = await detector.detect(testDir);
|
||||||
|
|
||||||
|
expect(result.primaryLanguage).toBe('Unknown');
|
||||||
|
expect(result.languages).toHaveLength(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
20
.ai-context-generator-cli/tsconfig.json
Normal file
20
.ai-context-generator-cli/tsconfig.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"module": "commonjs",
|
||||||
|
"lib": ["ES2020"],
|
||||||
|
"outDir": "./dist",
|
||||||
|
"rootDir": "./src",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"moduleResolution": "node"
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"],
|
||||||
|
"exclude": ["node_modules", "dist", "tests"]
|
||||||
|
}
|
||||||
17
.ai-context-generator-cli/tsconfig.test.json
Normal file
17
.ai-context-generator-cli/tsconfig.test.json
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"module": "commonjs",
|
||||||
|
"lib": ["ES2020"],
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"declaration": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"moduleResolution": "node"
|
||||||
|
},
|
||||||
|
"include": ["src/**/*", "tests/**/*"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
@@ -2,29 +2,28 @@ name: CI
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [main, master]
|
branches: [main]
|
||||||
|
paths:
|
||||||
|
- 'ai-context-generator-cli/**'
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [main, master]
|
branches: [main]
|
||||||
|
paths:
|
||||||
|
- 'ai-context-generator-cli/**'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: ./ai-context-generator-cli
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: '20'
|
node-version: '20'
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
|
cache-dependency-path: package-lock.json
|
||||||
- name: Install dependencies
|
- run: npm install
|
||||||
run: npm ci
|
- run: npm run build
|
||||||
|
- run: npm test
|
||||||
- name: Build
|
- run: npm run lint
|
||||||
run: npm run build
|
|
||||||
|
|
||||||
- name: Run tests
|
|
||||||
run: npm test --if-present
|
|
||||||
|
|
||||||
- name: Run lint
|
|
||||||
run: npm run lint --if-present
|
|
||||||
|
|||||||
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
*.log
|
||||||
|
.DS_Store
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
coverage/
|
||||||
|
.nyc_output/
|
||||||
282
README.md
282
README.md
@@ -1,3 +1,281 @@
|
|||||||
# ai-context-generator-cli
|
# AI Context Generator CLI
|
||||||
|
|
||||||
A CLI tool that generates comprehensive context files for AI coding assistants by analyzing project structure, dependencies, and coding patterns. Supports JSON/YAML output with templates for Cursor, Copilot, and generic AI assistants.
|
A CLI tool that generates comprehensive context files for AI coding assistants by analyzing project structure, dependencies, and coding patterns. Outputs structured JSON/YAML context files optimized for different AI tools.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Automatic Project Detection**: Detects project type (Node.js, Python, Go, Rust, Java, etc.) and frameworks (React, Django, FastAPI, etc.)
|
||||||
|
- **Dependency Analysis**: Extracts dependencies from package.json, requirements.txt, go.mod, Cargo.toml, and more
|
||||||
|
- **Convention Extraction**: Analyzes coding patterns, naming conventions, import styles, and testing frameworks
|
||||||
|
- **Multi-format Output**: Generates context files in JSON or YAML format
|
||||||
|
- **AI-specific Templates**: Predefined templates for Cursor, Copilot, and generic AI assistants
|
||||||
|
- **Configurable**: Customize analysis scope and output via `.ai-context-config.json`
|
||||||
|
- **Ignore Pattern Support**: Respects `.gitignore` and custom ignore rules
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- Node.js 16+
|
||||||
|
- npm or yarn
|
||||||
|
|
||||||
|
### Install from Source
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/yourusername/ai-context-generator-cli.git
|
||||||
|
cd ai-context-generator-cli
|
||||||
|
npm install
|
||||||
|
npm run build
|
||||||
|
npm link
|
||||||
|
```
|
||||||
|
|
||||||
|
### Install Globally
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install -g ai-context-generator-cli
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Basic Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Analyze current directory and generate context
|
||||||
|
ai-context
|
||||||
|
|
||||||
|
# Analyze specific directory
|
||||||
|
ai-context --dir /path/to/project
|
||||||
|
|
||||||
|
# Save to specific output file
|
||||||
|
ai-context --output my-context
|
||||||
|
|
||||||
|
# Generate YAML instead of JSON
|
||||||
|
ai-context --format yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
### Command Options
|
||||||
|
|
||||||
|
| Option | Short | Description | Default |
|
||||||
|
|--------|-------|-------------|---------|
|
||||||
|
| `--dir` | `-d` | Project directory to analyze | Current directory |
|
||||||
|
| `--output` | `-o` | Output file path | `ai-context` |
|
||||||
|
| `--format` | `-f` | Output format: `json` or `yaml` | `json` |
|
||||||
|
| `--template` | `-t` | Template: `default`, `cursor`, `copilot`, `generic` | `default` |
|
||||||
|
| `--config` | `-c` | Config file path | Auto-detected |
|
||||||
|
| `--verbose` | `-v` | Enable verbose output | `false` |
|
||||||
|
| `--no-conventions` | | Skip convention analysis | `false` |
|
||||||
|
| `--include-dev` | | Include dev dependencies | `false` |
|
||||||
|
| `--no-gitignore` | | Do not respect .gitignore | `false` |
|
||||||
|
|
||||||
|
### Examples
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Generate context with all analysis
|
||||||
|
ai-context --dir ./my-project --verbose
|
||||||
|
|
||||||
|
# Generate YAML output for documentation
|
||||||
|
ai-context --format yaml --output project-context
|
||||||
|
|
||||||
|
# Use Cursor-optimized template
|
||||||
|
ai-context --template cursor --output cursor-context
|
||||||
|
|
||||||
|
# Quick analysis without conventions
|
||||||
|
ai-context --no-conventions
|
||||||
|
|
||||||
|
# Include development dependencies
|
||||||
|
ai-context --include-dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Create a `.ai-context-config.json` file in your project root:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"includes": [
|
||||||
|
"**/*.ts",
|
||||||
|
"**/*.js",
|
||||||
|
"**/*.py",
|
||||||
|
"**/*.go",
|
||||||
|
"**/*.rs"
|
||||||
|
],
|
||||||
|
"excludes": [
|
||||||
|
"node_modules/**",
|
||||||
|
"dist/**",
|
||||||
|
".git/**"
|
||||||
|
],
|
||||||
|
"outputFormat": "json",
|
||||||
|
"template": "default",
|
||||||
|
"outputFile": "ai-context.json",
|
||||||
|
"analyzeConventions": true,
|
||||||
|
"includeDevDependencies": false,
|
||||||
|
"respectGitignore": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Configuration Options
|
||||||
|
|
||||||
|
| Option | Type | Description | Default |
|
||||||
|
|--------|------|-------------|---------|
|
||||||
|
| `includes` | `string[]` | File patterns to include | All common source files |
|
||||||
|
| `excludes` | `string[]` | File patterns to exclude | Common ignore patterns |
|
||||||
|
| `outputFormat` | `json | yaml` | Output format | `json` |
|
||||||
|
| `template` | `string` | Template name | `default` |
|
||||||
|
| `outputFile` | `string` | Output filename | `ai-context.json` |
|
||||||
|
| `analyzeConventions` | `boolean` | Extract coding conventions | `true` |
|
||||||
|
| `includeDevDependencies` | `boolean` | Include dev dependencies | `false` |
|
||||||
|
| `respectGitignore` | `boolean` | Respect .gitignore | `true` |
|
||||||
|
|
||||||
|
## Templates
|
||||||
|
|
||||||
|
### Default Template
|
||||||
|
Generates structured JSON/YAML with full project analysis.
|
||||||
|
|
||||||
|
### Cursor Template
|
||||||
|
Optimized for Cursor AI, focusing on:
|
||||||
|
- Key dependencies
|
||||||
|
- Coding conventions
|
||||||
|
- File structure overview
|
||||||
|
|
||||||
|
### Copilot Template
|
||||||
|
Tailored for GitHub Copilot, featuring:
|
||||||
|
- Concise project summary
|
||||||
|
- Dependency overview
|
||||||
|
- Style guidelines
|
||||||
|
|
||||||
|
### Generic Template
|
||||||
|
Simple format suitable for any AI assistant:
|
||||||
|
- Project information
|
||||||
|
- Dependency list
|
||||||
|
- Conventions summary
|
||||||
|
|
||||||
|
## Output Structure
|
||||||
|
|
||||||
|
### JSON Output
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"projectInfo": {
|
||||||
|
"projectType": {
|
||||||
|
"primaryLanguage": "TypeScript",
|
||||||
|
"languages": ["TypeScript", "JavaScript"],
|
||||||
|
"frameworks": ["React", "Next.js"],
|
||||||
|
"buildTools": ["npm"]
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"direct": [...],
|
||||||
|
"dev": [...],
|
||||||
|
"total": 50
|
||||||
|
},
|
||||||
|
"conventions": {
|
||||||
|
"namingConvention": {
|
||||||
|
"files": "kebab-case",
|
||||||
|
"variables": "camelCase",
|
||||||
|
"functions": "camelCase",
|
||||||
|
"classes": "PascalCase"
|
||||||
|
},
|
||||||
|
"importStyle": {
|
||||||
|
"style": "ESM",
|
||||||
|
"aliasPrefix": "@/"
|
||||||
|
},
|
||||||
|
"testingFramework": "Jest",
|
||||||
|
"codeStyle": {
|
||||||
|
"indentSize": 2,
|
||||||
|
"indentType": "spaces",
|
||||||
|
"lineEndings": "LF",
|
||||||
|
"quoteStyle": "single"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fileCount": 150,
|
||||||
|
"analysisDate": "2024-01-15T10:30:00.000Z"
|
||||||
|
},
|
||||||
|
"files": [...],
|
||||||
|
"config": {...},
|
||||||
|
"generatedAt": "2024-01-15T10:30:00.000Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Supported Languages
|
||||||
|
|
||||||
|
| Language | Detected By | Dependency Files |
|
||||||
|
|----------|-------------|------------------|
|
||||||
|
| TypeScript | `.ts`, `.tsx`, `tsconfig.json` | `package.json` |
|
||||||
|
| JavaScript | `.js`, `.jsx`, `package.json` | `package.json` |
|
||||||
|
| Python | `.py`, `requirements.txt` | `requirements.txt`, `pyproject.toml`, `Pipfile` |
|
||||||
|
| Go | `.go`, `go.mod` | `go.mod` |
|
||||||
|
| Rust | `.rs`, `Cargo.toml` | `Cargo.toml` |
|
||||||
|
| Java | `.java`, `pom.xml` | `pom.xml`, `build.gradle` |
|
||||||
|
| C/C++ | `.c`, `.cpp`, `CMakeLists.txt` | `CMakeLists.txt` |
|
||||||
|
| Ruby | `.rb`, `Gemfile` | `Gemfile` |
|
||||||
|
| PHP | `.php`, `composer.json` | `composer.json` |
|
||||||
|
|
||||||
|
## Supported Frameworks
|
||||||
|
|
||||||
|
- **Frontend**: React, Vue, Next.js, NestJS
|
||||||
|
- **Backend**: Express, Django, FastAPI, Flask, Gin, Echo
|
||||||
|
- **Testing**: Jest, Mocha, Pytest, Go testing, JUnit
|
||||||
|
|
||||||
|
## Programmatic Usage
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { ContextGenerator } from 'ai-context-generator-cli';
|
||||||
|
|
||||||
|
const generator = new ContextGenerator();
|
||||||
|
|
||||||
|
// Generate context
|
||||||
|
const projectInfo = await generator.generate('/path/to/project');
|
||||||
|
|
||||||
|
// Generate JSON output
|
||||||
|
const jsonOutput = await generator.generateJson('/path/to/project');
|
||||||
|
|
||||||
|
// Generate YAML output
|
||||||
|
const yamlOutput = await generator.generateYaml('/path/to/project');
|
||||||
|
|
||||||
|
// Save to file
|
||||||
|
await generator.saveContext('/path/to/project', 'output', 'json');
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
### Common Errors
|
||||||
|
|
||||||
|
| Error | Solution |
|
||||||
|
|-------|----------|
|
||||||
|
| `No package.json found` | Check if running in project root or use `--dir` flag |
|
||||||
|
| `Invalid config file format` | Validate JSON syntax and required fields |
|
||||||
|
| `Permission denied on output file` | Check write permissions in target directory |
|
||||||
|
| `Directory not found` | Verify the directory path exists |
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install dependencies
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# Run tests
|
||||||
|
npm test
|
||||||
|
|
||||||
|
# Build
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
# Run locally
|
||||||
|
npm run dev -- --dir ./my-project
|
||||||
|
|
||||||
|
# Lint
|
||||||
|
npm run lint
|
||||||
|
|
||||||
|
# Lint with fix
|
||||||
|
npm run lint:fix
|
||||||
|
```
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
1. Fork the repository
|
||||||
|
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
|
||||||
|
3. Commit your changes (`git commit -m 'Add amazing feature'`)
|
||||||
|
4. Push to the branch (`git push origin feature/amazing-feature`)
|
||||||
|
5. Open a Pull Request
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT License - see [LICENSE](LICENSE) for details.
|
||||||
|
|||||||
251
app/src/analyzers/conventionExtractor.ts
Normal file
251
app/src/analyzers/conventionExtractor.ts
Normal file
@@ -0,0 +1,251 @@
|
|||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import { CodingConventions, FileNamingConvention } from '../types';
|
||||||
|
import { readFileContent, globFiles } from '../utils/fileUtils';
|
||||||
|
|
||||||
|
const TEST_FRAMEWORKS: Record<string, string> = {
|
||||||
|
jest: 'jest',
|
||||||
|
'jest-environment-jsdom': 'jest',
|
||||||
|
vitest: 'vitest',
|
||||||
|
mocha: 'mocha',
|
||||||
|
chai: 'mocha',
|
||||||
|
pytest: 'pytest',
|
||||||
|
'pytest-cov': 'pytest',
|
||||||
|
unittest: 'unittest',
|
||||||
|
nose: 'nose',
|
||||||
|
'testing-library/jest-dom': 'jest',
|
||||||
|
'testing-library/react': 'jest',
|
||||||
|
'@testing-library/jest-dom': 'jest',
|
||||||
|
'@testing-library/react': 'jest'
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function extractConventions(
|
||||||
|
directory: string,
|
||||||
|
projectType: string
|
||||||
|
): Promise<CodingConventions> {
|
||||||
|
const files = await globFiles(
|
||||||
|
['**/*.{ts,js,tsx,jsx,py,go,rs,java}'],
|
||||||
|
directory,
|
||||||
|
['node_modules/**', 'dist/**', 'build/**', '.git/**']
|
||||||
|
);
|
||||||
|
|
||||||
|
const fileNamingConventions = detectFileNamingConvention(files);
|
||||||
|
const importStyle = detectImportStyle(files);
|
||||||
|
const testingFramework = detectTestingFramework(directory, files);
|
||||||
|
|
||||||
|
return {
|
||||||
|
fileNamingConvention: fileNamingConventions,
|
||||||
|
importStyle,
|
||||||
|
testingFramework: testingFramework?.framework,
|
||||||
|
testingStyle: testingFramework?.style || null,
|
||||||
|
componentStyle: detectComponentStyle(files, projectType),
|
||||||
|
modulePattern: detectModulePattern(files, projectType)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function detectFileNamingConvention(files: string[]): FileNamingConvention {
|
||||||
|
const sampleSize = Math.min(files.length, 50);
|
||||||
|
const samples = files.slice(0, sampleSize);
|
||||||
|
|
||||||
|
const scores: Record<string, number> = {
|
||||||
|
camelCase: 0,
|
||||||
|
snake_case: 0,
|
||||||
|
PascalCase: 0,
|
||||||
|
'kebab-case': 0
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const file of samples) {
|
||||||
|
const filename = path.basename(file, path.extname(file));
|
||||||
|
|
||||||
|
if (/^[a-z]+([A-Z][a-z0-9]*)*$/.test(filename)) {
|
||||||
|
scores.camelCase++;
|
||||||
|
} else if (/^[a-z]+(_[a-z0-9]+)*$/.test(filename)) {
|
||||||
|
scores.snake_case++;
|
||||||
|
} else if (/^[A-Z][a-zA-Z0-9]*$/.test(filename)) {
|
||||||
|
scores.PascalCase++;
|
||||||
|
} else if (/^[a-z]+(-[a-z0-9]+)*$/.test(filename)) {
|
||||||
|
scores['kebab-case']++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxScore = Math.max(...Object.values(scores));
|
||||||
|
|
||||||
|
if (maxScore === 0) {
|
||||||
|
return 'unknown';
|
||||||
|
}
|
||||||
|
|
||||||
|
const bestMatch = Object.entries(scores).find(([_, score]) => score === maxScore);
|
||||||
|
return (bestMatch?.[0] as FileNamingConvention) || 'unknown';
|
||||||
|
}
|
||||||
|
|
||||||
|
function detectImportStyle(files: string[]): CodingConventions['importStyle'] {
|
||||||
|
const sampleSize = Math.min(files.length, 30);
|
||||||
|
const samples = files.slice(0, sampleSize);
|
||||||
|
|
||||||
|
let es6Imports = 0;
|
||||||
|
let commonjsImports = 0;
|
||||||
|
let relativeImports = 0;
|
||||||
|
let pythonImports = 0;
|
||||||
|
let goImports = 0;
|
||||||
|
|
||||||
|
for (const file of samples) {
|
||||||
|
const content = readFileContent(file);
|
||||||
|
if (!content) continue;
|
||||||
|
|
||||||
|
const ext = path.extname(file);
|
||||||
|
|
||||||
|
if (['.ts', '.js', '.tsx', '.jsx'].includes(ext)) {
|
||||||
|
if (/\bimport\s+.*\s+from\s+['"]/.test(content)) {
|
||||||
|
es6Imports++;
|
||||||
|
}
|
||||||
|
if (/\brequire\s*\(\s*['"]/.test(content)) {
|
||||||
|
commonjsImports++;
|
||||||
|
}
|
||||||
|
if (/from\s+['"]\.?[\/'"].test(content)) {
|
||||||
|
relativeImports++;
|
||||||
|
}
|
||||||
|
} else if (ext === '.py') {
|
||||||
|
if (/\bimport\s+\w+/.test(content) || /\bfrom\s+\w+/.test(content)) {
|
||||||
|
pythonImports++;
|
||||||
|
}
|
||||||
|
} else if (ext === '.go') {
|
||||||
|
if (/\bimport\s*\(/m.test(content) || /\bimport\s+["']/.test(content)) {
|
||||||
|
goImports++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pythonImports > 0) {
|
||||||
|
return 'python';
|
||||||
|
} else if (goImports > 0) {
|
||||||
|
return 'go';
|
||||||
|
} else if (es6Imports > commonjsImports && es6Imports > relativeImports) {
|
||||||
|
return 'es6';
|
||||||
|
} else if (commonjsImports > relativeImports) {
|
||||||
|
return 'commonjs';
|
||||||
|
} else if (relativeImports > es6Imports + commonjsImports) {
|
||||||
|
return 'relative';
|
||||||
|
} else if (es6Imports > 0 || commonjsImports > 0) {
|
||||||
|
return 'mixed';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'es6';
|
||||||
|
}
|
||||||
|
|
||||||
|
function detectTestingFramework(
|
||||||
|
directory: string,
|
||||||
|
files: string[]
|
||||||
|
): { framework: string; style: CodingConventions['testingStyle'] } | null {
|
||||||
|
const packageJsonPath = path.join(directory, 'package.json');
|
||||||
|
|
||||||
|
const packageJson = fs.existsSync(packageJsonPath)
|
||||||
|
? JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'))
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const allDeps = {
|
||||||
|
...(packageJson?.dependencies || {}),
|
||||||
|
...(packageJson?.devDependencies || {})
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const [pkg, framework] of Object.entries(TEST_FRAMEWORKS)) {
|
||||||
|
if (allDeps[pkg]) {
|
||||||
|
const styleMap: Record<string, CodingConventions['testingStyle']> = {
|
||||||
|
jest: 'jest',
|
||||||
|
vitest: 'vitest',
|
||||||
|
mocha: 'mocha',
|
||||||
|
pytest: 'pytest',
|
||||||
|
unittest: 'unittest',
|
||||||
|
testing: 'testing'
|
||||||
|
};
|
||||||
|
return { framework: pkg, style: styleMap[framework] || null };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
const basename = path.basename(file);
|
||||||
|
|
||||||
|
if (basename.includes('.test.') || basename.includes('.spec.')) {
|
||||||
|
return { framework: 'test runner', style: 'jest' };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (basename.startsWith('test_') || basename.endsWith('_test.py')) {
|
||||||
|
return { framework: 'pytest', style: 'pytest' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function detectComponentStyle(files: string[], _projectType: string): CodingConventions['componentStyle'] {
|
||||||
|
const tsxFiles = files.filter(f => f.endsWith('.tsx'));
|
||||||
|
|
||||||
|
if (tsxFiles.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sampleSize = Math.min(tsxFiles.length, 20);
|
||||||
|
const samples = tsxFiles.slice(0, sampleSize);
|
||||||
|
|
||||||
|
let functional = 0;
|
||||||
|
let classBased = 0;
|
||||||
|
let hooks = 0;
|
||||||
|
|
||||||
|
for (const file of samples) {
|
||||||
|
const content = readFileContent(file);
|
||||||
|
if (!content) continue;
|
||||||
|
|
||||||
|
if (/\bfunction\s+\w+\s*\(/.test(content)) {
|
||||||
|
functional++;
|
||||||
|
}
|
||||||
|
if (/\bclass\s+\w+\s+extends\s+(React\.)?Component/.test(content)) {
|
||||||
|
classBased++;
|
||||||
|
}
|
||||||
|
if (/\buse\w+\s*\(/.test(content)) {
|
||||||
|
hooks++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hooks > functional + classBased) {
|
||||||
|
return 'hooks';
|
||||||
|
} else if (functional > classBased) {
|
||||||
|
return 'functional';
|
||||||
|
} else if (classBased > 0) {
|
||||||
|
return 'class';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'functional';
|
||||||
|
}
|
||||||
|
|
||||||
|
function detectModulePattern(files: string[], _projectType: string): CodingConventions['modulePattern'] {
|
||||||
|
const sampleSize = Math.min(files.length, 30);
|
||||||
|
const samples = files.slice(0, sampleSize);
|
||||||
|
|
||||||
|
let defaultExports = 0;
|
||||||
|
let namedExports = 0;
|
||||||
|
let wildcardImports = 0;
|
||||||
|
|
||||||
|
for (const file of samples) {
|
||||||
|
const content = readFileContent(file);
|
||||||
|
if (!content) continue;
|
||||||
|
|
||||||
|
if (/export\s+default/.test(content)) {
|
||||||
|
defaultExports++;
|
||||||
|
}
|
||||||
|
if (/export\s+{/.test(content)) {
|
||||||
|
namedExports++;
|
||||||
|
}
|
||||||
|
if (/import\s+\*\s+as/.test(content)) {
|
||||||
|
wildcardImports++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (defaultExports > namedExports && defaultExports > wildcardImports) {
|
||||||
|
return 'default';
|
||||||
|
} else if (namedExports > defaultExports && namedExports > wildcardImports) {
|
||||||
|
return 'named';
|
||||||
|
} else if (wildcardImports > defaultExports + namedExports) {
|
||||||
|
return 'wildcard';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'default';
|
||||||
|
}
|
||||||
263
app/src/analyzers/dependencyAnalyzer.ts
Normal file
263
app/src/analyzers/dependencyAnalyzer.ts
Normal file
@@ -0,0 +1,263 @@
|
|||||||
|
import * as path from 'path';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import { DependencyAnalysis, DependencyInfo, ProjectType } from '../types';
|
||||||
|
import { parseJSONFile, parseTOMLFile } from '../utils/fileUtils';
|
||||||
|
|
||||||
|
const NODE_PACKAGE_MANAGERS = ['yarn.lock', 'pnpm-lock.yaml', 'bun.lockb'];
|
||||||
|
const PYTHON_PACKAGE_MANAGERS = ['Pipfile.lock', 'poetry.lock'];
|
||||||
|
|
||||||
|
export async function analyzeDependencies(
|
||||||
|
directory: string,
|
||||||
|
projectType: ProjectType
|
||||||
|
): Promise<DependencyAnalysis> {
|
||||||
|
let dependencies: DependencyInfo[] = [];
|
||||||
|
let packageManager: DependencyAnalysis['packageManager'] = null;
|
||||||
|
let lockFile: string | undefined;
|
||||||
|
|
||||||
|
switch (projectType) {
|
||||||
|
case 'node':
|
||||||
|
const nodeDeps = await analyzeNodeDependencies(directory);
|
||||||
|
dependencies = nodeDeps.dependencies;
|
||||||
|
packageManager = nodeDeps.packageManager;
|
||||||
|
lockFile = nodeDeps.lockFile;
|
||||||
|
break;
|
||||||
|
case 'python':
|
||||||
|
const pyDeps = await analyzePythonDependencies(directory);
|
||||||
|
dependencies = pyDeps.dependencies;
|
||||||
|
packageManager = pyDeps.packageManager;
|
||||||
|
lockFile = pyDeps.lockFile;
|
||||||
|
break;
|
||||||
|
case 'go':
|
||||||
|
const goDeps = await analyzeGoDependencies(directory);
|
||||||
|
dependencies = goDeps.dependencies;
|
||||||
|
packageManager = goDeps.packageManager;
|
||||||
|
break;
|
||||||
|
case 'rust':
|
||||||
|
const rustDeps = await analyzeRustDependencies(directory);
|
||||||
|
dependencies = rustDeps.dependencies;
|
||||||
|
packageManager = rustDeps.packageManager;
|
||||||
|
break;
|
||||||
|
case 'java':
|
||||||
|
const javaDeps = await analyzeJavaDependencies(directory);
|
||||||
|
dependencies = javaDeps.dependencies;
|
||||||
|
packageManager = javaDeps.packageManager;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
dependencies = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
dependencies,
|
||||||
|
packageManager,
|
||||||
|
lockFile
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function analyzeNodeDependencies(
|
||||||
|
directory: string
|
||||||
|
): Promise<{ dependencies: DependencyInfo[]; packageManager: DependencyAnalysis['packageManager']; lockFile?: string }> {
|
||||||
|
const packageJsonPath = path.join(directory, 'package.json');
|
||||||
|
const packageJson = parseJSONFile<{
|
||||||
|
dependencies?: Record<string, string>;
|
||||||
|
devDependencies?: Record<string, string>;
|
||||||
|
}>(packageJsonPath);
|
||||||
|
|
||||||
|
if (!packageJson) {
|
||||||
|
return { dependencies: [], packageManager: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
const dependencies: DependencyInfo[] = [];
|
||||||
|
|
||||||
|
if (packageJson.dependencies) {
|
||||||
|
for (const [name, version] of Object.entries(packageJson.dependencies)) {
|
||||||
|
dependencies.push({ name, version: String(version), type: 'prod' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (packageJson.devDependencies) {
|
||||||
|
for (const [name, version] of Object.entries(packageJson.devDependencies)) {
|
||||||
|
dependencies.push({ name, version: String(version), type: 'dev' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let packageManager: DependencyAnalysis['packageManager'] = 'npm';
|
||||||
|
let lockFile: string | undefined;
|
||||||
|
|
||||||
|
for (const lockFileName of NODE_PACKAGE_MANAGERS) {
|
||||||
|
if (fs.existsSync(path.join(directory, lockFileName))) {
|
||||||
|
lockFile = lockFileName;
|
||||||
|
if (lockFileName === 'yarn.lock') packageManager = 'yarn';
|
||||||
|
else if (lockFileName === 'pnpm-lock.yaml') packageManager = 'pnpm';
|
||||||
|
else if (lockFileName === 'bun.lockb') packageManager = 'npm';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { dependencies, packageManager, lockFile };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function analyzePythonDependencies(
|
||||||
|
directory: string
|
||||||
|
): Promise<{ dependencies: DependencyInfo[]; packageManager: DependencyAnalysis['packageManager']; lockFile?: string }> {
|
||||||
|
const dependencies: DependencyInfo[] = [];
|
||||||
|
|
||||||
|
const requirementsPath = path.join(directory, 'requirements.txt');
|
||||||
|
if (fs.existsSync(requirementsPath)) {
|
||||||
|
const content = fs.readFileSync(requirementsPath, 'utf-8');
|
||||||
|
const lines = content.split('\n');
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
const trimmed = line.trim();
|
||||||
|
if (trimmed && !trimmed.startsWith('#')) {
|
||||||
|
const match = trimmed.match(/^([a-zA-Z0-9_-]+)([<>=!~]+[^;]+)?/);
|
||||||
|
if (match) {
|
||||||
|
dependencies.push({
|
||||||
|
name: match[1],
|
||||||
|
version: match[2] || 'latest',
|
||||||
|
type: 'prod'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const pyprojectPath = path.join(directory, 'pyproject.toml');
|
||||||
|
const pyproject = parseTOMLFile(pyprojectPath);
|
||||||
|
|
||||||
|
const projectDeps = pyproject?.['project'] as Record<string, unknown> | undefined;
|
||||||
|
if (projectDeps && Array.isArray(projectDeps['dependencies'])) {
|
||||||
|
const deps = projectDeps['dependencies'] as string[];
|
||||||
|
for (const dep of deps) {
|
||||||
|
const match = dep.match(/^([a-zA-Z0-9_-]+)([<>=!~]+[^;]+)?/);
|
||||||
|
if (match) {
|
||||||
|
dependencies.push({
|
||||||
|
name: match[1],
|
||||||
|
version: match[2] || 'latest',
|
||||||
|
type: 'prod'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let packageManager: DependencyAnalysis['packageManager'] = 'pip';
|
||||||
|
let lockFile: string | undefined;
|
||||||
|
|
||||||
|
for (const lockFileName of PYTHON_PACKAGE_MANAGERS) {
|
||||||
|
if (fs.existsSync(path.join(directory, lockFileName))) {
|
||||||
|
lockFile = lockFileName;
|
||||||
|
if (lockFileName === 'poetry.lock') packageManager = 'poetry';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { dependencies, packageManager, lockFile };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function analyzeGoDependencies(
|
||||||
|
directory: string
|
||||||
|
): Promise<{ dependencies: DependencyInfo[]; packageManager: DependencyAnalysis['packageManager'] }> {
|
||||||
|
const goModPath = path.join(directory, 'go.mod');
|
||||||
|
|
||||||
|
if (!fs.existsSync(goModPath)) {
|
||||||
|
return { dependencies: [], packageManager: 'go' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = fs.readFileSync(goModPath, 'utf-8');
|
||||||
|
const dependencies: DependencyInfo[] = [];
|
||||||
|
|
||||||
|
const requireBlockMatch = content.match(/require\s*\(\s*([\s\S]*?)\s*\)/);
|
||||||
|
if (requireBlockMatch) {
|
||||||
|
const requireBlock = requireBlockMatch[1];
|
||||||
|
const depRegex = /\b([^\s]+)\s+v?([^\s]+)/g;
|
||||||
|
let match;
|
||||||
|
while ((match = depRegex.exec(requireBlock)) !== null) {
|
||||||
|
const path = match[1];
|
||||||
|
if (path && !path.startsWith('//') && path !== 'require') {
|
||||||
|
dependencies.push({
|
||||||
|
name: path,
|
||||||
|
version: match[2] || 'latest',
|
||||||
|
type: 'prod'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { dependencies, packageManager: 'go' };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function analyzeRustDependencies(
|
||||||
|
directory: string
|
||||||
|
): Promise<{ dependencies: DependencyInfo[]; packageManager: DependencyAnalysis['packageManager'] }> {
|
||||||
|
const cargoTomlPath = path.join(directory, 'Cargo.toml');
|
||||||
|
const cargoToml = parseTOMLFile(cargoTomlPath);
|
||||||
|
|
||||||
|
if (!cargoToml) {
|
||||||
|
return { dependencies: [], packageManager: 'cargo' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const dependencies: DependencyInfo[] = [];
|
||||||
|
|
||||||
|
const deps = cargoToml.dependencies as Record<string, string | { version?: string }> | undefined;
|
||||||
|
if (deps) {
|
||||||
|
for (const [name, config] of Object.entries(deps)) {
|
||||||
|
const version = typeof config === 'string' ? config : (config as { version?: string }).version || '*';
|
||||||
|
dependencies.push({ name, version, type: 'prod' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const dev = cargoToml.dev as { dependencies?: Record<string, string | { version?: string }> } | undefined;
|
||||||
|
if (dev?.dependencies) {
|
||||||
|
for (const [name, config] of Object.entries(dev.dependencies)) {
|
||||||
|
const version = typeof config === 'string' ? config : (config as { version?: string }).version || '*';
|
||||||
|
dependencies.push({ name, version, type: 'dev' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { dependencies, packageManager: 'cargo' };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function analyzeJavaDependencies(
|
||||||
|
directory: string
|
||||||
|
): Promise<{ dependencies: DependencyInfo[]; packageManager: DependencyAnalysis['packageManager'] }> {
|
||||||
|
const dependencies: DependencyInfo[] = [];
|
||||||
|
|
||||||
|
const pomPath = path.join(directory, 'pom.xml');
|
||||||
|
if (fs.existsSync(pomPath)) {
|
||||||
|
const content = fs.readFileSync(pomPath, 'utf-8');
|
||||||
|
|
||||||
|
const dependencyRegex = /<dependency>\s*<groupId>([^<]+)<\/groupId>\s*<artifactId>([^<]+)<\/artifactId>\s*<version>([^<]+)<\/version>\s*<\/dependency>/g;
|
||||||
|
let match;
|
||||||
|
|
||||||
|
while ((match = dependencyRegex.exec(content)) !== null) {
|
||||||
|
dependencies.push({
|
||||||
|
name: `${match[1]}:${match[2]}`,
|
||||||
|
version: match[3],
|
||||||
|
type: 'prod'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return { dependencies, packageManager: 'maven' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const buildGradlePath = path.join(directory, 'build.gradle');
|
||||||
|
const buildGradleKtsPath = path.join(directory, 'build.gradle.kts');
|
||||||
|
|
||||||
|
if (fs.existsSync(buildGradlePath) || fs.existsSync(buildGradleKtsPath)) {
|
||||||
|
const content = fs.readFileSync(buildGradlePath || buildGradleKtsPath, 'utf-8');
|
||||||
|
|
||||||
|
const dependencyRegex = /implementation\s+['"]([^:'"]+):([^:'"']+):([^'"@]+)@?(jar)?['"]/g;
|
||||||
|
let match;
|
||||||
|
|
||||||
|
while ((match = dependencyRegex.exec(content)) !== null) {
|
||||||
|
dependencies.push({
|
||||||
|
name: `${match[1]}:${match[2]}`,
|
||||||
|
version: match[3],
|
||||||
|
type: 'prod'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return { dependencies, packageManager: 'gradle' };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { dependencies, packageManager: null };
|
||||||
|
}
|
||||||
171
app/src/analyzers/projectTypeDetector.ts
Normal file
171
app/src/analyzers/projectTypeDetector.ts
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
import * as path from 'path';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import { ProjectInfo, ProjectType, Framework } from '../types';
|
||||||
|
import { parseJSONFile, parseTOMLFile, getFileExtension, isFileExists } from '../utils/fileUtils';
|
||||||
|
|
||||||
|
const LANGUAGE_FILE_EXTENSIONS: Record<ProjectType, string[]> = {
|
||||||
|
node: ['.js', '.ts', '.jsx', '.tsx', '.mjs', '.cjs'],
|
||||||
|
python: ['.py', '.pyw'],
|
||||||
|
go: ['.go'],
|
||||||
|
rust: ['.rs'],
|
||||||
|
java: ['.java', '.kt', '.scala'],
|
||||||
|
unknown: []
|
||||||
|
};
|
||||||
|
|
||||||
|
const DEPENDENCY_FILES: Record<string, ProjectType> = {
|
||||||
|
'package.json': 'node',
|
||||||
|
'requirements.txt': 'python',
|
||||||
|
'pyproject.toml': 'python',
|
||||||
|
'setup.py': 'python',
|
||||||
|
'go.mod': 'go',
|
||||||
|
'Cargo.toml': 'rust',
|
||||||
|
'pom.xml': 'java',
|
||||||
|
'build.gradle': 'java',
|
||||||
|
'build.gradle.kts': 'java'
|
||||||
|
};
|
||||||
|
|
||||||
|
export function detectProjectType(directory: string): ProjectType {
|
||||||
|
const files = fs.readdirSync(directory);
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
if (DEPENDENCY_FILES[file]) {
|
||||||
|
return DEPENDENCY_FILES[file];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileExtensions = new Set<ProjectType>();
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
const fullPath = path.join(directory, file);
|
||||||
|
if (fs.statSync(fullPath).isFile()) {
|
||||||
|
const ext = getFileExtension(file);
|
||||||
|
|
||||||
|
for (const [type, extensions] of Object.entries(LANGUAGE_FILE_EXTENSIONS)) {
|
||||||
|
if (extensions.includes(ext)) {
|
||||||
|
fileExtensions.add(type as ProjectType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fileExtensions.size === 1) {
|
||||||
|
return fileExtensions.values().next().value as ProjectType;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fileExtensions.has('node') && fileExtensions.has('python')) {
|
||||||
|
return 'node';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'unknown';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function detectFramework(directory: string, projectType: ProjectType): Framework {
|
||||||
|
if (projectType === 'unknown') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const packageJsonPath = path.join(directory, 'package.json');
|
||||||
|
const packageJson = parseJSONFile<{ dependencies?: Record<string, string>; devDependencies?: Record<string, string> }>(packageJsonPath);
|
||||||
|
const deps = {
|
||||||
|
...packageJson?.dependencies,
|
||||||
|
...packageJson?.devDependencies
|
||||||
|
};
|
||||||
|
|
||||||
|
const frameworkPatterns: Record<string, string[]> = {
|
||||||
|
react: ['react', 'react-dom'],
|
||||||
|
vue: ['vue'],
|
||||||
|
angular: ['@angular/core'],
|
||||||
|
nextjs: ['next'],
|
||||||
|
nuxt: ['nuxt'],
|
||||||
|
express: ['express'],
|
||||||
|
fastify: ['fastify'],
|
||||||
|
nestjs: ['@nestjs/core'],
|
||||||
|
django: ['django'],
|
||||||
|
flask: ['flask'],
|
||||||
|
fastapi: ['fastapi'],
|
||||||
|
gin: ['github.com/gin-gonic/gin'],
|
||||||
|
fiber: ['github.com/gofiber/fiber'],
|
||||||
|
spring: ['spring-boot']
|
||||||
|
};
|
||||||
|
|
||||||
|
if (projectType === 'node' && packageJson) {
|
||||||
|
for (const [framework, packages] of Object.entries(frameworkPatterns)) {
|
||||||
|
if (['react', 'vue', 'angular', 'nextjs', 'nuxt', 'express', 'fastify', 'nestjs'].includes(framework)) {
|
||||||
|
for (const pkg of packages) {
|
||||||
|
if (deps[pkg]) {
|
||||||
|
return framework as Framework;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (projectType === 'python') {
|
||||||
|
const pyprojectPath = path.join(directory, 'pyproject.toml');
|
||||||
|
const pyproject = parseTOMLFile(pyprojectPath);
|
||||||
|
|
||||||
|
if (pyproject?.tool) {
|
||||||
|
const tool = pyproject.tool as Record<string, unknown>;
|
||||||
|
if (tool['fastapi']) return 'fastapi';
|
||||||
|
if (tool['flask']) return 'flask';
|
||||||
|
if (tool['django']) return 'django';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (projectType === 'go') {
|
||||||
|
const goModPath = path.join(directory, 'go.mod');
|
||||||
|
const goMod = parseTOMLFile(goModPath);
|
||||||
|
|
||||||
|
if (goMod?.require) {
|
||||||
|
const require = goMod.require as Array<{ path: string }>;
|
||||||
|
for (const dep of require) {
|
||||||
|
if (dep.path === 'github.com/gin-gonic/gin') return 'gin';
|
||||||
|
if (dep.path === 'github.com/gofiber/fiber') return 'fiber';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (projectType === 'java') {
|
||||||
|
if (isFileExists(path.join(directory, 'pom.xml'))) {
|
||||||
|
return 'maven';
|
||||||
|
}
|
||||||
|
if (isFileExists(path.join(directory, 'build.gradle')) ||
|
||||||
|
isFileExists(path.join(directory, 'build.gradle.kts'))) {
|
||||||
|
return 'gradle';
|
||||||
|
}
|
||||||
|
if (isFileExists(path.join(directory, 'pom.xml'))) {
|
||||||
|
const pom = parseJSONFile<{ project?: { packaging?: string } }>(path.join(directory, 'pom.xml'));
|
||||||
|
if (pom?.project?.packaging === 'jar') {
|
||||||
|
return 'spring';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getProjectInfo(directory: string): Promise<ProjectInfo> {
|
||||||
|
const projectType = detectProjectType(directory);
|
||||||
|
const framework = detectFramework(directory, projectType);
|
||||||
|
|
||||||
|
let languageVersion: string | undefined;
|
||||||
|
|
||||||
|
if (projectType === 'node') {
|
||||||
|
const packageJson = parseJSONFile<{ engines?: { node?: string } }>(path.join(directory, 'package.json'));
|
||||||
|
languageVersion = packageJson?.engines?.node;
|
||||||
|
} else if (projectType === 'python') {
|
||||||
|
const pyproject = parseJSONFile<{ 'requires-python'?: string }>(path.join(directory, 'pyproject.toml'));
|
||||||
|
languageVersion = pyproject?.['requires-python'];
|
||||||
|
} else if (projectType === 'go') {
|
||||||
|
const goMod = parseJSONFile<{ go?: string }>(path.join(directory, 'go.mod'));
|
||||||
|
languageVersion = goMod?.go;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: projectType,
|
||||||
|
framework: framework,
|
||||||
|
language: projectType.charAt(0).toUpperCase() + projectType.slice(1),
|
||||||
|
languageVersion,
|
||||||
|
frameworkVersion: undefined
|
||||||
|
};
|
||||||
|
}
|
||||||
192
app/src/generators/contextGenerator.ts
Normal file
192
app/src/generators/contextGenerator.ts
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import yaml from 'js-yaml';
|
||||||
|
import {
|
||||||
|
ContextOutput,
|
||||||
|
TemplateContext,
|
||||||
|
OutputFormat,
|
||||||
|
TemplateType,
|
||||||
|
ConfigOptions
|
||||||
|
} from '../types';
|
||||||
|
import { getProjectInfo } from '../analyzers/projectTypeDetector';
|
||||||
|
import { analyzeDependencies } from '../analyzers/dependencyAnalyzer';
|
||||||
|
import { extractConventions } from '../analyzers/conventionExtractor';
|
||||||
|
import { getAllFiles } from '../utils/fileUtils';
|
||||||
|
import { formatContextForAI } from '../templates/templateLoader';
|
||||||
|
import { loadConfig } from '../config/configLoader';
|
||||||
|
|
||||||
|
const SOURCE_DIR_PATTERNS = ['src/', 'lib/', 'app/', 'packages/', 'internal/'];
|
||||||
|
const TEST_DIR_PATTERNS = ['test/', 'tests/', '__tests__/', 'spec/', 'specs/'];
|
||||||
|
|
||||||
|
export async function generateContext(
|
||||||
|
directory: string,
|
||||||
|
options?: Partial<ConfigOptions>
|
||||||
|
): Promise<ContextOutput> {
|
||||||
|
const config = loadConfig(undefined, directory);
|
||||||
|
const mergedOptions = { ...config, ...options };
|
||||||
|
|
||||||
|
const projectInfo = await getProjectInfo(directory);
|
||||||
|
const dependencyAnalysis = await analyzeDependencies(directory, projectInfo.type);
|
||||||
|
const conventions = await extractConventions(directory, projectInfo.type);
|
||||||
|
|
||||||
|
const fileStructure = await analyzeFileStructure(directory, mergedOptions);
|
||||||
|
|
||||||
|
const context: ContextOutput = {
|
||||||
|
version: '1.0.0',
|
||||||
|
generatedAt: new Date().toISOString(),
|
||||||
|
project: projectInfo,
|
||||||
|
dependencies: dependencyAnalysis,
|
||||||
|
conventions,
|
||||||
|
structure: fileStructure,
|
||||||
|
config: mergedOptions
|
||||||
|
};
|
||||||
|
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function generateFormattedContext(
|
||||||
|
directory: string,
|
||||||
|
format: OutputFormat = 'yaml',
|
||||||
|
_templateType: TemplateType = 'generic',
|
||||||
|
options?: Partial<ConfigOptions>
|
||||||
|
): Promise<string> {
|
||||||
|
const context = await generateContext(directory, options);
|
||||||
|
|
||||||
|
if (format === 'yaml') {
|
||||||
|
return yaml.dump(context, { indent: 2, lineWidth: -1, noRefs: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
return JSON.stringify(context, null, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function generateAIContext(
|
||||||
|
directory: string,
|
||||||
|
templateType: TemplateType = 'generic',
|
||||||
|
options?: Partial<ConfigOptions>
|
||||||
|
): Promise<string> {
|
||||||
|
const context = await generateContext(directory, options);
|
||||||
|
|
||||||
|
const templateContext: TemplateContext = {
|
||||||
|
projectInfo: context.project,
|
||||||
|
dependencyAnalysis: context.dependencies,
|
||||||
|
conventions: context.conventions,
|
||||||
|
fileStructure: context.structure
|
||||||
|
};
|
||||||
|
|
||||||
|
return formatContextForAI(templateContext, templateType);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function saveContext(
|
||||||
|
output: string,
|
||||||
|
content: string,
|
||||||
|
format: OutputFormat
|
||||||
|
): boolean {
|
||||||
|
try {
|
||||||
|
if (output === '-' || output === 'stdout') {
|
||||||
|
console.log(content);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let finalContent = content;
|
||||||
|
|
||||||
|
if (format === 'yaml' && !output.endsWith('.yaml') && !output.endsWith('.yml')) {
|
||||||
|
finalContent = '# YAML format context\n' + content;
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.writeFileSync(output, finalContent, 'utf-8');
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function analyzeFileStructure(
|
||||||
|
directory: string,
|
||||||
|
options: ConfigOptions
|
||||||
|
) {
|
||||||
|
const allFiles = getAllFiles(directory, 5);
|
||||||
|
|
||||||
|
const directories = new Set<string>();
|
||||||
|
const sourceDirectories = new Set<string>();
|
||||||
|
const testDirectories = new Set<string>();
|
||||||
|
const keyFiles: string[] = [];
|
||||||
|
|
||||||
|
for (const file of allFiles) {
|
||||||
|
if (file.endsWith('/')) {
|
||||||
|
const relativePath = path.relative(directory, file);
|
||||||
|
|
||||||
|
if (relativePath) {
|
||||||
|
directories.add(relativePath);
|
||||||
|
|
||||||
|
for (const pattern of SOURCE_DIR_PATTERNS) {
|
||||||
|
if (relativePath.startsWith(pattern)) {
|
||||||
|
sourceDirectories.add(relativePath);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const pattern of TEST_DIR_PATTERNS) {
|
||||||
|
if (relativePath.startsWith(pattern)) {
|
||||||
|
testDirectories.add(relativePath);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const shouldInclude = options.includes.some(pattern => {
|
||||||
|
if (pattern.startsWith('**')) return true;
|
||||||
|
if (pattern.startsWith('*')) return pattern.endsWith(path.extname(file));
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
const shouldExclude = options.excludes.some(pattern => {
|
||||||
|
if (pattern.endsWith('**')) {
|
||||||
|
return file.includes(pattern.slice(0, -3));
|
||||||
|
}
|
||||||
|
return file.endsWith(pattern.replace('*', ''));
|
||||||
|
});
|
||||||
|
|
||||||
|
if (shouldInclude && !shouldExclude) {
|
||||||
|
const relativePath = path.relative(directory, file);
|
||||||
|
keyFiles.push(relativePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const sortedKeyFiles = keyFiles
|
||||||
|
.sort((a, b) => {
|
||||||
|
const aDepth = a.split('/').length;
|
||||||
|
const bDepth = b.split('/').length;
|
||||||
|
|
||||||
|
if (aDepth !== bDepth) return aDepth - bDepth;
|
||||||
|
return a.localeCompare(b);
|
||||||
|
})
|
||||||
|
.slice(0, 50);
|
||||||
|
|
||||||
|
return {
|
||||||
|
totalFiles: allFiles.filter(f => !f.endsWith('/')).length,
|
||||||
|
directories: Array.from(directories).sort(),
|
||||||
|
keyFiles: sortedKeyFiles,
|
||||||
|
sourceDirectories: Array.from(sourceDirectories).sort(),
|
||||||
|
testDirectories: Array.from(testDirectories).sort()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function validateContext(context: ContextOutput): Promise<boolean> {
|
||||||
|
if (!context.version) {
|
||||||
|
console.error('Error: Context missing version');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!context.project) {
|
||||||
|
console.error('Error: Context missing project info');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!context.generatedAt) {
|
||||||
|
console.error('Error: Context missing generatedAt timestamp');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
136
app/src/utils/fileUtils.ts
Normal file
136
app/src/utils/fileUtils.ts
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import { glob } from 'glob';
|
||||||
|
import ignore from 'ignore';
|
||||||
|
import yaml from 'js-yaml';
|
||||||
|
import toml from 'toml';
|
||||||
|
|
||||||
|
export function readFileContent(filePath: string): string | null {
|
||||||
|
try {
|
||||||
|
if (fs.existsSync(filePath)) {
|
||||||
|
return fs.readFileSync(filePath, 'utf-8');
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseJSONFile<T>(filePath: string): T | null {
|
||||||
|
const content = readFileContent(filePath);
|
||||||
|
if (!content) return null;
|
||||||
|
try {
|
||||||
|
return JSON.parse(content) as T;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseYAMLFile(filePath: string): Record<string, unknown> | null {
|
||||||
|
const content = readFileContent(filePath);
|
||||||
|
if (!content) return null;
|
||||||
|
try {
|
||||||
|
return yaml.load(content) as Record<string, unknown>;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseTOMLFile(filePath: string): Record<string, unknown> | null {
|
||||||
|
const content = readFileContent(filePath);
|
||||||
|
if (!content) return null;
|
||||||
|
try {
|
||||||
|
return toml.parse(content) as Record<string, unknown>;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function globFiles(
|
||||||
|
patterns: string[],
|
||||||
|
directory: string,
|
||||||
|
ignorePatterns: string[]
|
||||||
|
): Promise<string[]> {
|
||||||
|
const ig = ignore().add(ignorePatterns);
|
||||||
|
|
||||||
|
const allFiles: string[] = [];
|
||||||
|
|
||||||
|
for (const pattern of patterns) {
|
||||||
|
const files = await glob(pattern, {
|
||||||
|
cwd: directory,
|
||||||
|
absolute: true,
|
||||||
|
ignore: ['node_modules/**', 'dist/**', 'build/**', '.git/**']
|
||||||
|
});
|
||||||
|
|
||||||
|
const filtered = files.filter(f => !ig.test(path.relative(directory, f)));
|
||||||
|
allFiles.push(...filtered);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...new Set(allFiles)];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function loadGitignorePatterns(directory: string): string[] {
|
||||||
|
const gitignorePath = path.join(directory, '.gitignore');
|
||||||
|
const content = readFileContent(gitignorePath);
|
||||||
|
|
||||||
|
if (!content) return [];
|
||||||
|
|
||||||
|
return content
|
||||||
|
.split('\n')
|
||||||
|
.map(line => line.trim())
|
||||||
|
.filter(line => line && !line.startsWith('#'));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAllFiles(
|
||||||
|
directory: string,
|
||||||
|
maxDepth: number = 10
|
||||||
|
): string[] {
|
||||||
|
const files: string[] = [];
|
||||||
|
|
||||||
|
function traverse(dir: string, depth: number) {
|
||||||
|
if (depth > maxDepth) return;
|
||||||
|
|
||||||
|
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
const fullPath = path.join(dir, entry.name);
|
||||||
|
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
if (!shouldIgnore(entry.name)) {
|
||||||
|
files.push(fullPath + '/');
|
||||||
|
traverse(fullPath, depth + 1);
|
||||||
|
}
|
||||||
|
} else if (entry.isFile()) {
|
||||||
|
files.push(fullPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
traverse(directory, 0);
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldIgnore(name: string): boolean {
|
||||||
|
const ignoreDirs = ['node_modules', '.git', 'dist', 'build', '__pycache__', '.venv', 'venv'];
|
||||||
|
return ignoreDirs.includes(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getFileExtension(filePath: string): string {
|
||||||
|
return path.extname(filePath).toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getFileName(filePath: string): string {
|
||||||
|
return path.basename(filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDirectoryName(filePath: string): string {
|
||||||
|
return path.dirname(filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isFileExists(filePath: string): boolean {
|
||||||
|
return fs.existsSync(filePath) && fs.statSync(filePath).isFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isDirectoryExists(filePath: string): boolean {
|
||||||
|
return fs.existsSync(filePath) && fs.statSync(filePath).isDirectory();
|
||||||
|
}
|
||||||
219
app/tests/dependencyAnalyzer.test.ts
Normal file
219
app/tests/dependencyAnalyzer.test.ts
Normal file
@@ -0,0 +1,219 @@
|
|||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import * as os from 'os';
|
||||||
|
import { analyzeDependencies } from '../src/analyzers/dependencyAnalyzer';
|
||||||
|
|
||||||
|
describe('DependencyAnalyzer', () => {
|
||||||
|
let tempDir: string;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ai-context-dep-test-'));
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Node.js dependencies', () => {
|
||||||
|
it('should parse package.json dependencies', async () => {
|
||||||
|
const packageJson = {
|
||||||
|
name: 'test-project',
|
||||||
|
version: '1.0.0',
|
||||||
|
dependencies: {
|
||||||
|
express: '^4.18.2',
|
||||||
|
lodash: '^4.17.21'
|
||||||
|
},
|
||||||
|
devDependencies: {
|
||||||
|
jest: '^29.7.0',
|
||||||
|
typescript: '^5.3.3'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(tempDir, 'package.json'),
|
||||||
|
JSON.stringify(packageJson)
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await analyzeDependencies(tempDir, 'node');
|
||||||
|
|
||||||
|
expect(result.dependencies).toHaveLength(4);
|
||||||
|
expect(result.dependencies.find(d => d.name === 'express')).toEqual({
|
||||||
|
name: 'express',
|
||||||
|
version: '^4.18.2',
|
||||||
|
type: 'prod'
|
||||||
|
});
|
||||||
|
expect(result.dependencies.find(d => d.name === 'jest')).toEqual({
|
||||||
|
name: 'jest',
|
||||||
|
version: '^29.7.0',
|
||||||
|
type: 'dev'
|
||||||
|
});
|
||||||
|
expect(result.packageManager).toBe('npm');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect yarn package manager', async () => {
|
||||||
|
const packageJson = {
|
||||||
|
name: 'test-project',
|
||||||
|
dependencies: { express: '^4.18.0' }
|
||||||
|
};
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(tempDir, 'package.json'),
|
||||||
|
JSON.stringify(packageJson)
|
||||||
|
);
|
||||||
|
fs.writeFileSync(path.join(tempDir, 'yarn.lock'), '# yarn lock file');
|
||||||
|
|
||||||
|
const result = await analyzeDependencies(tempDir, 'node');
|
||||||
|
|
||||||
|
expect(result.packageManager).toBe('yarn');
|
||||||
|
expect(result.lockFile).toBe('yarn.lock');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect pnpm package manager', async () => {
|
||||||
|
const packageJson = {
|
||||||
|
name: 'test-project',
|
||||||
|
dependencies: { express: '^4.18.0' }
|
||||||
|
};
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(tempDir, 'package.json'),
|
||||||
|
JSON.stringify(packageJson)
|
||||||
|
);
|
||||||
|
fs.writeFileSync(path.join(tempDir, 'pnpm-lock.yaml'), '# pnpm lock file');
|
||||||
|
|
||||||
|
const result = await analyzeDependencies(tempDir, 'node');
|
||||||
|
|
||||||
|
expect(result.packageManager).toBe('pnpm');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Python dependencies', () => {
|
||||||
|
it('should parse requirements.txt', async () => {
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(tempDir, 'requirements.txt'),
|
||||||
|
`flask>=2.0.0
|
||||||
|
requests==2.28.0
|
||||||
|
numpy~=1.24.0
|
||||||
|
pandas # pandas for data analysis
|
||||||
|
django<4.0
|
||||||
|
black; python_version >= "3.6"
|
||||||
|
`
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await analyzeDependencies(tempDir, 'python');
|
||||||
|
|
||||||
|
expect(result.dependencies.length).toBeGreaterThan(0);
|
||||||
|
const flaskDep = result.dependencies.find(d => d.name === 'flask');
|
||||||
|
expect(flaskDep).toBeDefined();
|
||||||
|
expect(flaskDep?.type).toBe('prod');
|
||||||
|
expect(result.packageManager).toBe('pip');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect poetry package manager', async () => {
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(tempDir, 'pyproject.toml'),
|
||||||
|
`[tool.poetry]
|
||||||
|
name = "test-project"
|
||||||
|
version = "0.1.0"
|
||||||
|
|
||||||
|
[tool.poetry.dependencies]
|
||||||
|
python = "^3.8"
|
||||||
|
fastapi = "^0.100.0"
|
||||||
|
`
|
||||||
|
);
|
||||||
|
fs.writeFileSync(path.join(tempDir, 'poetry.lock'), '# poetry lock');
|
||||||
|
|
||||||
|
const result = await analyzeDependencies(tempDir, 'python');
|
||||||
|
|
||||||
|
expect(result.packageManager).toBe('poetry');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Go dependencies', () => {
|
||||||
|
it('should parse go.mod', async () => {
|
||||||
|
const goMod = `module github.com/test/project
|
||||||
|
|
||||||
|
go 1.20
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/gin-gonic/gin v1.9.1
|
||||||
|
github.com/gofiber/fiber v2.52.0
|
||||||
|
)
|
||||||
|
`;
|
||||||
|
fs.writeFileSync(path.join(tempDir, 'go.mod'), goMod);
|
||||||
|
|
||||||
|
const result = await analyzeDependencies(tempDir, 'go');
|
||||||
|
|
||||||
|
expect(result.dependencies.length).toBe(2);
|
||||||
|
expect(result.packageManager).toBe('go');
|
||||||
|
const ginDep = result.dependencies.find(d => d.name.includes('gin-gonic/gin'));
|
||||||
|
expect(ginDep).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Rust dependencies', () => {
|
||||||
|
it('should parse Cargo.toml', async () => {
|
||||||
|
const cargoToml = `[package]
|
||||||
|
name = "test"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
serde = "1.0"
|
||||||
|
tokio = "1.0"
|
||||||
|
|
||||||
|
[dev.dependencies]
|
||||||
|
tempfile = "3.0"
|
||||||
|
`;
|
||||||
|
fs.writeFileSync(path.join(tempDir, 'Cargo.toml'), cargoToml);
|
||||||
|
|
||||||
|
const result = await analyzeDependencies(tempDir, 'rust');
|
||||||
|
|
||||||
|
expect(result.dependencies.length).toBe(3);
|
||||||
|
expect(result.packageManager).toBe('cargo');
|
||||||
|
const serdeDep = result.dependencies.find(d => d.name === 'serde');
|
||||||
|
expect(serdeDep).toBeDefined();
|
||||||
|
expect(serdeDep?.type).toBe('prod');
|
||||||
|
const tempfileDep = result.dependencies.find(d => d.name === 'tempfile');
|
||||||
|
expect(tempfileDep?.type).toBe('dev');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Java dependencies', () => {
|
||||||
|
it('should parse pom.xml', async () => {
|
||||||
|
const pomXml = `<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
<groupId>com.test</groupId>
|
||||||
|
<artifactId>test</artifactId>
|
||||||
|
<version>1.0.0</version>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter</artifactId>
|
||||||
|
<version>3.1.0</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.fasterxml.jackson.core</groupId>
|
||||||
|
<artifactId>jackson-databind</artifactId>
|
||||||
|
<version>2.15.0</version>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
</project>`;
|
||||||
|
fs.writeFileSync(path.join(tempDir, 'pom.xml'), pomXml);
|
||||||
|
|
||||||
|
const result = await analyzeDependencies(tempDir, 'java');
|
||||||
|
|
||||||
|
expect(result.dependencies.length).toBe(2);
|
||||||
|
expect(result.packageManager).toBe('maven');
|
||||||
|
const springDep = result.dependencies.find(d => d.name.includes('spring-boot-starter'));
|
||||||
|
expect(springDep).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Empty project', () => {
|
||||||
|
it('should return empty dependencies for unknown project type', async () => {
|
||||||
|
const result = await analyzeDependencies(tempDir, 'unknown');
|
||||||
|
|
||||||
|
expect(result.dependencies).toHaveLength(0);
|
||||||
|
expect(result.packageManager).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
13
jest.config.js
Normal file
13
jest.config.js
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
module.exports = {
|
||||||
|
preset: 'ts-jest',
|
||||||
|
testEnvironment: 'node',
|
||||||
|
roots: ['<rootDir>/tests'],
|
||||||
|
testMatch: ['**/*.test.ts'],
|
||||||
|
moduleFileExtensions: ['ts', 'js', 'json'],
|
||||||
|
collectCoverageFrom: [
|
||||||
|
'src/**/*.ts',
|
||||||
|
'!src/**/*.d.ts',
|
||||||
|
],
|
||||||
|
coverageDirectory: 'coverage',
|
||||||
|
verbose: true,
|
||||||
|
};
|
||||||
42
package.json
Normal file
42
package.json
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
{
|
||||||
|
"name": "ai-context-generator-cli",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "A CLI tool that generates comprehensive context files for AI coding assistants",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"bin": {
|
||||||
|
"ai-context": "./dist/index.js"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc",
|
||||||
|
"test": "jest",
|
||||||
|
"test:coverage": "jest --coverage",
|
||||||
|
"lint": "eslint src/**/*.ts",
|
||||||
|
"lint:fix": "eslint src/**/*.ts --fix",
|
||||||
|
"dev": "ts-node src/index.ts",
|
||||||
|
"start": "node dist/index.js"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"cli",
|
||||||
|
"ai",
|
||||||
|
"context",
|
||||||
|
"coding-assistant",
|
||||||
|
"typescript"
|
||||||
|
],
|
||||||
|
"author": "",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"commander": "^12.0.0",
|
||||||
|
"glob": "^10.3.10",
|
||||||
|
"ignore": "^5.3.0",
|
||||||
|
"js-yaml": "^4.1.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/glob": "^8.1.0",
|
||||||
|
"@types/jest": "^29.5.11",
|
||||||
|
"@types/js-yaml": "^4.0.9",
|
||||||
|
"@types/node": "^20.10.6",
|
||||||
|
"jest": "^29.7.0",
|
||||||
|
"ts-jest": "^29.1.1",
|
||||||
|
"typescript": "^5.3.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
314
src/analyzers/conventionExtractor.ts
Normal file
314
src/analyzers/conventionExtractor.ts
Normal file
@@ -0,0 +1,314 @@
|
|||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import { ConventionInfo, NamingConvention, ImportStyle, CodeStyle } from '../types';
|
||||||
|
|
||||||
|
interface NamingPattern {
|
||||||
|
regex: RegExp;
|
||||||
|
type: 'camelCase' | 'snake_case' | 'kebab-case' | 'PascalCase';
|
||||||
|
}
|
||||||
|
|
||||||
|
const NAMING_PATTERNS: NamingPattern[] = [
|
||||||
|
{ regex: /^[a-z][a-zA-Z0-9]*$/, type: 'camelCase' },
|
||||||
|
{ regex: /^[a-z]+_[a-z0-9_]+$/, type: 'snake_case' },
|
||||||
|
{ regex: /^[a-z]+-[a-z0-9-]+$/, type: 'kebab-case' },
|
||||||
|
{ regex: /^[A-Z][a-zA-Z0-9]*$/, type: 'PascalCase' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const TEST_FRAMEWORK_PATTERNS = [
|
||||||
|
{ name: 'Jest', indicators: ['jest', '@types/jest'] },
|
||||||
|
{ name: 'Mocha', indicators: ['mocha'] },
|
||||||
|
{ name: 'Vitest', indicators: ['vitest'] },
|
||||||
|
{ name: 'Pytest', indicators: ['pytest'] },
|
||||||
|
{ name: 'unittest', indicators: ['unittest', 'unittest.mock'] },
|
||||||
|
{ name: 'Go testing', indicators: ['testing'] },
|
||||||
|
{ name: 'JUnit', indicators: ['junit', '@junit'] },
|
||||||
|
{ name: 'pytest', indicators: ['pytest'] },
|
||||||
|
];
|
||||||
|
|
||||||
|
export class ConventionExtractor {
|
||||||
|
async extract(dir: string, files: string[]): Promise<ConventionInfo> {
|
||||||
|
const namingConvention = await this.extractNamingConvention(files);
|
||||||
|
const importStyle = await this.extractImportStyle(dir, files);
|
||||||
|
const testingFramework = await this.detectTestingFramework(dir);
|
||||||
|
const codeStyle = await this.extractCodeStyle(files);
|
||||||
|
|
||||||
|
return {
|
||||||
|
namingConvention,
|
||||||
|
importStyle,
|
||||||
|
testingFramework,
|
||||||
|
codeStyle,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async extractNamingConvention(
|
||||||
|
files: string[]
|
||||||
|
): Promise<NamingConvention> {
|
||||||
|
const fileNames = files.map(f => path.basename(f));
|
||||||
|
const fileNameScores = this.scoreNamingPatterns(fileNames);
|
||||||
|
|
||||||
|
const allNames: string[] = [];
|
||||||
|
for (const file of files) {
|
||||||
|
try {
|
||||||
|
const content = await fs.promises.readFile(file, 'utf-8');
|
||||||
|
const identifiers = this.extractIdentifiers(content);
|
||||||
|
allNames.push(...identifiers);
|
||||||
|
} catch {
|
||||||
|
// Skip files that can't be read
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const variableNames = allNames.filter(n =>
|
||||||
|
/^[a-z]/.test(n) && !n.includes('_') && !n.contains('-')
|
||||||
|
);
|
||||||
|
const functionNames = allNames.filter(n =>
|
||||||
|
/^[a-z]/.test(n) && !n.includes('_') && !n.contains('-')
|
||||||
|
);
|
||||||
|
const classNames = allNames.filter(n =>
|
||||||
|
/^[A-Z]/.test(n)
|
||||||
|
);
|
||||||
|
|
||||||
|
const variableScores = this.scoreNamingPatterns(variableNames.slice(0, 100));
|
||||||
|
const functionScores = this.scoreNamingPatterns(functionNames.slice(0, 100));
|
||||||
|
const classScores = this.scoreNamingPatterns(classNames.slice(0, 50));
|
||||||
|
|
||||||
|
return {
|
||||||
|
files: this.getBestType(fileNameScores),
|
||||||
|
variables: this.getBestType(variableScores),
|
||||||
|
functions: this.getBestType(functionScores),
|
||||||
|
classes: this.getBestType(classScores),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private scoreNamingPatterns(names: string[]): Record<string, number> {
|
||||||
|
const scores: Record<string, number> = {
|
||||||
|
camelCase: 0,
|
||||||
|
snake_case: 0,
|
||||||
|
'kebab-case': 0,
|
||||||
|
PascalCase: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const name of names) {
|
||||||
|
for (const pattern of NAMING_PATTERNS) {
|
||||||
|
if (pattern.regex.test(name)) {
|
||||||
|
scores[pattern.type]++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return scores;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getBestType(
|
||||||
|
scores: Record<string, number>
|
||||||
|
): 'camelCase' | 'snake_case' | 'kebab-case' | 'PascalCase' {
|
||||||
|
let maxScore = 0;
|
||||||
|
let bestType: 'camelCase' | 'snake_case' | 'kebab-case' | 'PascalCase' =
|
||||||
|
'camelCase';
|
||||||
|
|
||||||
|
for (const [type, score] of Object.entries(scores)) {
|
||||||
|
if (score > maxScore) {
|
||||||
|
maxScore = score;
|
||||||
|
bestType = type as typeof bestType;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return bestType;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async extractImportStyle(
|
||||||
|
dir: string,
|
||||||
|
files: string[]
|
||||||
|
): Promise<ImportStyle> {
|
||||||
|
let hasESMImports = false;
|
||||||
|
let hasCommonJSImports = false;
|
||||||
|
let hasCommonJSRequires = false;
|
||||||
|
let hasAliasImports = false;
|
||||||
|
const commonPatterns: string[] = [];
|
||||||
|
|
||||||
|
const aliasPatterns = [
|
||||||
|
/^@\//,
|
||||||
|
/^~/,
|
||||||
|
/^src\//,
|
||||||
|
/^components\//,
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
try {
|
||||||
|
const content = await fs.promises.readFile(file, 'utf-8');
|
||||||
|
|
||||||
|
if (/import\s+.*\s+from\s+['"]/.test(content)) {
|
||||||
|
hasESMImports = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/require\s*\(/.test(content)) {
|
||||||
|
hasCommonJSRequires = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/export\s+(const|function|class|interface|type)/.test(content)) {
|
||||||
|
hasESMImports = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const pattern of aliasPatterns) {
|
||||||
|
if (pattern.test(content.replace(/import\s+.*\s+from\s+/, ''))) {
|
||||||
|
hasAliasImports = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const importMatches = content.match(/import\s+.*\s+from\s+['"]([^'"]+)['"]/g);
|
||||||
|
if (importMatches) {
|
||||||
|
for (const match of importMatches) {
|
||||||
|
const modMatch = match.match(/['"]([^'"]+)['"]$/);
|
||||||
|
if (modMatch) {
|
||||||
|
const module = modMatch[1];
|
||||||
|
if (!commonPatterns.includes(module)) {
|
||||||
|
commonPatterns.push(module);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Skip unreadable files
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hasCommonJSImports = hasCommonJSRequires;
|
||||||
|
|
||||||
|
let style: 'ESM' | 'CommonJS' | 'mixed' = 'CommonJS';
|
||||||
|
if (hasESMImports && hasCommonJSImports) {
|
||||||
|
style = 'mixed';
|
||||||
|
} else if (hasESMImports) {
|
||||||
|
style = 'ESM';
|
||||||
|
}
|
||||||
|
|
||||||
|
const aliasPrefix = hasAliasImports ? '@/' : null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
style,
|
||||||
|
aliasPrefix,
|
||||||
|
commonPatterns: commonPatterns.slice(0, 10),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async detectTestingFramework(dir: string): Promise<string | null> {
|
||||||
|
const packageJsonPath = path.join(dir, 'package.json');
|
||||||
|
if (await this.fileExists(packageJsonPath)) {
|
||||||
|
const content = await fs.promises.readFile(packageJsonPath, 'utf-8');
|
||||||
|
const packageJson = JSON.parse(content);
|
||||||
|
const allDeps = {
|
||||||
|
...packageJson.dependencies,
|
||||||
|
...packageJson.devDependencies,
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const framework of TEST_FRAMEWORK_PATTERNS) {
|
||||||
|
for (const indicator of framework.indicators) {
|
||||||
|
if (Object.keys(allDeps).some(dep => dep.includes(indicator))) {
|
||||||
|
return framework.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const requirementsPath = path.join(dir, 'requirements.txt');
|
||||||
|
if (await this.fileExists(requirementsPath)) {
|
||||||
|
const content = await fs.promises.readFile(requirementsPath, 'utf-8');
|
||||||
|
for (const framework of TEST_FRAMEWORK_PATTERNS) {
|
||||||
|
for (const indicator of framework.indicators) {
|
||||||
|
if (content.toLowerCase().includes(indicator.toLowerCase())) {
|
||||||
|
return framework.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const file of await fs.promises.readdir(dir)) {
|
||||||
|
if (file.endsWith('.test.ts') || file.endsWith('.spec.ts')) {
|
||||||
|
return 'Jest';
|
||||||
|
}
|
||||||
|
if (file.endsWith('.test.js') || file.endsWith('.spec.js')) {
|
||||||
|
return 'Jest';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async extractCodeStyle(files: string[]): Promise<CodeStyle> {
|
||||||
|
let spaceIndentCount = 0;
|
||||||
|
let tabIndentCount = 0;
|
||||||
|
let singleQuoteCount = 0;
|
||||||
|
let doubleQuoteCount = 0;
|
||||||
|
let lfLineEndings = 0;
|
||||||
|
let crlfLineEndings = 0;
|
||||||
|
|
||||||
|
let sampleSize = 0;
|
||||||
|
const maxSamples = 50;
|
||||||
|
|
||||||
|
for (const file of files.slice(0, maxSamples)) {
|
||||||
|
try {
|
||||||
|
const content = await fs.promises.readFile(file, 'utf-8');
|
||||||
|
const lines = content.split('\n').slice(0, 100);
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
if (/^\s+ /.test(line)) spaceIndentCount += 2;
|
||||||
|
if (/^\s+\t/.test(line)) tabIndentCount++;
|
||||||
|
if (/'[^']*'/.test(line) && !/\\'/.test(line)) singleQuoteCount++;
|
||||||
|
if (/"[^"]*"/.test(line) && !/\\"/.test(line)) doubleQuoteCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (content.includes('\r\n')) crlfLineEndings++;
|
||||||
|
if (!content.includes('\r\n') && content.includes('\n')) lfLineEndings++;
|
||||||
|
sampleSize++;
|
||||||
|
} catch {
|
||||||
|
// Skip unreadable files
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const indentType = spaceIndentCount > tabIndentCount ? 'spaces' : 'tabs';
|
||||||
|
const quoteStyle = singleQuoteCount > doubleQuoteCount ? 'single' : 'double';
|
||||||
|
const lineEndings = lfLineEndings > crlfLineEndings ? 'LF' : 'CRLF';
|
||||||
|
|
||||||
|
return {
|
||||||
|
indentSize: 2,
|
||||||
|
indentType,
|
||||||
|
lineEndings,
|
||||||
|
quoteStyle,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private extractIdentifiers(content: string): string[] {
|
||||||
|
const identifiers: string[] = [];
|
||||||
|
|
||||||
|
const varPattern = /\b(const|let|var)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/g;
|
||||||
|
let match;
|
||||||
|
while ((match = varPattern.exec(content)) !== null) {
|
||||||
|
identifiers.push(match[2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const funcPattern = /function\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/g;
|
||||||
|
while ((match = funcPattern.exec(content)) !== null) {
|
||||||
|
identifiers.push(match[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const arrowFuncPattern = /const\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=/g;
|
||||||
|
while ((match = arrowFuncPattern.exec(content)) !== null) {
|
||||||
|
identifiers.push(match[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const classPattern = /class\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/g;
|
||||||
|
while ((match = classPattern.exec(content)) !== null) {
|
||||||
|
identifiers.push(match[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...new Set(identifiers)];
|
||||||
|
}
|
||||||
|
|
||||||
|
private async fileExists(filePath: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
await fs.promises.access(filePath, fs.constants.F_OK);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
353
src/analyzers/dependencyAnalyzer.ts
Normal file
353
src/analyzers/dependencyAnalyzer.ts
Normal file
@@ -0,0 +1,353 @@
|
|||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import { DependencyInfo, Dependency } from '../types';
|
||||||
|
|
||||||
|
interface DependencyFile {
|
||||||
|
path: string;
|
||||||
|
parser: (content: string) => Dependency[];
|
||||||
|
type: 'prod' | 'dev' | 'both';
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DependencyAnalyzer {
|
||||||
|
private dependencyFiles: DependencyFile[] = [
|
||||||
|
{
|
||||||
|
path: 'package.json',
|
||||||
|
parser: this.parsePackageJson.bind(this),
|
||||||
|
type: 'both',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'requirements.txt',
|
||||||
|
parser: this.parseRequirementsTxt.bind(this),
|
||||||
|
type: 'prod',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'pyproject.toml',
|
||||||
|
parser: this.parsePyprojectToml.bind(this),
|
||||||
|
type: 'both',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'go.mod',
|
||||||
|
parser: this.parseGoMod.bind(this),
|
||||||
|
type: 'prod',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'Cargo.toml',
|
||||||
|
parser: this.parseCargoToml.bind(this),
|
||||||
|
type: 'both',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'Pipfile',
|
||||||
|
parser: this.parsePipfile.bind(this),
|
||||||
|
type: 'both',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'Gemfile',
|
||||||
|
parser: this.parseGemfile.bind(this),
|
||||||
|
type: 'prod',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'composer.json',
|
||||||
|
parser: this.parseComposerJson.bind(this),
|
||||||
|
type: 'both',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
async analyze(
|
||||||
|
dir: string,
|
||||||
|
includeDev: boolean = false
|
||||||
|
): Promise<DependencyInfo> {
|
||||||
|
const allDependencies: Dependency[] = [];
|
||||||
|
|
||||||
|
for (const depFile of this.dependencyFiles) {
|
||||||
|
const filePath = path.join(dir, depFile.path);
|
||||||
|
if (await this.fileExists(filePath)) {
|
||||||
|
try {
|
||||||
|
const content = await fs.promises.readFile(filePath, 'utf-8');
|
||||||
|
const deps = depFile.parser(content);
|
||||||
|
|
||||||
|
if (depFile.type === 'both') {
|
||||||
|
allDependencies.push(...deps);
|
||||||
|
} else if (depFile.type === 'prod' || includeDev) {
|
||||||
|
allDependencies.push(...deps);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`Failed to parse ${depFile.path}: ${error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const direct = allDependencies.filter(d => !d.isLocal);
|
||||||
|
const dev = allDependencies.filter(d => d.type === 'dev');
|
||||||
|
|
||||||
|
return {
|
||||||
|
direct,
|
||||||
|
dev,
|
||||||
|
total: direct.length + dev.length,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private parsePackageJson(content: string): Dependency[] {
|
||||||
|
const packageJson = JSON.parse(content);
|
||||||
|
const dependencies: Dependency[] = [];
|
||||||
|
|
||||||
|
const parseDeps = (
|
||||||
|
deps: Record<string, string> | undefined,
|
||||||
|
type: 'prod' | 'dev'
|
||||||
|
) => {
|
||||||
|
if (!deps) return;
|
||||||
|
for (const [name, version] of Object.entries(deps)) {
|
||||||
|
dependencies.push({
|
||||||
|
name,
|
||||||
|
version: version as string,
|
||||||
|
type,
|
||||||
|
isLocal: name.startsWith('.') || name.startsWith('/') || name.startsWith('@'),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
parseDeps(packageJson.dependencies, 'prod');
|
||||||
|
parseDeps(packageJson.devDependencies, 'dev');
|
||||||
|
|
||||||
|
return dependencies;
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseRequirementsTxt(content: string): Dependency[] {
|
||||||
|
const lines = content.split('\n');
|
||||||
|
const dependencies: Dependency[] = [];
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
const trimmed = line.trim();
|
||||||
|
if (!trimmed || trimmed.startsWith('#') || trimmed.startsWith('-')) continue;
|
||||||
|
|
||||||
|
const match = trimmed.match(/^([a-zA-Z0-9_-]+)([<>=!~]+)(.+)$/);
|
||||||
|
if (match) {
|
||||||
|
dependencies.push({
|
||||||
|
name: match[1],
|
||||||
|
version: match[2] + match[3],
|
||||||
|
type: 'prod',
|
||||||
|
isLocal: false,
|
||||||
|
});
|
||||||
|
} else if (/^[a-zA-Z0-9_-]+$/.test(trimmed)) {
|
||||||
|
dependencies.push({
|
||||||
|
name: trimmed,
|
||||||
|
version: '*',
|
||||||
|
type: 'prod',
|
||||||
|
isLocal: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return dependencies;
|
||||||
|
}
|
||||||
|
|
||||||
|
private parsePyprojectToml(content: string): Dependency[] {
|
||||||
|
const dependencies: Dependency[] = [];
|
||||||
|
const lines = content.split('\n');
|
||||||
|
let inDependencies = false;
|
||||||
|
let inDevDependencies = false;
|
||||||
|
let currentSection: 'prod' | 'dev' | null = null;
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
const trimmed = line.trim();
|
||||||
|
|
||||||
|
if (trimmed.startsWith('[') && trimmed.endsWith(']')) {
|
||||||
|
const section = trimmed.slice(1, -1).toLowerCase();
|
||||||
|
if (section.includes('dependencies') || section.includes('requires')) {
|
||||||
|
currentSection = 'prod';
|
||||||
|
inDependencies = true;
|
||||||
|
inDevDependencies = false;
|
||||||
|
} else if (section.includes('dev-dependencies')) {
|
||||||
|
currentSection = 'dev';
|
||||||
|
inDevDependencies = true;
|
||||||
|
inDependencies = false;
|
||||||
|
} else {
|
||||||
|
inDependencies = false;
|
||||||
|
inDevDependencies = false;
|
||||||
|
currentSection = null;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!inDependencies && !inDevDependencies) continue;
|
||||||
|
if (trimmed.startsWith('#') || !trimmed) continue;
|
||||||
|
|
||||||
|
const match = trimmed.match(/^([a-zA-Z0-9_-]+)([<>=!~]+)(.+)$/);
|
||||||
|
if (match) {
|
||||||
|
dependencies.push({
|
||||||
|
name: match[1],
|
||||||
|
version: match[2] + match[3],
|
||||||
|
type: currentSection || 'prod',
|
||||||
|
isLocal: false,
|
||||||
|
});
|
||||||
|
} else if (/^[a-zA-Z0-9_-]+$/.test(trimmed)) {
|
||||||
|
dependencies.push({
|
||||||
|
name: trimmed,
|
||||||
|
version: '*',
|
||||||
|
type: currentSection || 'prod',
|
||||||
|
isLocal: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return dependencies;
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseGoMod(content: string): Dependency[] {
|
||||||
|
const dependencies: Dependency[] = [];
|
||||||
|
const lines = content.split('\n');
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
const trimmed = line.trim();
|
||||||
|
if (!trimmed || trimmed.startsWith('module') || trimmed.startsWith('go')) continue;
|
||||||
|
|
||||||
|
const match = trimmed.match(/^([a-zA-Z0-9./_-]+)\s+v?([0-9.]+)/);
|
||||||
|
if (match) {
|
||||||
|
dependencies.push({
|
||||||
|
name: match[1],
|
||||||
|
version: match[2],
|
||||||
|
type: 'prod',
|
||||||
|
isLocal: match[1].startsWith('./') || match[1].startsWith('../'),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return dependencies;
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseCargoToml(content: string): Dependency[] {
|
||||||
|
const dependencies: Dependency[] = [];
|
||||||
|
const lines = content.split('\n');
|
||||||
|
let currentSection: 'prod' | 'dev' | null = null;
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
const trimmed = line.trim();
|
||||||
|
|
||||||
|
if (trimmed.startsWith('[') && trimmed.endsWith(']')) {
|
||||||
|
const section = trimmed.slice(1, -1).toLowerCase();
|
||||||
|
if (section === 'dependencies') {
|
||||||
|
currentSection = 'prod';
|
||||||
|
} else if (section === 'dev-dependencies') {
|
||||||
|
currentSection = 'dev';
|
||||||
|
} else {
|
||||||
|
currentSection = null;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!currentSection) continue;
|
||||||
|
if (trimmed.startsWith('#') || !trimmed) continue;
|
||||||
|
|
||||||
|
const match = trimmed.match(/^([a-zA-Z0-9_-]+)\s*=\s*["\']?(.+?)["\']?\s*,?$/);
|
||||||
|
if (match) {
|
||||||
|
dependencies.push({
|
||||||
|
name: match[1],
|
||||||
|
version: match[2].replace(/["']/g, ''),
|
||||||
|
type: currentSection,
|
||||||
|
isLocal: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return dependencies;
|
||||||
|
}
|
||||||
|
|
||||||
|
private parsePipfile(content: string): Dependency[] {
|
||||||
|
const dependencies: Dependency[] = [];
|
||||||
|
const lines = content.split('\n');
|
||||||
|
let currentSection: 'prod' | 'dev' | null = null;
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
const trimmed = line.trim();
|
||||||
|
|
||||||
|
if (trimmed.startsWith('[') && trimmed.endsWith(']')) {
|
||||||
|
const section = trimmed.slice(1, -1).toLowerCase();
|
||||||
|
if (section === 'packages') {
|
||||||
|
currentSection = 'prod';
|
||||||
|
} else if (section === 'dev-packages') {
|
||||||
|
currentSection = 'dev';
|
||||||
|
} else {
|
||||||
|
currentSection = null;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!currentSection) continue;
|
||||||
|
if (trimmed.startsWith('#') || !trimmed) continue;
|
||||||
|
|
||||||
|
const match = trimmed.match(/^([a-zA-Z0-9_-]+)\s*=\s*\{?\s*version\s*=\s*["\']([^"\']+)["\']/);
|
||||||
|
if (match) {
|
||||||
|
dependencies.push({
|
||||||
|
name: match[1],
|
||||||
|
version: match[2],
|
||||||
|
type: currentSection,
|
||||||
|
isLocal: false,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const simpleMatch = trimmed.match(/^([a-zA-Z0-9_-]+)/);
|
||||||
|
if (simpleMatch) {
|
||||||
|
dependencies.push({
|
||||||
|
name: simpleMatch[1],
|
||||||
|
version: '*',
|
||||||
|
type: currentSection,
|
||||||
|
isLocal: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return dependencies;
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseGemfile(content: string): Dependency[] {
|
||||||
|
const dependencies: Dependency[] = [];
|
||||||
|
const lines = content.split('\n');
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
const trimmed = line.trim();
|
||||||
|
if (!trimmed || trimmed.startsWith('#')) continue;
|
||||||
|
|
||||||
|
const match = trimmed.match(/^gem\s+["']([^"']+)["'](?:\s*,\s*version:\s*["']([^"']+)["'])?/);
|
||||||
|
if (match) {
|
||||||
|
dependencies.push({
|
||||||
|
name: match[1],
|
||||||
|
version: match[2] || '*',
|
||||||
|
type: 'prod',
|
||||||
|
isLocal: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return dependencies;
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseComposerJson(content: string): Dependency[] {
|
||||||
|
const composerJson = JSON.parse(content);
|
||||||
|
const dependencies: Dependency[] = [];
|
||||||
|
|
||||||
|
const parseDeps = (deps: Record<string, string> | undefined, type: 'prod' | 'dev') => {
|
||||||
|
if (!deps) return;
|
||||||
|
for (const [name, version] of Object.entries(deps)) {
|
||||||
|
dependencies.push({
|
||||||
|
name,
|
||||||
|
version: version as string,
|
||||||
|
type,
|
||||||
|
isLocal: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
parseDeps(composerJson.require, 'prod');
|
||||||
|
parseDeps(composerJson['require-dev'], 'dev');
|
||||||
|
|
||||||
|
return dependencies;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async fileExists(filePath: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
await fs.promises.access(filePath, fs.constants.F_OK);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
326
src/analyzers/projectTypeDetector.ts
Normal file
326
src/analyzers/projectTypeDetector.ts
Normal file
@@ -0,0 +1,326 @@
|
|||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import { ProjectType } from '../types';
|
||||||
|
|
||||||
|
interface FrameworkPattern {
|
||||||
|
name: string;
|
||||||
|
indicators: string[];
|
||||||
|
language: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LanguagePattern {
|
||||||
|
extensions: string[];
|
||||||
|
configFiles: string[];
|
||||||
|
language: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ProjectTypeDetector {
|
||||||
|
private languagePatterns: LanguagePattern[] = [
|
||||||
|
{
|
||||||
|
extensions: ['.ts', '.tsx'],
|
||||||
|
language: 'TypeScript',
|
||||||
|
configFiles: ['tsconfig.json'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
extensions: ['.js', '.jsx'],
|
||||||
|
language: 'JavaScript',
|
||||||
|
configFiles: ['package.json', 'jsconfig.json'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
extensions: ['.py'],
|
||||||
|
language: 'Python',
|
||||||
|
configFiles: ['pyproject.toml', 'setup.py', 'requirements.txt'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
extensions: ['.go'],
|
||||||
|
language: 'Go',
|
||||||
|
configFiles: ['go.mod', 'go.sum'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
extensions: ['.rs'],
|
||||||
|
language: 'Rust',
|
||||||
|
configFiles: ['Cargo.toml', 'Cargo.lock'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
extensions: ['.java'],
|
||||||
|
language: 'Java',
|
||||||
|
configFiles: ['pom.xml', 'build.gradle', 'build.gradle.kts'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
extensions: ['.c', '.h'],
|
||||||
|
language: 'C',
|
||||||
|
configFiles: ['CMakeLists.txt', 'Makefile'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
extensions: ['.cpp', '.cc', '.cxx', '.hpp'],
|
||||||
|
language: 'C++',
|
||||||
|
configFiles: ['CMakeLists.txt', 'Makefile'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
extensions: ['.cs'],
|
||||||
|
language: 'C#',
|
||||||
|
configFiles: ['*.csproj', '*.sln'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
extensions: ['.rb'],
|
||||||
|
language: 'Ruby',
|
||||||
|
configFiles: ['Gemfile', 'Rakefile'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
extensions: ['.php'],
|
||||||
|
language: 'PHP',
|
||||||
|
configFiles: ['composer.json', 'phpunit.xml'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
extensions: ['.swift'],
|
||||||
|
language: 'Swift',
|
||||||
|
configFiles: ['Package.swift', '*.xcodeproj'],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
private frameworkPatterns: FrameworkPattern[] = [
|
||||||
|
{
|
||||||
|
name: 'React',
|
||||||
|
indicators: ['react', 'react-dom', 'react-scripts'],
|
||||||
|
language: 'TypeScript',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Vue',
|
||||||
|
indicators: ['vue', 'vue-loader', '@vue/'],
|
||||||
|
language: 'TypeScript',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Express',
|
||||||
|
indicators: ['express'],
|
||||||
|
language: 'JavaScript',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Next.js',
|
||||||
|
indicators: ['next'],
|
||||||
|
language: 'TypeScript',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'NestJS',
|
||||||
|
indicators: ['@nestjs/'],
|
||||||
|
language: 'TypeScript',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Django',
|
||||||
|
indicators: ['django'],
|
||||||
|
language: 'Python',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'FastAPI',
|
||||||
|
indicators: ['fastapi', 'uvicorn'],
|
||||||
|
language: 'Python',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Flask',
|
||||||
|
indicators: ['flask'],
|
||||||
|
language: 'Python',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Gin',
|
||||||
|
indicators: ['gin-gonic/gin'],
|
||||||
|
language: 'Go',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Echo',
|
||||||
|
indicators: ['labstack/echo'],
|
||||||
|
language: 'Go',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Actix-web',
|
||||||
|
indicators: ['actix-web'],
|
||||||
|
language: 'Rust',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Rocket',
|
||||||
|
indicators: ['rocket'],
|
||||||
|
language: 'Rust',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Spring Boot',
|
||||||
|
indicators: ['spring-boot'],
|
||||||
|
language: 'Java',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
async detect(dir: string): Promise<ProjectType> {
|
||||||
|
const languages = await this.detectLanguages(dir);
|
||||||
|
const primaryLanguage = await this.determinePrimaryLanguage(languages);
|
||||||
|
const frameworks = await this.detectFrameworks(dir, primaryLanguage);
|
||||||
|
const buildTools = await this.detectBuildTools(dir, languages);
|
||||||
|
|
||||||
|
return {
|
||||||
|
primaryLanguage,
|
||||||
|
languages,
|
||||||
|
frameworks,
|
||||||
|
buildTools,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async determinePrimaryLanguage(
|
||||||
|
languages: string[]
|
||||||
|
): Promise<string> {
|
||||||
|
if (languages.length === 0) return 'Unknown';
|
||||||
|
return languages[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
private async detectLanguages(dir: string): Promise<string[]> {
|
||||||
|
const detectedLanguages: string[] = [];
|
||||||
|
const contents = await fs.promises.readdir(dir, { withFileTypes: true });
|
||||||
|
|
||||||
|
const fileExtensions = new Set<string>();
|
||||||
|
for (const item of contents) {
|
||||||
|
if (item.isFile()) {
|
||||||
|
const ext = path.extname(item.name).toLowerCase();
|
||||||
|
fileExtensions.add(ext);
|
||||||
|
} else if (item.isDirectory()) {
|
||||||
|
const subDirContents = await fs.promises.readdir(
|
||||||
|
path.join(dir, item.name),
|
||||||
|
{ withFileTypes: true }
|
||||||
|
);
|
||||||
|
for (const subItem of subDirContents) {
|
||||||
|
if (subItem.isFile()) {
|
||||||
|
const ext = path.extname(subItem.name).toLowerCase();
|
||||||
|
fileExtensions.add(ext);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const pattern of this.languagePatterns) {
|
||||||
|
for (const ext of pattern.extensions) {
|
||||||
|
if (fileExtensions.has(ext)) {
|
||||||
|
if (!detectedLanguages.includes(pattern.language)) {
|
||||||
|
detectedLanguages.push(pattern.language);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const pattern of this.languagePatterns) {
|
||||||
|
for (const configFile of pattern.configFiles) {
|
||||||
|
const fullPath = path.join(dir, configFile);
|
||||||
|
if (await this.fileExists(fullPath)) {
|
||||||
|
if (!detectedLanguages.includes(pattern.language)) {
|
||||||
|
detectedLanguages.push(pattern.language);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return detectedLanguages;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async detectFrameworks(
|
||||||
|
dir: string,
|
||||||
|
primaryLanguage: string
|
||||||
|
): Promise<string[]> {
|
||||||
|
const detectedFrameworks: string[] = [];
|
||||||
|
|
||||||
|
for (const item of this.frameworkPatterns) {
|
||||||
|
if (item.language !== primaryLanguage) continue;
|
||||||
|
|
||||||
|
const packageJsonPath = path.join(dir, 'package.json');
|
||||||
|
if (await this.fileExists(packageJsonPath)) {
|
||||||
|
const content = await fs.promises.readFile(packageJsonPath, 'utf-8');
|
||||||
|
const packageJson = JSON.parse(content);
|
||||||
|
const allDeps = {
|
||||||
|
...packageJson.dependencies,
|
||||||
|
...packageJson.devDependencies,
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const indicator of item.indicators) {
|
||||||
|
if (Object.keys(allDeps).some(dep => dep.startsWith(indicator))) {
|
||||||
|
if (!detectedFrameworks.includes(item.name)) {
|
||||||
|
detectedFrameworks.push(item.name);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.language === 'Python') {
|
||||||
|
const requirementsPath = path.join(dir, 'requirements.txt');
|
||||||
|
const pyprojectPath = path.join(dir, 'pyproject.toml');
|
||||||
|
|
||||||
|
const filesToCheck = [requirementsPath, pyprojectPath];
|
||||||
|
for (const filePath of filesToCheck) {
|
||||||
|
if (await this.fileExists(filePath)) {
|
||||||
|
const content = await fs.promises.readFile(filePath, 'utf-8');
|
||||||
|
for (const indicator of item.indicators) {
|
||||||
|
if (content.toLowerCase().includes(indicator.toLowerCase())) {
|
||||||
|
if (!detectedFrameworks.includes(item.name)) {
|
||||||
|
detectedFrameworks.push(item.name);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.language === 'Go') {
|
||||||
|
const goModPath = path.join(dir, 'go.mod');
|
||||||
|
if (await this.fileExists(goModPath)) {
|
||||||
|
const content = await fs.promises.readFile(goModPath, 'utf-8');
|
||||||
|
for (const indicator of item.indicators) {
|
||||||
|
if (content.includes(indicator)) {
|
||||||
|
if (!detectedFrameworks.includes(item.name)) {
|
||||||
|
detectedFrameworks.push(item.name);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return detectedFrameworks;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async detectBuildTools(
|
||||||
|
dir: string,
|
||||||
|
languages: string[]
|
||||||
|
): Promise<string[]> {
|
||||||
|
const buildTools: string[] = [];
|
||||||
|
|
||||||
|
const buildToolFiles: Record<string, string[]> = {
|
||||||
|
npm: ['package.json'],
|
||||||
|
yarn: ['yarn.lock', '.yarnrc'],
|
||||||
|
pnpm: ['pnpm-lock.yaml'],
|
||||||
|
make: ['Makefile'],
|
||||||
|
cmake: ['CMakeLists.txt'],
|
||||||
|
gradle: ['build.gradle', 'build.gradle.kts'],
|
||||||
|
maven: ['pom.xml'],
|
||||||
|
cargo: ['Cargo.toml', 'Cargo.lock'],
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const [tool, files] of Object.entries(buildToolFiles)) {
|
||||||
|
for (const file of files) {
|
||||||
|
const filePath = path.join(dir, file);
|
||||||
|
if (await this.fileExists(filePath)) {
|
||||||
|
if (!buildTools.includes(tool)) {
|
||||||
|
buildTools.push(tool);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return buildTools;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async fileExists(filePath: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
await fs.promises.access(filePath, fs.constants.F_OK);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
99
src/config/configLoader.ts
Normal file
99
src/config/configLoader.ts
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import { ContextConfig } from '../types';
|
||||||
|
|
||||||
|
export class ConfigLoader {
|
||||||
|
private static defaultConfig: ContextConfig = {
|
||||||
|
includes: [
|
||||||
|
'**/*.ts',
|
||||||
|
'**/*.js',
|
||||||
|
'**/*.py',
|
||||||
|
'**/*.go',
|
||||||
|
'**/*.rs',
|
||||||
|
'**/*.java',
|
||||||
|
'**/*.c',
|
||||||
|
'**/*.cpp',
|
||||||
|
'**/*.h',
|
||||||
|
'**/*.hpp',
|
||||||
|
],
|
||||||
|
excludes: [
|
||||||
|
'node_modules/**',
|
||||||
|
'dist/**',
|
||||||
|
'build/**',
|
||||||
|
'.git/**',
|
||||||
|
'.DS_Store',
|
||||||
|
'*.log',
|
||||||
|
],
|
||||||
|
outputFormat: 'json',
|
||||||
|
template: 'default',
|
||||||
|
outputFile: 'ai-context.json',
|
||||||
|
analyzeConventions: true,
|
||||||
|
includeDevDependencies: false,
|
||||||
|
respectGitignore: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
static async load(
|
||||||
|
configPath?: string
|
||||||
|
): Promise<ContextConfig> {
|
||||||
|
if (!configPath) {
|
||||||
|
const localConfig = path.join(process.cwd(), '.ai-context-config.json');
|
||||||
|
if (await this.fileExists(localConfig)) {
|
||||||
|
return this.loadFromFile(localConfig);
|
||||||
|
}
|
||||||
|
return { ...this.defaultConfig };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (await this.fileExists(configPath)) {
|
||||||
|
return this.loadFromFile(configPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Config file not found: ${configPath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async loadFromFile(
|
||||||
|
configPath: string
|
||||||
|
): Promise<ContextConfig> {
|
||||||
|
const content = await fs.promises.readFile(configPath, 'utf-8');
|
||||||
|
const userConfig = JSON.parse(content);
|
||||||
|
|
||||||
|
return this.mergeConfig(userConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static mergeConfig(
|
||||||
|
userConfig: Partial<ContextConfig>
|
||||||
|
): ContextConfig {
|
||||||
|
return {
|
||||||
|
includes: userConfig.includes ?? this.defaultConfig.includes,
|
||||||
|
excludes: userConfig.excludes ?? this.defaultConfig.excludes,
|
||||||
|
outputFormat:
|
||||||
|
userConfig.outputFormat ?? this.defaultConfig.outputFormat,
|
||||||
|
template: userConfig.template ?? this.defaultConfig.template,
|
||||||
|
outputFile:
|
||||||
|
userConfig.outputFile ?? this.defaultConfig.outputFile,
|
||||||
|
analyzeConventions:
|
||||||
|
userConfig.analyzeConventions ?? this.defaultConfig.analyzeConventions,
|
||||||
|
includeDevDependencies:
|
||||||
|
userConfig.includeDevDependencies ??
|
||||||
|
this.defaultConfig.includeDevDependencies,
|
||||||
|
respectGitignore:
|
||||||
|
userConfig.respectGitignore ?? this.defaultConfig.respectGitignore,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async fileExists(filePath: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
await fs.promises.access(filePath, fs.constants.F_OK);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static save(
|
||||||
|
config: ContextConfig,
|
||||||
|
outputPath: string
|
||||||
|
): void {
|
||||||
|
const content = JSON.stringify(config, null, 2);
|
||||||
|
fs.writeFileSync(outputPath, content, 'utf-8');
|
||||||
|
}
|
||||||
|
}
|
||||||
167
src/generators/contextGenerator.ts
Normal file
167
src/generators/contextGenerator.ts
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import yaml from 'js-yaml';
|
||||||
|
import {
|
||||||
|
ProjectInfo,
|
||||||
|
ContextConfig,
|
||||||
|
TemplateData,
|
||||||
|
FileInfo,
|
||||||
|
ProjectType,
|
||||||
|
DependencyInfo,
|
||||||
|
ConventionInfo,
|
||||||
|
} from '../types';
|
||||||
|
import { ProjectTypeDetector } from '../analyzers/projectTypeDetector';
|
||||||
|
import { DependencyAnalyzer } from '../analyzers/dependencyAnalyzer';
|
||||||
|
import { ConventionExtractor } from '../analyzers/conventionExtractor';
|
||||||
|
import { FileUtils } from '../utils/fileUtils';
|
||||||
|
import { ConfigLoader } from '../config/configLoader';
|
||||||
|
|
||||||
|
export class ContextGenerator {
|
||||||
|
private projectTypeDetector: ProjectTypeDetector;
|
||||||
|
private dependencyAnalyzer: DependencyAnalyzer;
|
||||||
|
private conventionExtractor: ConventionExtractor;
|
||||||
|
private fileUtils: FileUtils;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.projectTypeDetector = new ProjectTypeDetector();
|
||||||
|
this.dependencyAnalyzer = new DependencyAnalyzer();
|
||||||
|
this.conventionExtractor = new ConventionExtractor();
|
||||||
|
this.fileUtils = FileUtils.getInstance();
|
||||||
|
}
|
||||||
|
|
||||||
|
async generate(
|
||||||
|
dir: string,
|
||||||
|
config?: ContextConfig
|
||||||
|
): Promise<ProjectInfo> {
|
||||||
|
const resolvedDir = this.fileUtils.resolveDirectory(dir);
|
||||||
|
|
||||||
|
const contextConfig = config ?? await ConfigLoader.load();
|
||||||
|
|
||||||
|
if (contextConfig.respectGitignore) {
|
||||||
|
await this.fileUtils.loadGitignore(resolvedDir);
|
||||||
|
this.fileUtils.addCustomPatterns(contextConfig.excludes);
|
||||||
|
}
|
||||||
|
|
||||||
|
const files = await this.fileUtils.getFiles(
|
||||||
|
resolvedDir,
|
||||||
|
contextConfig.includes,
|
||||||
|
contextConfig.excludes
|
||||||
|
);
|
||||||
|
|
||||||
|
const projectType = await this.projectTypeDetector.detect(resolvedDir);
|
||||||
|
const dependencies = await this.dependencyAnalyzer.analyze(
|
||||||
|
resolvedDir,
|
||||||
|
contextConfig.includeDevDependencies
|
||||||
|
);
|
||||||
|
|
||||||
|
let conventions: ConventionInfo | null = null;
|
||||||
|
if (contextConfig.analyzeConventions) {
|
||||||
|
conventions = await this.conventionExtractor.extract(resolvedDir, files) ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
projectType,
|
||||||
|
language: projectType.primaryLanguage,
|
||||||
|
framework: projectType.frameworks[0] || null,
|
||||||
|
dependencies,
|
||||||
|
conventions,
|
||||||
|
fileCount: files.length,
|
||||||
|
analysisDate: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async generateWithFiles(
|
||||||
|
dir: string,
|
||||||
|
config?: ContextConfig
|
||||||
|
): Promise<TemplateData> {
|
||||||
|
const projectInfo = await this.generate(dir, config);
|
||||||
|
const contextConfig = config ?? await ConfigLoader.load();
|
||||||
|
const resolvedDir = this.fileUtils.resolveDirectory(dir);
|
||||||
|
|
||||||
|
const files = await this.fileUtils.getFiles(
|
||||||
|
resolvedDir,
|
||||||
|
contextConfig.includes,
|
||||||
|
contextConfig.excludes
|
||||||
|
);
|
||||||
|
|
||||||
|
const fileInfos: FileInfo[] = [];
|
||||||
|
for (const file of files) {
|
||||||
|
try {
|
||||||
|
const size = await this.fileUtils.getFileSize(file);
|
||||||
|
fileInfos.push({
|
||||||
|
path: path.relative(resolvedDir, file),
|
||||||
|
size,
|
||||||
|
type: this.fileUtils.getFileExtension(file),
|
||||||
|
language: this.detectLanguage(file),
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
// Skip files that can't be read
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
projectInfo,
|
||||||
|
files: fileInfos,
|
||||||
|
config: contextConfig,
|
||||||
|
generatedAt: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async generateJson(dir: string, config?: ContextConfig): Promise<string> {
|
||||||
|
const data = await this.generateWithFiles(dir, config);
|
||||||
|
return JSON.stringify(data, null, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
async generateYaml(dir: string, config?: ContextConfig): Promise<string> {
|
||||||
|
const data = await this.generateWithFiles(dir, config);
|
||||||
|
return yaml.dump(data, { indent: 2, lineWidth: -1 });
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveContext(
|
||||||
|
dir: string,
|
||||||
|
outputPath: string,
|
||||||
|
format: 'json' | 'yaml',
|
||||||
|
config?: ContextConfig
|
||||||
|
): Promise<void> {
|
||||||
|
let content: string;
|
||||||
|
let finalPath = outputPath;
|
||||||
|
|
||||||
|
if (format === 'json') {
|
||||||
|
content = await this.generateJson(dir, config);
|
||||||
|
if (!finalPath.endsWith('.json')) {
|
||||||
|
finalPath = `${finalPath}.json`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
content = await this.generateYaml(dir, config);
|
||||||
|
if (!finalPath.endsWith('.yaml') && !finalPath.endsWith('.yml')) {
|
||||||
|
finalPath = `${finalPath}.yaml`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.fileUtils.writeFile(finalPath, content);
|
||||||
|
}
|
||||||
|
|
||||||
|
private detectLanguage(filePath: string): string {
|
||||||
|
const ext = this.fileUtils.getFileExtension(filePath);
|
||||||
|
const languageMap: Record<string, string> = {
|
||||||
|
'.ts': 'TypeScript',
|
||||||
|
'.tsx': 'TypeScript',
|
||||||
|
'.js': 'JavaScript',
|
||||||
|
'.jsx': 'JavaScript',
|
||||||
|
'.py': 'Python',
|
||||||
|
'.go': 'Go',
|
||||||
|
'.rs': 'Rust',
|
||||||
|
'.java': 'Java',
|
||||||
|
'.c': 'C',
|
||||||
|
'.cpp': 'C++',
|
||||||
|
'.h': 'C',
|
||||||
|
'.hpp': 'C++',
|
||||||
|
'.cs': 'C#',
|
||||||
|
'.rb': 'Ruby',
|
||||||
|
'.php': 'PHP',
|
||||||
|
'.swift': 'Swift',
|
||||||
|
};
|
||||||
|
|
||||||
|
return languageMap[ext] || 'Unknown';
|
||||||
|
}
|
||||||
|
}
|
||||||
40
src/generators/jsonGenerator.ts
Normal file
40
src/generators/jsonGenerator.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { TemplateData } from '../types';
|
||||||
|
|
||||||
|
export class JsonGenerator {
|
||||||
|
static generate(data: TemplateData): string {
|
||||||
|
return JSON.stringify(data, null, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
static generateSummary(data: TemplateData): object {
|
||||||
|
const { projectInfo, files, config, generatedAt } = data;
|
||||||
|
|
||||||
|
return {
|
||||||
|
generatedAt,
|
||||||
|
project: {
|
||||||
|
type: projectInfo.projectType.primaryLanguage,
|
||||||
|
languages: projectInfo.projectType.languages,
|
||||||
|
frameworks: projectInfo.projectType.frameworks,
|
||||||
|
buildTools: projectInfo.projectType.buildTools,
|
||||||
|
fileCount: projectInfo.fileCount,
|
||||||
|
},
|
||||||
|
dependencies: {
|
||||||
|
total: projectInfo.dependencies.total,
|
||||||
|
production: projectInfo.dependencies.direct.length,
|
||||||
|
development: projectInfo.dependencies.dev.length,
|
||||||
|
topDependencies: projectInfo.dependencies.direct
|
||||||
|
.slice(0, 10)
|
||||||
|
.map(d => d.name),
|
||||||
|
},
|
||||||
|
conventions: projectInfo.conventions
|
||||||
|
? {
|
||||||
|
naming: projectInfo.conventions.namingConvention,
|
||||||
|
importStyle: projectInfo.conventions.importStyle.style,
|
||||||
|
testingFramework: projectInfo.conventions.testingFramework,
|
||||||
|
codeStyle: projectInfo.conventions.codeStyle,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
config,
|
||||||
|
sampleFiles: files.slice(0, 20),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
54
src/generators/yamlGenerator.ts
Normal file
54
src/generators/yamlGenerator.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import yaml from 'js-yaml';
|
||||||
|
import { TemplateData } from '../types';
|
||||||
|
|
||||||
|
export class YamlGenerator {
|
||||||
|
static generate(data: TemplateData): string {
|
||||||
|
return yaml.dump(data, {
|
||||||
|
indent: 2,
|
||||||
|
lineWidth: -1,
|
||||||
|
noRefs: true,
|
||||||
|
sortKeys: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static generateSummary(data: TemplateData): string {
|
||||||
|
const summary = {
|
||||||
|
generated_at: data.generatedAt,
|
||||||
|
project: {
|
||||||
|
type: data.projectInfo.projectType.primaryLanguage,
|
||||||
|
languages: data.projectInfo.projectType.languages,
|
||||||
|
frameworks: data.projectInfo.projectType.frameworks,
|
||||||
|
build_tools: data.projectInfo.projectType.buildTools,
|
||||||
|
file_count: data.projectInfo.fileCount,
|
||||||
|
},
|
||||||
|
dependencies: {
|
||||||
|
total: data.projectInfo.dependencies.total,
|
||||||
|
production: data.projectInfo.dependencies.direct.length,
|
||||||
|
development: data.projectInfo.dependencies.dev.length,
|
||||||
|
top_dependencies: data.projectInfo.dependencies.direct
|
||||||
|
.slice(0, 10)
|
||||||
|
.map(d => d.name),
|
||||||
|
},
|
||||||
|
conventions: data.projectInfo.conventions
|
||||||
|
? {
|
||||||
|
naming: {
|
||||||
|
files: data.projectInfo.conventions.namingConvention.files,
|
||||||
|
variables: data.projectInfo.conventions.namingConvention.variables,
|
||||||
|
functions: data.projectInfo.conventions.namingConvention.functions,
|
||||||
|
classes: data.projectInfo.conventions.namingConvention.classes,
|
||||||
|
},
|
||||||
|
import_style: data.projectInfo.conventions.importStyle.style,
|
||||||
|
testing_framework: data.projectInfo.conventions.testingFramework,
|
||||||
|
code_style: {
|
||||||
|
indent_size: data.projectInfo.conventions.codeStyle.indentSize,
|
||||||
|
indent_type: data.projectInfo.conventions.codeStyle.indentType,
|
||||||
|
line_endings: data.projectInfo.conventions.codeStyle.lineEndings,
|
||||||
|
quote_style: data.projectInfo.conventions.codeStyle.quoteStyle,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
return yaml.dump(summary, { indent: 2, lineWidth: -1 });
|
||||||
|
}
|
||||||
|
}
|
||||||
102
src/index.ts
Normal file
102
src/index.ts
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import { Command } from 'commander';
|
||||||
|
import * as path from 'path';
|
||||||
|
import { ContextGenerator } from './generators/contextGenerator';
|
||||||
|
import { TemplateLoader } from './templates/templateLoader';
|
||||||
|
import { ConfigLoader } from './config/configLoader';
|
||||||
|
import { CLIUtils } from './utils/cli';
|
||||||
|
import { FileUtils } from './utils/fileUtils';
|
||||||
|
import { ContextConfig } from './types';
|
||||||
|
|
||||||
|
const packageJson = {
|
||||||
|
name: 'ai-context-generator-cli',
|
||||||
|
version: '1.0.0',
|
||||||
|
description: 'A CLI tool that generates comprehensive context files for AI coding assistants',
|
||||||
|
};
|
||||||
|
|
||||||
|
async function main(): Promise<void> {
|
||||||
|
const program = new Command();
|
||||||
|
|
||||||
|
program
|
||||||
|
.name('ai-context')
|
||||||
|
.description('Generate comprehensive context files for AI coding assistants')
|
||||||
|
.version(packageJson.version);
|
||||||
|
|
||||||
|
program
|
||||||
|
.option('-d, --dir <directory>', 'Project directory to analyze', process.cwd())
|
||||||
|
.option('-o, --output <file>', 'Output file path', 'ai-context')
|
||||||
|
.option('-f, --format <format>', 'Output format (json or yaml)', 'json')
|
||||||
|
.option('-t, --template <template>', 'Template to use (default, cursor, copilot, generic)', 'default')
|
||||||
|
.option('-c, --config <file>', 'Config file path', '')
|
||||||
|
.option('-v, --verbose', 'Enable verbose output', false)
|
||||||
|
.option('--no-conventions', 'Skip convention analysis')
|
||||||
|
.option('--include-dev', 'Include dev dependencies')
|
||||||
|
.option('--no-gitignore', 'Do not respect .gitignore patterns');
|
||||||
|
|
||||||
|
program.parse(process.argv);
|
||||||
|
|
||||||
|
const options = program.opts();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const dir = CLIUtils.resolveDirectory(options.dir);
|
||||||
|
const outputPath = CLIUtils.resolveOutputPath(options.output, options.format as 'json' | 'yaml');
|
||||||
|
|
||||||
|
const fileUtils = FileUtils.getInstance();
|
||||||
|
if (!(await fileUtils.fileExists(dir))) {
|
||||||
|
console.error(`Error: Directory not found: ${dir}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
let config: ContextConfig | undefined;
|
||||||
|
if (options.config) {
|
||||||
|
config = await ConfigLoader.load(options.config);
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
config = await ConfigLoader.load();
|
||||||
|
} catch {
|
||||||
|
config = await ConfigLoader.load();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
config.analyzeConventions = options.conventions !== false;
|
||||||
|
config.includeDevDependencies = options.includeDev === true;
|
||||||
|
config.respectGitignore = options.gitignore !== false;
|
||||||
|
config.outputFormat = options.format as 'json' | 'yaml';
|
||||||
|
config.template = options.template as 'default' | 'cursor' | 'copilot' | 'generic';
|
||||||
|
config.outputFile = options.output;
|
||||||
|
|
||||||
|
if (options.verbose) {
|
||||||
|
console.log('Analyzing directory:', dir);
|
||||||
|
console.log('Output file:', outputPath);
|
||||||
|
console.log('Format:', config.outputFormat);
|
||||||
|
console.log('Template:', config.template);
|
||||||
|
console.log('Analyzing conventions:', config.analyzeConventions);
|
||||||
|
console.log('Including dev deps:', config.includeDevDependencies);
|
||||||
|
console.log('Respecting .gitignore:', config.respectGitignore);
|
||||||
|
}
|
||||||
|
|
||||||
|
const generator = new ContextGenerator();
|
||||||
|
|
||||||
|
if (options.verbose) {
|
||||||
|
console.log('\nGenerating context...');
|
||||||
|
}
|
||||||
|
|
||||||
|
await generator.saveContext(dir, outputPath, config.outputFormat, config);
|
||||||
|
|
||||||
|
if (options.verbose) {
|
||||||
|
console.log(`\nContext saved to: ${outputPath}`);
|
||||||
|
} else {
|
||||||
|
console.log(`Context saved to: ${outputPath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error generating context:', error instanceof Error ? error.message : error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(error => {
|
||||||
|
console.error('Fatal error:', error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
20
src/templates/copilot.template.md
Normal file
20
src/templates/copilot.template.md
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# Project Context for GitHub Copilot
|
||||||
|
|
||||||
|
## Project Details
|
||||||
|
```
|
||||||
|
Language: {{project.primaryLanguage}}
|
||||||
|
Frameworks: {{project.frameworks}}
|
||||||
|
Build Tools: {{project.buildTools}}
|
||||||
|
Files: {{project.fileCount}}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
```
|
||||||
|
{{dependencies}}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Coding Conventions
|
||||||
|
{{conventions}}
|
||||||
|
|
||||||
|
---
|
||||||
|
*Generated by AI Context Generator*
|
||||||
21
src/templates/cursor.template.md
Normal file
21
src/templates/cursor.template.md
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# Project Context for Cursor AI
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
- **Language**: {{project.primaryLanguage}}
|
||||||
|
- **Languages**: {{project.languages}}
|
||||||
|
- **Frameworks**: {{project.frameworks}}
|
||||||
|
- **Build Tools**: {{project.buildTools}}
|
||||||
|
- **Files Analyzed**: {{project.fileCount}}
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
{{dependencies}}
|
||||||
|
|
||||||
|
## Conventions
|
||||||
|
{{conventions}}
|
||||||
|
|
||||||
|
## Key Files
|
||||||
|
{{files}}
|
||||||
|
|
||||||
|
---
|
||||||
|
*Generated by AI Context Generator*
|
||||||
|
*Date: {{generatedAt}}*
|
||||||
28
src/templates/generic.template.md
Normal file
28
src/templates/generic.template.md
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# Project Context - AI Assistant Guide
|
||||||
|
|
||||||
|
## Project Information
|
||||||
|
- **Primary Language**: {{project.primaryLanguage}}
|
||||||
|
- **All Languages**: {{project.languages}}
|
||||||
|
- **Frameworks**: {{project.frameworks}}
|
||||||
|
- **Build Tools**: {{project.buildTools}}
|
||||||
|
|
||||||
|
## Dependency Overview
|
||||||
|
- **Total Dependencies**: {{dependencies.count}}
|
||||||
|
{{dependencies}}
|
||||||
|
|
||||||
|
## Code Analysis
|
||||||
|
### File Statistics
|
||||||
|
- **Total Files**: {{files.count}}
|
||||||
|
- **Naming Convention**: {{conventions.naming}}
|
||||||
|
- **Import Style**: {{conventions.importStyle}}
|
||||||
|
- **Testing Framework**: {{conventions.testing}}
|
||||||
|
|
||||||
|
### Code Style
|
||||||
|
- **Indent**: {{conventions.indent}}
|
||||||
|
- **Quotes**: {{conventions.quotes}}
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
{{files}}
|
||||||
|
|
||||||
|
---
|
||||||
|
*Generated at {{generatedAt}}*
|
||||||
238
src/templates/templateLoader.ts
Normal file
238
src/templates/templateLoader.ts
Normal file
@@ -0,0 +1,238 @@
|
|||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import { TemplateData } from '../types';
|
||||||
|
|
||||||
|
export class TemplateLoader {
|
||||||
|
private templates: Map<string, (data: TemplateData) => string>;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.templates = new Map();
|
||||||
|
this.registerDefaultTemplates();
|
||||||
|
}
|
||||||
|
|
||||||
|
private registerDefaultTemplates(): void {
|
||||||
|
this.templates.set('default', this.renderDefaultTemplate.bind(this));
|
||||||
|
this.templates.set('cursor', this.renderCursorTemplate.bind(this));
|
||||||
|
this.templates.set('copilot', this.renderCopilotTemplate.bind(this));
|
||||||
|
this.templates.set('generic', this.renderGenericTemplate.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadTemplate(name: string): Promise<(data: TemplateData) => string> {
|
||||||
|
const builtInTemplates = ['default', 'cursor', 'copilot', 'generic'];
|
||||||
|
|
||||||
|
if (builtInTemplates.includes(name)) {
|
||||||
|
return this.templates.get(name)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
const templatePath = path.resolve(name);
|
||||||
|
if (await this.fileExists(templatePath)) {
|
||||||
|
const content = await fs.promises.readFile(templatePath, 'utf-8');
|
||||||
|
return this.compileTemplate(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
const customTemplateDir = path.join(process.cwd(), 'templates');
|
||||||
|
const customTemplatePath = path.join(customTemplateDir, `${name}.template`);
|
||||||
|
if (await this.fileExists(customTemplatePath)) {
|
||||||
|
const content = await fs.promises.readFile(customTemplatePath, 'utf-8');
|
||||||
|
return this.compileTemplate(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Template not found: ${name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
compileTemplate(templateContent: string): (data: TemplateData) => string {
|
||||||
|
return (data: TemplateData): string => {
|
||||||
|
let result = templateContent;
|
||||||
|
|
||||||
|
result = result.replace(/\{\{\s*project\.type\s*\}\}/g,
|
||||||
|
data.projectInfo.projectType.primaryLanguage);
|
||||||
|
result = result.replace(/\{\{\s*project\.languages\s*\}\}/g,
|
||||||
|
data.projectInfo.projectType.languages.join(', '));
|
||||||
|
result = result.replace(/\{\{\s*project\.frameworks\s*\}\}/g,
|
||||||
|
data.projectInfo.projectType.frameworks.join(', '));
|
||||||
|
result = result.replace(/\{\{\s*project\.fileCount\s*\}\}/g,
|
||||||
|
String(data.projectInfo.fileCount));
|
||||||
|
result = result.replace(/\{\{\s*dependencies\.total\s*\}\}/g,
|
||||||
|
String(data.projectInfo.dependencies.total));
|
||||||
|
result = result.replace(/\{\{\s*generatedAt\s*\}\}/g,
|
||||||
|
data.generatedAt);
|
||||||
|
|
||||||
|
result = this.renderDependencies(result, data);
|
||||||
|
result = this.renderConventions(result, data);
|
||||||
|
result = this.renderFileList(result, data);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderDefaultTemplate(data: TemplateData): string {
|
||||||
|
const jsonOutput = JSON.stringify({
|
||||||
|
project: data.projectInfo,
|
||||||
|
files: data.files,
|
||||||
|
generatedAt: data.generatedAt,
|
||||||
|
}, null, 2);
|
||||||
|
|
||||||
|
return `## AI Context
|
||||||
|
|
||||||
|
\`\`\`json
|
||||||
|
${jsonOutput}
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
- **Language**: ${data.projectInfo.projectType.primaryLanguage}
|
||||||
|
- **Frameworks**: ${data.projectInfo.projectType.frameworks.join(', ') || 'None detected'}
|
||||||
|
- **Dependencies**: ${data.projectInfo.dependencies.total}
|
||||||
|
- **Files Analyzed**: ${data.projectInfo.fileCount}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderCursorTemplate(data: TemplateData): string {
|
||||||
|
const topDeps = data.projectInfo.dependencies.direct
|
||||||
|
.slice(0, 15)
|
||||||
|
.map(d => ` - ${d.name}@${d.version}`)
|
||||||
|
.join('\n');
|
||||||
|
|
||||||
|
return `## Project Context
|
||||||
|
|
||||||
|
**Language**: ${data.projectInfo.projectType.primaryLanguage}
|
||||||
|
**Frameworks**: ${data.projectInfo.projectType.frameworks.join(', ') || 'None'}
|
||||||
|
**Build Tools**: ${data.projectInfo.projectType.buildTools.join(', ') || 'None'}
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
${topDeps || ' No dependencies detected'}
|
||||||
|
|
||||||
|
### Conventions
|
||||||
|
${data.projectInfo.conventions ? `
|
||||||
|
- **File Naming**: ${data.projectInfo.conventions.namingConvention.files}
|
||||||
|
- **Import Style**: ${data.projectInfo.conventions.importStyle.style}
|
||||||
|
- **Testing Framework**: ${data.projectInfo.conventions.testingFramework || 'None'}
|
||||||
|
- **Code Style**:
|
||||||
|
- Indent: ${data.projectInfo.conventions.codeStyle.indentSize} ${data.projectInfo.conventions.codeStyle.indentType}
|
||||||
|
- Quotes: ${data.projectInfo.conventions.codeStyle.quoteStyle}
|
||||||
|
` : ' Not analyzed'}
|
||||||
|
|
||||||
|
### Key Files
|
||||||
|
${data.files.slice(0, 10).map(f => `- \`${f.path}\``).join('\n')}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderCopilotTemplate(data: TemplateData): string {
|
||||||
|
const deps = data.projectInfo.dependencies.direct
|
||||||
|
.map(d => ` "${d.name}": "${d.version}"`)
|
||||||
|
.join(',\n');
|
||||||
|
|
||||||
|
return `/* Project Context */
|
||||||
|
Language: ${data.projectInfo.projectType.primaryLanguage}
|
||||||
|
Frameworks: ${data.projectInfo.projectType.frameworks.join(', ') || 'None'}
|
||||||
|
Dependencies: ${data.projectInfo.dependencies.total}
|
||||||
|
|
||||||
|
/* Dependencies */
|
||||||
|
{
|
||||||
|
${deps}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Conventions */
|
||||||
|
File Naming: ${data.projectInfo.conventions?.namingConvention.files || 'Unknown'}
|
||||||
|
Import Style: ${data.projectInfo.conventions?.importStyle.style || 'Unknown'}
|
||||||
|
Testing: ${data.projectInfo.conventions?.testingFramework || 'None'}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderGenericTemplate(data: TemplateData): string {
|
||||||
|
return `=== PROJECT CONTEXT ===
|
||||||
|
|
||||||
|
Project Type: ${data.projectInfo.projectType.primaryLanguage}
|
||||||
|
Languages: ${data.projectInfo.projectType.languages.join(', ')}
|
||||||
|
Frameworks: ${data.projectInfo.projectType.frameworks.join(', ') || 'None'}
|
||||||
|
Build Tools: ${data.projectInfo.projectType.buildTools.join(', ') || 'None'}
|
||||||
|
|
||||||
|
=== DEPENDENCIES ===
|
||||||
|
Total: ${data.projectInfo.dependencies.total}
|
||||||
|
Production: ${data.projectInfo.dependencies.direct.length}
|
||||||
|
Development: ${data.projectInfo.dependencies.dev.length}
|
||||||
|
|
||||||
|
Top Dependencies:
|
||||||
|
${data.projectInfo.dependencies.direct.slice(0, 10).map(d => ` - ${d.name} (${d.version})`).join('\n')}
|
||||||
|
|
||||||
|
=== CONVENTIONS ===
|
||||||
|
${data.projectInfo.conventions ? `
|
||||||
|
Naming:
|
||||||
|
Files: ${data.projectInfo.conventions.namingConvention.files}
|
||||||
|
Variables: ${data.projectInfo.conventions.namingConvention.variables}
|
||||||
|
Functions: ${data.projectInfo.conventions.namingConvention.functions}
|
||||||
|
Classes: ${data.projectInfo.conventions.namingConvention.classes}
|
||||||
|
|
||||||
|
Import Style: ${data.projectInfo.conventions.importStyle.style}
|
||||||
|
${data.projectInfo.conventions.importStyle.aliasPrefix ? `Alias Prefix: ${data.projectInfo.conventions.importStyle.aliasPrefix}` : ''}
|
||||||
|
Testing Framework: ${data.projectInfo.conventions.testingFramework || 'None'}
|
||||||
|
|
||||||
|
Code Style:
|
||||||
|
Indent: ${data.projectInfo.conventions.codeStyle.indentSize} ${data.projectInfo.conventions.codeStyle.indentType}
|
||||||
|
Line Endings: ${data.projectInfo.conventions.codeStyle.lineEndings}
|
||||||
|
Quote Style: ${data.projectInfo.conventions.codeStyle.quoteStyle}
|
||||||
|
` : ' Not analyzed'}
|
||||||
|
|
||||||
|
=== FILES ===
|
||||||
|
Total Files: ${data.projectInfo.fileCount}
|
||||||
|
${data.files.slice(0, 20).map(f => ` - ${f.path}`).join('\n')}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderDependencies(
|
||||||
|
template: string,
|
||||||
|
data: TemplateData
|
||||||
|
): string {
|
||||||
|
const deps = data.projectInfo.dependencies.direct
|
||||||
|
.map(d => ` - ${d.name}@${d.version}`)
|
||||||
|
.join('\n');
|
||||||
|
|
||||||
|
return template
|
||||||
|
.replace(/\{\{\s*dependencies\s*\}\}/g, deps)
|
||||||
|
.replace(/\{\{\s*dependencies\.count\s*\}\}/g,
|
||||||
|
String(data.projectInfo.dependencies.total));
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderConventions(
|
||||||
|
template: string,
|
||||||
|
data: TemplateData
|
||||||
|
): string {
|
||||||
|
if (!data.projectInfo.conventions) {
|
||||||
|
return template;
|
||||||
|
}
|
||||||
|
|
||||||
|
const conventions = data.projectInfo.conventions;
|
||||||
|
|
||||||
|
return template
|
||||||
|
.replace(/\{\{\s*conventions\.naming\s*\}\}/g,
|
||||||
|
conventions.namingConvention.files)
|
||||||
|
.replace(/\{\{\s*conventions\.importStyle\s*\}\}/g,
|
||||||
|
conventions.importStyle.style)
|
||||||
|
.replace(/\{\{\s*conventions\.testing\s*\}\}/g,
|
||||||
|
conventions.testingFramework || 'None');
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderFileList(
|
||||||
|
template: string,
|
||||||
|
data: TemplateData
|
||||||
|
): string {
|
||||||
|
const fileList = data.files
|
||||||
|
.slice(0, 30)
|
||||||
|
.map(f => ` - ${f.path}`)
|
||||||
|
.join('\n');
|
||||||
|
|
||||||
|
return template
|
||||||
|
.replace(/\{\{\s*files\s*\}\}/g, fileList)
|
||||||
|
.replace(/\{\{\s*files\.count\s*\}\}/g,
|
||||||
|
String(data.projectInfo.fileCount));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async fileExists(filePath: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
await fs.promises.access(filePath, fs.constants.F_OK);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
90
src/types.ts
Normal file
90
src/types.ts
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
export interface CLIOptions {
|
||||||
|
dir: string;
|
||||||
|
output: string;
|
||||||
|
format: 'json' | 'yaml';
|
||||||
|
template: string;
|
||||||
|
config: string;
|
||||||
|
verbose: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProjectInfo {
|
||||||
|
projectType: ProjectType;
|
||||||
|
language: string;
|
||||||
|
framework: string | null;
|
||||||
|
dependencies: DependencyInfo;
|
||||||
|
conventions: ConventionInfo | null;
|
||||||
|
fileCount: number;
|
||||||
|
analysisDate: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProjectType {
|
||||||
|
primaryLanguage: string;
|
||||||
|
languages: string[];
|
||||||
|
frameworks: string[];
|
||||||
|
buildTools: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DependencyInfo {
|
||||||
|
direct: Dependency[];
|
||||||
|
dev: Dependency[];
|
||||||
|
total: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Dependency {
|
||||||
|
name: string;
|
||||||
|
version: string;
|
||||||
|
type: 'prod' | 'dev';
|
||||||
|
isLocal: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ConventionInfo {
|
||||||
|
namingConvention: NamingConvention;
|
||||||
|
importStyle: ImportStyle;
|
||||||
|
testingFramework: string | null;
|
||||||
|
codeStyle: CodeStyle;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NamingConvention {
|
||||||
|
files: 'camelCase' | 'snake_case' | 'kebab-case' | 'PascalCase';
|
||||||
|
variables: 'camelCase' | 'snake_case' | 'kebab-case' | 'PascalCase';
|
||||||
|
functions: 'camelCase' | 'snake_case' | 'kebab-case' | 'PascalCase';
|
||||||
|
classes: 'PascalCase' | 'camelCase' | 'snake_case' | 'kebab-case';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ImportStyle {
|
||||||
|
style: 'ESM' | 'CommonJS' | 'mixed';
|
||||||
|
aliasPrefix: string | null;
|
||||||
|
commonPatterns: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CodeStyle {
|
||||||
|
indentSize: number;
|
||||||
|
indentType: 'spaces' | 'tabs';
|
||||||
|
lineEndings: 'LF' | 'CRLF';
|
||||||
|
quoteStyle: 'single' | 'double';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ContextConfig {
|
||||||
|
includes: string[];
|
||||||
|
excludes: string[];
|
||||||
|
outputFormat: 'json' | 'yaml';
|
||||||
|
template: 'cursor' | 'copilot' | 'generic' | 'default';
|
||||||
|
outputFile: string;
|
||||||
|
analyzeConventions: boolean;
|
||||||
|
includeDevDependencies: boolean;
|
||||||
|
respectGitignore: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TemplateData {
|
||||||
|
projectInfo: ProjectInfo;
|
||||||
|
files: FileInfo[];
|
||||||
|
config: ContextConfig;
|
||||||
|
generatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FileInfo {
|
||||||
|
path: string;
|
||||||
|
size: number;
|
||||||
|
type: string;
|
||||||
|
language: string;
|
||||||
|
}
|
||||||
39
src/utils/cli.ts
Normal file
39
src/utils/cli.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import * as path from 'path';
|
||||||
|
|
||||||
|
export class CLIUtils {
|
||||||
|
static resolveDirectory(dir: string): string {
|
||||||
|
if (path.isAbsolute(dir)) {
|
||||||
|
return dir;
|
||||||
|
}
|
||||||
|
return path.resolve(process.cwd(), dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
static resolveOutputPath(
|
||||||
|
output: string,
|
||||||
|
format: 'json' | 'yaml'
|
||||||
|
): string {
|
||||||
|
if (path.isAbsolute(output)) {
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!output.endsWith(`.${format}`)) {
|
||||||
|
return `${output}.${format}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
static formatBytes(bytes: number): string {
|
||||||
|
if (bytes === 0) return '0 B';
|
||||||
|
const k = 1024;
|
||||||
|
const sizes = ['B', 'KB', 'MB', 'GB'];
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
|
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static sanitizePattern(pattern: string): string {
|
||||||
|
return pattern
|
||||||
|
.replace(/\*/g, '.*')
|
||||||
|
.replace(/\?/g, '.');
|
||||||
|
}
|
||||||
|
}
|
||||||
126
src/utils/fileUtils.ts
Normal file
126
src/utils/fileUtils.ts
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import ignore from 'ignore';
|
||||||
|
import { glob } from 'glob';
|
||||||
|
|
||||||
|
export class FileUtils {
|
||||||
|
private static instance: FileUtils;
|
||||||
|
private ig: ReturnType<typeof ignore>;
|
||||||
|
|
||||||
|
private constructor() {
|
||||||
|
this.ig = ignore();
|
||||||
|
}
|
||||||
|
|
||||||
|
static getInstance(): FileUtils {
|
||||||
|
if (!FileUtils.instance) {
|
||||||
|
FileUtils.instance = new FileUtils();
|
||||||
|
}
|
||||||
|
return FileUtils.instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadGitignore(dir: string): Promise<void> {
|
||||||
|
const gitignorePath = path.join(dir, '.gitignore');
|
||||||
|
if (await this.fileExists(gitignorePath)) {
|
||||||
|
const content = await this.readFile(gitignorePath);
|
||||||
|
const patterns = content
|
||||||
|
.split('\n')
|
||||||
|
.filter(line => line.trim() && !line.trim().startsWith('#'));
|
||||||
|
this.ig.add(patterns);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addCustomPatterns(patterns: string[]): void {
|
||||||
|
this.ig.add(patterns);
|
||||||
|
}
|
||||||
|
|
||||||
|
isIgnored(filePath: string): boolean {
|
||||||
|
const relativePath = path.relative(process.cwd(), filePath);
|
||||||
|
return this.ig.ignores(relativePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
resolveDirectory(dir: string): string {
|
||||||
|
const path = require('path');
|
||||||
|
if (path.isAbsolute(dir)) {
|
||||||
|
return dir;
|
||||||
|
}
|
||||||
|
return path.resolve(process.cwd(), dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getFiles(
|
||||||
|
dir: string,
|
||||||
|
includes: string[],
|
||||||
|
excludes: string[]
|
||||||
|
): Promise<string[]> {
|
||||||
|
const allFiles: string[] = [];
|
||||||
|
|
||||||
|
for (const pattern of includes) {
|
||||||
|
const files = await glob(pattern, {
|
||||||
|
cwd: dir,
|
||||||
|
ignore: excludes,
|
||||||
|
absolute: true,
|
||||||
|
});
|
||||||
|
allFiles.push(...files);
|
||||||
|
}
|
||||||
|
|
||||||
|
const uniqueFiles = [...new Set(allFiles)].filter(
|
||||||
|
file => !this.isIgnored(file)
|
||||||
|
);
|
||||||
|
|
||||||
|
return uniqueFiles.sort();
|
||||||
|
}
|
||||||
|
|
||||||
|
async fileExists(filePath: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
await fs.promises.access(filePath, fs.constants.F_OK);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async readFile(filePath: string): Promise<string> {
|
||||||
|
return fs.promises.readFile(filePath, 'utf-8');
|
||||||
|
}
|
||||||
|
|
||||||
|
async writeFile(
|
||||||
|
filePath: string,
|
||||||
|
content: string
|
||||||
|
): Promise<void> {
|
||||||
|
const dir = path.dirname(filePath);
|
||||||
|
if (!(await this.fileExists(dir))) {
|
||||||
|
await fs.promises.mkdir(dir, { recursive: true });
|
||||||
|
}
|
||||||
|
await fs.promises.writeFile(filePath, content, 'utf-8');
|
||||||
|
}
|
||||||
|
|
||||||
|
async getFileSize(filePath: string): Promise<number> {
|
||||||
|
const stats = await fs.promises.stat(filePath);
|
||||||
|
return stats.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getDirectoryContents(
|
||||||
|
dir: string
|
||||||
|
): Promise<string[]> {
|
||||||
|
try {
|
||||||
|
const entries = await fs.promises.readdir(dir, {
|
||||||
|
withFileTypes: true,
|
||||||
|
});
|
||||||
|
return entries.map(entry => entry.name);
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getFileExtension(filePath: string): string {
|
||||||
|
return path.extname(filePath).toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
isTextFile(filePath: string): boolean {
|
||||||
|
const textExtensions = [
|
||||||
|
'.ts', '.js', '.py', '.go', '.rs', '.java', '.c', '.cpp',
|
||||||
|
'.h', '.hpp', '.json', '.yaml', '.yml', '.xml', '.html',
|
||||||
|
'.css', '.scss', '.md', '.txt', '.sql', '.sh', '.bash',
|
||||||
|
];
|
||||||
|
return textExtensions.includes(this.getFileExtension(filePath));
|
||||||
|
}
|
||||||
|
}
|
||||||
181
tests/cli.test.ts
Normal file
181
tests/cli.test.ts
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import { execSync } from 'child_process';
|
||||||
|
|
||||||
|
describe('CLI', () => {
|
||||||
|
let testDir: string;
|
||||||
|
const projectPath = path.join(__dirname, '..');
|
||||||
|
const cliPath = path.join(projectPath, 'dist', 'index.js');
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
if (!fs.existsSync(cliPath)) {
|
||||||
|
throw new Error('CLI not built. Run npm run build first.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
testDir = path.join(__dirname, 'test-projects', `test-${Date.now()}`);
|
||||||
|
await fs.promises.mkdir(testDir, { recursive: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
if (await fs.promises.stat(testDir).catch(() => null)) {
|
||||||
|
await fs.promises.rm(testDir, { recursive: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('--version', () => {
|
||||||
|
it('should display version information', () => {
|
||||||
|
const output = execSync(`node ${cliPath} --version`, { encoding: 'utf-8' });
|
||||||
|
expect(output).toContain('1.0.0');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('--help', () => {
|
||||||
|
it('should display help information', () => {
|
||||||
|
const output = execSync(`node ${cliPath} --help`, { encoding: 'utf-8' });
|
||||||
|
expect(output).toContain('Usage:');
|
||||||
|
expect(output).toContain('Options:');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('--dir option', () => {
|
||||||
|
it('should analyze specified directory', async () => {
|
||||||
|
await fs.promises.writeFile(
|
||||||
|
path.join(testDir, 'tsconfig.json'),
|
||||||
|
JSON.stringify({})
|
||||||
|
);
|
||||||
|
await fs.promises.writeFile(
|
||||||
|
path.join(testDir, 'index.ts'),
|
||||||
|
'const x = 1;'
|
||||||
|
);
|
||||||
|
|
||||||
|
const outputPath = path.join(testDir, 'output.json');
|
||||||
|
execSync(
|
||||||
|
`node ${cliPath} --dir "${testDir}" --output "${outputPath}" --no-conventions`,
|
||||||
|
{ encoding: 'utf-8' }
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(fs.existsSync(outputPath)).toBe(true);
|
||||||
|
const content = fs.readFileSync(outputPath, 'utf-8');
|
||||||
|
expect(() => JSON.parse(content)).not.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('--output option', () => {
|
||||||
|
it('should save to specified file path', async () => {
|
||||||
|
await fs.promises.writeFile(path.join(testDir, 'tsconfig.json'), '{}');
|
||||||
|
|
||||||
|
const customPath = path.join(testDir, 'custom-context');
|
||||||
|
execSync(
|
||||||
|
`node ${cliPath} --dir "${testDir}" --output "${customPath}" --format json --no-conventions`,
|
||||||
|
{ encoding: 'utf-8' }
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(fs.existsSync(`${customPath}.json`)).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('--format option', () => {
|
||||||
|
it('should generate JSON output', async () => {
|
||||||
|
await fs.promises.writeFile(path.join(testDir, 'tsconfig.json'), '{}');
|
||||||
|
|
||||||
|
const outputPath = path.join(testDir, 'output');
|
||||||
|
execSync(
|
||||||
|
`node ${cliPath} --dir "${testDir}" --output "${outputPath}" --format json --no-conventions`,
|
||||||
|
{ encoding: 'utf-8' }
|
||||||
|
);
|
||||||
|
|
||||||
|
const content = fs.readFileSync(`${outputPath}.json`, 'utf-8');
|
||||||
|
expect(() => JSON.parse(content)).not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should generate YAML output', async () => {
|
||||||
|
await fs.promises.writeFile(path.join(testDir, 'tsconfig.json'), '{}');
|
||||||
|
|
||||||
|
const outputPath = path.join(testDir, 'output');
|
||||||
|
execSync(
|
||||||
|
`node ${cliPath} --dir "${testDir}" --output "${outputPath}" --format yaml --no-conventions`,
|
||||||
|
{ encoding: 'utf-8' }
|
||||||
|
);
|
||||||
|
|
||||||
|
const content = fs.readFileSync(`${outputPath}.yaml`, 'utf-8');
|
||||||
|
expect(content).toContain('projectInfo:');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('--template option', () => {
|
||||||
|
it('should accept default template', async () => {
|
||||||
|
await fs.promises.writeFile(path.join(testDir, 'tsconfig.json'), '{}');
|
||||||
|
|
||||||
|
const outputPath = path.join(testDir, 'output');
|
||||||
|
execSync(
|
||||||
|
`node ${cliPath} --dir "${testDir}" --output "${outputPath}" --template default --no-conventions`,
|
||||||
|
{ encoding: 'utf-8' }
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(fs.existsSync(`${outputPath}.json`)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should accept cursor template', async () => {
|
||||||
|
await fs.promises.writeFile(path.join(testDir, 'tsconfig.json'), '{}');
|
||||||
|
|
||||||
|
const outputPath = path.join(testDir, 'output');
|
||||||
|
execSync(
|
||||||
|
`node ${cliPath} --dir "${testDir}" --output "${outputPath}" --template cursor --no-conventions`,
|
||||||
|
{ encoding: 'utf-8' }
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(fs.existsSync(`${outputPath}.json`)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should accept copilot template', async () => {
|
||||||
|
await fs.promises.writeFile(path.join(testDir, 'tsconfig.json'), '{}');
|
||||||
|
|
||||||
|
const outputPath = path.join(testDir, 'output');
|
||||||
|
execSync(
|
||||||
|
`node ${cliPath} --dir "${testDir}" --output "${outputPath}" --template copilot --no-conventions`,
|
||||||
|
{ encoding: 'utf-8' }
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(fs.existsSync(`${outputPath}.json`)).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('--no-conventions option', () => {
|
||||||
|
it('should skip convention analysis', async () => {
|
||||||
|
await fs.promises.writeFile(path.join(testDir, 'tsconfig.json'), '{}');
|
||||||
|
|
||||||
|
const outputPath = path.join(testDir, 'output');
|
||||||
|
execSync(
|
||||||
|
`node ${cliPath} --dir "${testDir}" --output "${outputPath}" --no-conventions`,
|
||||||
|
{ encoding: 'utf-8' }
|
||||||
|
);
|
||||||
|
|
||||||
|
const content = fs.readFileSync(`${outputPath}.json`, 'utf-8');
|
||||||
|
const parsed = JSON.parse(content);
|
||||||
|
expect(parsed.projectInfo.conventions).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('--verbose option', () => {
|
||||||
|
it('should output verbose information', () => {
|
||||||
|
const output = execSync(
|
||||||
|
`node ${cliPath} --dir . --output /dev/null --format json --verbose --no-conventions 2>&1 || true`,
|
||||||
|
{ encoding: 'utf-8' }
|
||||||
|
);
|
||||||
|
expect(output).toContain('Analyzing directory');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('error handling', () => {
|
||||||
|
it('should handle non-existent directory', () => {
|
||||||
|
expect(() => {
|
||||||
|
execSync(
|
||||||
|
`node ${cliPath} --dir "/non/existent/path" --output /dev/null --no-conventions`,
|
||||||
|
{ encoding: 'utf-8' }
|
||||||
|
);
|
||||||
|
}).toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
162
tests/contextGenerator.test.ts
Normal file
162
tests/contextGenerator.test.ts
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import { ContextGenerator } from '../src/generators/contextGenerator';
|
||||||
|
import { ContextConfig } from '../src/types';
|
||||||
|
|
||||||
|
describe('ContextGenerator', () => {
|
||||||
|
let generator: ContextGenerator;
|
||||||
|
let testDir: string;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
generator = new ContextGenerator();
|
||||||
|
testDir = path.join(__dirname, 'test-projects', `test-${Date.now()}`);
|
||||||
|
await fs.promises.mkdir(testDir, { recursive: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
if (await fs.promises.stat(testDir).catch(() => null)) {
|
||||||
|
await fs.promises.rm(testDir, { recursive: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('generate', () => {
|
||||||
|
it('should generate project info for TypeScript project', async () => {
|
||||||
|
await fs.promises.writeFile(
|
||||||
|
path.join(testDir, 'tsconfig.json'),
|
||||||
|
JSON.stringify({ compilerOptions: { target: 'ES2020' } })
|
||||||
|
);
|
||||||
|
await fs.promises.writeFile(
|
||||||
|
path.join(testDir, 'package.json'),
|
||||||
|
JSON.stringify({
|
||||||
|
dependencies: { express: '^4.18.0' },
|
||||||
|
devDependencies: { jest: '^29.0.0' },
|
||||||
|
})
|
||||||
|
);
|
||||||
|
await fs.promises.writeFile(
|
||||||
|
path.join(testDir, 'index.ts'),
|
||||||
|
'const x: string = "hello";\nexport { x };'
|
||||||
|
);
|
||||||
|
|
||||||
|
const config: ContextConfig = {
|
||||||
|
includes: ['**/*.ts', '**/*.json'],
|
||||||
|
excludes: [],
|
||||||
|
outputFormat: 'json',
|
||||||
|
template: 'default',
|
||||||
|
outputFile: 'test.json',
|
||||||
|
analyzeConventions: false,
|
||||||
|
includeDevDependencies: false,
|
||||||
|
respectGitignore: false,
|
||||||
|
};
|
||||||
|
const result = await generator.generate(testDir, config);
|
||||||
|
|
||||||
|
expect(result.projectType.primaryLanguage).toBe('TypeScript');
|
||||||
|
expect(result.fileCount).toBe(3);
|
||||||
|
expect(result.analysisDate).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should include conventions when enabled', async () => {
|
||||||
|
await fs.promises.writeFile(
|
||||||
|
path.join(testDir, 'tsconfig.json'),
|
||||||
|
JSON.stringify({})
|
||||||
|
);
|
||||||
|
await fs.promises.writeFile(
|
||||||
|
path.join(testDir, 'index.ts'),
|
||||||
|
`const myVariable = "test";
|
||||||
|
function myFunction() {
|
||||||
|
return myVariable;
|
||||||
|
}
|
||||||
|
export class MyClass {}`
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await generator.generate(testDir);
|
||||||
|
|
||||||
|
expect(result.conventions).toBeDefined();
|
||||||
|
expect(result.conventions?.namingConvention.files).toBeDefined();
|
||||||
|
expect(result.conventions?.importStyle).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should analyze file count correctly', async () => {
|
||||||
|
await fs.promises.writeFile(path.join(testDir, 'file1.ts'), '// file 1');
|
||||||
|
await fs.promises.writeFile(path.join(testDir, 'file2.ts'), '// file 2');
|
||||||
|
await fs.promises.writeFile(path.join(testDir, 'file3.ts'), '// file 3');
|
||||||
|
await fs.promises.writeFile(path.join(testDir, 'tsconfig.json'), '{}');
|
||||||
|
|
||||||
|
const config: ContextConfig = {
|
||||||
|
includes: ['**/*.ts', '**/*.json'],
|
||||||
|
excludes: [],
|
||||||
|
outputFormat: 'json',
|
||||||
|
template: 'default',
|
||||||
|
outputFile: 'test.json',
|
||||||
|
analyzeConventions: false,
|
||||||
|
includeDevDependencies: false,
|
||||||
|
respectGitignore: false,
|
||||||
|
};
|
||||||
|
const result = await generator.generate(testDir, config);
|
||||||
|
|
||||||
|
expect(result.fileCount).toBe(4);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('generateJson', () => {
|
||||||
|
it('should generate valid JSON output', async () => {
|
||||||
|
await fs.promises.writeFile(path.join(testDir, 'tsconfig.json'), '{}');
|
||||||
|
await fs.promises.writeFile(path.join(testDir, 'index.ts'), 'const x = 1;');
|
||||||
|
|
||||||
|
const result = await generator.generateJson(testDir);
|
||||||
|
|
||||||
|
expect(() => JSON.parse(result)).not.toThrow();
|
||||||
|
const parsed = JSON.parse(result);
|
||||||
|
expect(parsed.projectInfo).toBeDefined();
|
||||||
|
expect(parsed.files).toBeDefined();
|
||||||
|
expect(parsed.generatedAt).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('generateYaml', () => {
|
||||||
|
it('should generate valid YAML output', async () => {
|
||||||
|
await fs.promises.writeFile(path.join(testDir, 'tsconfig.json'), '{}');
|
||||||
|
await fs.promises.writeFile(path.join(testDir, 'index.ts'), 'const x = 1;');
|
||||||
|
|
||||||
|
const result = await generator.generateYaml(testDir);
|
||||||
|
|
||||||
|
expect(result).toContain('projectInfo:');
|
||||||
|
expect(result).toContain('files:');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('saveContext', () => {
|
||||||
|
it('should save JSON file correctly', async () => {
|
||||||
|
const outputPath = path.join(testDir, 'output');
|
||||||
|
await fs.promises.writeFile(path.join(testDir, 'tsconfig.json'), '{}');
|
||||||
|
await fs.promises.writeFile(path.join(testDir, 'index.ts'), 'const x = 1;');
|
||||||
|
|
||||||
|
await generator.saveContext(testDir, outputPath, 'json');
|
||||||
|
|
||||||
|
const filePath = outputPath.endsWith('.json') ? outputPath : `${outputPath}.json`;
|
||||||
|
const content = await fs.promises.readFile(filePath, 'utf-8');
|
||||||
|
expect(() => JSON.parse(content)).not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should save YAML file correctly', async () => {
|
||||||
|
const outputPath = path.join(testDir, 'output');
|
||||||
|
await fs.promises.writeFile(path.join(testDir, 'tsconfig.json'), '{}');
|
||||||
|
await fs.promises.writeFile(path.join(testDir, 'index.ts'), 'const x = 1;');
|
||||||
|
|
||||||
|
await generator.saveContext(testDir, outputPath, 'yaml');
|
||||||
|
|
||||||
|
const filePath = outputPath.endsWith('.yaml') ? outputPath : `${outputPath}.yaml`;
|
||||||
|
const content = await fs.promises.readFile(filePath, 'utf-8');
|
||||||
|
expect(content).toContain('projectInfo:');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should auto-add extension when missing', async () => {
|
||||||
|
const outputPath = path.join(testDir, 'output');
|
||||||
|
await fs.promises.writeFile(path.join(testDir, 'tsconfig.json'), '{}');
|
||||||
|
|
||||||
|
await generator.saveContext(testDir, outputPath, 'json');
|
||||||
|
|
||||||
|
const exists = await fs.promises.access(`${outputPath}.json`).then(() => true).catch(() => false);
|
||||||
|
expect(exists).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
225
tests/dependencyAnalyzer.test.ts
Normal file
225
tests/dependencyAnalyzer.test.ts
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import { DependencyAnalyzer } from '../src/analyzers/dependencyAnalyzer';
|
||||||
|
|
||||||
|
describe('DependencyAnalyzer', () => {
|
||||||
|
let analyzer: DependencyAnalyzer;
|
||||||
|
let testDir: string;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
analyzer = new DependencyAnalyzer();
|
||||||
|
testDir = path.join('/tmp', `ai-context-test-${Date.now()}`);
|
||||||
|
await fs.promises.mkdir(testDir, { recursive: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
if (await fs.promises.stat(testDir).catch(() => null)) {
|
||||||
|
await fs.promises.rm(testDir, { recursive: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('analyze', () => {
|
||||||
|
it('should parse package.json dependencies correctly', async () => {
|
||||||
|
const packageJson = {
|
||||||
|
dependencies: {
|
||||||
|
express: '^4.18.0',
|
||||||
|
lodash: '^4.17.21',
|
||||||
|
},
|
||||||
|
devDependencies: {
|
||||||
|
jest: '^29.0.0',
|
||||||
|
typescript: '^5.0.0',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
await fs.promises.writeFile(
|
||||||
|
path.join(testDir, 'package.json'),
|
||||||
|
JSON.stringify(packageJson)
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await analyzer.analyze(testDir, false);
|
||||||
|
|
||||||
|
expect(result.total).toBe(2);
|
||||||
|
expect(result.direct.length).toBe(2);
|
||||||
|
expect(result.direct.find(d => d.name === 'express')?.version).toBe('^4.18.0');
|
||||||
|
expect(result.dev.length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should include dev dependencies when requested', async () => {
|
||||||
|
const packageJson = {
|
||||||
|
dependencies: { express: '^4.18.0' },
|
||||||
|
devDependencies: { jest: '^29.0.0' },
|
||||||
|
};
|
||||||
|
await fs.promises.writeFile(
|
||||||
|
path.join(testDir, 'package.json'),
|
||||||
|
JSON.stringify(packageJson)
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await analyzer.analyze(testDir, true);
|
||||||
|
|
||||||
|
expect(result.total).toBe(2);
|
||||||
|
expect(result.dev.length).toBe(1);
|
||||||
|
expect(result.dev[0].name).toBe('jest');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse requirements.txt dependencies', async () => {
|
||||||
|
await fs.promises.writeFile(
|
||||||
|
path.join(testDir, 'requirements.txt'),
|
||||||
|
`django>=4.0.0
|
||||||
|
flask>=2.0.0
|
||||||
|
requests==2.28.0
|
||||||
|
numpy~=1.24.0`
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await analyzer.analyze(testDir, false);
|
||||||
|
|
||||||
|
expect(result.total).toBeGreaterThan(0);
|
||||||
|
const deps = result.direct.map(d => d.name);
|
||||||
|
expect(deps).toContain('django');
|
||||||
|
expect(deps).toContain('flask');
|
||||||
|
expect(deps).toContain('requests');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse go.mod dependencies', async () => {
|
||||||
|
await fs.promises.writeFile(
|
||||||
|
path.join(testDir, 'go.mod'),
|
||||||
|
`module example.com/mymodule
|
||||||
|
|
||||||
|
go 1.21
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/gin-gonic/gin v1.9.0
|
||||||
|
github.com/stretchr/testify v1.8.0
|
||||||
|
)`
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await analyzer.analyze(testDir, false);
|
||||||
|
|
||||||
|
expect(result.total).toBeGreaterThan(0);
|
||||||
|
const deps = result.direct.map(d => d.name);
|
||||||
|
expect(deps).toContain('github.com/gin-gonic/gin');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse Cargo.toml dependencies', async () => {
|
||||||
|
await fs.promises.writeFile(
|
||||||
|
path.join(testDir, 'Cargo.toml'),
|
||||||
|
`[package]
|
||||||
|
name = "myproject"
|
||||||
|
version = "0.1.0"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
tokio = { version = "1.0", features = ["full"] }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
assertions = "0.3"`
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await analyzer.analyze(testDir, true);
|
||||||
|
|
||||||
|
expect(result.total).toBe(2);
|
||||||
|
expect(result.direct.length).toBe(2);
|
||||||
|
expect(result.dev.length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse pyproject.toml dependencies', async () => {
|
||||||
|
await fs.promises.writeFile(
|
||||||
|
path.join(testDir, 'pyproject.toml'),
|
||||||
|
`[project]
|
||||||
|
name = "myproject"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"requests>=2.28.0",
|
||||||
|
"flask>=2.0.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.optional-dependencies]
|
||||||
|
dev = [
|
||||||
|
"pytest>=7.0.0",
|
||||||
|
"black>=23.0.0",
|
||||||
|
]`
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await analyzer.analyze(testDir, false);
|
||||||
|
|
||||||
|
expect(result.total).toBe(2);
|
||||||
|
expect(result.direct.length).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse composer.json dependencies', async () => {
|
||||||
|
await fs.promises.writeFile(
|
||||||
|
path.join(testDir, 'composer.json'),
|
||||||
|
JSON.stringify({
|
||||||
|
require: {
|
||||||
|
php: '^8.0',
|
||||||
|
'laravel/framework': '^10.0',
|
||||||
|
},
|
||||||
|
'require-dev': {
|
||||||
|
'phpunit/phpunit': '^10.0',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await analyzer.analyze(testDir, true);
|
||||||
|
|
||||||
|
expect(result.total).toBe(2);
|
||||||
|
expect(result.dev.length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse Gemfile dependencies', async () => {
|
||||||
|
await fs.promises.writeFile(
|
||||||
|
path.join(testDir, 'Gemfile'),
|
||||||
|
`source 'https://rubygems.org'
|
||||||
|
|
||||||
|
gem 'rails', '~> 7.0.0'
|
||||||
|
gem 'nokogiri', '>= 1.13'
|
||||||
|
|
||||||
|
group :development, :test do
|
||||||
|
gem 'rspec-rails', '~> 6.0'
|
||||||
|
end`
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await analyzer.analyze(testDir, true);
|
||||||
|
|
||||||
|
expect(result.total).toBe(2);
|
||||||
|
expect(result.dev.length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle empty project gracefully', async () => {
|
||||||
|
const result = await analyzer.analyze(testDir, false);
|
||||||
|
|
||||||
|
expect(result.total).toBe(0);
|
||||||
|
expect(result.direct).toHaveLength(0);
|
||||||
|
expect(result.dev).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle invalid JSON gracefully', async () => {
|
||||||
|
await fs.promises.writeFile(
|
||||||
|
path.join(testDir, 'package.json'),
|
||||||
|
'invalid json {'
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await analyzer.analyze(testDir, false);
|
||||||
|
|
||||||
|
expect(result.total).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should mark local dependencies correctly', async () => {
|
||||||
|
const packageJson = {
|
||||||
|
dependencies: {
|
||||||
|
'./local-module': '*',
|
||||||
|
'@local/package': '*',
|
||||||
|
express: '^4.18.0',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
await fs.promises.writeFile(
|
||||||
|
path.join(testDir, 'package.json'),
|
||||||
|
JSON.stringify(packageJson)
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await analyzer.analyze(testDir, false);
|
||||||
|
|
||||||
|
expect(result.direct.find(d => d.name === './local-module')?.isLocal).toBe(true);
|
||||||
|
expect(result.direct.find(d => d.name === '@local/package')?.isLocal).toBe(true);
|
||||||
|
expect(result.direct.find(d => d.name === 'express')?.isLocal).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
205
tests/projectTypeDetector.test.ts
Normal file
205
tests/projectTypeDetector.test.ts
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import { ProjectTypeDetector } from '../src/analyzers/projectTypeDetector';
|
||||||
|
|
||||||
|
describe('ProjectTypeDetector', () => {
|
||||||
|
let detector: ProjectTypeDetector;
|
||||||
|
let testDir: string;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
detector = new ProjectTypeDetector();
|
||||||
|
testDir = path.join(__dirname, 'test-projects', `test-${Date.now()}`);
|
||||||
|
await fs.promises.mkdir(testDir, { recursive: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
if (await fs.promises.stat(testDir).catch(() => null)) {
|
||||||
|
await fs.promises.rm(testDir, { recursive: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('detect', () => {
|
||||||
|
it('should detect TypeScript project with tsconfig.json', async () => {
|
||||||
|
const tsconfig = { compilerOptions: { target: 'ES2020' } };
|
||||||
|
await fs.promises.writeFile(
|
||||||
|
path.join(testDir, 'tsconfig.json'),
|
||||||
|
JSON.stringify(tsconfig)
|
||||||
|
);
|
||||||
|
await fs.promises.writeFile(
|
||||||
|
path.join(testDir, 'index.ts'),
|
||||||
|
'const x: string = "hello";'
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await detector.detect(testDir);
|
||||||
|
|
||||||
|
expect(result.languages).toContain('TypeScript');
|
||||||
|
expect(result.primaryLanguage).toBe('TypeScript');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect JavaScript project with package.json', async () => {
|
||||||
|
const packageJson = { dependencies: { express: '^4.0.0' } };
|
||||||
|
await fs.promises.writeFile(
|
||||||
|
path.join(testDir, 'package.json'),
|
||||||
|
JSON.stringify(packageJson)
|
||||||
|
);
|
||||||
|
await fs.promises.writeFile(
|
||||||
|
path.join(testDir, 'index.js'),
|
||||||
|
'const express = require("express");'
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await detector.detect(testDir);
|
||||||
|
|
||||||
|
expect(result.languages).toContain('JavaScript');
|
||||||
|
expect(result.primaryLanguage).toBe('JavaScript');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect Python project with requirements.txt', async () => {
|
||||||
|
await fs.promises.writeFile(
|
||||||
|
path.join(testDir, 'requirements.txt'),
|
||||||
|
'django>=3.0.0\nflask>=1.0.0'
|
||||||
|
);
|
||||||
|
await fs.promises.writeFile(
|
||||||
|
path.join(testDir, 'main.py'),
|
||||||
|
'import django\nfrom flask import Flask'
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await detector.detect(testDir);
|
||||||
|
|
||||||
|
expect(result.languages).toContain('Python');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect Go project with go.mod', async () => {
|
||||||
|
await fs.promises.writeFile(
|
||||||
|
path.join(testDir, 'go.mod'),
|
||||||
|
'module example.com/mymodule\n\ngo 1.21\n\nrequire github.com/gin-gonic/gin v1.9.0'
|
||||||
|
);
|
||||||
|
await fs.promises.writeFile(
|
||||||
|
path.join(testDir, 'main.go'),
|
||||||
|
'package main\nimport "fmt"'
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await detector.detect(testDir);
|
||||||
|
|
||||||
|
expect(result.languages).toContain('Go');
|
||||||
|
expect(result.frameworks).toContain('Gin');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect Rust project with Cargo.toml', async () => {
|
||||||
|
await fs.promises.writeFile(
|
||||||
|
path.join(testDir, 'Cargo.toml'),
|
||||||
|
'[package]\nname = "myproject"\nversion = "0.1.0"\n\n[dependencies]\nserde = { version = "1.0", features = ["derive"] }'
|
||||||
|
);
|
||||||
|
await fs.promises.writeFile(
|
||||||
|
path.join(testDir, 'main.rs'),
|
||||||
|
'fn main() { println!("Hello"); }'
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await detector.detect(testDir);
|
||||||
|
|
||||||
|
expect(result.languages).toContain('Rust');
|
||||||
|
expect(result.buildTools).toContain('cargo');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect React framework from package.json', async () => {
|
||||||
|
const packageJson = {
|
||||||
|
dependencies: { react: '^18.0.0', 'react-dom': '^18.0.0' },
|
||||||
|
devDependencies: { '@types/react': '^18.0.0' }
|
||||||
|
};
|
||||||
|
await fs.promises.writeFile(
|
||||||
|
path.join(testDir, 'package.json'),
|
||||||
|
JSON.stringify(packageJson)
|
||||||
|
);
|
||||||
|
await fs.promises.writeFile(
|
||||||
|
path.join(testDir, 'App.tsx'),
|
||||||
|
'import React from "react";'
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await detector.detect(testDir);
|
||||||
|
|
||||||
|
expect(result.languages).toContain('TypeScript');
|
||||||
|
expect(result.frameworks).toContain('React');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect Next.js framework', async () => {
|
||||||
|
const packageJson = {
|
||||||
|
dependencies: {
|
||||||
|
next: '^14.0.0',
|
||||||
|
react: '^18.0.0',
|
||||||
|
'react-dom': '^18.0.0'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
await fs.promises.writeFile(
|
||||||
|
path.join(testDir, 'package.json'),
|
||||||
|
JSON.stringify(packageJson)
|
||||||
|
);
|
||||||
|
await fs.promises.writeFile(
|
||||||
|
path.join(testDir, 'next.config.js'),
|
||||||
|
'module.exports = {}'
|
||||||
|
);
|
||||||
|
await fs.promises.mkdir(path.join(testDir, 'pages'), { recursive: true });
|
||||||
|
await fs.promises.writeFile(
|
||||||
|
path.join(testDir, 'pages', 'index.tsx'),
|
||||||
|
'export default function() { return null; }'
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await detector.detect(testDir);
|
||||||
|
|
||||||
|
expect(result.frameworks).toContain('Next.js');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect Django framework', async () => {
|
||||||
|
await fs.promises.writeFile(
|
||||||
|
path.join(testDir, 'requirements.txt'),
|
||||||
|
'django>=4.0.0\ndjangorestframework>=3.0.0'
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await detector.detect(testDir);
|
||||||
|
|
||||||
|
expect(result.frameworks).toContain('Django');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect FastAPI framework', async () => {
|
||||||
|
await fs.promises.writeFile(
|
||||||
|
path.join(testDir, 'requirements.txt'),
|
||||||
|
'fastapi>=0.100.0\nuvicorn>=0.23.0'
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await detector.detect(testDir);
|
||||||
|
|
||||||
|
expect(result.frameworks).toContain('FastAPI');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect build tools from config files', async () => {
|
||||||
|
await fs.promises.writeFile(
|
||||||
|
path.join(testDir, 'package.json'),
|
||||||
|
JSON.stringify({ dependencies: {} })
|
||||||
|
);
|
||||||
|
await fs.promises.writeFile(path.join(testDir, 'Makefile'), 'all:\n\techo "build"');
|
||||||
|
|
||||||
|
const result = await detector.detect(testDir);
|
||||||
|
|
||||||
|
expect(result.buildTools).toContain('npm');
|
||||||
|
expect(result.buildTools).toContain('make');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect multiple languages in same project', async () => {
|
||||||
|
await fs.promises.writeFile(
|
||||||
|
path.join(testDir, 'package.json'),
|
||||||
|
JSON.stringify({ dependencies: {} })
|
||||||
|
);
|
||||||
|
await fs.promises.writeFile(path.join(testDir, 'index.js'), 'console.log("js");');
|
||||||
|
await fs.promises.writeFile(path.join(testDir, 'main.py'), 'print("python")');
|
||||||
|
|
||||||
|
const result = await detector.detect(testDir);
|
||||||
|
|
||||||
|
expect(result.languages.length).toBeGreaterThan(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return Unknown for empty directory', async () => {
|
||||||
|
const result = await detector.detect(testDir);
|
||||||
|
|
||||||
|
expect(result.primaryLanguage).toBe('Unknown');
|
||||||
|
expect(result.languages).toHaveLength(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
20
tsconfig.json
Normal file
20
tsconfig.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"module": "commonjs",
|
||||||
|
"lib": ["ES2020"],
|
||||||
|
"outDir": "./dist",
|
||||||
|
"rootDir": "./src",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"moduleResolution": "node"
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"],
|
||||||
|
"exclude": ["node_modules", "dist", "tests"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user