diff --git a/CODEBASE-CONTEXT.md b/CODEBASE-CONTEXT.md index 6eed857..8d3f545 100644 --- a/CODEBASE-CONTEXT.md +++ b/CODEBASE-CONTEXT.md @@ -371,17 +371,11 @@ src/deprecated-module.js **Warning:** Tooling may be required for proper implementation of `.contextignore`. AI agents may not consistently or easily use `.contextignore` as strictly as dedicated tooling can. Your mileage may vary (YMMV) depending on the AI model used. -## 7. Security Considerations - -1. Avoid including sensitive information (API keys, passwords) in context files. -2. Be cautious with proprietary algorithms or trade secrets. -3. Use `.gitignore` to exclude sensitive context files from version control if necessary. - -## 8. The .contextdocs File +## 7. The .contextdocs File The `.contextdocs` file allows developers to specify external documentation sources that should be incorporated into the project's context. This feature is particularly useful for including documentation from dependencies, libraries, or related projects. -### 8.1 Location and Naming +### 7.1 Location and Naming - The `.contextdocs` file should be placed in the root directory of the project. - It must use one of the following extensions: @@ -389,7 +383,7 @@ The `.contextdocs` file allows developers to specify external documentation sour - `.contextdocs.yaml` or `.contextdocs.yml` - `.contextdocs.json` -### 8.2 File Structure +### 7.2 File Structure The `.contextdocs` file should contain an array of documentation sources. Each source can be: @@ -397,7 +391,7 @@ The `.contextdocs` file should contain an array of documentation sources. Each s - A URL to a markdown file - A package name with associated documentation files -### 8.3 Examples +### 7.3 Examples #### Markdown Format (.contextdocs.md) - Default @@ -511,7 +505,7 @@ contextdocs: } ``` -### 8.4 Behavior +### 7.4 Behavior - When an AI model or related tool is processing the project context, it should fetch and incorporate the specified documentation. - For local files, the content should be read from the specified path. @@ -520,24 +514,24 @@ contextdocs: **Warning:** Tooling may be required for proper implementation of `.contextdocs`. AI agents may not consistently or easily use `.contextdocs` as strictly as dedicated tooling can. Your mileage may vary (YMMV) depending on the AI model used. -### 8.5 Use Cases +### 7.5 Use Cases - Including documentation for key dependencies - Referencing company-wide coding standards or guidelines - Incorporating design documents or architectural overviews - Linking to relevant external resources or tutorials -### 8.6 Considerations +### 7.6 Considerations - Ensure that URLs point to stable, version-controlled documentation to maintain consistency. - Be mindful of the total volume of documentation to avoid overwhelming the AI model with irrelevant information. - Regularly review and update the `.contextdocs` file to ensure all referenced documentation remains relevant and up-to-date. - Consider implementing caching mechanisms for external documentation to improve performance and reduce network requests. -## 9. Conclusion +## 8. Conclusion The Codebase Context Specification provides a flexible, standardized approach to enriching codebases with contextual information for AI models. By adopting this convention and including role-specific information, development teams can enhance AI-assisted workflows, improving code quality and development efficiency across projects of any scale or complexity. The addition of role-specific guidelines and consistent naming conventions ensures that AI models have access to comprehensive, relevant, and well-structured information tailored to different aspects of the software development lifecycle. -## 10. TypeScript Linter Implementation +## 9. TypeScript Linter Implementation For details on the TypeScript implementation of the linter for validating Codebase Context Specification files, please refer to the [TypeScript Linter README](linters/typescript/README.md). diff --git a/README.md b/README.md index dfea0bc..1d80aa5 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,16 @@ Welcome to the [Codebase Context Specification (CCS)](./CODEBASE-CONTEXT.md) rep - [Context File Example (.context.md)](.context.md) - [AI Assistant Prompt (CODING-ASSISTANT-PROMPT.md)](CODING-ASSISTANT-PROMPT.md) +## Supported Node.js Versions + +This project supports the following Node.js versions: + +- Node.js 18.x +- Node.js 20.x +- Node.js 22.x + +We recommend using the latest LTS (Long Term Support) version of Node.js for optimal performance and security. + ## Codebase Context: A New Convention The Codebase Context Specification introduces a convention similar to `.env` and `.editorconfig` systems, but focused on documenting your code for both AI and humans. Just as `.env` files manage environment variables and `.editorconfig` ensures consistent coding styles, CCS files (`.context.md`, `.context.yaml`, `.context.json`) provide a standardized way to capture and communicate the context of your codebase. @@ -46,8 +56,14 @@ To install the linter: npm install -g codebase-context-lint ``` +Note: Make sure you're using a supported Node.js version (18.x, 20.x, or 22.x) when installing and running the linter. + For more information on using the linter, please refer to the [linter's README](./linters/typescript/README.md). +## Recent Updates + +We've recently updated our dependencies to address security vulnerabilities and improve compatibility with different Node.js versions. If you encounter any issues after updating, please report them in our GitHub issues. + ## Using with AI Assistants The [CODING-ASSISTANT-PROMPT.md](./CODING-ASSISTANT-PROMPT.md) file provides guidelines for AI assistants to understand and use the Codebase Context Specification. This allows for immediate adoption of the specification without requiring specific tooling integration. diff --git a/examples/context-editor/.nvmrc b/examples/context-editor/.nvmrc new file mode 100644 index 0000000..cdb4fc8 --- /dev/null +++ b/examples/context-editor/.nvmrc @@ -0,0 +1 @@ +18.0.0 || 20.0.0 || 22.0.0 \ No newline at end of file diff --git a/examples/context-editor/sample.context.md b/examples/context-editor/sample.context.md deleted file mode 100644 index 5322fc2..0000000 --- a/examples/context-editor/sample.context.md +++ /dev/null @@ -1,38 +0,0 @@ -# Metadata -- Module Name: Sample Module -- Related Modules: Module A, Module B -- Version: 1.0.0 -- Description: This is a sample module for testing the import functionality. -- Diagrams: diagram1.png, diagram2.png -- Technologies: React, TypeScript -- Conventions: Follows React Hooks pattern -- Directives: Use functional components - -# Architecture -- Style: Microservices -- Components: Frontend, Backend, Database -- Data Flow: RESTful API - -# Development -- Setup Steps: npm install, npm start -- Build Command: npm run build -- Test Command: npm test - -# Business Requirements -- Key Features: User authentication, Data visualization -- Target Audience: Developers, Project Managers -- Success Metrics: User engagement, Response time - -# Quality Assurance -- Testing Frameworks: Jest, React Testing Library -- Coverage Threshold: 80% -- Performance Benchmarks: Load time < 3s, API response < 200ms - -# Deployment -- Platform: AWS -- CI/CD Pipeline: GitHub Actions -- Staging Environment: staging.example.com -- Production Environment: www.example.com - -# Additional Information -This section contains any additional markdown content that doesn't fit into the above categories. \ No newline at end of file diff --git a/linters/typescript/README.md b/linters/typescript/README.md index 0a405bc..5761467 100644 --- a/linters/typescript/README.md +++ b/linters/typescript/README.md @@ -35,7 +35,17 @@ By adopting this convention, teams can ensure that both human developers and AI For more detailed information about the Codebase Context Specification, please refer to the [main repository](https://github.com/Agentic-Insights/codebase-context-spec) and the [full specification](https://github.com/Agentic-Insights/codebase-context-spec/blob/main/CODEBASE-CONTEXT.md). -## 📦 Installation +## � Supported Node.js Versions + +This linter supports the following Node.js versions: + +- Node.js 18.x +- Node.js 20.x +- Node.js 22.x + +We recommend using the latest LTS (Long Term Support) version of Node.js for optimal performance and security. + +## �📦 Installation You can install the linter globally using npm: @@ -43,6 +53,8 @@ You can install the linter globally using npm: npm install -g codebase-context-lint ``` +Note: Make sure you're using a supported Node.js version (18.x, 20.x, or 22.x) when installing and running the linter. + ## 🚀 Usage After installation, you can use the linter from the command line: @@ -58,9 +70,37 @@ Replace `` with the path to the directory containing your Cod - 🔍 Validates the structure and content of `.context.md`, `.context.yaml`, and `.context.json` files - ✅ Checks for required fields and sections - 📄 Verifies the format of `.contextdocs.md` files -- 🚫 Validates ignore patterns in `.contextignore` files +- 🚫 Supports and validates `.contextignore` files for excluding specific files or directories - 💬 Provides detailed error messages and warnings +## 📁 .contextignore Files + +`.contextignore` files allow you to specify patterns for files and directories that should be ignored by the Codebase Context Lint. This is useful for excluding generated files, dependencies, or any other content that doesn't need context documentation. + +### How to use .contextignore + +1. Create a file named `.contextignore` in any directory of your project. +2. Add patterns to the file, one per line. These patterns follow the same rules as `.gitignore` files. +3. The linter will respect these ignore patterns when processing files in that directory and its subdirectories. + +Example `.contextignore` file: + +``` +# Ignore node_modules directory +node_modules/ + +# Ignore all .log files +*.log + +# Ignore a specific file +path/to/specific/file.js + +# Ignore all files in a specific directory +path/to/ignore/* +``` + +The linter will validate the syntax of your `.contextignore` files and warn about any problematic patterns, such as attempting to ignore critical context files. + ## 🤖 Using with AI Assistants While this linter provides automated validation of CCS files, you can also use the Codebase Context Specification with AI assistants without any specific tooling. The [CODING-ASSISTANT-PROMPT.md](https://github.com/Agentic-Insights/codebase-context-spec/blob/main/CODING-ASSISTANT-PROMPT.md) file in the main repository provides guidelines for AI assistants to understand and use the Codebase Context Specification. @@ -71,7 +111,7 @@ To use the Codebase Context Specification with an AI assistant: 2. Ask the AI to analyze your project's context files based on these guidelines. 3. The AI will be able to provide more accurate and context-aware responses by following the instructions in the prompt. -Note that while this approach allows for immediate use of the specification, some features like .contextignore should eventually be applied by tooling (such as this linter) for more robust implementation. +Note that while this approach allows for immediate use of the specification, some features like .contextignore are best implemented by tooling (such as this linter) for more robust and consistent application. ## 🛠️ Development @@ -104,11 +144,15 @@ To contribute to this project: 7. Push to the branch (`git push origin feature/AmazingFeature`) 8. Open a Pull Request -## � Learn More +## 🔄 Recent Updates + +We've recently updated our dependencies to address security vulnerabilities and improve compatibility with different Node.js versions. If you encounter any issues after updating, please report them in our GitHub issues. + +## 📖 Learn More For a deeper dive into the Codebase Context Specification, check out this [SubStack article by Vaskin](https://agenticinsights.substack.com/p/codebase-context-specification-rfc), the author of the specification. -## �📄 License +## 📄 License This project is licensed under the MIT License. diff --git a/linters/typescript/package-lock.json b/linters/typescript/package-lock.json index 5750483..2affb38 100644 --- a/linters/typescript/package-lock.json +++ b/linters/typescript/package-lock.json @@ -1,17 +1,18 @@ { "name": "codebase-context-lint", - "version": "1.2.0", + "version": "1.3.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "codebase-context-lint", - "version": "1.2.0", + "version": "1.3.1", "license": "MIT", "dependencies": { "gray-matter": "^4.0.3", + "ignore": "^5.3.1", "js-yaml": "^4.1.0", - "markdown-it": "^14.1.0" + "markdown-it": "^14.0.0" }, "bin": { "codebase-context-lint": "dist/cli.js" @@ -22,14 +23,14 @@ "@types/jest": "^29.5.12", "@types/js-yaml": "^4.0.9", "@types/markdown-it": "^14.1.2", - "@types/node": "^22.5.1", + "@types/node": "^20.11.19", "jest": "^29.7.0", "semantic-release": "^22.0.12", "ts-jest": "^29.1.2", - "typescript": "^5.5.4" + "typescript": "^5.6.2" }, "engines": { - "node": ">=20.0.0" + "node": "^18.0.0 || ^20.0.0 || ^22.0.0" } }, "node_modules/@ampproject/remapping": { @@ -1486,19 +1487,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@semantic-release/github/node_modules/escape-string-regexp": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", - "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/@semantic-release/github/node_modules/indent-string": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-5.0.0.tgz", @@ -1583,19 +1571,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@semantic-release/npm/node_modules/escape-string-regexp": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", - "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/@semantic-release/npm/node_modules/execa": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", @@ -1983,9 +1958,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.5.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.1.tgz", - "integrity": "sha512-KkHsxej0j9IW1KKOOAA/XBA0z08UFSrRQHErzEfA3Vgq57eXIMYboIlHJuYIfd+lwCQjtKqUu3UnmKbtUc9yRw==", + "version": "20.16.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.5.tgz", + "integrity": "sha512-VwYCweNo3ERajwy0IUlqqcyZ8/A7Zwa9ZP3MnENWcB11AejO+tLy3pu850goUW2FC/IJMdZUfKpX/yxL1gymCA==", "dev": true, "license": "MIT", "dependencies": { @@ -2385,9 +2360,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001655", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001655.tgz", - "integrity": "sha512-jRGVy3iSGO5Uutn2owlb5gR6qsGngTw9ZTb4ali9f3glshcNmJ2noam4Mo9zia5P9Dk3jNNydy7vQjuE5dQmfg==", + "version": "1.0.30001660", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001660.tgz", + "integrity": "sha512-GacvNTTuATm26qC74pt+ad1fW15mlQ/zuTzzY1ZoIzECTP8HURDfF43kNxPgf7H1jmelCBQTTbBNxdSXOA7Bqg==", "dev": true, "funding": [ { @@ -2463,9 +2438,9 @@ } }, "node_modules/cjs-module-lexer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.0.tgz", - "integrity": "sha512-N1NGmowPlGBLsOZLPvm48StN04V4YvQRL0i6b7ctrVY3epjP/ct7hFLOItz6pDIvRjwpfPxi52a2UWV2ziir8g==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.1.tgz", + "integrity": "sha512-cuSVIHi9/9E/+821Qjdvngor+xpnlwnuwIyZOaLmHBVdXL+gP+I6QQB9VkO7RI77YIcTV+S1W9AreJ5eN63JBA==", "dev": true, "license": "MIT" }, @@ -2761,13 +2736,13 @@ } }, "node_modules/debug": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", - "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", "dev": true, "license": "MIT", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -2893,9 +2868,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.13", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.13.tgz", - "integrity": "sha512-lbBcvtIJ4J6sS4tb5TLp1b4LyfCdMkwStzXPyAgVgTRAsep4bvrAGaBOP7ZJtQMNJpSQ9SqG4brWOroNaQtm7Q==", + "version": "1.5.19", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.19.tgz", + "integrity": "sha512-kpLJJi3zxTR1U828P+LIUDZ5ohixyo68/IcYOHLqnbTPr/wdgn4i1ECvmALN9E16JPA6cvCG5UG79gVwVdEK5w==", "dev": true, "license": "ISC" }, @@ -3117,13 +3092,16 @@ } }, "node_modules/escape-string-regexp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", - "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", "dev": true, "license": "MIT", "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/esprima": { @@ -3739,7 +3717,6 @@ "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 4" @@ -4015,9 +3992,9 @@ } }, "node_modules/is-unicode-supported": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.0.0.tgz", - "integrity": "sha512-FRdAyx5lusK1iHG0TWpVtk9+1i+GjrzRffhDg4ovQ7mcidMQ6mj+MhKPmvh7Xwyv5gIS06ns49CA7Sqg7lC22Q==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", "dev": true, "license": "MIT", "engines": { @@ -5260,9 +5237,9 @@ } }, "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true, "license": "MIT" }, @@ -8146,9 +8123,9 @@ } }, "node_modules/picocolors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", - "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", + "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==", "dev": true, "license": "ISC" }, @@ -8461,9 +8438,9 @@ } }, "node_modules/read-pkg-up/node_modules/type-fest": { - "version": "4.26.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.26.0.tgz", - "integrity": "sha512-OduNjVJsFbifKb57UqZ2EMP1i4u64Xwow3NYXUtBbD4vIwJdQd4+xl8YDou1dlm4DVrtwT/7Ky8z8WyCULVfxw==", + "version": "4.26.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.26.1.tgz", + "integrity": "sha512-yOGpmOAL7CkKe/91I5O3gPICmJNLJ1G4zFYVAsRHg7M64biSnPtRj0WNQt++bRkjYOqjWXrhnUw1utzmVErAdg==", "dev": true, "license": "(MIT OR CC0-1.0)", "engines": { @@ -8492,9 +8469,9 @@ } }, "node_modules/read-pkg/node_modules/type-fest": { - "version": "4.26.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.26.0.tgz", - "integrity": "sha512-OduNjVJsFbifKb57UqZ2EMP1i4u64Xwow3NYXUtBbD4vIwJdQd4+xl8YDou1dlm4DVrtwT/7Ky8z8WyCULVfxw==", + "version": "4.26.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.26.1.tgz", + "integrity": "sha512-yOGpmOAL7CkKe/91I5O3gPICmJNLJ1G4zFYVAsRHg7M64biSnPtRj0WNQt++bRkjYOqjWXrhnUw1utzmVErAdg==", "dev": true, "license": "(MIT OR CC0-1.0)", "engines": { @@ -8746,19 +8723,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/semantic-release/node_modules/escape-string-regexp": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", - "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/semantic-release/node_modules/execa": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", @@ -9253,6 +9217,16 @@ "node": ">=10" } }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/stream-combiner2": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/stream-combiner2/-/stream-combiner2-1.1.1.tgz", @@ -9607,6 +9581,16 @@ "node": ">=10" } }, + "node_modules/ts-jest/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/type-detect": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", @@ -9631,9 +9615,9 @@ } }, "node_modules/typescript": { - "version": "5.5.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", - "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.2.tgz", + "integrity": "sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==", "dev": true, "license": "Apache-2.0", "bin": { @@ -9919,7 +9903,7 @@ "node": ">=12" } }, - "node_modules/yargs-parser": { + "node_modules/yargs/node_modules/yargs-parser": { "version": "21.1.1", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", diff --git a/linters/typescript/package.json b/linters/typescript/package.json index 38d2877..fb9100a 100644 --- a/linters/typescript/package.json +++ b/linters/typescript/package.json @@ -12,7 +12,8 @@ "lint": "node dist/cli.js", "test": "jest", "prepublishOnly": "npm run build", - "semantic-release": "semantic-release" + "semantic-release": "semantic-release", + "check-updates": "npm outdated" }, "keywords": [ "linter", @@ -37,18 +38,19 @@ "@types/jest": "^29.5.12", "@types/js-yaml": "^4.0.9", "@types/markdown-it": "^14.1.2", - "@types/node": "^22.5.1", + "@types/node": "^20.11.19", "jest": "^29.7.0", "semantic-release": "^22.0.12", "ts-jest": "^29.1.2", - "typescript": "^5.5.4" + "typescript": "^5.6.2" }, "dependencies": { "gray-matter": "^4.0.3", + "ignore": "^5.3.1", "js-yaml": "^4.1.0", - "markdown-it": "^14.1.0" + "markdown-it": "^14.0.0" }, "engines": { - "node": ">=20.0.0" + "node": "^18.0.0 || ^20.0.0 || ^22.0.0" } } diff --git a/linters/typescript/src/__tests__/context_linter.test.ts b/linters/typescript/src/__tests__/context_linter.test.ts new file mode 100644 index 0000000..75d2992 --- /dev/null +++ b/linters/typescript/src/__tests__/context_linter.test.ts @@ -0,0 +1,73 @@ +import { ContextLinter } from '../context_linter'; +import * as fs from 'fs'; +import * as path from 'path'; + +describe('ContextLinter', () => { + let linter: ContextLinter; + const testDir = path.join(__dirname, 'test_context'); + + beforeAll(() => { + // Create test directory and files + fs.mkdirSync(testDir, { recursive: true }); + fs.writeFileSync(path.join(testDir, '.contextignore'), ` + ignored.md + `); + fs.writeFileSync(path.join(testDir, '.context.md'), `--- +module-name: test-module +description: A test module +--- +# Test Module + +This is a test module. + `); + fs.writeFileSync(path.join(testDir, 'ignored.md'), 'This file should be ignored'); + fs.writeFileSync(path.join(testDir, 'not_ignored.md'), 'This file should not be ignored'); + + // Create a subdirectory with its own .contextignore + const subDir = path.join(testDir, 'subdir'); + fs.mkdirSync(subDir, { recursive: true }); + fs.writeFileSync(path.join(subDir, '.contextignore'), '# Subdir ignore rules'); + }); + + afterAll(() => { + // Clean up test directory + fs.rmSync(testDir, { recursive: true, force: true }); + }); + + beforeEach(() => { + linter = new ContextLinter(); + }); + + describe('lintDirectory', () => { + it('should lint a directory successfully', async () => { + const result = await linter.lintDirectory(testDir, '1.0.0'); + expect(result).toBe(true); + }); + + it('should respect .contextignore rules', async () => { + const consoleSpy = jest.spyOn(console, 'log'); + await linter.lintDirectory(testDir, '1.0.0'); + + // Check if ignored.md was listed in ignored files + expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('ignored.md')); + // Check if not_ignored.md was not listed in ignored files + expect(consoleSpy).not.toHaveBeenCalledWith(expect.stringContaining('not_ignored.md')); + + consoleSpy.mockRestore(); + }); + }); + + describe('handleContextFilesRecursively', () => { + it('should process .context files in nested directories', async () => { + const consoleSpy = jest.spyOn(console, 'log'); + await linter.lintDirectory(testDir, '1.0.0'); + + // Check if the main context was processed (which should be the .context.md file) + expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('main context: 100.00%')); + + consoleSpy.mockRestore(); + }); + }); + + // Add more tests for other methods as needed +}); \ No newline at end of file diff --git a/linters/typescript/src/__tests__/contextignore_linter.test.ts b/linters/typescript/src/__tests__/contextignore_linter.test.ts new file mode 100644 index 0000000..903c30a --- /dev/null +++ b/linters/typescript/src/__tests__/contextignore_linter.test.ts @@ -0,0 +1,93 @@ +import { ContextignoreLinter } from '../contextignore_linter'; +import * as path from 'path'; +import * as fs from 'fs'; + +describe('ContextignoreLinter', () => { + let linter: ContextignoreLinter; + const testDir = path.join(__dirname, 'test_contextignore'); + + beforeAll(() => { + // Create test directory and files + fs.mkdirSync(testDir, { recursive: true }); + fs.writeFileSync(path.join(testDir, '.contextignore'), ` + *.log + /build + `); + fs.writeFileSync(path.join(testDir, 'test.log'), 'Test log file'); + fs.mkdirSync(path.join(testDir, 'build'), { recursive: true }); + fs.writeFileSync(path.join(testDir, 'build', 'main.js'), 'console.log("Hello");'); + fs.writeFileSync(path.join(testDir, 'src.js'), 'console.log("Not ignored");'); + }); + + afterAll(() => { + // Clean up test directory + fs.rmSync(testDir, { recursive: true, force: true }); + }); + + beforeEach(() => { + linter = new ContextignoreLinter(); + }); + + describe('lintContextignoreFile', () => { + it('should validate a correct .contextignore file', async () => { + const content = await fs.promises.readFile(path.join(testDir, '.contextignore'), 'utf-8'); + const result = await linter.lintContextignoreFile(content, path.join(testDir, '.contextignore')); + expect(result).toBe(true); + }); + + it('should detect invalid patterns', async () => { + const content = ` + node_modules/ + *.log + /build + invalid**pattern + `; + const result = await linter.lintContextignoreFile(content, 'test/.contextignore'); + expect(result).toBe(false); + }); + + it('should detect attempts to ignore critical files', async () => { + const content = ` + node_modules/ + *.log + .context.md + `; + const result = await linter.lintContextignoreFile(content, 'test/.contextignore'); + expect(result).toBe(false); + }); + }); + + describe('isIgnored', () => { + beforeEach(async () => { + const content = await fs.promises.readFile(path.join(testDir, '.contextignore'), 'utf-8'); + await linter.lintContextignoreFile(content, path.join(testDir, '.contextignore')); + }); + + it('should correctly identify ignored files', () => { + expect(linter.isIgnored(path.join(testDir, 'test.log'), testDir)).toBe(true); + expect(linter.isIgnored(path.join(testDir, 'build', 'main.js'), testDir)).toBe(true); + }); + + it('should correctly identify non-ignored files', () => { + expect(linter.isIgnored(path.join(testDir, 'src.js'), testDir)).toBe(false); + }); + }); + + describe('getIgnoredFiles', () => { + beforeEach(async () => { + const content = await fs.promises.readFile(path.join(testDir, '.contextignore'), 'utf-8'); + await linter.lintContextignoreFile(content, path.join(testDir, '.contextignore')); + }); + + it('should return a list of ignored files', () => { + const ignoredFiles = linter.getIgnoredFiles(testDir); + const normalizedIgnoredFiles = ignoredFiles.map(file => path.normalize(file)); + + expect(normalizedIgnoredFiles).toEqual(expect.arrayContaining([ + path.join(testDir, 'test.log'), + path.join(testDir, 'build', 'main.js') + ])); + expect(normalizedIgnoredFiles).not.toContain(path.join(testDir, 'src.js')); + }); + }); +}); \ No newline at end of file diff --git a/linters/typescript/src/context_linter.ts b/linters/typescript/src/context_linter.ts index c6d0646..46b1548 100644 --- a/linters/typescript/src/context_linter.ts +++ b/linters/typescript/src/context_linter.ts @@ -8,6 +8,10 @@ import { ContextignoreLinter } from './contextignore_linter'; import { getContextFiles, lintFileIfExists, fileExists, printHeader } from './utils/file_utils'; import { ContextValidator, ValidationResult, SectionValidationResult } from './utils/validator'; +/** + * ContextLinter class handles the linting of .context files (md, yaml, json) + * and coordinates the use of ContextignoreLinter and ContextdocsLinter. + */ export class ContextLinter { private md: MarkdownIt; private contextdocsLinter: ContextdocsLinter; @@ -21,102 +25,133 @@ export class ContextLinter { this.contextValidator = new ContextValidator(); } + /** + * Main method to lint a directory + * @param directoryPath The path of the directory to lint + * @param packageVersion The version of the linter package + * @returns A boolean indicating whether the linting was successful + */ public async lintDirectory(directoryPath: string, packageVersion: string): Promise { - printHeader(packageVersion, directoryPath); - let isValid = true; - isValid = await this.handleContextignore(directoryPath) && isValid; - isValid = await this.handleContextdocs(directoryPath) && isValid; - isValid = await this.handleContextFilesRecursively(directoryPath) && isValid; + try { + printHeader(packageVersion, directoryPath); + console.log(`Linting directory: ${this.normalizePath(directoryPath)}\n`); + let isValid = true; + + // Initialize ignore patterns + await this.initializeIgnorePatterns(directoryPath); + + // Lint .context.md file in the root directory + const rootContextFile = path.join(directoryPath, '.context.md'); + if (await fileExists(rootContextFile)) { + const content = await fs.promises.readFile(rootContextFile, 'utf-8'); + const result = await this.lintMarkdownFile(content, rootContextFile); + this.printValidationResult(result, rootContextFile); + isValid = isValid && result.isValid; + } + + isValid = await this.handleContextdocs(directoryPath) && isValid; + isValid = await this.handleContextFilesRecursively(directoryPath) && isValid; - console.log('\nLinting completed.'); - - return isValid; + // Log ignored files + this.logIgnoredFiles(directoryPath); + + // Clear ignore cache after processing the directory + this.contextignoreLinter.clearCache(); + + console.log('Linting completed.'); + + return isValid; + } catch (error) { + console.error(`Error linting directory: ${error instanceof Error ? error.message : String(error)}`); + return false; + } } - private async handleContextignore(directoryPath: string): Promise { + /** + * Initialize ignore patterns from .contextignore file + * @param directoryPath The path of the directory containing .contextignore + */ + private async initializeIgnorePatterns(directoryPath: string): Promise { const contextignorePath = path.join(directoryPath, '.contextignore'); - return await lintFileIfExists(contextignorePath, this.contextignoreLinter.lintContextignoreFile.bind(this.contextignoreLinter)) || true; + if (await fileExists(contextignorePath)) { + const content = await fs.promises.readFile(contextignorePath, 'utf-8'); + await this.contextignoreLinter.lintContextignoreFile(content, contextignorePath); + } } + /** + * Handle .contextdocs file in the directory + * @param directoryPath The path of the directory to check for .contextdocs + * @returns A boolean indicating whether the .contextdocs file is valid + */ private async handleContextdocs(directoryPath: string): Promise { const contextdocsPath = path.join(directoryPath, '.contextdocs.md'); - const result = await lintFileIfExists(contextdocsPath, this.contextdocsLinter.lintContextdocsFile.bind(this.contextdocsLinter)); - - if (path.resolve(directoryPath) === path.resolve(process.cwd()) && !await fileExists(contextdocsPath)) { - console.error('\nError: .contextdocs.md file is missing in the root directory.'); - return false; + if (await fileExists(contextdocsPath)) { + const content = await fs.promises.readFile(contextdocsPath, 'utf-8'); + return await this.contextdocsLinter.lintContextdocsFile(content, contextdocsPath); } - - return result || true; + return true; } + /** + * Handle .context files recursively in the directory + * @param directoryPath The path of the directory to process + * @returns A boolean indicating whether all .context files are valid + */ private async handleContextFilesRecursively(directoryPath: string): Promise { - const entries = await fs.promises.readdir(directoryPath, { withFileTypes: true }); let isValid = true; + const contextFiles = await getContextFiles(directoryPath); + + for (const filePath of contextFiles) { + if (!this.contextignoreLinter.isIgnored(filePath, directoryPath)) { + const fileContent = await fs.promises.readFile(filePath, 'utf-8'); + const fileExtension = path.extname(filePath); + let result: ValidationResult; - for (const entry of entries) { - const fullPath = path.join(directoryPath, entry.name); + switch (fileExtension) { + case '.md': + result = await this.lintMarkdownFile(fileContent, filePath); + break; + case '.yaml': + case '.yml': + result = await this.lintYamlFile(fileContent, filePath); + break; + case '.json': + result = await this.lintJsonFile(fileContent, filePath); + break; + default: + console.warn(`Unsupported file extension: ${fileExtension}`); + continue; + } - if (entry.isDirectory()) { - isValid = await this.handleContextFilesRecursively(fullPath) && isValid; - } else if (entry.isFile() && (entry.name.endsWith('.context.md') || entry.name.endsWith('.context.yaml') || entry.name.endsWith('.context.json'))) { - isValid = await this.lintContextFile(fullPath) && isValid; + this.printValidationResult(result, filePath); + isValid = isValid && result.isValid; } } return isValid; } - private async lintContextFile(filePath: string): Promise { - console.log(`\nLinting file: ${filePath}`); - return await lintFileIfExists(filePath, async (fileContent) => { - let result: ValidationResult; - if (filePath.endsWith('.context.md')) { - result = await this.lintMarkdownFile(fileContent, filePath); - } else if (filePath.endsWith('.context.yaml')) { - result = await this.lintYamlFile(fileContent, filePath); - } else if (filePath.endsWith('.context.json')) { - result = await this.lintJsonFile(fileContent, filePath); - } else { - result = { - isValid: false, - coveragePercentage: 0, - coveredFields: 0, - totalFields: 0, - missingFields: [], - sections: {} - }; - } - this.printValidationResult(result, filePath); - return result.isValid; - }) || false; - } - - private printValidationResult(result: ValidationResult, filePath: string): void { - const fileName = path.basename(filePath); - console.log(`main context: ${result.coveragePercentage.toFixed(2)}% (${result.coveredFields}/${result.totalFields} top level fields)`); - - if (result.missingFields.length > 0) { - console.warn(`└-⚠️ Missing fields: ${result.missingFields.join(', ')}`); - } - - for (const [sectionName, sectionResult] of Object.entries(result.sections)) { - console.log(`|- ${sectionName}: ${sectionResult.coveragePercentage.toFixed(2)}% (${sectionResult.coveredFields}/${sectionResult.totalFields} fields)`); - - if (sectionResult.missingFields.length > 0) { - console.warn(` └-⚠️ Missing fields: ${sectionResult.missingFields.join(', ')}`); + /** + * Log ignored files in the directory + * @param directoryPath The path of the directory to check for ignored files + */ + private logIgnoredFiles(directoryPath: string): void { + const ignoredFiles = this.contextignoreLinter.getIgnoredFiles(directoryPath); + if (ignoredFiles.length > 0) { + console.log('\nIgnored files:'); + for (const file of ignoredFiles) { + console.log(` ${this.normalizePath(file)}`); } - - if (sectionResult.unexpectedFields && sectionResult.unexpectedFields.length > 0) { - console.warn(` └-⚠️ Unexpected fields: ${sectionResult.unexpectedFields.join(', ')}`); - } - } - - if (!result.isValid) { - console.warn(`⚠️ ${fileName} has coverage warnings`); } } + /** + * Lint a Markdown .context file + * @param content The content of the file + * @param filePath The path of the file + * @returns A ValidationResult object + */ private async lintMarkdownFile(content: string, filePath: string): Promise { try { const { data: frontmatterData, content: markdownContent } = matter(content); @@ -140,6 +175,11 @@ export class ContextLinter { } } + /** + * Validate the content of a Markdown file + * @param content The Markdown content to validate + * @returns A boolean indicating whether the content is valid + */ private validateMarkdownContent(content: string): boolean { const tokens = this.md.parse(content, {}); let hasTitle = false; @@ -172,6 +212,12 @@ export class ContextLinter { return isValid; } + /** + * Lint a YAML .context file + * @param content The content of the file + * @param filePath The path of the file + * @returns A ValidationResult object + */ private async lintYamlFile(content: string, filePath: string): Promise { console.log(' - Validating YAML structure'); @@ -195,6 +241,12 @@ export class ContextLinter { } } + /** + * Lint a JSON .context file + * @param content The content of the file + * @param filePath The path of the file + * @returns A ValidationResult object + */ private async lintJsonFile(content: string, filePath: string): Promise { console.log(' - Validating JSON structure'); @@ -218,6 +270,28 @@ export class ContextLinter { } } + /** + * Print the validation result for a .context file + * @param result The validation result + * @param filePath The path of the file + */ + private printValidationResult(result: ValidationResult, filePath: string): void { + const relativePath = this.normalizePath(path.relative(process.cwd(), filePath)); + console.log(`Linting file: ${relativePath}`); + + console.log(`main context: ${result.coveragePercentage.toFixed(2)}% (${result.coveredFields}/${result.totalFields} top level fields)`); + + for (const [sectionName, sectionResult] of Object.entries(result.sections)) { + console.log(`|- ${sectionName}: ${sectionResult.coveragePercentage.toFixed(2)}% (${sectionResult.coveredFields}/${sectionResult.totalFields} fields)`); + } + + if (!result.isValid) { + console.warn(`⚠️ File has coverage warnings`); + } + + console.log(''); // Add a blank line for better readability + } + private parseYaml(content: string): Record { const documents = yaml.loadAll(content) as Record[]; if (documents.length === 0) { @@ -249,4 +323,8 @@ export class ContextLinter { } return error.message; } + + private normalizePath(filePath: string): string { + return filePath.replace(/\\/g, '/'); + } } \ No newline at end of file diff --git a/linters/typescript/src/contextdocs_linter.ts b/linters/typescript/src/contextdocs_linter.ts index 6b3f52a..398fc71 100644 --- a/linters/typescript/src/contextdocs_linter.ts +++ b/linters/typescript/src/contextdocs_linter.ts @@ -1,5 +1,6 @@ import MarkdownIt from 'markdown-it'; import matter from 'gray-matter'; +import * as path from 'path'; export class ContextdocsLinter { private md: MarkdownIt; @@ -8,15 +9,23 @@ export class ContextdocsLinter { this.md = new MarkdownIt(); } - public async lintContextdocsFile(content: string): Promise { - console.log('\nLinting .contextdocs.md file'); - console.log(' - Checking for similar links in markdown content'); + public async lintContextdocsFile(content: string, filePath: string): Promise { + const relativePath = path.relative(process.cwd(), filePath); + console.log(`\nLinting file: ${relativePath}`); const { data: frontmatter, content: markdownContent } = matter(content); const frontmatterLinksResult = this.lintFrontmatter(frontmatter); const similarLinksResult = this.checkSimilarLinks(markdownContent, frontmatterLinksResult.links); + console.log('- Validating YAML front matter structure'); + console.log('- Checking for similar links in markdown content'); + + if (!frontmatterLinksResult.isValid || !similarLinksResult) { + console.warn('⚠️ File has validation warnings'); + } + + console.log(''); // Add a blank line for better readability return frontmatterLinksResult.isValid && similarLinksResult; } diff --git a/linters/typescript/src/contextignore_linter.ts b/linters/typescript/src/contextignore_linter.ts index fc03d2e..78937d0 100644 --- a/linters/typescript/src/contextignore_linter.ts +++ b/linters/typescript/src/contextignore_linter.ts @@ -1,57 +1,201 @@ +import ignore from 'ignore'; +import * as path from 'path'; +import * as fs from 'fs'; + +/** + * ContextignoreLinter class handles the linting of .contextignore files + * and provides functionality to check if files should be ignored based on the ignore patterns. + */ export class ContextignoreLinter { + // Set of patterns that should never be ignored private criticalPatterns: Set; + // Cache of ignore instances for each directory + private ignoreCache: Map>; constructor() { this.criticalPatterns = new Set(['.context.md', '.context.yaml', '.context.json', '.contextdocs.md', '.contextignore']); + this.ignoreCache = new Map(); } - public async lintContextignoreFile(content: string): Promise { - console.log('\nLinting .contextignore file'); - console.log(' - Validating .contextignore format'); - console.log(' - Checking for valid ignore patterns'); - - const lines = content.split('\n').map(line => line.trim()).filter(line => line !== '' && !line.startsWith('#')); - const patterns = new Set(); - let isValid = true; - - for (let i = 0; i < lines.length; i++) { - const line = lines[i]; - - if (!/^[!#]?[\w\-./\*\?]+$/.test(line)) { - console.error(` Error: Invalid ignore pattern on line ${i + 1}: ${line}`); - isValid = false; - } + /** + * Lint a .contextignore file + * @param content The content of the .contextignore file + * @param filePath The path of the .contextignore file + * @returns A boolean indicating whether the file is valid + */ + public async lintContextignoreFile(content: string, filePath: string): Promise { + try { + const relativePath = path.relative(process.cwd(), filePath); + console.log(`\nLinting file: ${relativePath}`); + console.log('- Validating .contextignore format'); + console.log('- Checking for valid ignore patterns'); - if (patterns.has(line)) { - console.warn(` Warning: Redundant pattern on line ${i + 1}: ${line}`); - } + const lines = content.split('\n').map(line => line.trim()).filter(line => line !== '' && !line.startsWith('#')); + const patterns = new Set(); + let isValid = true; - for (const criticalPattern of this.criticalPatterns) { - if (line.endsWith(criticalPattern) || line.includes(`/${criticalPattern}`)) { - console.error(` Error: Ignoring critical context file on line ${i + 1}: ${line}`); + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + + // Updated regex to catch more invalid patterns, including "invalid**pattern" + if (!/^!?(?:[\w\-./]+|\*(?!\*)|\?+|\[.+\])+$/.test(line)) { + console.error(` Error: Invalid pattern at line ${i + 1}: ${line}`); isValid = false; } + + // Validate that critical patterns are not being ignored + isValid = this.validateCriticalPattern(line, i) && isValid; + + patterns.add(line); } - patterns.add(line); - } + // Check for conflicting patterns + isValid = this.checkConflictingPatterns(Array.from(patterns)) && isValid; - isValid = this.checkConflictingPatterns(Array.from(patterns)) && isValid; + if (isValid) { + this.updateIgnoreCache(filePath, Array.from(patterns)); + } else { + console.warn('⚠️ File has validation warnings'); + } - return isValid; + console.log(''); // Add a blank line for better readability + return isValid; + } catch (error) { + console.error(`Error linting .contextignore file ${filePath}: ${error instanceof Error ? error.message : String(error)}`); + return false; + } } + /** + * Validate that a pattern does not ignore critical files + * @param pattern The pattern to validate + * @param lineNumber The line number of the pattern in the file + * @returns A boolean indicating whether the pattern is valid + */ + private validateCriticalPattern(pattern: string, lineNumber: number): boolean { + for (const criticalPattern of this.criticalPatterns) { + if (pattern.endsWith(criticalPattern) || pattern.includes(`/${criticalPattern}`)) { + console.error(` Error: Pattern at line ${lineNumber + 1} ignores critical file: ${criticalPattern}`); + return false; + } + } + return true; + } + + /** + * Check for conflicting patterns (e.g., a pattern and its negation) + * @param patterns The list of patterns to check + * @returns A boolean indicating whether there are no conflicts + */ private checkConflictingPatterns(patterns: string[]): boolean { - let isValid = true; for (let i = 0; i < patterns.length; i++) { for (let j = i + 1; j < patterns.length; j++) { if (patterns[i].startsWith('!') && patterns[j] === patterns[i].slice(1) || patterns[j].startsWith('!') && patterns[i] === patterns[j].slice(1)) { - console.warn(` Warning: Conflicting patterns: "${patterns[i]}" and "${patterns[j]}"`); - isValid = false; + console.error(` Error: Conflicting patterns found: ${patterns[i]} and ${patterns[j]}`); + return false; } } } - return isValid; + return true; + } + + /** + * Update the ignore cache for a directory + * @param filePath The path of the .contextignore file + * @param patterns The list of ignore patterns + */ + private updateIgnoreCache(filePath: string, patterns: string[]): void { + try { + const ig = ignore().add(patterns); + this.ignoreCache.set(path.dirname(filePath), ig); + } catch (error) { + console.error(`Error updating ignore cache for ${filePath}: ${error instanceof Error ? error.message : String(error)}`); + } + } + + /** + * Check if a file should be ignored based on the ignore patterns + * @param filePath The path of the file to check + * @param relativeTo The base directory to resolve relative paths + * @returns A boolean indicating whether the file should be ignored + */ + public isIgnored(filePath: string, relativeTo: string): boolean { + try { + const directoryPath = path.dirname(filePath); + let currentDir = directoryPath; + + // Traverse up the directory tree to find the nearest .contextignore file + while (currentDir.length >= relativeTo.length) { + const ig = this.ignoreCache.get(currentDir); + if (ig) { + const relativeFilePath = path.relative(currentDir, filePath); + return ig.ignores(relativeFilePath); + } + currentDir = path.dirname(currentDir); + } + + return false; + } catch (error) { + console.error(`Error checking if file ${filePath} is ignored: ${error instanceof Error ? error.message : String(error)}`); + return false; + } + } + + /** + * Clear the ignore cache + */ + public clearCache(): void { + this.ignoreCache.clear(); + } + + /** + * Get a list of ignored files in a directory + * @param directoryPath The path of the directory to check + * @returns An array of ignored file paths + */ + public getIgnoredFiles(directoryPath: string): string[] { + try { + const ig = this.ignoreCache.get(directoryPath); + if (!ig) { + return []; + } + + // Get all files in the directory + const allFiles = this.getAllFiles(directoryPath); + + // Filter the files using the ignore patterns + return allFiles.filter(file => { + const relativePath = path.relative(directoryPath, file); + return ig.ignores(relativePath); + }); + } catch (error) { + console.error(`Error getting ignored files for directory ${directoryPath}: ${error instanceof Error ? error.message : String(error)}`); + return []; + } + } + + /** + * Get all files in a directory recursively + * @param directoryPath The path of the directory to check + * @returns An array of file paths + */ + private getAllFiles(directoryPath: string): string[] { + const files: string[] = []; + + const walk = (dir: string) => { + const dirents = fs.readdirSync(dir, { withFileTypes: true }); + for (const dirent of dirents) { + const res = path.join(dir, dirent.name); + if (dirent.isDirectory()) { + walk(res); + } else { + files.push(res); + } + } + }; + + walk(directoryPath); + return files; } } \ No newline at end of file