diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0349ba6..30b08e1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,6 +20,8 @@ jobs: run: pnpm format:check - name: Lint run: pnpm lint + - name: Run knip + run: pnpm knip test: strategy: matrix: diff --git a/.gitignore b/.gitignore index 3c83cde..c286060 100644 --- a/.gitignore +++ b/.gitignore @@ -11,22 +11,21 @@ dist/ *.iml # cursor -.cursor -!tests/fixtures/*/.cursor/ +/.cursor/* +!/.cursor/plans/ -# windsurf -.windsurfrules -!tests/fixtures/*/.windsurfrules +# claude +/.claude/* +!/.claude/plans/ +.mcp.json -# codex +# aicm generated files AGENTS.md !tests/fixtures/*/AGENTS.md - -# claude CLAUDE.md !tests/fixtures/*/CLAUDE.md - -# aicm +.agents +!tests/fixtures/*/.agents .aicm !tests/fixtures/*/.aicm @@ -45,4 +44,4 @@ tmp-test/ # ai workflow PRD -tasks \ No newline at end of file +tasks diff --git a/.husky/pre-commit b/.husky/pre-commit index d0a7784..066a699 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1 +1 @@ -npx lint-staged \ No newline at end of file +pnpm exec lint-staged && pnpm exec knip --files \ No newline at end of file diff --git a/rules/general.mdc b/AGENTS.src.md similarity index 79% rename from rules/general.mdc rename to AGENTS.src.md index e3e951b..64fc6d8 100644 --- a/rules/general.mdc +++ b/AGENTS.src.md @@ -1,13 +1,7 @@ ---- -description: General development standards -globs: -alwaysApply: true ---- - ## Development Standards - Use pnpm for package management -- Development workflow: e2e tests → implementation → verification +- Development workflow: e2e tests -> implementation -> verification - Document all features in README.md - Never modify CHANGELOG.md - Never use dynamic imports unless absolutely necessary diff --git a/MIGRATION.md b/MIGRATION.md new file mode 100644 index 0000000..41c2749 --- /dev/null +++ b/MIGRATION.md @@ -0,0 +1,122 @@ +# Migration Guide: v0.x to v1.0 + +AICM v1.0 replaces Cursor-specific `.mdc` rules with an industry-standard instructions system targeting `AGENTS.md`. + +## Quick Summary + +| v0.x | v1.0 | +| ------------------------------ | ---------------------------------------------------------- | +| `rules/*.mdc` | `AGENTS.src.md`, `instructions/*.md`, or keep `rules/*.md` | +| `commands/*.md` | `skills/*/SKILL.md` | +| `assets/*` | Embed in skills or instructions | +| `"targets": ["claude"]` | `"targets": ["claude-code"]` | +| Output to `.cursor/rules/aicm` | Output to `AGENTS.md` | +| Windsurf support | Removed (new: `opencode`, `codex`) | + +## Migrating Rules + +In Cursor, `.mdc` rules have three modes: **Apply Always**, **Apply Intelligently**, and **Apply to Specific Files**. Each maps to a different v1.0 concept: + +### "Apply Always" Rules → Instructions + +These become inline instructions in `AGENTS.md`. If you're using a single file (`AGENTS.src.md`), just strip the `.mdc` frontmatter entirely — no frontmatter is needed. If you're using a directory with multiple files, rename `.mdc` to `.md`, remove `globs`, and replace `alwaysApply: true` with `inline: true`. + +### "Apply Intelligently" Rules + +These rules don't have a single migration path — choose the option that best fits each rule: + +1. **Progressive disclosure instructions** (`inline: false`) — Best for general guidelines that the agent should load on demand. The description is inlined into `AGENTS.md` with a link to the full content. Use a descriptive `description` field so the agent knows when to read it. + +2. **Skills** — Best for workflow-specific or invokable rules (e.g. a testing workflow, a code review checklist). Convert these into a `skills//SKILL.md` directory. + +### "Apply to Specific Files" Rules + +Glob-based rules (`globs` frontmatter) don't have a direct equivalent in v1.0. The recommended approach is to convert them to progressive disclosure instructions (`inline: false`) with a description that tells the agent when to apply them: + +```markdown +--- +description: TypeScript coding conventions — read this when working with *.ts files +inline: false +--- + +## TypeScript Conventions + +... +``` + +## Migrating Commands to Skills + +Cursor 2.4+ includes a built-in `/migrate-to-skills` command that automatically converts dynamic rules and slash commands to skills. [See Cursor docs](https://cursor.com/docs/context/skills#migrating-rules-and-commands-to-skills). + +Run it first—it handles most of the Cursor-side migration for you. + +## Step-by-Step + +### 1. Migrate your rules + +You have three options — pick the one that fits your setup: + +**Option A: Single file** — Merge your `.mdc` rules into one `AGENTS.src.md` file: + +- Remove all `.mdc` frontmatter (`globs`, `alwaysApply`) and combine the content +- No frontmatter needed — a single file is plain markdown and is always fully inlined +- You can also use a custom filename via `"instructionsFile": "MY-RULES.md"` in `aicm.json` + +**Option B: Keep your directory** — You can keep `rules/` (or any directory name) as-is. Convert the files (`.mdc` → `.md`, remove `globs`) and add frontmatter with `description` and `inline: true` or `false` (see [Migrating Rules](#migrating-rules)). Then point to it explicitly in `aicm.json`: + +```json +{ + "rootDir": "./", + "instructionsDir": "rules", + "targets": ["cursor", "claude-code"] +} +``` + +**Option C: Rename to `instructions/`** — Same as Option B, but rename `rules/` to `instructions/`. This follows the convention and enables auto-detection, so you don't need the explicit `"instructionsDir"` field. + +> **Note:** `AGENTS.src.md` and `instructions/` are auto-detected when present under `rootDir`. Any other name requires an explicit `"instructionsFile"` (for a single file) or `"instructionsDir"` (for a directory) field. + +### 2. Convert `commands/` to `skills/` + +Move each command file into a named skill directory: + +``` +commands/review.md → skills/review/SKILL.md +``` + +### 3. Remove `assets/` + +Move asset files into the relevant skill directory or inline their content into instructions. + +### 4. Update `aicm.json` + +Update `targets` with the new preset names. `AGENTS.src.md` and `instructions/` are auto-detected when present under `rootDir`: + +```json +{ + "rootDir": "./", + "targets": ["cursor", "claude-code"] +} +``` + +Available target presets: `cursor`, `claude-code`, `opencode`, `codex`. Each preset automatically resolves to the correct output paths for that environment. Remove `"windsurf"` if present — it is no longer supported. + +### 5. Update `.gitignore` + +Replace old entries: + +```gitignore +# AICM managed files +**/.cursor/*/aicm/** +**/.claude/*/aicm/** +``` + +### 6. install + +```bash +npx aicm install +``` + +### 7. Update Preset Packages (if applicable) + +If you maintain a preset: rename `rules/` to `instructions/`, convert `.mdc` to `.md`, remove `commands/` and `assets/`, and remove `"targets"` from the preset config. The `instructions/` directory is auto-detected when present under `rootDir`. diff --git a/README.md b/README.md index 82e4f31..aa8efa1 100644 --- a/README.md +++ b/README.md @@ -1,764 +1,358 @@ -# 🗂️ aicm +# aicm -> AI Configuration Manager +AI Configuration Manager for sharing coding agent instructions and tooling across projects. -A CLI tool for managing Agentic configurations across projects. +## Why aicm -![aicm](https://github.com/user-attachments/assets/ca38f2d6-ece6-43ad-a127-6f4fce8b2a5a) +aicm solves the distribution challenge of managing coding agent configurations across projects. +It gives you a single source of truth for `AGENTS.md` instructions, skills, subagents, hooks, and MCP configurations, +then installs them into each project so coding agents like Cursor, Claude Code, OpenCode, and Codex can use them. -## Table of Contents +## Supported Targets -- [Why](#why) -- [Supported Environments](#supported-environments) -- [Getting Started](#getting-started) - - [Creating a Preset](#creating-a-preset) - - [Using a Preset](#using-a-preset) -- [Features](#features) - - [Rules](#rules) - - [Commands](#commands) - - [Skills](#skills) - - [Agents](#agents) - - [Hooks](#hooks) - - [MCP Servers](#mcp-servers) - - [Assets](#assets) -- [Workspaces Support](#workspaces-support) -- [Configuration](#configuration) -- [CLI Commands](#cli-commands) -- [Node.js API](#nodejs-api) -- [FAQ](#faq) - -## Why - -Modern AI-powered IDEs like Cursor and Agents like Codex allow developers to add custom instructions, commands, and MCP servers. However, keeping these configurations consistent across a team or multiple projects is a challenge. - -**aicm** enables **"Write Once, Use Everywhere"** for your AI configurations. - -- **Team Consistency:** Ensure every developer on your team uses the same rules and best practices. -- **Reusable Presets:** Bundle your rules, commands & MCP configurations into npm packages (e.g., `@company/ai-preset`) to share them across your organization. -- **Multi-Target Support:** Write rules once in the comprehensive `.mdc` format, and automatically deploy them to Cursor, Windsurf, Codex, and Claude. - -## Supported Environments - -aicm acts as a bridge between your configuration and your AI tools. It accepts Cursor's `.mdc` format and can transform it for other environments: - -| Target | Installation | -| ------------ | ------------------------------------------------------------------------------ | -| **Cursor** | Copies `.mdc` files to `.cursor/rules/aicm/` and configures `.cursor/mcp.json` | -| **Windsurf** | Generates a `.windsurfrules` file that links to rules in `.aicm/` | -| **Codex** | Generates an `AGENTS.md` file that references rules in `.aicm/` | -| **Claude** | Generates a `CLAUDE.md` file that references rules in `.aicm/` | - -## Getting Started - -The easiest way to get started with aicm is by using **presets** - npm packages containing rules and MCP configurations that you can install in any project. - -### Demo - -We'll install [an npm package](https://github.com/ranyitz/pirate-coding) containing a simple "Pirate Coding" preset to demonstrate how aicm works. - -1. **Install the demo preset package**: - -```bash -npm install --save-dev pirate-coding -``` - -2. **Create an `aicm.json` file** in your project: - -```bash -echo '{ "presets": ["pirate-coding"] }' > aicm.json -``` - -3. **Install all rules & MCPs from your configuration**: - -```bash -npx aicm install -``` - -After installation, open Cursor and ask it to do something. Your AI assistant will respond with pirate-themed coding advice. - -### Creating a Preset - -1. **Create an npm package** with the following structure: - -``` -@team/ai-preset/ -├── package.json -├── aicm.json -├── rules/ # Rule files (.mdc) -│ ├── typescript.mdc -│ └── react.mdc -├── commands/ # Command files (.md) [optional] -├── skills/ # Agent Skills [optional] -├── agents/ # Subagents (.md) [optional] -├── assets/ # Auxiliary files [optional] -└── hooks.json # Hook configuration [optional] -``` - -2. **Configure the preset's `aicm.json`**: +By default, aicm installs to `cursor` and `claude-code`. You can customize this with the `targets` field: ```json { - "rootDir": "./", - "mcpServers": { - "my-mcp": { "url": "https://example.com/sse" } - } + "targets": ["cursor", "claude-code", "opencode", "codex"] } ``` -3. **Publish the package** and use it in your project's `aicm.json`: +| Target preset | Instructions | Skills | Agents | MCP | Hooks | +| ------------- | ------------ | ------------------- | ------------------- | -------------------- | ---------- | +| `cursor` | `AGENTS.md` | `.cursor/skills/` | `.cursor/agents/` | `.cursor/mcp.json` | `.cursor/` | +| `claude-code` | `CLAUDE.md` | `.claude/skills/` | `.claude/agents/` | `.mcp.json` | `.claude/` | +| `opencode` | `AGENTS.md` | `.opencode/skills/` | `.opencode/agents/` | `opencode.json` | - | +| `codex` | `AGENTS.md` | `.agents/skills/` | - | `.codex/config.toml` | - | -```json -{ "presets": ["@team/ai-preset"] } -``` - -> **Note:** This is syntactic sugar for `@team/ai-preset/aicm.json`. - -### Using a Preset +## Quick Start -To use a real preset in your production project: +The main workflow in aicm is: -1. **Install a preset npm package**: +1. Create a reusable preset repo. +2. Consume it from app repos and run install. -```bash -npm install --save-dev @team/ai-preset -``` +### Create a preset -2. **Create an `aicm.json` file** in your project root: +Create a preset repository with an `aicm.json` and instruction sources: -```json -{ "presets": ["@team/ai-preset"] } +```text +my-preset/ + aicm.json + instructions/ + TESTING.md + skills/ + code-review/ + SKILL.md ``` -3. **Add a prepare script** to your `package.json` to ensure rules are always up to date: +Example `aicm.json` in the preset repo: ```json { - "scripts": { - "prepare": "npx aicm -y install" - } + "rootDir": "./" } ``` -The rules are now installed in `.cursor/rules/aicm/` and any MCP servers are configured in `.cursor/mcp.json`. - -### Notes - -- Generated files are always placed in subdirectories for deterministic cleanup and easy gitignore. -- Users may add `.cursor/*/aicm/`, `.cursor/skills/`, `.cursor/agents/`, `.claude/`, and `.codex/` to `.gitignore` to avoid tracking generated files. - -## Features +Use `instructions/*.md` for content that should either always be visible to the agent or loaded on demand. -### Rules +### Consume the preset in an project repo -aicm uses Cursor's `.mdc` files for rules. Read more about the format [here](https://cursor.com/docs/context/rules). - -Create a `rules/` directory in your project (at the `rootDir` location): - -``` -my-project/ -├── aicm.json -└── rules/ - ├── typescript.mdc - └── react.mdc -``` - -Configure your `aicm.json`: +In the consumer project, configure `aicm.json`: ```json { "rootDir": "./", - "targets": ["cursor"] + "presets": ["https://github.com/acme/my-preset"], + "targets": ["cursor", "claude-code"] } ``` -Rules are installed in `.cursor/rules/aicm/` and are loaded automatically by Cursor. - -### Commands - -Cursor supports custom commands that can be invoked directly in the chat interface. aicm can manage these command files alongside your rules and MCP configurations. - -Create a `commands/` directory in your project (at the `rootDir` location): - -``` -my-project/ -├── aicm.json -└── commands/ - ├── review.md - └── generate.md -``` - -Configure your `aicm.json`: +Run install: -```json -{ - "rootDir": "./", - "targets": ["cursor"] -} +```bash +pnpm dlx aicm install ``` -Command files ending in `.md` are installed to `.cursor/commands/aicm/` and appear in Cursor under the `/` command menu. - -### Skills - -aicm supports [Agent Skills](https://agentskills.io) - a standard format for giving AI agents new capabilities and expertise. Skills are folders containing instructions, scripts, and resources that agents can discover and use. +This generates or modifies target files (for example `AGENTS.md`, `CLAUDE.md`, `.cursor/mcp.json`, `.mcp.json`, `.cursor/skills/code-review/SKILL.md`, `.claude/skills/code-review/SKILL.md`). -Create a `skills/` directory where each subdirectory is a skill (containing a `SKILL.md` file): +**Result after `npx aicm install`:** ``` -my-project/ -├── aicm.json -└── skills/ - ├── pdf-processing/ - │ ├── SKILL.md - │ ├── scripts/ - │ │ └── extract.py - │ └── references/ - │ └── REFERENCE.md - └── code-review/ - └── SKILL.md +my-app/ +├── AGENTS.md # Generated +├── AGENTS.src.md # You write this +├── .agents/ +│ ├── instructions/ +│ │ └── TESTING.md # Progressive disclosure file +├── .cursor/ +│ └── skills/ +│ └── code-review/ +│ └── SKILL.md # Copied from preset +└── aicm.json ``` -Each skill must have a `SKILL.md` file with YAML frontmatter: +**generated `AGENTS.md` file** ```markdown ---- -name: pdf-processing -description: Extract text and tables from PDF files, fill forms, merge documents. ---- + + -# PDF Processing Skill +## Project Guidelines -This skill enables working with PDF documents. +- Use pnpm for package management -## Usage + -Run the extraction script: -scripts/extract.py -``` +- [TESTING](.agents/instructions/TESTING.md): How to run and write tests -Configure your `aicm.json`: - -```json -{ - "rootDir": "./", - "targets": ["cursor"] -} + ``` -Skills are installed to different locations based on the target: +## Core Concepts -| Target | Skills Location | -| ---------- | ----------------- | -| **Cursor** | `.cursor/skills/` | -| **Claude** | `.claude/skills/` | -| **Codex** | `.codex/skills/` | +### Instructions -When installed, each skill directory is copied in its entirety (including `scripts/`, `references/`, `assets/` subdirectories). A `.aicm.json` file is added inside each installed skill to track that it's managed by aicm. +aicm supports two instruction sources: -In workspace mode, skills are installed both to each package and merged at the root level, similar to commands. +- Single file: `AGENTS.src.md` (plain markdown, no frontmatter) +- Directory: `instructions/*.md` (requires frontmatter) -### Agents +When `rootDir` is set, aicm auto-detects `AGENTS.src.md` and `instructions/*.md` if present. -aicm supports [Cursor Subagents](https://cursor.com/docs/context/subagents) and [Claude Code Subagents](https://code.claude.com/docs/en/sub-agents) - specialized AI assistants that can be delegated specific tasks. Agents are markdown files with YAML frontmatter that define custom prompts, descriptions, and model configurations. +#### Single-file instructions (`AGENTS.src.md`). -Create an `agents/` directory in your project (at the `rootDir` location): +```md +## Coding Standards -``` -my-project/ -├── aicm.json -└── agents/ - ├── code-reviewer.md - ├── debugger.md - └── specialized/ - └── security-auditor.md +- Keep functions small +- Prefer explicit error handling ``` -Each agent file should have YAML frontmatter with at least a `name` and `description`: +#### Instructions directory (`instructions/*.md`) -```markdown +Use this when you want multiple instruction files and progressive disclosure. + +```yml --- -name: code-reviewer -description: Reviews code for quality and best practices. Use after code changes. -model: inherit +description: Test strategy and patterns +inline: false --- +## Testing -You are a senior code reviewer ensuring high standards of code quality and security. - -When invoked: - -1. Run git diff to see recent changes -2. Focus on modified files -3. Begin review immediately - -Review checklist: - -- Code is clear and readable -- Functions and variables are well-named -- No duplicated code -- Proper error handling +- Use integration tests for critical flows ``` -Configure your `aicm.json`: +Frontmatter fields: -```json -{ - "rootDir": "./", - "targets": ["cursor", "claude"] -} -``` +- `description` (required) +- `inline` (optional, default `false`) -Agents are installed to different locations based on the target: +Behavior: -| Target | Agents Location | -| ---------- | ----------------- | -| **Cursor** | `.cursor/agents/` | -| **Claude** | `.claude/agents/` | +- `inline: true`: full content is embedded into generated AGENTS.md file +- `inline: false`: only a link is embedded, content is written to `.agents/instructions/...` with a link in AGENTS.md for the agent to read on demand. -A `.aicm.json` metadata file is created in the agents directory to track which agents are managed by aicm. This allows the clean command to remove only aicm-managed agents while preserving any manually created agents. +### Presets -**Supported Configuration Fields:** +Presets are reusable aicm configurations referenced from either: -Only fields that work in both Cursor and Claude Code are documented: +- Local path: `"./presets/team"` +- npm package: `"@acme/my-preset"` +- GitHub URL: `"https://github.com/acme/my-preset"` -- `name` - Unique identifier (defaults to filename without extension) -- `description` - When the agent should be used for task delegation -- `model` - Model to use (`inherit`, or platform-specific values like `sonnet`, `haiku`, `fast`) +GitHub presets also support: -> **Note:** Users may include additional platform-specific fields (e.g., `tools`, `hooks` for Claude Code, or `readonly`, `is_background` for Cursor) - aicm will preserve them, but they only work on the respective platform. - -In workspace mode, agents are installed both to each package and merged at the root level, similar to commands and skills. - -### Hooks - -aicm provides first-class support for [Cursor Agent Hooks](https://docs.cursor.com/advanced/hooks), allowing you to intercept and extend the agent's behavior. Hooks enable you to run custom scripts before/after shell execution, file edits, MCP calls, and more. - -#### Basic Setup - -Hooks follow a convention similar to Cursor's own structure: - -``` -my-project/ -├── aicm.json -├── hooks.json -└── hooks/ - ├── audit.sh - └── format.js -``` +- Branch/tag refs: `/tree/main` +- Subpath presets: `/tree/main/path/to/preset` +- Private repos via `GITHUB_TOKEN`, `GH_TOKEN`, or `gh auth token` -Your `hooks.json` file should reference scripts within the `hooks/` directory: +Example: ```json { - "version": 1, - "hooks": { - "beforeShellExecution": [{ "command": "./hooks/audit.sh" }], - "afterFileEdit": [{ "command": "./hooks/format.js" }] - } + "presets": [ + "./presets/local-team", + "@acme/my-preset", + "https://github.com/acme/my-preset/tree/main/frontend" + ] } ``` -> **Important:** All hook scripts must be within the `hooks/` directory. References to files outside this directory will be warned about and skipped. - -#### Installation Behavior - -When you run `aicm install`, the following happens: - -1. **Directory Copy**: All files in the `hooks/` directory (except `hooks.json`) are copied -2. **Path Rewriting**: Command paths in `hooks.json` are rewritten to point to `.cursor/hooks/aicm/` -3. **File Installation**: Scripts are copied to `.cursor/hooks/aicm/` (for local hooks) or `.cursor/hooks/aicm//` (for preset hooks) with their directory structure preserved -4. **Config Merging**: Your hooks configuration is merged into `.cursor/hooks.json` - -#### Preset Namespacing - -aicm uses directory-based namespacing to prevent collisions: - -``` -.cursor/hooks/aicm/ -├── preset-a/ -│ └── validate.sh # From preset-a -└── preset-b/ - └── validate.sh # From preset-b -``` - -#### Workspace Support - -In monorepo/workspace mode, hooks are: - -- Installed individually for each package (in `package-x/.cursor/hooks.json`) -- Merged and installed at the root (in `.cursor/hooks.json`) -- Deduplicated by full path (including preset namespace) - -**Example workspace structure:** - -``` -my-monorepo/ -├── aicm.json (workspaces: true) -├── .cursor/hooks.json (merged from all packages) -├── package-a/ -│ ├── aicm.json -│ ├── hooks.json -│ ├── hooks/ -│ │ └── check.sh -│ └── .cursor/hooks.json (package-specific) -└── package-b/ - ├── aicm.json - ├── hooks.json - ├── hooks/ - │ └── validate.js - └── .cursor/hooks.json (package-specific) -``` - -#### Content Collision Detection - -If the same hook file (by path) has different content across workspace packages, aicm will: +> **Note:** Presets are recursive and can depend on other presets. -1. Warn you about the collision with full source information -2. Use the last occurrence (last-writer-wins) -3. Continue installation +### Targets -### MCP Servers - -You can configure MCP servers directly in your `aicm.json`, which is useful for sharing mcp configurations across your team or bundling them into presets. +`targets` configures where the output files are written to. ```json { - "mcpServers": { - "Playwright": { - "command": "npx", - "args": ["@playwright/mcp"] - } - } + "targets": ["cursor", "claude-code", "opencode", "codex"] } ``` -When installed, these servers are automatically added to your `.cursor/mcp.json`. - -### Assets - -You can include assets (examples, schemas, scripts, etc.) that can be referenced by your rules, commands, and hooks by placing them in the `assets/` directory. - -All files in `assets/` are copied to `.cursor/assets/aicm/` (for Cursor) or `.aicm/` (for Windsurf/Codex/Claude). - -**Example structure:** - -``` -my-project/ -├── aicm.json -├── rules/ -│ └── api-guide.mdc # References ../assets/schema.json -├── commands/ -│ └── generate.md # References ../assets/schema.json -├── assets/ -│ ├── schema.json -│ ├── examples/ -│ │ └── config.ts -│ └── hooks/ -│ └── validate.sh -└── hooks.json # References ./hooks/validate.sh -``` - -**Referencing assets from rules and commands:** - -```markdown - - -Use [this schema](../assets/schema.json) for validation. -Check the example at `../assets/examples/response.json`. -``` - -**Note:** The `../assets/` path is automatically adjusted during installation to `../../assets/aicm/` to match the final directory structure. You don't need to worry about the installation paths - just use `../assets/`. - -**After installation:** - -``` -.cursor/ -├── assets/aicm/ # All assets copied here -│ ├── schema.json -│ ├── examples/ -│ │ └── config.ts -│ └── hooks/ -│ └── validate.sh -├── rules/aicm/ -│ └── api-guide.mdc # References ../../assets/aicm/schema.json -├── commands/aicm/ -│ └── generate.md # References ../../assets/aicm/schema.json -└── hooks/ - ├── aicm/ - └── hooks.json -``` - -## Workspaces Support - -aicm supports workspaces by automatically discovering and installing configurations across multiple packages in your repository. - -You can enable workspaces mode by setting the `workspaces` property to `true` in your root `aicm.json`: +If omitted, defaults to: ```json -{ - "workspaces": true -} -``` - -aicm automatically detects workspaces if your `package.json` contains a `workspaces` configuration. - -### How It Works - -1. **Discover packages**: Automatically find all directories containing `aicm.json` files in your repository. -2. **Install per package**: Install rules, commands, skills, and agents for each package individually in their respective directories. -3. **Merge MCP servers**: Write a merged `.cursor/mcp.json` at the repository root containing all MCP servers from every package. -4. **Merge commands**: Write a merged `.cursor/commands/aicm/` at the repository root containing all commands from every package. -5. **Merge skills**: Write merged skills to the repository root (e.g., `.cursor/skills/`) containing all skills from every package. -6. **Merge agents**: Write merged agents to the repository root (e.g., `.cursor/agents/`) containing all agents from every package. - -For example, in a workspace structure like: - -``` -├── aicm.json (with "workspaces": true) -├── packages/ -│ ├── frontend/ -│ │ └── aicm.json -│ └── backend/ -│ └── aicm.json -└── services/ - └── api/ - └── aicm.json +["cursor", "claude-code"] ``` -Running `npx aicm install` will install rules for each package in their respective directories: +### AGENTS.md -- `packages/frontend/.cursor/rules/aicm/` -- `packages/backend/.cursor/rules/aicm/` -- `services/api/.cursor/rules/aicm/` +`AGENTS.src.md` is your source file, and aicm generates target instruction files from it (for example `AGENTS.md` and `CLAUDE.md`). -**Why install in both places?** -`aicm` installs configurations at both the package level AND the root level to support different workflows: +[`AGENTS.md`](https://agents.md/) is an open standard for sharing persistent project guidance with coding agents. -- **Package-level context:** When a developer opens a specific package folder (e.g., `packages/frontend`) in their IDE, they get the specific rules, commands, and MCP servers for that package. -- **Root-level context:** When a developer opens the monorepo root, `aicm` ensures they have access to all commands and MCP servers from all packages via the merged root configuration. While rules are typically read from nested directories by Cursor, commands and MCP servers must be configured at the root to be accessible. +### Skills -### Preset Packages in Workspaces +> [Agent Skills](https://agentskills.io) -When you have a preset package within your workspace (a package that provides rules to be consumed by others), you can prevent aicm from installing rules into it by setting `skipInstall: true`: +Put skills in `skills//SKILL.md`. +Each skill directory is copied to target skill locations. -```json -{ - "skipInstall": true, - "rootDir": "./", - "targets": ["cursor"] -} +```text +skills/ + code-review/ + SKILL.md ``` -This is useful when your workspace contains both consumer packages (that need rules installed) and provider packages (that only export rules). +### Agents (_Subagents_) -## Configuration +> [Cursor Subagents](https://cursor.com/docs/agent/subagents), [Claude Code Subagents](https://code.claude.com/docs/en/sub-agents) -Create an `aicm.json` file in your project root, or an `aicm` key in your project's `package.json`. +Put markdown file in `agents/*.md`. +They are installed to the target agents directories. -```json -{ - "rootDir": "./", - "targets": ["cursor"], - "presets": [], - "mcpServers": {}, - "skipInstall": false -} +```text +agents/ + reviewer.md ``` -### Configuration Options +### Hooks -- **rootDir**: Directory containing your aicm structure. Must contain one or more of: `rules/`, `commands/`, `skills/`, `agents/`, `assets/`, `hooks/`, or `hooks.json`. If not specified, aicm will only install rules from presets and will not pick up any local directories. -- **targets**: IDEs/Agent targets where rules should be installed. Defaults to `["cursor"]`. Supported targets: `cursor`, `windsurf`, `codex`, `claude`. -- **presets**: List of preset packages or paths to include. -- **mcpServers**: MCP server configurations. -- **workspaces**: Set to `true` to enable workspace mode. If not specified, aicm will automatically detect workspaces from your `package.json`. -- **skipInstall**: Set to `true` to skip rule installation for this package. Useful for preset packages that provide rules but shouldn't have rules installed into them. +> [Cursor Hooks](https://cursor.com/docs/agent/hooks), [Claude Code Hooks](https://docs.claude.com/en/docs/claude-code/hooks) -### Configuration Examples +Define hooks with: -#### Preset-Only Configuration +- `hooks.json` +- `hooks/` scripts directory -For projects that only consume presets and don't have their own rules, you can omit `rootDir`: +Local and preset hooks are merged and namespaced during install. ```json { - "presets": ["@company/ai-preset"] + "version": 1, + "hooks": { + "beforeShellExecution": [{ "command": "./hooks/audit.sh" }] + } } ``` -This ensures that only rules from the preset are installed, and any local directories like `commands/` or `rules/` in your project (used for your application) won't be accidentally picked up by aicm. +### MCP servers -#### Mixed Local and Preset Configuration +> [Model Context Protocol (MCP)](https://modelcontextprotocol.io/docs/getting-started/intro) -To combine your own rules with preset rules: +Define MCP servers in `aicm.json` and install them to target MCP files. ```json { - "rootDir": "./ai-config", - "presets": ["@company/ai-preset"], - "targets": ["cursor", "windsurf"] + "mcpServers": { + "Playwright": { + "command": "npx", + "args": ["@playwright/mcp"] + } + } } ``` -This will load rules from both `./ai-config/rules/` and the preset, installing them to both Cursor and Windsurf. +### Workspaces -### Directory Structure +aicm supports monorepos/workspaces: -aicm uses a convention-based directory structure: - -``` -my-project/ -├── aicm.json -├── rules/ # Rule files (.mdc) [optional] -│ ├── api.mdc -│ └── testing.mdc -├── commands/ # Command files (.md) [optional] -│ └── generate.md -├── skills/ # Agent Skills [optional] -│ └── my-skill/ -│ └── SKILL.md -├── agents/ # Subagents (.md) [optional] -│ └── code-reviewer.md -├── assets/ # Auxiliary files [optional] -│ ├── schema.json -│ └── examples/ -├── hooks/ # Hook scripts [optional] -│ └── validate.sh -└── hooks.json # Hook configuration [optional] -``` +- Auto-detected from `package.json` `workspaces` field, or set `"workspaces": true` in `aicm.json` +- Installs per package +- Also merges non-instruction outputs at the repository root (`skills`, `agents`, `mcp`, `hooks`) -## CLI Commands +Use `"skipInstall": true` in packages that only provide shared presets/content. -### Global Options - -These options are available for all commands: +## Configuration -- `--help`, `-h`: Show help information -- `--version`, `-v`: Show version information +Use either: -### `init` +- `aicm.json` at project root, or +- `aicm` key inside `package.json` -Initializes a new configuration file in your current directory. +### Configuration fields -```bash -npx aicm init -``` +- `rootDir`: base directory for local sources +- `instructionsFile`: single instructions file path (relative to `rootDir` if set) +- `instructionsDir`: directory instructions path (relative to `rootDir` if set) +- `targets`: target presets (`cursor`, `claude-code`, `opencode`, `codex`) +- `presets`: preset sources (local path, npm package, GitHub URL) +- `mcpServers`: MCP configuration +- `workspaces`: boolean override for workspace mode +- `skipInstall`: skip this package during install (workspace use case) -Edit this file to add your rules, presets, or other settings. - -### `install` - -Installs all rules and MCPs configured in your `aicm.json`. +## CLI ```bash -npx aicm install +aicm init +aicm install [--ci] [--dry-run] [--verbose] +aicm list +aicm clean [--verbose] ``` -Options: - -- `--ci`: run in CI environments (default: `false`) -- `--verbose`: show detailed output and stack traces for debugging -- `--dry-run`: simulate installation without writing files, useful for validating presets in CI +- `init`: scaffold `aicm.json`, `AGENTS.src.md`, and optional directories +- `install`: resolve config/presets and write target output +- `list`: show configured instructions +- `clean`: remove aicm-managed generated content -### `clean` +Global options: -Removes all files, directories & changes made by aicm. - -```bash -npx aicm clean -``` +- `-h, --help` +- `-v, --version` ## Node.js API -In addition to the CLI, aicm can be used programmatically in Node.js applications: +```js +const { install, checkWorkspacesEnabled } = require("aicm"); -```javascript -const { install, Config } = require("aicm"); - -install().then((result) => { - if (result.success) { - console.log(`Successfully installed ${result.installedRuleCount} rules`); - } else { - console.error(`Error: ${result.error}`); - } -}); - -// Install with custom options -const customConfig = { - targets: ["cursor"], - rootDir: "./", - presets: ["@team/ai-preset"], -}; - -install({ - config: customConfig, - cwd: "/path/to/project", -}).then((result) => { - // Handle result -}); +const result = await install({ dryRun: true }); +const workspaces = await checkWorkspacesEnabled(); ``` -## Security Note +## Generated Files and Git -To prevent [prompt-injection](https://en.wikipedia.org/wiki/Prompt_injection), use only packages from trusted sources. +Most installed output is generated from source config/content. +A common setup is to ignore generated output: -## FAQ - -### Can I reference rules from commands or vice versa? - -**No, direct references between rules and commands are not supported.** This is because: - -- **Commands are hoisted** to the root level in workspace mode (`.cursor/commands/aicm/`) -- **Rules remain nested** at the package level (`package-a/.cursor/rules/aicm/`) -- This creates broken relative paths when commands try to reference rules - -**❌ Don't do this:** - -```markdown - - -Follow the rules in [api-rule.mdc](../rules/api-rule.mdc) -``` - -**✅ Do this instead:** - -```markdown - - -# Coding Standards - -- Use TypeScript for all new code -- Follow ESLint rules -- Write unit tests for all functions -``` - -```markdown - - -Follow the coding standards in [coding-standards.md](../assets/coding-standards.md). +```gitignore +AGENTS.md +CLAUDE.md +.cursor/ +.claude/ +.agents/ ``` -```markdown - +Keep your **local setup** in source files, and commit them: -Validate against our [coding standards](../assets/coding-standards.md). -``` +- `aicm.json` +- `AGENTS.src.md` +- `skills//SKILL.md` +- `agents/*.md` -Use shared assets for content that needs to be referenced by both rules and commands. Assets are properly rewritten and work in all modes. +After each change, run `aicm install` to regenerate the generated files and merge with 3rd party presets. -## Contributing +## CLAUDE.md -Contributions are welcome! Please feel free to open an issue or submit a Pull Request. +If both `AGENTS.md` and `CLAUDE.md` are targets, aicm writes the full merged +content to `AGENTS.md`. `CLAUDE.md` is created as `@AGENTS.md` only when it +does not already exist, if it already exists, it is up to the user to point to `AGENTS.md` instead. -## Development +## Migration from v0.x -### Testing +See [MIGRATION.md](MIGRATION.md). -```bash -pnpm test -``` - -### Publishing +## Security Note -```bash -npm run release -``` +Treat presets as executable team policy. Use trusted sources only, especially for hooks/scripts and remote presets. diff --git a/aicm.json b/aicm.json index 232b8a7..058aea3 100644 --- a/aicm.json +++ b/aicm.json @@ -1,6 +1,6 @@ { - "targets": ["cursor", "codex", "claude"], "rootDir": "./", + "targets": ["cursor", "claude-code"], "mcpServers": { "context7": { "command": "npx", diff --git a/rules/test.mdc b/instructions/TESTING.md similarity index 80% rename from rules/test.mdc rename to instructions/TESTING.md index a9a860f..3fa8b71 100644 --- a/rules/test.mdc +++ b/instructions/TESTING.md @@ -1,35 +1,36 @@ --- description: E2E testing best practices and fixture management guidelines -globs: tests/**/*.ts -alwaysApply: false +inline: false --- ## E2E Testing Best Practices - Store initial test state in fixtures -- Reference documentation: [E2E_TESTS.md](mdc:tests/E2E_TESTS.md) +- Reference documentation: [E2E_TESTS.md](tests/E2E_TESTS.md) - Include .gitkeep file in empty fixture directories - **Never** create test files on-the-fly with `fs.writeFileSync()` or similar methods ## Example Usage -✅ **DO:** Good practice +DO: Good practice + ```typescript test("should do something with files", async () => { await setupFromFixture("my-test-fixture", expect.getState().currentTestName); - + // Test logic here }); ``` -❌ **DON'T:** Bad practice (avoid this) +DON'T: Bad practice (avoid this) + ```typescript test("should not create files directly", async () => { await setupTestDir(expect.getState().currentTestName); - + // DON'T DO THIS - violates test standards fs.writeFileSync(path.join(testDir, "some-file.txt"), "content"); - + // Test logic here }); -``` \ No newline at end of file +``` diff --git a/jest.config.js b/jest.config.js index 9a56f24..a745539 100644 --- a/jest.config.js +++ b/jest.config.js @@ -4,7 +4,7 @@ module.exports = { preset: "ts-jest", testEnvironment: "node", - testMatch: ["**/tests/e2e/**/*.test.ts"], + testMatch: ["**/tests/e2e/**/*.test.ts", "**/tests/unit/**/*.test.ts"], verbose: true, clearMocks: true, resetMocks: false, diff --git a/knip.json b/knip.json new file mode 100644 index 0000000..518a05d --- /dev/null +++ b/knip.json @@ -0,0 +1,5 @@ +{ + "$schema": "https://unpkg.com/knip@5/schema.json", + "tags": ["-lintignore"], + "ignore": ["tests/fixtures/**"] +} diff --git a/package.json b/package.json index cffc3df..7b9c220 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,8 @@ "lint": "eslint", "prepare": "husky install && npx ts-node src/bin/aicm.ts install", "version": "auto-changelog -p && git add CHANGELOG.md", - "release": "np --no-tests --no-publish" + "release": "np --no-tests --no-publish", + "knip": "knip" }, "keywords": [ "ai", @@ -46,7 +47,8 @@ "chalk": "^4.1.2", "cosmiconfig": "^9.0.0", "fast-glob": "^3.3.3", - "fs-extra": "^11.1.1" + "fs-extra": "^11.1.1", + "smol-toml": "^1.6.0" }, "devDependencies": { "@eslint/js": "^9.26.0", @@ -57,6 +59,7 @@ "eslint": "^9.26.0", "husky": "^8.0.3", "jest": "^29.7.0", + "knip": "^5.82.1", "lint-staged": "^15.2.0", "np": "^10.2.0", "prettier": "^3.1.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ba0d21c..b7c420d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -16,17 +16,20 @@ importers: version: 4.1.2 cosmiconfig: specifier: ^9.0.0 - version: 9.0.0(typescript@5.8.3) + version: 9.0.0(typescript@5.9.3) fast-glob: specifier: ^3.3.3 version: 3.3.3 fs-extra: specifier: ^11.1.1 - version: 11.3.0 + version: 11.3.3 + smol-toml: + specifier: ^1.6.0 + version: 1.6.0 devDependencies: '@eslint/js': specifier: ^9.26.0 - version: 9.28.0 + version: 9.39.2 '@types/fs-extra': specifier: ^11.0.4 version: 11.0.4 @@ -35,102 +38,105 @@ importers: version: 29.5.14 '@types/node': specifier: ^20.9.0 - version: 20.17.58 + version: 20.19.32 auto-changelog: specifier: ^2.5.0 version: 2.5.0 eslint: specifier: ^9.26.0 - version: 9.28.0 + version: 9.39.2(jiti@2.6.1) husky: specifier: ^8.0.3 version: 8.0.3 jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@20.17.58)(ts-node@10.9.2(@types/node@20.17.58)(typescript@5.8.3)) + version: 29.7.0(@types/node@20.19.32)(ts-node@10.9.2(@types/node@20.19.32)(typescript@5.9.3)) + knip: + specifier: ^5.82.1 + version: 5.83.0(@types/node@20.19.32)(typescript@5.9.3) lint-staged: specifier: ^15.2.0 version: 15.5.2 np: specifier: ^10.2.0 - version: 10.2.0(@types/node@20.17.58)(typescript@5.8.3) + version: 10.3.0(@types/node@20.19.32)(typescript@5.9.3) prettier: specifier: ^3.1.0 - version: 3.5.3 + version: 3.8.1 rimraf: specifier: ^5.0.5 version: 5.0.10 ts-jest: specifier: ^29.1.1 - version: 29.3.4(@babel/core@7.27.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.27.4))(jest@29.7.0(@types/node@20.17.58)(ts-node@10.9.2(@types/node@20.17.58)(typescript@5.8.3)))(typescript@5.8.3) + version: 29.4.6(@babel/core@7.29.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.0))(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.32)(ts-node@10.9.2(@types/node@20.19.32)(typescript@5.9.3)))(typescript@5.9.3) ts-node: specifier: ^10.9.1 - version: 10.9.2(@types/node@20.17.58)(typescript@5.8.3) + version: 10.9.2(@types/node@20.19.32)(typescript@5.9.3) typescript: specifier: ^5.8.3 - version: 5.8.3 + version: 5.9.3 typescript-eslint: specifier: ^8.31.1 - version: 8.33.1(eslint@9.28.0)(typescript@5.8.3) + version: 8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) packages: - '@ampproject/remapping@2.3.0': - resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} - engines: {node: '>=6.0.0'} + '@babel/code-frame@7.29.0': + resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} + engines: {node: '>=6.9.0'} - '@babel/code-frame@7.27.1': - resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} + '@babel/compat-data@7.29.0': + resolution: {integrity: sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==} engines: {node: '>=6.9.0'} - '@babel/compat-data@7.27.5': - resolution: {integrity: sha512-KiRAp/VoJaWkkte84TvUd9qjdbZAdiqyvMxrGl1N6vzFogKmaLgoM3L1kgtLicp2HP5fBJS8JrZKLVIZGVJAVg==} + '@babel/core@7.29.0': + resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==} engines: {node: '>=6.9.0'} - '@babel/core@7.27.4': - resolution: {integrity: sha512-bXYxrXFubeYdvB0NhD/NBB3Qi6aZeV20GOWVI47t2dkecCEoneR4NPVcb7abpXDEvejgrUfFtG6vG/zxAKmg+g==} + '@babel/generator@7.29.1': + resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==} engines: {node: '>=6.9.0'} - '@babel/generator@7.27.5': - resolution: {integrity: sha512-ZGhA37l0e/g2s1Cnzdix0O3aLYm66eF8aufiVteOgnwxgnRP8GoyMj7VWsgWnQbVKXyge7hqrFh2K2TQM6t1Hw==} + '@babel/helper-compilation-targets@7.28.6': + resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==} engines: {node: '>=6.9.0'} - '@babel/helper-compilation-targets@7.27.2': - resolution: {integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==} + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} engines: {node: '>=6.9.0'} - '@babel/helper-module-imports@7.27.1': - resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==} + '@babel/helper-module-imports@7.28.6': + resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==} engines: {node: '>=6.9.0'} - '@babel/helper-module-transforms@7.27.3': - resolution: {integrity: sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==} + '@babel/helper-module-transforms@7.28.6': + resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 - '@babel/helper-plugin-utils@7.27.1': - resolution: {integrity: sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==} + '@babel/helper-plugin-utils@7.28.6': + resolution: {integrity: sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==} engines: {node: '>=6.9.0'} '@babel/helper-string-parser@7.27.1': resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} engines: {node: '>=6.9.0'} - '@babel/helper-validator-identifier@7.27.1': - resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} engines: {node: '>=6.9.0'} '@babel/helper-validator-option@7.27.1': resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} engines: {node: '>=6.9.0'} - '@babel/helpers@7.27.6': - resolution: {integrity: sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==} + '@babel/helpers@7.28.6': + resolution: {integrity: sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==} engines: {node: '>=6.9.0'} - '@babel/parser@7.27.5': - resolution: {integrity: sha512-OsQd175SxWkGlzbny8J3K8TnnDD0N3lrIUtB92xwyRpzaenGZhxDvxN/JgU00U3CDZNj9tPuDJ5H0WS4Nt3vKg==} + '@babel/parser@7.29.0': + resolution: {integrity: sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==} engines: {node: '>=6.0.0'} hasBin: true @@ -155,8 +161,8 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-syntax-import-attributes@7.27.1': - resolution: {integrity: sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==} + '@babel/plugin-syntax-import-attributes@7.28.6': + resolution: {integrity: sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 @@ -171,8 +177,8 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-syntax-jsx@7.27.1': - resolution: {integrity: sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==} + '@babel/plugin-syntax-jsx@7.28.6': + resolution: {integrity: sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 @@ -219,22 +225,22 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-syntax-typescript@7.27.1': - resolution: {integrity: sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==} + '@babel/plugin-syntax-typescript@7.28.6': + resolution: {integrity: sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/template@7.27.2': - resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} + '@babel/template@7.28.6': + resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} engines: {node: '>=6.9.0'} - '@babel/traverse@7.27.4': - resolution: {integrity: sha512-oNcu2QbHqts9BtOWJosOVJapWjBDSxGCpFvikNR5TGDYDQf3JwpIoMzIKrvfoti93cLfPJEG4tH9SPVeyCGgdA==} + '@babel/traverse@7.29.0': + resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==} engines: {node: '>=6.9.0'} - '@babel/types@7.27.6': - resolution: {integrity: sha512-ETyHEk2VHHvl9b9jZP5IHPavHYk57EhanlRRuae9XCpb/j5bDCbPPMOBfCWhnl/7EDJz0jEMCi/RhccCE8r1+Q==} + '@babel/types@7.29.0': + resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} engines: {node: '>=6.9.0'} '@bcoe/v8-coverage@0.2.3': @@ -244,66 +250,84 @@ packages: resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} - '@eslint-community/eslint-utils@4.7.0': - resolution: {integrity: sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==} + '@emnapi/core@1.8.1': + resolution: {integrity: sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==} + + '@emnapi/runtime@1.8.1': + resolution: {integrity: sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==} + + '@emnapi/wasi-threads@1.1.0': + resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==} + + '@eslint-community/eslint-utils@4.9.1': + resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 - '@eslint-community/regexpp@4.12.1': - resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - '@eslint/config-array@0.20.0': - resolution: {integrity: sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==} + '@eslint/config-array@0.21.1': + resolution: {integrity: sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/config-helpers@0.2.2': - resolution: {integrity: sha512-+GPzk8PlG0sPpzdU5ZvIRMPidzAnZDl/s9L+y13iodqvb8leL53bTannOrQ/Im7UkpsmFU5Ily5U60LWixnmLg==} + '@eslint/config-helpers@0.4.2': + resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/core@0.14.0': - resolution: {integrity: sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==} + '@eslint/core@0.17.0': + resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/eslintrc@3.3.1': - resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==} + '@eslint/eslintrc@3.3.3': + resolution: {integrity: sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/js@9.28.0': - resolution: {integrity: sha512-fnqSjGWd/CoIp4EXIxWVK/sHA6DOHN4+8Ix2cX5ycOY7LG0UY8nHCU5pIp2eaE1Mc7Qd8kHspYNzYXT2ojPLzg==} + '@eslint/js@9.39.2': + resolution: {integrity: sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/object-schema@2.1.6': - resolution: {integrity: sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==} + '@eslint/object-schema@2.1.7': + resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/plugin-kit@0.3.1': - resolution: {integrity: sha512-0J+zgWxHN+xXONWIyPWKFMgVuJoZuGiIFu8yxk7RJjxkzpGmyja5wRFqZIVtjDVOQpV+Rw0iOAjYPE2eQyjr0w==} + '@eslint/plugin-kit@0.4.1': + resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@humanfs/core@0.19.1': resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} engines: {node: '>=18.18.0'} - '@humanfs/node@0.16.6': - resolution: {integrity: sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==} + '@humanfs/node@0.16.7': + resolution: {integrity: sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==} engines: {node: '>=18.18.0'} '@humanwhocodes/module-importer@1.0.1': resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} engines: {node: '>=12.22'} - '@humanwhocodes/retry@0.3.1': - resolution: {integrity: sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==} - engines: {node: '>=18.18'} - '@humanwhocodes/retry@0.4.3': resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} engines: {node: '>=18.18'} - '@inquirer/checkbox@4.1.8': - resolution: {integrity: sha512-d/QAsnwuHX2OPolxvYcgSj7A9DO9H6gVOy2DvBTx+P2LH2iRTo/RSGV3iwCzW024nP9hw98KIuDmdyhZQj1UQg==} + '@inquirer/ansi@1.0.2': + resolution: {integrity: sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==} + engines: {node: '>=18'} + + '@inquirer/checkbox@4.3.2': + resolution: {integrity: sha512-VXukHf0RR1doGe6Sm4F0Em7SWYLTHSsbGfJdS9Ja2bX5/D5uwVOEjr07cncLROdBvmnvCATYEWlHqYmXv2IlQA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/confirm@5.1.21': + resolution: {integrity: sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==} engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' @@ -311,8 +335,8 @@ packages: '@types/node': optional: true - '@inquirer/confirm@5.1.12': - resolution: {integrity: sha512-dpq+ielV9/bqgXRUbNH//KsY6WEw9DrGPmipkpmgC1Y46cwuBTNx7PXFWTjc3MQ+urcc0QxoVHcMI0FW4Ok0hg==} + '@inquirer/core@10.3.2': + resolution: {integrity: sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==} engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' @@ -320,8 +344,8 @@ packages: '@types/node': optional: true - '@inquirer/core@10.1.13': - resolution: {integrity: sha512-1viSxebkYN2nJULlzCxES6G9/stgHSepZ9LqqfdIGPHj5OHhiBUXVS0a6R0bEC2A+VL4D9w6QB66ebCr6HGllA==} + '@inquirer/editor@4.2.23': + resolution: {integrity: sha512-aLSROkEwirotxZ1pBaP8tugXRFCxW94gwrQLxXfrZsKkfjOYC1aRvAZuhpJOb5cu4IBTJdsCigUlf2iCOu4ZDQ==} engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' @@ -329,8 +353,8 @@ packages: '@types/node': optional: true - '@inquirer/editor@4.2.13': - resolution: {integrity: sha512-WbicD9SUQt/K8O5Vyk9iC2ojq5RHoCLK6itpp2fHsWe44VxxcA9z3GTWlvjSTGmMQpZr+lbVmrxdHcumJoLbMA==} + '@inquirer/expand@4.0.23': + resolution: {integrity: sha512-nRzdOyFYnpeYTTR2qFwEVmIWypzdAx/sIkCMeTNTcflFOovfqUk+HcFhQQVBftAh9gmGrpFj6QcGEqrDMDOiew==} engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' @@ -338,8 +362,8 @@ packages: '@types/node': optional: true - '@inquirer/expand@4.0.15': - resolution: {integrity: sha512-4Y+pbr/U9Qcvf+N/goHzPEXiHH8680lM3Dr3Y9h9FFw4gHS+zVpbj8LfbKWIb/jayIB4aSO4pWiBTrBYWkvi5A==} + '@inquirer/external-editor@1.0.3': + resolution: {integrity: sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==} engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' @@ -347,12 +371,12 @@ packages: '@types/node': optional: true - '@inquirer/figures@1.0.12': - resolution: {integrity: sha512-MJttijd8rMFcKJC8NYmprWr6hD3r9Gd9qUC0XwPNwoEPWSMVJwA2MlXxF+nhZZNMY+HXsWa+o7KY2emWYIn0jQ==} + '@inquirer/figures@1.0.15': + resolution: {integrity: sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==} engines: {node: '>=18'} - '@inquirer/input@4.1.12': - resolution: {integrity: sha512-xJ6PFZpDjC+tC1P8ImGprgcsrzQRsUh9aH3IZixm1lAZFK49UGHxM3ltFfuInN2kPYNfyoPRh+tU4ftsjPLKqQ==} + '@inquirer/input@4.3.1': + resolution: {integrity: sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g==} engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' @@ -360,8 +384,8 @@ packages: '@types/node': optional: true - '@inquirer/number@3.0.15': - resolution: {integrity: sha512-xWg+iYfqdhRiM55MvqiTCleHzszpoigUpN5+t1OMcRkJrUrw7va3AzXaxvS+Ak7Gny0j2mFSTv2JJj8sMtbV2g==} + '@inquirer/number@3.0.23': + resolution: {integrity: sha512-5Smv0OK7K0KUzUfYUXDXQc9jrf8OHo4ktlEayFlelCjwMXz0299Y8OrI+lj7i4gCBY15UObk76q0QtxjzFcFcg==} engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' @@ -369,8 +393,8 @@ packages: '@types/node': optional: true - '@inquirer/password@4.0.15': - resolution: {integrity: sha512-75CT2p43DGEnfGTaqFpbDC2p2EEMrq0S+IRrf9iJvYreMy5mAWj087+mdKyLHapUEPLjN10mNvABpGbk8Wdraw==} + '@inquirer/password@4.0.23': + resolution: {integrity: sha512-zREJHjhT5vJBMZX/IUbyI9zVtVfOLiTO66MrF/3GFZYZ7T4YILW5MSkEYHceSii/KtRk+4i3RE7E1CUXA2jHcA==} engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' @@ -378,8 +402,8 @@ packages: '@types/node': optional: true - '@inquirer/prompts@7.5.3': - resolution: {integrity: sha512-8YL0WiV7J86hVAxrh3fE5mDCzcTDe1670unmJRz6ArDgN+DBK1a0+rbnNWp4DUB5rPMwqD5ZP6YHl9KK1mbZRg==} + '@inquirer/prompts@7.10.1': + resolution: {integrity: sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg==} engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' @@ -387,8 +411,8 @@ packages: '@types/node': optional: true - '@inquirer/rawlist@4.1.3': - resolution: {integrity: sha512-7XrV//6kwYumNDSsvJIPeAqa8+p7GJh7H5kRuxirct2cgOcSWwwNGoXDRgpNFbY/MG2vQ4ccIWCi8+IXXyFMZA==} + '@inquirer/rawlist@4.1.11': + resolution: {integrity: sha512-+LLQB8XGr3I5LZN/GuAHo+GpDJegQwuPARLChlMICNdwW7OwV2izlCSCxN6cqpL0sMXmbKbFcItJgdQq5EBXTw==} engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' @@ -396,8 +420,8 @@ packages: '@types/node': optional: true - '@inquirer/search@3.0.15': - resolution: {integrity: sha512-YBMwPxYBrADqyvP4nNItpwkBnGGglAvCLVW8u4pRmmvOsHUtCAUIMbUrLX5B3tFL1/WsLGdQ2HNzkqswMs5Uaw==} + '@inquirer/search@3.2.2': + resolution: {integrity: sha512-p2bvRfENXCZdWF/U2BXvnSI9h+tuA8iNqtUKb9UWbmLYCRQxd8WkvwWvYn+3NgYaNwdUkHytJMGG4MMLucI1kA==} engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' @@ -405,8 +429,8 @@ packages: '@types/node': optional: true - '@inquirer/select@4.2.3': - resolution: {integrity: sha512-OAGhXU0Cvh0PhLz9xTF/kx6g6x+sP+PcyTiLvCrewI99P3BBeexD+VbuwkNDvqGkk3y2h5ZiWLeRP7BFlhkUDg==} + '@inquirer/select@4.4.2': + resolution: {integrity: sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w==} engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' @@ -414,8 +438,8 @@ packages: '@types/node': optional: true - '@inquirer/type@3.0.7': - resolution: {integrity: sha512-PfunHQcjwnju84L+ycmcMKB/pTPIngjUJvfnRhKY6FKPuYXlM4aQCb/nIdTFR6BEhMjFvngzvng/vBAJMZpLSA==} + '@inquirer/type@3.0.10': + resolution: {integrity: sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==} engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' @@ -501,27 +525,28 @@ packages: resolution: {integrity: sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - '@jridgewell/gen-mapping@0.3.8': - resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==} - engines: {node: '>=6.0.0'} + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} '@jridgewell/resolve-uri@3.1.2': resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} - '@jridgewell/set-array@1.2.1': - resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} - engines: {node: '>=6.0.0'} - - '@jridgewell/sourcemap-codec@1.5.0': - resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} - '@jridgewell/trace-mapping@0.3.25': - resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} '@jridgewell/trace-mapping@0.3.9': resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + '@napi-rs/wasm-runtime@1.1.1': + resolution: {integrity: sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==} + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -534,6 +559,106 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} + '@oxc-resolver/binding-android-arm-eabi@11.17.0': + resolution: {integrity: sha512-kVnY21v0GyZ/+LG6EIO48wK3mE79BUuakHUYLIqobO/Qqq4mJsjuYXMSn3JtLcKZpN1HDVit4UHpGJHef1lrlw==} + cpu: [arm] + os: [android] + + '@oxc-resolver/binding-android-arm64@11.17.0': + resolution: {integrity: sha512-Pf8e3XcsK9a8RHInoAtEcrwf2vp7V9bSturyUUYxw9syW6E7cGi7z9+6ADXxm+8KAevVfLA7pfBg8NXTvz/HOw==} + cpu: [arm64] + os: [android] + + '@oxc-resolver/binding-darwin-arm64@11.17.0': + resolution: {integrity: sha512-lVSgKt3biecofXVr8e1hnfX0IYMd4A6VCxmvOmHsFt5Zbmt0lkO4S2ap2bvQwYDYh5ghUNamC7M2L8K6vishhQ==} + cpu: [arm64] + os: [darwin] + + '@oxc-resolver/binding-darwin-x64@11.17.0': + resolution: {integrity: sha512-+/raxVJE1bo7R4fA9Yp0wm3slaCOofTEeUzM01YqEGcRDLHB92WRGjRhagMG2wGlvqFuSiTp81DwSbBVo/g6AQ==} + cpu: [x64] + os: [darwin] + + '@oxc-resolver/binding-freebsd-x64@11.17.0': + resolution: {integrity: sha512-x9Ks56n+n8h0TLhzA6sJXa2tGh3uvMGpBppg6PWf8oF0s5S/3p/J6k1vJJ9lIUtTmenfCQEGKnFokpRP4fLTLg==} + cpu: [x64] + os: [freebsd] + + '@oxc-resolver/binding-linux-arm-gnueabihf@11.17.0': + resolution: {integrity: sha512-Wf3w07Ow9kXVJrS0zmsaFHKOGhXKXE8j1tNyy+qIYDsQWQ4UQZVx5SjlDTcqBnFerlp3Z3Is0RjmVzgoLG3qkA==} + cpu: [arm] + os: [linux] + + '@oxc-resolver/binding-linux-arm-musleabihf@11.17.0': + resolution: {integrity: sha512-N0OKA1al1gQ5Gm7Fui1RWlXaHRNZlwMoBLn3TVtSXX+WbnlZoVyDqqOqFL8+pVEHhhxEA2LR8kmM0JO6FAk6dg==} + cpu: [arm] + os: [linux] + + '@oxc-resolver/binding-linux-arm64-gnu@11.17.0': + resolution: {integrity: sha512-wdcQ7Niad9JpjZIGEeqKJnTvczVunqlZ/C06QzR5zOQNeLVRScQ9S5IesKWUAPsJQDizV+teQX53nTK+Z5Iy+g==} + cpu: [arm64] + os: [linux] + + '@oxc-resolver/binding-linux-arm64-musl@11.17.0': + resolution: {integrity: sha512-65B2/t39HQN5AEhkLsC+9yBD1iRUkKOIhfmJEJ7g6wQ9kylra7JRmNmALFjbsj0VJsoSQkpM8K07kUZuNJ9Kxw==} + cpu: [arm64] + os: [linux] + + '@oxc-resolver/binding-linux-ppc64-gnu@11.17.0': + resolution: {integrity: sha512-kExgm3TLK21dNMmcH+xiYGbc6BUWvT03PUZ2aYn8mUzGPeeORklBhg3iYcaBI3ZQHB25412X1Z6LLYNjt4aIaA==} + cpu: [ppc64] + os: [linux] + + '@oxc-resolver/binding-linux-riscv64-gnu@11.17.0': + resolution: {integrity: sha512-1utUJC714/ydykZQE8c7QhpEyM4SaslMfRXxN9G61KYazr6ndt85LaubK3EZCSD50vVEfF4PVwFysCSO7LN9uA==} + cpu: [riscv64] + os: [linux] + + '@oxc-resolver/binding-linux-riscv64-musl@11.17.0': + resolution: {integrity: sha512-mayiYOl3LMmtO2CLn4I5lhanfxEo0LAqlT/EQyFbu1ZN3RS+Xa7Q3JEM0wBpVIyfO/pqFrjvC5LXw/mHNDEL7A==} + cpu: [riscv64] + os: [linux] + + '@oxc-resolver/binding-linux-s390x-gnu@11.17.0': + resolution: {integrity: sha512-Ow/yI+CrUHxIIhn/Y1sP/xoRKbCC3x9O1giKr3G/pjMe+TCJ5ZmfqVWU61JWwh1naC8X5Xa7uyLnbzyYqPsHfg==} + cpu: [s390x] + os: [linux] + + '@oxc-resolver/binding-linux-x64-gnu@11.17.0': + resolution: {integrity: sha512-Z4J7XlPMQOLPANyu6y3B3V417Md4LKH5bV6bhqgaG99qLHmU5LV2k9ErV14fSqoRc/GU/qOpqMdotxiJqN/YWg==} + cpu: [x64] + os: [linux] + + '@oxc-resolver/binding-linux-x64-musl@11.17.0': + resolution: {integrity: sha512-0effK+8lhzXsgsh0Ny2ngdnTPF30v6QQzVFApJ1Ctk315YgpGkghkelvrLYYgtgeFJFrzwmOJ2nDvCrUFKsS2Q==} + cpu: [x64] + os: [linux] + + '@oxc-resolver/binding-openharmony-arm64@11.17.0': + resolution: {integrity: sha512-kFB48dRUW6RovAICZaxHKdtZe+e94fSTNA2OedXokzMctoU54NPZcv0vUX5PMqyikLIKJBIlW7laQidnAzNrDA==} + cpu: [arm64] + os: [openharmony] + + '@oxc-resolver/binding-wasm32-wasi@11.17.0': + resolution: {integrity: sha512-a3elKSBLPT0OoRPxTkCIIc+4xnOELolEBkPyvdj01a6PSdSmyJ1NExWjWLaXnT6wBMblvKde5RmSwEi3j+jZpg==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@oxc-resolver/binding-win32-arm64-msvc@11.17.0': + resolution: {integrity: sha512-4eszUsSDb9YVx0RtYkPWkxxtSZIOgfeiX//nG5cwRRArg178w4RCqEF1kbKPud9HPrp1rXh7gE4x911OhvTnPg==} + cpu: [arm64] + os: [win32] + + '@oxc-resolver/binding-win32-ia32-msvc@11.17.0': + resolution: {integrity: sha512-t946xTXMmR7yGH0KAe9rB055/X4EPIu93JUvjchl2cizR5QbuwkUV7vLS2BS6x6sfvDoQb6rWYnV1HCci6tBSg==} + cpu: [ia32] + os: [win32] + + '@oxc-resolver/binding-win32-x64-msvc@11.17.0': + resolution: {integrity: sha512-pX6s2kMXLQg+hlqKk5UqOW09iLLxnTkvn8ohpYp2Mhsm2yzDPCx9dyOHiB/CQixLzTkLQgWWJykN4Z3UfRKW4Q==} + cpu: [x64] + os: [win32] + '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} @@ -546,8 +671,8 @@ packages: resolution: {integrity: sha512-YcPQ8a0jwYU9bTdJDpXjMi7Brhkr1mXsXrUJvjqM2mQDgkRiz8jFaQGOdaLxgjtUfQgZhKy/O3cG/YwmgKaxLA==} engines: {node: '>=12.22.0'} - '@pnpm/npm-conf@2.3.1': - resolution: {integrity: sha512-c83qWb22rNRuB0UaVCI0uRPNRr8Z0FWnEIvT47jiHAmOIUHbBOg5XvV7pM5x+rKn9HRpjxquDbXYSXr3fAKFcw==} + '@pnpm/npm-conf@3.0.2': + resolution: {integrity: sha512-h104Kh26rR8tm+a3Qkc5S4VLYint3FE48as7+/5oCEcKR2idC/pF1G6AhIXKI+eHPJa/3J9i5z0Al47IeGHPkA==} engines: {node: '>=12'} '@samverschueren/stream-to-observable@0.3.1': @@ -562,8 +687,8 @@ packages: zen-observable: optional: true - '@sinclair/typebox@0.27.8': - resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} + '@sinclair/typebox@0.27.10': + resolution: {integrity: sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==} '@sindresorhus/merge-streams@2.3.0': resolution: {integrity: sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==} @@ -575,8 +700,8 @@ packages: '@sinonjs/fake-timers@10.3.0': resolution: {integrity: sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==} - '@tsconfig/node10@1.0.11': - resolution: {integrity: sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==} + '@tsconfig/node10@1.0.12': + resolution: {integrity: sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==} '@tsconfig/node12@1.0.11': resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==} @@ -587,6 +712,9 @@ packages: '@tsconfig/node16@1.0.4': resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} + '@tybys/wasm-util@0.10.1': + resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} + '@types/babel__core@7.20.5': resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} @@ -596,11 +724,11 @@ packages: '@types/babel__template@7.4.4': resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} - '@types/babel__traverse@7.20.7': - resolution: {integrity: sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==} + '@types/babel__traverse@7.28.0': + resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} - '@types/estree@1.0.7': - resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==} + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} '@types/fs-extra@11.0.4': resolution: {integrity: sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==} @@ -626,8 +754,8 @@ packages: '@types/jsonfile@6.1.4': resolution: {integrity: sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==} - '@types/node@20.17.58': - resolution: {integrity: sha512-UvxetCgGwZ9HmsgGZ2tpStt6CiFU1bu28ftHWpDyfthsCt7OHXas0C7j0VgO3gBq8UHKI785wXmtcQVhLekcRg==} + '@types/node@20.19.32': + resolution: {integrity: sha512-Ez8QE4DMfhjjTsES9K2dwfV258qBui7qxUsoaixZDiTzbde4U12e1pXGNu/ECsUIOi5/zoCxAQxIhQnaUQ2VvA==} '@types/normalize-package-data@2.4.4': resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} @@ -638,66 +766,66 @@ packages: '@types/yargs-parser@21.0.3': resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} - '@types/yargs@17.0.33': - resolution: {integrity: sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==} + '@types/yargs@17.0.35': + resolution: {integrity: sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==} - '@typescript-eslint/eslint-plugin@8.33.1': - resolution: {integrity: sha512-TDCXj+YxLgtvxvFlAvpoRv9MAncDLBV2oT9Bd7YBGC/b/sEURoOYuIwLI99rjWOfY3QtDzO+mk0n4AmdFExW8A==} + '@typescript-eslint/eslint-plugin@8.54.0': + resolution: {integrity: sha512-hAAP5io/7csFStuOmR782YmTthKBJ9ND3WVL60hcOjvtGFb+HJxH4O5huAcmcZ9v9G8P+JETiZ/G1B8MALnWZQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^8.33.1 + '@typescript-eslint/parser': ^8.54.0 eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <5.9.0' + typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/parser@8.33.1': - resolution: {integrity: sha512-qwxv6dq682yVvgKKp2qWwLgRbscDAYktPptK4JPojCwwi3R9cwrvIxS4lvBpzmcqzR4bdn54Z0IG1uHFskW4dA==} + '@typescript-eslint/parser@8.54.0': + resolution: {integrity: sha512-BtE0k6cjwjLZoZixN0t5AKP0kSzlGu7FctRXYuPAm//aaiZhmfq1JwdYpYr1brzEspYyFeF+8XF5j2VK6oalrA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <5.9.0' + typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/project-service@8.33.1': - resolution: {integrity: sha512-DZR0efeNklDIHHGRpMpR5gJITQpu6tLr9lDJnKdONTC7vvzOlLAG/wcfxcdxEWrbiZApcoBCzXqU/Z458Za5Iw==} + '@typescript-eslint/project-service@8.54.0': + resolution: {integrity: sha512-YPf+rvJ1s7MyiWM4uTRhE4DvBXrEV+d8oC3P9Y2eT7S+HBS0clybdMIPnhiATi9vZOYDc7OQ1L/i6ga6NFYK/g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - typescript: '>=4.8.4 <5.9.0' + typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/scope-manager@8.33.1': - resolution: {integrity: sha512-dM4UBtgmzHR9bS0Rv09JST0RcHYearoEoo3pG5B6GoTR9XcyeqX87FEhPo+5kTvVfKCvfHaHrcgeJQc6mrDKrA==} + '@typescript-eslint/scope-manager@8.54.0': + resolution: {integrity: sha512-27rYVQku26j/PbHYcVfRPonmOlVI6gihHtXFbTdB5sb6qA0wdAQAbyXFVarQ5t4HRojIz64IV90YtsjQSSGlQg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/tsconfig-utils@8.33.1': - resolution: {integrity: sha512-STAQsGYbHCF0/e+ShUQ4EatXQ7ceh3fBCXkNU7/MZVKulrlq1usH7t2FhxvCpuCi5O5oi1vmVaAjrGeL71OK1g==} + '@typescript-eslint/tsconfig-utils@8.54.0': + resolution: {integrity: sha512-dRgOyT2hPk/JwxNMZDsIXDgyl9axdJI3ogZ2XWhBPsnZUv+hPesa5iuhdYt2gzwA9t8RE5ytOJ6xB0moV0Ujvw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - typescript: '>=4.8.4 <5.9.0' + typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/type-utils@8.33.1': - resolution: {integrity: sha512-1cG37d9xOkhlykom55WVwG2QRNC7YXlxMaMzqw2uPeJixBFfKWZgaP/hjAObqMN/u3fr5BrTwTnc31/L9jQ2ww==} + '@typescript-eslint/type-utils@8.54.0': + resolution: {integrity: sha512-hiLguxJWHjjwL6xMBwD903ciAwd7DmK30Y9Axs/etOkftC3ZNN9K44IuRD/EB08amu+Zw6W37x9RecLkOo3pMA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <5.9.0' + typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/types@8.33.1': - resolution: {integrity: sha512-xid1WfizGhy/TKMTwhtVOgalHwPtV8T32MS9MaH50Cwvz6x6YqRIPdD2WvW0XaqOzTV9p5xdLY0h/ZusU5Lokg==} + '@typescript-eslint/types@8.54.0': + resolution: {integrity: sha512-PDUI9R1BVjqu7AUDsRBbKMtwmjWcn4J3le+5LpcFgWULN3LvHC5rkc9gCVxbrsrGmO1jfPybN5s6h4Jy+OnkAA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@8.33.1': - resolution: {integrity: sha512-+s9LYcT8LWjdYWu7IWs7FvUxpQ/DGkdjZeE/GGulHvv8rvYwQvVaUZ6DE+j5x/prADUgSbbCWZ2nPI3usuVeOA==} + '@typescript-eslint/typescript-estree@8.54.0': + resolution: {integrity: sha512-BUwcskRaPvTk6fzVWgDPdUndLjB87KYDrN5EYGetnktoeAvPtO4ONHlAZDnj5VFnUANg0Sjm7j4usBlnoVMHwA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - typescript: '>=4.8.4 <5.9.0' + typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/utils@8.33.1': - resolution: {integrity: sha512-52HaBiEQUaRYqAXpfzWSR2U3gxk92Kw006+xZpElaPMg3C4PgM+A5LqwoQI1f9E5aZ/qlxAZxzm42WX+vn92SQ==} + '@typescript-eslint/utils@8.54.0': + resolution: {integrity: sha512-9Cnda8GS57AQakvRyG0PTejJNlA2xhvyNtEVIMlDWOOeEyBkYWhGPnfrIAnqxLMTSTo6q8g12XVjjev5l1NvMA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <5.9.0' + typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/visitor-keys@8.33.1': - resolution: {integrity: sha512-3i8NrFcZeeDHJ+7ZUuDkGT+UHq+XoFGsymNK2jZCOHcfEzRQ0BdpRtdpSx/Iyf3MHLWIcLS0COuOPibKQboIiQ==} + '@typescript-eslint/visitor-keys@8.54.0': + resolution: {integrity: sha512-VFlhGSl4opC0bprJiItPQ1RfUhGDIBokcPwaFH4yiBCaNPeld/9VeXbiPO1cLyorQi1G1vL+ecBk1x8o1axORA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} acorn-jsx@5.3.2: @@ -709,8 +837,8 @@ packages: resolution: {integrity: sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==} engines: {node: '>=0.4.0'} - acorn@8.14.1: - resolution: {integrity: sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==} + acorn@8.15.0: + resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} engines: {node: '>=0.4.0'} hasBin: true @@ -732,8 +860,8 @@ packages: resolution: {integrity: sha512-5GFMVX8HqE/TB+FuBJGuO5XG0WrsA6ptUqoODaT/n9mmUaZFkqnBueB4leqGBCmrUHnCnC4PCZTCd0E7QQ83bA==} engines: {node: '>=12'} - ansi-escapes@7.0.0: - resolution: {integrity: sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==} + ansi-escapes@7.3.0: + resolution: {integrity: sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==} engines: {node: '>=18'} ansi-regex@2.1.1: @@ -752,8 +880,8 @@ packages: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} - ansi-regex@6.1.0: - resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==} + ansi-regex@6.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} engines: {node: '>=12'} ansi-styles@2.2.1: @@ -772,8 +900,8 @@ packages: resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} engines: {node: '>=10'} - ansi-styles@6.2.1: - resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} engines: {node: '>=12'} any-observable@0.3.0: @@ -804,11 +932,8 @@ packages: argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} - async@3.2.6: - resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} - - atomically@2.0.3: - resolution: {integrity: sha512-kU6FmrwZ3Lx7/7y3hPS5QnbJfaohcIul5fGqf7ok+4KklIEk9tJ0C2IQPdacSbVUWv6zVHXEBWoWd6NrVMT7Cw==} + atomically@2.1.0: + resolution: {integrity: sha512-+gDffFXRW6sl/HCwbta7zK4uNqbPjv4YJEAdz7Vu+FLQHe77eZ4bvbJGi4hE0QPeJlMYMA3piXEr1UL3dAwx7Q==} auto-changelog@2.5.0: resolution: {integrity: sha512-UTnLjT7I9U2U/xkCUH5buDlp8C7g0SGChfib+iDrJkamcj5kaMqNKHNfbKJw1kthJUq8sUo3i3q2S6FzO/l/wA==} @@ -829,10 +954,10 @@ packages: resolution: {integrity: sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - babel-preset-current-node-syntax@1.1.0: - resolution: {integrity: sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw==} + babel-preset-current-node-syntax@1.2.0: + resolution: {integrity: sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==} peerDependencies: - '@babel/core': ^7.0.0 + '@babel/core': ^7.0.0 || ^8.0.0-0 babel-preset-jest@29.6.3: resolution: {integrity: sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==} @@ -843,22 +968,26 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + baseline-browser-mapping@2.9.19: + resolution: {integrity: sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==} + hasBin: true + boxen@8.0.1: resolution: {integrity: sha512-F3PH5k5juxom4xktynS7MoFY+NUWH5LC4CnH11YB8NPew+HLpmBLCybSAEyb2F+4pRXhuhWqFesoQd6DAyc2hw==} engines: {node: '>=18'} - brace-expansion@1.1.11: - resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + brace-expansion@1.1.12: + resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} - brace-expansion@2.0.1: - resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + brace-expansion@2.0.2: + resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} - browserslist@4.25.0: - resolution: {integrity: sha512-PJ8gYKeS5e/whHBh8xrwYK+dAvEj7JXtz6uTucnMRB8OiGTsKccFekoRrjajPBHV8oOY+2tI4uxeceSimKwMFA==} + browserslist@4.28.1: + resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true @@ -892,11 +1021,11 @@ packages: resolution: {integrity: sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==} engines: {node: '>=16'} - caniuse-lite@1.0.30001721: - resolution: {integrity: sha512-cOuvmUVtKrtEaoKiO0rSc29jcjwMwX5tOHDy4MgVFEWiUXj4uBMJkwI8MDySkgXidpMiHUcviogAvFi4pA2hDQ==} + caniuse-lite@1.0.30001769: + resolution: {integrity: sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg==} - chalk-template@1.1.0: - resolution: {integrity: sha512-T2VJbcDuZQ0Tb2EWwSotMPJjgpy1/tGee1BTpUNsGZ/qgNjV2t7Mvu+d4600U564nbLesN1x2dPL+xii174Ekg==} + chalk-template@1.1.2: + resolution: {integrity: sha512-2bxTP2yUH7AJj/VAXfcA+4IcWGdQ87HwBANLt5XxGTeomo8yG0y95N1um9i5StvhT/Bl0/2cARA5v1PpPXUxUA==} engines: {node: '>=14.16'} chalk@1.1.3: @@ -911,8 +1040,8 @@ packages: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} - chalk@5.4.1: - resolution: {integrity: sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==} + chalk@5.6.2: + resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} char-regex@1.0.2: @@ -922,6 +1051,9 @@ packages: chardet@0.7.0: resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} + chardet@2.1.1: + resolution: {integrity: sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==} + ci-info@3.9.0: resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} engines: {node: '>=8'} @@ -976,8 +1108,8 @@ packages: resolution: {integrity: sha512-RpAVKQA5T63xEj6/giIbUEtZwJ4UFIc3ZtvEkiaUERylqe8xb5IvqcgOurZLahv93CLKfxcw5YI+DZcUBRyLXA==} engines: {node: '>=0.10.0'} - collect-v8-coverage@1.0.2: - resolution: {integrity: sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==} + collect-v8-coverage@1.0.3: + resolution: {integrity: sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==} color-convert@1.9.3: resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} @@ -1009,8 +1141,8 @@ packages: config-chain@1.1.13: resolution: {integrity: sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==} - configstore@7.0.0: - resolution: {integrity: sha512-yk7/5PN5im4qwz0WFZW3PXnzHgPu9mX29Y8uZ3aefe2lBPC1FYttWZRcaW9fKkT0pBCJyuQ2HfbmPVaODi9jcQ==} + configstore@7.1.0: + resolution: {integrity: sha512-N4oog6YJWbR9kGyXvS7jEykLDXIE2C0ILYqNBZBp9iwiJpoCBWYsuAdW6PPFn6w06jjnC+3JstVvWHO4cZqvRg==} engines: {node: '>=18'} convert-source-map@2.0.0: @@ -1049,8 +1181,8 @@ packages: date-fns@1.30.1: resolution: {integrity: sha512-hBSVCvSmWC+QypYObzwGOd9wqdDpOt+0wl0KbU+R+uuZBS1jN8VsD1ss3irQDknRj5NvxiTF6oj/nDRnN/UQNw==} - debug@4.4.1: - resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} peerDependencies: supports-color: '*' @@ -1058,8 +1190,8 @@ packages: supports-color: optional: true - dedent@1.6.0: - resolution: {integrity: sha512-F1Z+5UCFpmQUzJa11agbyPVMbpgT/qA3/SKyJ1jyBgm7dUcUEa8v9JwDkerSQXfakBwFljIxhOJqGkjUwZ9FSA==} + dedent@1.7.1: + resolution: {integrity: sha512-9JmrhGZpOlEgOLdQgSm0zxFaYoQon408V1v49aqTWuXENVlnCuY9JBZcXZiCsZQWDjTm5Qf/nIvAy77mXDAjEg==} peerDependencies: babel-plugin-macros: ^3.1.0 peerDependenciesMeta: @@ -1077,20 +1209,20 @@ packages: resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} engines: {node: '>=0.10.0'} - default-browser-id@5.0.0: - resolution: {integrity: sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==} + default-browser-id@5.0.1: + resolution: {integrity: sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==} engines: {node: '>=18'} - default-browser@5.2.1: - resolution: {integrity: sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==} + default-browser@5.5.0: + resolution: {integrity: sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==} engines: {node: '>=18'} define-lazy-prop@3.0.0: resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==} engines: {node: '>=12'} - del@8.0.0: - resolution: {integrity: sha512-R6ep6JJ+eOBZsBr9esiNN1gxFbZE4Q2cULkUSFumGYecAiS6qodDvcPx/sFuWHMNul7DWmrtoEOpYSm7o6tbSA==} + del@8.0.1: + resolution: {integrity: sha512-gPqh0mKTPvaUZGAuHbrBUYKZWBNAeHG7TU3QH5EhVwPMyKvmfJaNXhcD2jTcXsJRRcffuho4vaYweu80dRrMGA==} engines: {node: '>=18'} detect-newline@3.1.0: @@ -1101,8 +1233,8 @@ packages: resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - diff@4.0.2: - resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} + diff@4.0.4: + resolution: {integrity: sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==} engines: {node: '>=0.3.1'} dot-prop@9.0.0: @@ -1112,13 +1244,8 @@ packages: eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} - ejs@3.1.10: - resolution: {integrity: sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==} - engines: {node: '>=0.10.0'} - hasBin: true - - electron-to-chromium@1.5.165: - resolution: {integrity: sha512-naiMx1Z6Nb2TxPU6fiFrUrDTjyPMLdTtaOd2oLmG8zVSg2hCWGkhPyxwk+qRmZ1ytwVqUv0u7ZcDA5+ALhaUtw==} + electron-to-chromium@1.5.286: + resolution: {integrity: sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==} elegant-spinner@1.0.1: resolution: {integrity: sha512-B+ZM+RXvRqQaAmkMlO/oSe5nMUOaUnyfGYCEHoR8wrXsZR2mA0XVibsxV1bvTwxdRWah1PkQqso2EzhILGHtEQ==} @@ -1128,8 +1255,8 @@ packages: resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==} engines: {node: '>=12'} - emoji-regex@10.4.0: - resolution: {integrity: sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==} + emoji-regex@10.6.0: + resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -1145,8 +1272,8 @@ packages: resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} engines: {node: '>=18'} - error-ex@1.3.2: - resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} + error-ex@1.3.4: + resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} @@ -1172,20 +1299,20 @@ packages: resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} engines: {node: '>=12'} - eslint-scope@8.3.0: - resolution: {integrity: sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==} + eslint-scope@8.4.0: + resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} eslint-visitor-keys@3.4.3: resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - eslint-visitor-keys@4.2.0: - resolution: {integrity: sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==} + eslint-visitor-keys@4.2.1: + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - eslint@9.28.0: - resolution: {integrity: sha512-ocgh41VhRlf9+fVpe7QKzwLj9c92fDiqOj8Y3Sd4/ZmVA4Btx4PlUYPq4pp9JDyupkf1upbEXecxL2mwNV7jPQ==} + eslint@9.39.2: + resolution: {integrity: sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} hasBin: true peerDependencies: @@ -1194,8 +1321,8 @@ packages: jiti: optional: true - espree@10.3.0: - resolution: {integrity: sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==} + espree@10.4.0: + resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} esprima@4.0.1: @@ -1203,8 +1330,8 @@ packages: engines: {node: '>=4'} hasBin: true - esquery@1.6.0: - resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} engines: {node: '>=0.10'} esrecurse@4.3.0: @@ -1219,8 +1346,8 @@ packages: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} - eventemitter3@5.0.1: - resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} + eventemitter3@5.0.4: + resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} execa@5.1.1: resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} @@ -1259,12 +1386,24 @@ packages: fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} - fastq@1.19.1: - resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} + fastq@1.20.1: + resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} fb-watchman@2.0.2: resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} + fd-package-json@2.0.0: + resolution: {integrity: sha512-jKmm9YtsNXN789RS/0mSzOC1NUq9mkVd65vbSSVsKdjGvYXBuE4oWe2QOEoFeRmJg+lPuZxpmrfFclNhoRMneQ==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + figures@1.7.0: resolution: {integrity: sha512-UxKlfCRuCBxSXU4C6t9scbDyWZ4VlaFFdojKtzJuSkuOBQ5CNFum+zZXFwHjo+CxBC1t6zlYPgHIgFjL8ggoEQ==} engines: {node: '>=0.10.0'} @@ -1281,9 +1420,6 @@ packages: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} - filelist@1.0.4: - resolution: {integrity: sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==} - fill-range@7.1.1: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} @@ -1311,8 +1447,13 @@ packages: resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} engines: {node: '>=14'} - fs-extra@11.3.0: - resolution: {integrity: sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==} + formatly@0.3.0: + resolution: {integrity: sha512-9XNj/o4wrRFyhSMJOvsuyMwy8aUfBaZ1VrqHVfohyXf0Sw0e+yfKG+xZaY3arGCOMdwFsqObtzVOc1gU9KiT9w==} + engines: {node: '>=18.3.0'} + hasBin: true + + fs-extra@11.3.3: + resolution: {integrity: sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg==} engines: {node: '>=14.14'} fs.realpath@1.0.0: @@ -1334,8 +1475,8 @@ packages: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} - get-east-asian-width@1.3.0: - resolution: {integrity: sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==} + get-east-asian-width@1.4.0: + resolution: {integrity: sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==} engines: {node: '>=18'} get-package-type@0.1.0: @@ -1361,22 +1502,19 @@ packages: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} - glob@10.4.5: - resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} + glob@10.5.0: + resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me hasBin: true glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} - deprecated: Glob versions prior to v9 are no longer supported + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me global-directory@4.0.1: resolution: {integrity: sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==} engines: {node: '>=18'} - globals@11.12.0: - resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} - engines: {node: '>=4'} - globals@14.0.0: resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} engines: {node: '>=18'} @@ -1391,9 +1529,6 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} - graphemer@1.4.0: - resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} - handlebars@4.7.8: resolution: {integrity: sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==} engines: {node: '>=0.4.7'} @@ -1443,6 +1578,10 @@ packages: resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} engines: {node: '>=0.10.0'} + iconv-lite@0.7.2: + resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} + engines: {node: '>=0.10.0'} + ignore-walk@7.0.0: resolution: {integrity: sha512-T4gbf83A4NH95zvhVYZc+qWocBBGlpzUXLPGurJggw/WIOwicfXJChLDP/iBZnN5WqROSu5Bm3hhle4z8a8YGQ==} engines: {node: ^18.17.0 || >=20.5.0} @@ -1480,8 +1619,8 @@ packages: resolution: {integrity: sha512-BYqTHXTGUIvg7t1r4sJNKcbDZkL92nkXA8YtRpbjFHRHGDL/NtUeiBJMeE60kIFN/Mg8ESaWQvftaYMGJzQZCQ==} engines: {node: '>=4'} - index-to-position@1.1.0: - resolution: {integrity: sha512-XPdx9Dq4t9Qk1mTMbWONJqU7boCoumEH7fRET37HX5+khDUl3J2W6PdALxhILYlIYx2amlwYcRPp28p0tSiojg==} + index-to-position@1.2.0: + resolution: {integrity: sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw==} engines: {node: '>=18'} inflight@1.0.6: @@ -1501,8 +1640,8 @@ packages: inquirer-autosubmit-prompt@0.2.0: resolution: {integrity: sha512-mzNrusCk5L6kSzlN0Ioddn8yzrhYNLli+Sn2ZxMuLechMYAzakiFCIULxsxlQb5YKzthLGfrFACcWoAvM7p04Q==} - inquirer@12.6.3: - resolution: {integrity: sha512-eX9beYAjr1MqYsIjx1vAheXsRk1jbZRvHLcBu5nA9wX0rXR1IfCZLnVLp4Ym4mrhqmh7AuANwcdtgQ291fZDfQ==} + inquirer@12.11.1: + resolution: {integrity: sha512-9VF7mrY+3OmsAfjH3yKz/pLbJ5z22E23hENKw3/LNSaA/sAt3v49bDRY+Ygct1xwuKT+U+cBfTzjCPySna69Qw==} engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' @@ -1550,8 +1689,8 @@ packages: resolution: {integrity: sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==} engines: {node: '>=12'} - is-fullwidth-code-point@5.0.0: - resolution: {integrity: sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==} + is-fullwidth-code-point@5.1.0: + resolution: {integrity: sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==} engines: {node: '>=18'} is-generator-fn@2.1.0: @@ -1580,8 +1719,8 @@ packages: resolution: {integrity: sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==} engines: {node: '>=12'} - is-npm@6.0.0: - resolution: {integrity: sha512-JEjxbSmtPSt1c8XTkVrlujcXdKV1/tvuQ7GwKcAlyiVLeYFQ2VHat8xfrDJsIkhCdF/tZ7CiIR3sy141c6+gPQ==} + is-npm@6.1.0: + resolution: {integrity: sha512-O2z4/kNgyjhQwVR1Wpkbfc19JIhggF97NZNCpWTnjH7kVcZMUrnut9XSN7txI7VdyIYk5ZatOq3zvSuWpU8hoA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} is-number@7.0.0: @@ -1658,18 +1797,13 @@ packages: resolution: {integrity: sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==} engines: {node: '>=10'} - istanbul-reports@3.1.7: - resolution: {integrity: sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==} + istanbul-reports@3.2.0: + resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} engines: {node: '>=8'} jackspeak@3.4.3: resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} - jake@10.9.2: - resolution: {integrity: sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==} - engines: {node: '>=10'} - hasBin: true - jest-changed-files@29.7.0: resolution: {integrity: sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -1799,15 +1933,19 @@ packages: node-notifier: optional: true + jiti@2.6.1: + resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} + hasBin: true + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - js-yaml@3.14.1: - resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} + js-yaml@3.14.2: + resolution: {integrity: sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==} hasBin: true - js-yaml@4.1.0: - resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + js-yaml@4.1.1: + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} hasBin: true jsesc@3.1.0: @@ -1832,8 +1970,8 @@ packages: engines: {node: '>=6'} hasBin: true - jsonfile@6.1.0: - resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} + jsonfile@6.2.0: + resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==} keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} @@ -1842,8 +1980,16 @@ packages: resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} engines: {node: '>=6'} - ky@1.8.1: - resolution: {integrity: sha512-7Bp3TpsE+L+TARSnnDpk3xg8Idi8RwSLdj6CMbNWoOARIrGrbuLGusV0dYwbZOm4bB3jHNxSw8Wk/ByDqJEnDw==} + knip@5.83.0: + resolution: {integrity: sha512-FfmaHMntpZB13B1oJQMSs1hTOZxd0TOn+FYB3oWEI02XlxTW3RH4H7d8z5Us3g0ziHCYyl7z0B1xi8ENP3QEKA==} + engines: {node: '>=18.18.0'} + hasBin: true + peerDependencies: + '@types/node': '>=18' + typescript: '>=5.0.4 <7' + + ky@1.14.3: + resolution: {integrity: sha512-9zy9lkjac+TR1c2tG+mkNSVlyOpInnWdSMiue4F+kq8TwJSgv6o8jhLRg8Ho6SnZ9wOYUq/yozts9qQCfk7bIw==} engines: {node: '>=18'} latest-version@9.0.0: @@ -1913,8 +2059,8 @@ packages: lodash.zip@4.2.0: resolution: {integrity: sha512-C7IOaBBK/0gMORRBd8OETNx3kmOkgIWIPvyDpZSCTwUrpYmgZwJkjZeOD8ww4xbOUOs4/attY+pciKvadNfFbg==} - lodash@4.17.21: - resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + lodash@4.17.23: + resolution: {integrity: sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==} log-symbols@1.0.2: resolution: {integrity: sha512-mmPrW0Fh2fxOzdBbFv4g1m6pR72haFLPJ2G5SJEELf1y+iaQrDG6cWCPjy54RHYbZAt7X+ls690Kw62AdWXBzQ==} @@ -1982,10 +2128,6 @@ packages: minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} - minimatch@5.1.6: - resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} - engines: {node: '>=10'} - minimatch@9.0.5: resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} engines: {node: '>=16 || 14 >=14.17'} @@ -2032,8 +2174,8 @@ packages: node-int64@0.4.0: resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} - node-releases@2.0.19: - resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} + node-releases@2.0.27: + resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} normalize-package-data@6.0.2: resolution: {integrity: sha512-V6gygoYb/5EmNI+MEGrWkC+e6+Rr7mTmfHrxDbLzxQogBkgzo76rkok0Am6thgSF7Mv2nLOajAJj5vDJZEFn7g==} @@ -2043,13 +2185,13 @@ packages: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} - np@10.2.0: - resolution: {integrity: sha512-7Pwk8qcsks2c9ETS35aeJSON6uJAbOsx7TwTFzZNUGgH4djT+Yt/p9S7PZuqH5pkcpNUhasne3cDRBzaUtvetg==} + np@10.3.0: + resolution: {integrity: sha512-ERkEM70wpiWxRNwlN3YkpqyE3QGrgKZEiyVvv+Z4Im2mRE9nqCjnS1YFAXVdhGqVP5wpqG8cVc/A2bOJhEYFYQ==} engines: {bun: '>=1', git: '>=2.11.0', node: '>=18', npm: '>=9', pnpm: '>=8', yarn: '>=1.7.0'} hasBin: true - npm-name@8.0.0: - resolution: {integrity: sha512-DIuCGcKYYhASAZW6Xh/tiaGMko8IHOHe0n3zOA7SzTi0Yvy00x8L7sa5yNiZ75Ny58O/KeRtNouy8Ut6gPbKiw==} + npm-name@8.1.0: + resolution: {integrity: sha512-0Fji7beCAW3yHaqfVPLlT8GOSt7IIWZGQshZqosjbUOhMvs7P4r7g0raOSQIUjKBJO7brLBdXsnX2/l/l5vmUw==} engines: {node: '>=18'} npm-run-path@4.0.1: @@ -2087,8 +2229,8 @@ packages: resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} engines: {node: '>=18'} - open@10.1.2: - resolution: {integrity: sha512-cxN6aIDPz6rm8hbebcP7vrQNhvRcveZoJU72Y7vskh4oIm+BZwBECnx5nTmrlres1Qapvx27Qo1Auukpf8PKXw==} + open@10.2.0: + resolution: {integrity: sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==} engines: {node: '>=18'} optionator@0.9.4: @@ -2103,6 +2245,9 @@ packages: resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==} engines: {node: '>=0.10.0'} + oxc-resolver@11.17.0: + resolution: {integrity: sha512-R5P2Tw6th+nQJdNcZGfuppBS/sM0x1EukqYffmlfX2xXLgLGCCPwu4ruEr9Sx29mrpkHgITc130Qps2JR90NdQ==} + p-limit@2.3.0: resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} engines: {node: '>=6'} @@ -2123,8 +2268,8 @@ packages: resolution: {integrity: sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==} engines: {node: '>=6'} - p-map@7.0.3: - resolution: {integrity: sha512-VkndIv2fIB99swvQoA65bm+fsmt6UNdGeIB0oxBs+WhAhdh08QA04JXpI7rbB9r08/nkbysKoya9rtDERYOYMA==} + p-map@7.0.4: + resolution: {integrity: sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==} engines: {node: '>=18'} p-memoize@7.1.1: @@ -2139,6 +2284,10 @@ packages: resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} engines: {node: '>=6'} + package-directory@8.2.0: + resolution: {integrity: sha512-qJSu5Mo6tHmRxCy2KCYYKYgcfBdUpy9dwReaZD/xwf608AUk/MoRtIOWzgDtUeGeC7n/55yC3MI1Q+MbSoektw==} + engines: {node: '>=18'} + package-json-from-dist@1.0.1: resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} @@ -2205,6 +2354,10 @@ packages: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} engines: {node: '>=8.6'} + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + pidtree@0.6.0: resolution: {integrity: sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==} engines: {node: '>=0.10'} @@ -2218,16 +2371,16 @@ packages: resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} engines: {node: '>=8'} - pkg-dir@8.0.0: - resolution: {integrity: sha512-4peoBq4Wks0riS0z8741NVv+/8IiTvqnZAr8QGgtdifrtpdXbNw/FxRS1l6NFqm4EMzuS0EDqNNx4XGaz8cuyQ==} - engines: {node: '>=18'} - prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} - prettier@3.5.3: - resolution: {integrity: sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==} + presentable-error@0.0.1: + resolution: {integrity: sha512-E6rsNU1QNJgB3sjj7OANinGncFKuK+164sLXw1/CqBjj/EkXSoSdHCtWQGBNlREIGLnL7IEUEGa08YFVUbrhVg==} + engines: {node: '>=16'} + + prettier@3.8.1: + resolution: {integrity: sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==} engines: {node: '>=14'} hasBin: true @@ -2246,8 +2399,8 @@ packages: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} - pupa@3.1.0: - resolution: {integrity: sha512-FLpr4flz5xZTSJxSeaheeMKN/EDzMdK7b8PTOC6a5PYFKTucWbdqjgqaEyH0shFiSJrVB1+Qqi4Tk19ccU6Aug==} + pupa@3.3.0: + resolution: {integrity: sha512-LjgDO2zPtoXP2wJpDjZrGdojii1uqO0cnwKoIoUzkfS98HDmbeiGmYiXo3lXeFlq2xvne1QFQhwYXSUCLKtEuA==} engines: {node: '>=12.20'} pure-rand@6.1.0: @@ -2271,8 +2424,8 @@ packages: resolution: {integrity: sha512-9viLL4/n1BJUCT1NXVTdS1jtm80yDEgR5T4yCelII49Mbj0v1rZdKqj7zCiYdbB0CuCgdrvHcNogAKTFPBocFA==} engines: {node: '>=18'} - registry-auth-token@5.1.0: - resolution: {integrity: sha512-GdekYuwLXLxMuFTwAPg5UKGLW/UXzQrZvH/Zj791BQif5T05T0RsaLfHc9q3ZOKi7n+BoprPD9mJ0O0k4xzUlw==} + registry-auth-token@5.1.1: + resolution: {integrity: sha512-P7B4+jq8DeD2nMsAcdfaqHbssgHtZ7Z5+++a5ask90fvmJ8p5je4mOa+wzu+DB4vQ5tdJV/xywY+UnVFeQLV5Q==} engines: {node: '>=14'} registry-url@6.0.1: @@ -2299,8 +2452,8 @@ packages: resolution: {integrity: sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==} engines: {node: '>=10'} - resolve@1.22.10: - resolution: {integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==} + resolve@1.22.11: + resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==} engines: {node: '>= 0.4'} hasBin: true @@ -2327,16 +2480,16 @@ packages: resolution: {integrity: sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==} hasBin: true - run-applescript@7.0.0: - resolution: {integrity: sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==} + run-applescript@7.1.0: + resolution: {integrity: sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==} engines: {node: '>=18'} run-async@2.4.1: resolution: {integrity: sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==} engines: {node: '>=0.12.0'} - run-async@3.0.0: - resolution: {integrity: sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q==} + run-async@4.0.6: + resolution: {integrity: sha512-IoDlSLTs3Yq593mb3ZoKWKXMNu3UpObxhgA/Xuid5p4bbfi2jdY1Hj0m1K+0/tEuQTxIGMhQDqGjKb7RuxGpAQ==} engines: {node: '>=0.12.0'} run-parallel@1.2.0: @@ -2360,8 +2513,8 @@ packages: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true - semver@7.7.2: - resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} + semver@7.7.4: + resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} engines: {node: '>=10'} hasBin: true @@ -2399,10 +2552,14 @@ packages: resolution: {integrity: sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==} engines: {node: '>=12'} - slice-ansi@7.1.0: - resolution: {integrity: sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==} + slice-ansi@7.1.2: + resolution: {integrity: sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==} engines: {node: '>=18'} + smol-toml@1.6.0: + resolution: {integrity: sha512-4zemZi0HvTnYwLfrpk/CF9LOd9Lt87kAt50GnqhMpyF9U3poDAP2+iukq2bZsO/ufegbYehBkqINbsWxj4l4cw==} + engines: {node: '>= 18'} + source-map-support@0.5.13: resolution: {integrity: sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==} @@ -2419,8 +2576,8 @@ packages: spdx-expression-parse@3.0.1: resolution: {integrity: sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==} - spdx-license-ids@3.0.21: - resolution: {integrity: sha512-Bvg/8F5XephndSK3JffaRqdT+gyhfqIPwDHpX80tJrF8QQRYMo8sNMeaZ2Dp5+jhwKnUmIOyFFQfHRkjJm5nXg==} + spdx-license-ids@3.0.22: + resolution: {integrity: sha512-4PRT4nh1EImPbt2jASOKHX7PB7I+e4IWNLvkKFDxNhJlfjbYlleYQh285Z/3mPTHSAK/AvdMmw5BNNuYH8ShgQ==} sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} @@ -2473,8 +2630,8 @@ packages: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} - strip-ansi@7.1.0: - resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} + strip-ansi@7.1.2: + resolution: {integrity: sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==} engines: {node: '>=12'} strip-bom@4.0.0: @@ -2497,8 +2654,15 @@ packages: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} - stubborn-fs@1.2.5: - resolution: {integrity: sha512-H2N9c26eXjzL/S/K+i/RHHcFanE74dptvvjM8iwzwbVcWY/zjBbgRqF3K0DY4+OD+uTTASTBvDoxPDaPN02D7g==} + strip-json-comments@5.0.3: + resolution: {integrity: sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==} + engines: {node: '>=14.16'} + + stubborn-fs@2.0.0: + resolution: {integrity: sha512-Y0AvSwDw8y+nlSNFXMm2g6L51rBGdAQT20J3YSOqxC53Lo3bjWRtr2BKcfYoAf352WYpsZSTURrA0tqhfgudPA==} + + stubborn-utils@1.0.2: + resolution: {integrity: sha512-zOh9jPYI+xrNOyisSelgym4tolKTJCQd5GBhK0+0xJvcYDcwlOoxF/rnFKQ2KRZknXSG9jWAp66fwP6AxN9STg==} supports-color@2.0.0: resolution: {integrity: sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==} @@ -2543,6 +2707,10 @@ packages: through@2.3.8: resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + tmp@0.0.33: resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} engines: {node: '>=0.6.0'} @@ -2557,23 +2725,24 @@ packages: tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} - ts-api-utils@2.1.0: - resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==} + ts-api-utils@2.4.0: + resolution: {integrity: sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==} engines: {node: '>=18.12'} peerDependencies: typescript: '>=4.8.4' - ts-jest@29.3.4: - resolution: {integrity: sha512-Iqbrm8IXOmV+ggWHOTEbjwyCf2xZlUMv5npExksXohL+tk8va4Fjhb+X2+Rt9NBmgO7bJ8WpnMLOwih/DnMlFA==} + ts-jest@29.4.6: + resolution: {integrity: sha512-fSpWtOO/1AjSNQguk43hb/JCo16oJDnMJf3CdEGNkqsEX3t0KX96xvyX1D7PfLCpVoKu4MfVrqUkFyblYoY4lA==} engines: {node: ^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0} hasBin: true peerDependencies: '@babel/core': '>=7.0.0-beta.0 <8' - '@jest/transform': ^29.0.0 - '@jest/types': ^29.0.0 - babel-jest: ^29.0.0 + '@jest/transform': ^29.0.0 || ^30.0.0 + '@jest/types': ^29.0.0 || ^30.0.0 + babel-jest: ^29.0.0 || ^30.0.0 esbuild: '*' - jest: ^29.0.0 + jest: ^29.0.0 || ^30.0.0 + jest-util: ^29.0.0 || ^30.0.0 typescript: '>=4.3 <6' peerDependenciesMeta: '@babel/core': @@ -2586,6 +2755,8 @@ packages: optional: true esbuild: optional: true + jest-util: + optional: true ts-node@10.9.2: resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} @@ -2635,15 +2806,15 @@ packages: resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} engines: {node: '>=16'} - typescript-eslint@8.33.1: - resolution: {integrity: sha512-AgRnV4sKkWOiZ0Kjbnf5ytTJXMUZQ0qhSVdQtDNYLPLnjsATEYhaO94GlRQwi4t4gO8FfjM6NnikHeKjUm8D7A==} + typescript-eslint@8.54.0: + resolution: {integrity: sha512-CKsJ+g53QpsNPqbzUsfKVgd3Lny4yKZ1pP4qN3jdMOg/sisIDLGyDMezycquXLE5JsEU0wp3dGNdzig0/fmSVQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <5.9.0' + typescript: '>=4.8.4 <6.0.0' - typescript@5.8.3: - resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==} + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} engines: {node: '>=14.17'} hasBin: true @@ -2652,8 +2823,8 @@ packages: engines: {node: '>=0.8.0'} hasBin: true - undici-types@6.19.8: - resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} unicorn-magic@0.1.0: resolution: {integrity: sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==} @@ -2667,8 +2838,8 @@ packages: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} engines: {node: '>= 10.0.0'} - update-browserslist-db@1.1.3: - resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==} + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} hasBin: true peerDependencies: browserslist: '>= 4.21.0' @@ -2694,6 +2865,10 @@ packages: resolution: {integrity: sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + walk-up-path@4.0.0: + resolution: {integrity: sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A==} + engines: {node: 20 || >=22} + walker@1.0.8: resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} @@ -2703,8 +2878,8 @@ packages: whatwg-url@5.0.0: resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} - when-exit@2.1.4: - resolution: {integrity: sha512-4rnvd3A1t16PWzrBUcSDZqcAmsUIy4minDXT/CZ8F2mVDgd65i4Aalimgz1aQkRGU0iH5eT5+6Rx2TK8o443Pg==} + when-exit@2.1.5: + resolution: {integrity: sha512-VGkKJ564kzt6Ms1dbgPP/yuIoQCrsFAnRbptpC5wOEsDaNsbCB2bnfnaA8i/vRs5tjUSEOtIuvl9/MyVsvQZCg==} which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} @@ -2738,8 +2913,8 @@ packages: resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} engines: {node: '>=12'} - wrap-ansi@9.0.0: - resolution: {integrity: sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==} + wrap-ansi@9.0.2: + resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} engines: {node: '>=18'} wrappy@1.0.2: @@ -2749,6 +2924,10 @@ packages: resolution: {integrity: sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==} engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + wsl-utils@0.1.0: + resolution: {integrity: sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==} + engines: {node: '>=18'} + xdg-basedir@5.1.0: resolution: {integrity: sha512-GCPAHLvrIH13+c0SuacwvRYj2SxJXQ4kaVTT5xgL3kPrz56XxkF21IGhjSE1+W0aw7gpBWRGXLCPnPby6lSpmQ==} engines: {node: '>=12'} @@ -2760,8 +2939,8 @@ packages: yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} - yaml@2.8.0: - resolution: {integrity: sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==} + yaml@2.8.2: + resolution: {integrity: sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==} engines: {node: '>= 14.6'} hasBin: true @@ -2781,205 +2960,205 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} - yoctocolors-cjs@2.1.2: - resolution: {integrity: sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==} + yoctocolors-cjs@2.1.3: + resolution: {integrity: sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==} engines: {node: '>=18'} - yoctocolors@2.1.1: - resolution: {integrity: sha512-GQHQqAopRhwU8Kt1DDM8NjibDXHC8eoh1erhGAJPEyveY9qqVeXvVikNKrDz69sHowPMorbPUrH/mx8c50eiBQ==} + yoctocolors@2.1.2: + resolution: {integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==} engines: {node: '>=18'} -snapshots: + zod@4.3.6: + resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} - '@ampproject/remapping@2.3.0': - dependencies: - '@jridgewell/gen-mapping': 0.3.8 - '@jridgewell/trace-mapping': 0.3.25 +snapshots: - '@babel/code-frame@7.27.1': + '@babel/code-frame@7.29.0': dependencies: - '@babel/helper-validator-identifier': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 js-tokens: 4.0.0 picocolors: 1.1.1 - '@babel/compat-data@7.27.5': {} + '@babel/compat-data@7.29.0': {} - '@babel/core@7.27.4': + '@babel/core@7.29.0': dependencies: - '@ampproject/remapping': 2.3.0 - '@babel/code-frame': 7.27.1 - '@babel/generator': 7.27.5 - '@babel/helper-compilation-targets': 7.27.2 - '@babel/helper-module-transforms': 7.27.3(@babel/core@7.27.4) - '@babel/helpers': 7.27.6 - '@babel/parser': 7.27.5 - '@babel/template': 7.27.2 - '@babel/traverse': 7.27.4 - '@babel/types': 7.27.6 + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) + '@babel/helpers': 7.28.6 + '@babel/parser': 7.29.0 + '@babel/template': 7.28.6 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + '@jridgewell/remapping': 2.3.5 convert-source-map: 2.0.0 - debug: 4.4.1 + debug: 4.4.3 gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 transitivePeerDependencies: - supports-color - '@babel/generator@7.27.5': + '@babel/generator@7.29.1': dependencies: - '@babel/parser': 7.27.5 - '@babel/types': 7.27.6 - '@jridgewell/gen-mapping': 0.3.8 - '@jridgewell/trace-mapping': 0.3.25 + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 jsesc: 3.1.0 - '@babel/helper-compilation-targets@7.27.2': + '@babel/helper-compilation-targets@7.28.6': dependencies: - '@babel/compat-data': 7.27.5 + '@babel/compat-data': 7.29.0 '@babel/helper-validator-option': 7.27.1 - browserslist: 4.25.0 + browserslist: 4.28.1 lru-cache: 5.1.1 semver: 6.3.1 - '@babel/helper-module-imports@7.27.1': + '@babel/helper-globals@7.28.0': {} + + '@babel/helper-module-imports@7.28.6': dependencies: - '@babel/traverse': 7.27.4 - '@babel/types': 7.27.6 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 transitivePeerDependencies: - supports-color - '@babel/helper-module-transforms@7.27.3(@babel/core@7.27.4)': + '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.27.4 - '@babel/helper-module-imports': 7.27.1 - '@babel/helper-validator-identifier': 7.27.1 - '@babel/traverse': 7.27.4 + '@babel/core': 7.29.0 + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.29.0 transitivePeerDependencies: - supports-color - '@babel/helper-plugin-utils@7.27.1': {} + '@babel/helper-plugin-utils@7.28.6': {} '@babel/helper-string-parser@7.27.1': {} - '@babel/helper-validator-identifier@7.27.1': {} + '@babel/helper-validator-identifier@7.28.5': {} '@babel/helper-validator-option@7.27.1': {} - '@babel/helpers@7.27.6': + '@babel/helpers@7.28.6': dependencies: - '@babel/template': 7.27.2 - '@babel/types': 7.27.6 + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 - '@babel/parser@7.27.5': + '@babel/parser@7.29.0': dependencies: - '@babel/types': 7.27.6 + '@babel/types': 7.29.0 - '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.27.4)': + '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.27.4 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.27.4)': + '@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.27.4 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.27.4)': + '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.27.4 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.27.4)': + '@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.27.4 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-import-attributes@7.27.1(@babel/core@7.27.4)': + '@babel/plugin-syntax-import-attributes@7.28.6(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.27.4 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.27.4)': + '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.27.4 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.27.4)': + '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.27.4 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-jsx@7.27.1(@babel/core@7.27.4)': + '@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.27.4 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.27.4)': + '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.27.4 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.27.4)': + '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.27.4 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.27.4)': + '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.27.4 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.27.4)': + '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.27.4 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.27.4)': + '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.27.4 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.27.4)': + '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.27.4 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.27.4)': + '@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.27.4 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.27.4)': + '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.27.4 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-typescript@7.27.1(@babel/core@7.27.4)': + '@babel/plugin-syntax-typescript@7.28.6(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.27.4 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 - '@babel/template@7.27.2': + '@babel/template@7.28.6': dependencies: - '@babel/code-frame': 7.27.1 - '@babel/parser': 7.27.5 - '@babel/types': 7.27.6 + '@babel/code-frame': 7.29.0 + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 - '@babel/traverse@7.27.4': + '@babel/traverse@7.29.0': dependencies: - '@babel/code-frame': 7.27.1 - '@babel/generator': 7.27.5 - '@babel/parser': 7.27.5 - '@babel/template': 7.27.2 - '@babel/types': 7.27.6 - debug: 4.4.1 - globals: 11.12.0 + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.29.0 + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + debug: 4.4.3 transitivePeerDependencies: - supports-color - '@babel/types@7.27.6': + '@babel/types@7.29.0': dependencies: '@babel/helper-string-parser': 7.27.1 - '@babel/helper-validator-identifier': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 '@bcoe/v8-coverage@0.2.3': {} @@ -2987,184 +3166,209 @@ snapshots: dependencies: '@jridgewell/trace-mapping': 0.3.9 - '@eslint-community/eslint-utils@4.7.0(eslint@9.28.0)': + '@emnapi/core@1.8.1': + dependencies: + '@emnapi/wasi-threads': 1.1.0 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.8.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.1.0': dependencies: - eslint: 9.28.0 + tslib: 2.8.1 + optional: true + + '@eslint-community/eslint-utils@4.9.1(eslint@9.39.2(jiti@2.6.1))': + dependencies: + eslint: 9.39.2(jiti@2.6.1) eslint-visitor-keys: 3.4.3 - '@eslint-community/regexpp@4.12.1': {} + '@eslint-community/regexpp@4.12.2': {} - '@eslint/config-array@0.20.0': + '@eslint/config-array@0.21.1': dependencies: - '@eslint/object-schema': 2.1.6 - debug: 4.4.1 + '@eslint/object-schema': 2.1.7 + debug: 4.4.3 minimatch: 3.1.2 transitivePeerDependencies: - supports-color - '@eslint/config-helpers@0.2.2': {} + '@eslint/config-helpers@0.4.2': + dependencies: + '@eslint/core': 0.17.0 - '@eslint/core@0.14.0': + '@eslint/core@0.17.0': dependencies: '@types/json-schema': 7.0.15 - '@eslint/eslintrc@3.3.1': + '@eslint/eslintrc@3.3.3': dependencies: ajv: 6.12.6 - debug: 4.4.1 - espree: 10.3.0 + debug: 4.4.3 + espree: 10.4.0 globals: 14.0.0 ignore: 5.3.2 import-fresh: 3.3.1 - js-yaml: 4.1.0 + js-yaml: 4.1.1 minimatch: 3.1.2 strip-json-comments: 3.1.1 transitivePeerDependencies: - supports-color - '@eslint/js@9.28.0': {} + '@eslint/js@9.39.2': {} - '@eslint/object-schema@2.1.6': {} + '@eslint/object-schema@2.1.7': {} - '@eslint/plugin-kit@0.3.1': + '@eslint/plugin-kit@0.4.1': dependencies: - '@eslint/core': 0.14.0 + '@eslint/core': 0.17.0 levn: 0.4.1 '@humanfs/core@0.19.1': {} - '@humanfs/node@0.16.6': + '@humanfs/node@0.16.7': dependencies: '@humanfs/core': 0.19.1 - '@humanwhocodes/retry': 0.3.1 + '@humanwhocodes/retry': 0.4.3 '@humanwhocodes/module-importer@1.0.1': {} - '@humanwhocodes/retry@0.3.1': {} - '@humanwhocodes/retry@0.4.3': {} - '@inquirer/checkbox@4.1.8(@types/node@20.17.58)': + '@inquirer/ansi@1.0.2': {} + + '@inquirer/checkbox@4.3.2(@types/node@20.19.32)': dependencies: - '@inquirer/core': 10.1.13(@types/node@20.17.58) - '@inquirer/figures': 1.0.12 - '@inquirer/type': 3.0.7(@types/node@20.17.58) - ansi-escapes: 4.3.2 - yoctocolors-cjs: 2.1.2 + '@inquirer/ansi': 1.0.2 + '@inquirer/core': 10.3.2(@types/node@20.19.32) + '@inquirer/figures': 1.0.15 + '@inquirer/type': 3.0.10(@types/node@20.19.32) + yoctocolors-cjs: 2.1.3 optionalDependencies: - '@types/node': 20.17.58 + '@types/node': 20.19.32 - '@inquirer/confirm@5.1.12(@types/node@20.17.58)': + '@inquirer/confirm@5.1.21(@types/node@20.19.32)': dependencies: - '@inquirer/core': 10.1.13(@types/node@20.17.58) - '@inquirer/type': 3.0.7(@types/node@20.17.58) + '@inquirer/core': 10.3.2(@types/node@20.19.32) + '@inquirer/type': 3.0.10(@types/node@20.19.32) optionalDependencies: - '@types/node': 20.17.58 + '@types/node': 20.19.32 - '@inquirer/core@10.1.13(@types/node@20.17.58)': + '@inquirer/core@10.3.2(@types/node@20.19.32)': dependencies: - '@inquirer/figures': 1.0.12 - '@inquirer/type': 3.0.7(@types/node@20.17.58) - ansi-escapes: 4.3.2 + '@inquirer/ansi': 1.0.2 + '@inquirer/figures': 1.0.15 + '@inquirer/type': 3.0.10(@types/node@20.19.32) cli-width: 4.1.0 mute-stream: 2.0.0 signal-exit: 4.1.0 wrap-ansi: 6.2.0 - yoctocolors-cjs: 2.1.2 + yoctocolors-cjs: 2.1.3 optionalDependencies: - '@types/node': 20.17.58 + '@types/node': 20.19.32 - '@inquirer/editor@4.2.13(@types/node@20.17.58)': + '@inquirer/editor@4.2.23(@types/node@20.19.32)': dependencies: - '@inquirer/core': 10.1.13(@types/node@20.17.58) - '@inquirer/type': 3.0.7(@types/node@20.17.58) - external-editor: 3.1.0 + '@inquirer/core': 10.3.2(@types/node@20.19.32) + '@inquirer/external-editor': 1.0.3(@types/node@20.19.32) + '@inquirer/type': 3.0.10(@types/node@20.19.32) optionalDependencies: - '@types/node': 20.17.58 + '@types/node': 20.19.32 - '@inquirer/expand@4.0.15(@types/node@20.17.58)': + '@inquirer/expand@4.0.23(@types/node@20.19.32)': dependencies: - '@inquirer/core': 10.1.13(@types/node@20.17.58) - '@inquirer/type': 3.0.7(@types/node@20.17.58) - yoctocolors-cjs: 2.1.2 + '@inquirer/core': 10.3.2(@types/node@20.19.32) + '@inquirer/type': 3.0.10(@types/node@20.19.32) + yoctocolors-cjs: 2.1.3 optionalDependencies: - '@types/node': 20.17.58 + '@types/node': 20.19.32 - '@inquirer/figures@1.0.12': {} + '@inquirer/external-editor@1.0.3(@types/node@20.19.32)': + dependencies: + chardet: 2.1.1 + iconv-lite: 0.7.2 + optionalDependencies: + '@types/node': 20.19.32 + + '@inquirer/figures@1.0.15': {} - '@inquirer/input@4.1.12(@types/node@20.17.58)': + '@inquirer/input@4.3.1(@types/node@20.19.32)': dependencies: - '@inquirer/core': 10.1.13(@types/node@20.17.58) - '@inquirer/type': 3.0.7(@types/node@20.17.58) + '@inquirer/core': 10.3.2(@types/node@20.19.32) + '@inquirer/type': 3.0.10(@types/node@20.19.32) optionalDependencies: - '@types/node': 20.17.58 + '@types/node': 20.19.32 - '@inquirer/number@3.0.15(@types/node@20.17.58)': + '@inquirer/number@3.0.23(@types/node@20.19.32)': dependencies: - '@inquirer/core': 10.1.13(@types/node@20.17.58) - '@inquirer/type': 3.0.7(@types/node@20.17.58) + '@inquirer/core': 10.3.2(@types/node@20.19.32) + '@inquirer/type': 3.0.10(@types/node@20.19.32) optionalDependencies: - '@types/node': 20.17.58 + '@types/node': 20.19.32 - '@inquirer/password@4.0.15(@types/node@20.17.58)': + '@inquirer/password@4.0.23(@types/node@20.19.32)': dependencies: - '@inquirer/core': 10.1.13(@types/node@20.17.58) - '@inquirer/type': 3.0.7(@types/node@20.17.58) - ansi-escapes: 4.3.2 + '@inquirer/ansi': 1.0.2 + '@inquirer/core': 10.3.2(@types/node@20.19.32) + '@inquirer/type': 3.0.10(@types/node@20.19.32) optionalDependencies: - '@types/node': 20.17.58 - - '@inquirer/prompts@7.5.3(@types/node@20.17.58)': - dependencies: - '@inquirer/checkbox': 4.1.8(@types/node@20.17.58) - '@inquirer/confirm': 5.1.12(@types/node@20.17.58) - '@inquirer/editor': 4.2.13(@types/node@20.17.58) - '@inquirer/expand': 4.0.15(@types/node@20.17.58) - '@inquirer/input': 4.1.12(@types/node@20.17.58) - '@inquirer/number': 3.0.15(@types/node@20.17.58) - '@inquirer/password': 4.0.15(@types/node@20.17.58) - '@inquirer/rawlist': 4.1.3(@types/node@20.17.58) - '@inquirer/search': 3.0.15(@types/node@20.17.58) - '@inquirer/select': 4.2.3(@types/node@20.17.58) + '@types/node': 20.19.32 + + '@inquirer/prompts@7.10.1(@types/node@20.19.32)': + dependencies: + '@inquirer/checkbox': 4.3.2(@types/node@20.19.32) + '@inquirer/confirm': 5.1.21(@types/node@20.19.32) + '@inquirer/editor': 4.2.23(@types/node@20.19.32) + '@inquirer/expand': 4.0.23(@types/node@20.19.32) + '@inquirer/input': 4.3.1(@types/node@20.19.32) + '@inquirer/number': 3.0.23(@types/node@20.19.32) + '@inquirer/password': 4.0.23(@types/node@20.19.32) + '@inquirer/rawlist': 4.1.11(@types/node@20.19.32) + '@inquirer/search': 3.2.2(@types/node@20.19.32) + '@inquirer/select': 4.4.2(@types/node@20.19.32) optionalDependencies: - '@types/node': 20.17.58 + '@types/node': 20.19.32 - '@inquirer/rawlist@4.1.3(@types/node@20.17.58)': + '@inquirer/rawlist@4.1.11(@types/node@20.19.32)': dependencies: - '@inquirer/core': 10.1.13(@types/node@20.17.58) - '@inquirer/type': 3.0.7(@types/node@20.17.58) - yoctocolors-cjs: 2.1.2 + '@inquirer/core': 10.3.2(@types/node@20.19.32) + '@inquirer/type': 3.0.10(@types/node@20.19.32) + yoctocolors-cjs: 2.1.3 optionalDependencies: - '@types/node': 20.17.58 + '@types/node': 20.19.32 - '@inquirer/search@3.0.15(@types/node@20.17.58)': + '@inquirer/search@3.2.2(@types/node@20.19.32)': dependencies: - '@inquirer/core': 10.1.13(@types/node@20.17.58) - '@inquirer/figures': 1.0.12 - '@inquirer/type': 3.0.7(@types/node@20.17.58) - yoctocolors-cjs: 2.1.2 + '@inquirer/core': 10.3.2(@types/node@20.19.32) + '@inquirer/figures': 1.0.15 + '@inquirer/type': 3.0.10(@types/node@20.19.32) + yoctocolors-cjs: 2.1.3 optionalDependencies: - '@types/node': 20.17.58 + '@types/node': 20.19.32 - '@inquirer/select@4.2.3(@types/node@20.17.58)': + '@inquirer/select@4.4.2(@types/node@20.19.32)': dependencies: - '@inquirer/core': 10.1.13(@types/node@20.17.58) - '@inquirer/figures': 1.0.12 - '@inquirer/type': 3.0.7(@types/node@20.17.58) - ansi-escapes: 4.3.2 - yoctocolors-cjs: 2.1.2 + '@inquirer/ansi': 1.0.2 + '@inquirer/core': 10.3.2(@types/node@20.19.32) + '@inquirer/figures': 1.0.15 + '@inquirer/type': 3.0.10(@types/node@20.19.32) + yoctocolors-cjs: 2.1.3 optionalDependencies: - '@types/node': 20.17.58 + '@types/node': 20.19.32 - '@inquirer/type@3.0.7(@types/node@20.17.58)': + '@inquirer/type@3.0.10(@types/node@20.19.32)': optionalDependencies: - '@types/node': 20.17.58 + '@types/node': 20.19.32 '@isaacs/cliui@8.0.2': dependencies: string-width: 5.1.2 string-width-cjs: string-width@4.2.3 - strip-ansi: 7.1.0 + strip-ansi: 7.1.2 strip-ansi-cjs: strip-ansi@6.0.1 wrap-ansi: 8.1.0 wrap-ansi-cjs: wrap-ansi@7.0.0 @@ -3174,7 +3378,7 @@ snapshots: camelcase: 5.3.1 find-up: 4.1.0 get-package-type: 0.1.0 - js-yaml: 3.14.1 + js-yaml: 3.14.2 resolve-from: 5.0.0 '@istanbuljs/schema@0.1.3': {} @@ -3182,27 +3386,27 @@ snapshots: '@jest/console@29.7.0': dependencies: '@jest/types': 29.6.3 - '@types/node': 20.17.58 + '@types/node': 20.19.32 chalk: 4.1.2 jest-message-util: 29.7.0 jest-util: 29.7.0 slash: 3.0.0 - '@jest/core@29.7.0(ts-node@10.9.2(@types/node@20.17.58)(typescript@5.8.3))': + '@jest/core@29.7.0(ts-node@10.9.2(@types/node@20.19.32)(typescript@5.9.3))': dependencies: '@jest/console': 29.7.0 '@jest/reporters': 29.7.0 '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.17.58 + '@types/node': 20.19.32 ansi-escapes: 4.3.2 chalk: 4.1.2 ci-info: 3.9.0 exit: 0.1.2 graceful-fs: 4.2.11 jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@20.17.58)(ts-node@10.9.2(@types/node@20.17.58)(typescript@5.8.3)) + jest-config: 29.7.0(@types/node@20.19.32)(ts-node@10.9.2(@types/node@20.19.32)(typescript@5.9.3)) jest-haste-map: 29.7.0 jest-message-util: 29.7.0 jest-regex-util: 29.6.3 @@ -3227,7 +3431,7 @@ snapshots: dependencies: '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.17.58 + '@types/node': 20.19.32 jest-mock: 29.7.0 '@jest/expect-utils@29.7.0': @@ -3245,7 +3449,7 @@ snapshots: dependencies: '@jest/types': 29.6.3 '@sinonjs/fake-timers': 10.3.0 - '@types/node': 20.17.58 + '@types/node': 20.19.32 jest-message-util: 29.7.0 jest-mock: 29.7.0 jest-util: 29.7.0 @@ -3266,10 +3470,10 @@ snapshots: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@jridgewell/trace-mapping': 0.3.25 - '@types/node': 20.17.58 + '@jridgewell/trace-mapping': 0.3.31 + '@types/node': 20.19.32 chalk: 4.1.2 - collect-v8-coverage: 1.0.2 + collect-v8-coverage: 1.0.3 exit: 0.1.2 glob: 7.2.3 graceful-fs: 4.2.11 @@ -3277,7 +3481,7 @@ snapshots: istanbul-lib-instrument: 6.0.3 istanbul-lib-report: 3.0.1 istanbul-lib-source-maps: 4.0.1 - istanbul-reports: 3.1.7 + istanbul-reports: 3.2.0 jest-message-util: 29.7.0 jest-util: 29.7.0 jest-worker: 29.7.0 @@ -3290,11 +3494,11 @@ snapshots: '@jest/schemas@29.6.3': dependencies: - '@sinclair/typebox': 0.27.8 + '@sinclair/typebox': 0.27.10 '@jest/source-map@29.6.3': dependencies: - '@jridgewell/trace-mapping': 0.3.25 + '@jridgewell/trace-mapping': 0.3.31 callsites: 3.1.0 graceful-fs: 4.2.11 @@ -3303,7 +3507,7 @@ snapshots: '@jest/console': 29.7.0 '@jest/types': 29.6.3 '@types/istanbul-lib-coverage': 2.0.6 - collect-v8-coverage: 1.0.2 + collect-v8-coverage: 1.0.3 '@jest/test-sequencer@29.7.0': dependencies: @@ -3314,9 +3518,9 @@ snapshots: '@jest/transform@29.7.0': dependencies: - '@babel/core': 7.27.4 + '@babel/core': 7.29.0 '@jest/types': 29.6.3 - '@jridgewell/trace-mapping': 0.3.25 + '@jridgewell/trace-mapping': 0.3.31 babel-plugin-istanbul: 6.1.1 chalk: 4.1.2 convert-source-map: 2.0.0 @@ -3337,31 +3541,40 @@ snapshots: '@jest/schemas': 29.6.3 '@types/istanbul-lib-coverage': 2.0.6 '@types/istanbul-reports': 3.0.4 - '@types/node': 20.17.58 - '@types/yargs': 17.0.33 + '@types/node': 20.19.32 + '@types/yargs': 17.0.35 chalk: 4.1.2 - '@jridgewell/gen-mapping@0.3.8': + '@jridgewell/gen-mapping@0.3.13': dependencies: - '@jridgewell/set-array': 1.2.1 - '@jridgewell/sourcemap-codec': 1.5.0 - '@jridgewell/trace-mapping': 0.3.25 + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 - '@jridgewell/resolve-uri@3.1.2': {} + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 - '@jridgewell/set-array@1.2.1': {} + '@jridgewell/resolve-uri@3.1.2': {} - '@jridgewell/sourcemap-codec@1.5.0': {} + '@jridgewell/sourcemap-codec@1.5.5': {} - '@jridgewell/trace-mapping@0.3.25': + '@jridgewell/trace-mapping@0.3.31': dependencies: '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/sourcemap-codec': 1.5.5 '@jridgewell/trace-mapping@0.3.9': dependencies: '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@napi-rs/wasm-runtime@1.1.1': + dependencies: + '@emnapi/core': 1.8.1 + '@emnapi/runtime': 1.8.1 + '@tybys/wasm-util': 0.10.1 + optional: true '@nodelib/fs.scandir@2.1.5': dependencies: @@ -3373,7 +3586,69 @@ snapshots: '@nodelib/fs.walk@1.2.8': dependencies: '@nodelib/fs.scandir': 2.1.5 - fastq: 1.19.1 + fastq: 1.20.1 + + '@oxc-resolver/binding-android-arm-eabi@11.17.0': + optional: true + + '@oxc-resolver/binding-android-arm64@11.17.0': + optional: true + + '@oxc-resolver/binding-darwin-arm64@11.17.0': + optional: true + + '@oxc-resolver/binding-darwin-x64@11.17.0': + optional: true + + '@oxc-resolver/binding-freebsd-x64@11.17.0': + optional: true + + '@oxc-resolver/binding-linux-arm-gnueabihf@11.17.0': + optional: true + + '@oxc-resolver/binding-linux-arm-musleabihf@11.17.0': + optional: true + + '@oxc-resolver/binding-linux-arm64-gnu@11.17.0': + optional: true + + '@oxc-resolver/binding-linux-arm64-musl@11.17.0': + optional: true + + '@oxc-resolver/binding-linux-ppc64-gnu@11.17.0': + optional: true + + '@oxc-resolver/binding-linux-riscv64-gnu@11.17.0': + optional: true + + '@oxc-resolver/binding-linux-riscv64-musl@11.17.0': + optional: true + + '@oxc-resolver/binding-linux-s390x-gnu@11.17.0': + optional: true + + '@oxc-resolver/binding-linux-x64-gnu@11.17.0': + optional: true + + '@oxc-resolver/binding-linux-x64-musl@11.17.0': + optional: true + + '@oxc-resolver/binding-openharmony-arm64@11.17.0': + optional: true + + '@oxc-resolver/binding-wasm32-wasi@11.17.0': + dependencies: + '@napi-rs/wasm-runtime': 1.1.1 + optional: true + + '@oxc-resolver/binding-win32-arm64-msvc@11.17.0': + optional: true + + '@oxc-resolver/binding-win32-ia32-msvc@11.17.0': + optional: true + + '@oxc-resolver/binding-win32-x64-msvc@11.17.0': + optional: true '@pkgjs/parseargs@0.11.0': optional: true @@ -3384,7 +3659,7 @@ snapshots: dependencies: graceful-fs: 4.2.10 - '@pnpm/npm-conf@2.3.1': + '@pnpm/npm-conf@3.0.2': dependencies: '@pnpm/config.env-replace': 1.1.0 '@pnpm/network.ca-file': 1.0.2 @@ -3398,7 +3673,7 @@ snapshots: transitivePeerDependencies: - zenObservable - '@sinclair/typebox@0.27.8': {} + '@sinclair/typebox@0.27.10': {} '@sindresorhus/merge-streams@2.3.0': {} @@ -3410,7 +3685,7 @@ snapshots: dependencies: '@sinonjs/commons': 3.0.1 - '@tsconfig/node10@1.0.11': {} + '@tsconfig/node10@1.0.12': {} '@tsconfig/node12@1.0.11': {} @@ -3418,37 +3693,42 @@ snapshots: '@tsconfig/node16@1.0.4': {} + '@tybys/wasm-util@0.10.1': + dependencies: + tslib: 2.8.1 + optional: true + '@types/babel__core@7.20.5': dependencies: - '@babel/parser': 7.27.5 - '@babel/types': 7.27.6 + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 '@types/babel__generator': 7.27.0 '@types/babel__template': 7.4.4 - '@types/babel__traverse': 7.20.7 + '@types/babel__traverse': 7.28.0 '@types/babel__generator@7.27.0': dependencies: - '@babel/types': 7.27.6 + '@babel/types': 7.29.0 '@types/babel__template@7.4.4': dependencies: - '@babel/parser': 7.27.5 - '@babel/types': 7.27.6 + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 - '@types/babel__traverse@7.20.7': + '@types/babel__traverse@7.28.0': dependencies: - '@babel/types': 7.27.6 + '@babel/types': 7.29.0 - '@types/estree@1.0.7': {} + '@types/estree@1.0.8': {} '@types/fs-extra@11.0.4': dependencies: '@types/jsonfile': 6.1.4 - '@types/node': 20.17.58 + '@types/node': 20.19.32 '@types/graceful-fs@4.1.9': dependencies: - '@types/node': 20.17.58 + '@types/node': 20.19.32 '@types/istanbul-lib-coverage@2.0.6': {} @@ -3469,11 +3749,11 @@ snapshots: '@types/jsonfile@6.1.4': dependencies: - '@types/node': 20.17.58 + '@types/node': 20.19.32 - '@types/node@20.17.58': + '@types/node@20.19.32': dependencies: - undici-types: 6.19.8 + undici-types: 6.21.0 '@types/normalize-package-data@2.4.4': {} @@ -3481,111 +3761,110 @@ snapshots: '@types/yargs-parser@21.0.3': {} - '@types/yargs@17.0.33': + '@types/yargs@17.0.35': dependencies: '@types/yargs-parser': 21.0.3 - '@typescript-eslint/eslint-plugin@8.33.1(@typescript-eslint/parser@8.33.1(eslint@9.28.0)(typescript@5.8.3))(eslint@9.28.0)(typescript@5.8.3)': + '@typescript-eslint/eslint-plugin@8.54.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@eslint-community/regexpp': 4.12.1 - '@typescript-eslint/parser': 8.33.1(eslint@9.28.0)(typescript@5.8.3) - '@typescript-eslint/scope-manager': 8.33.1 - '@typescript-eslint/type-utils': 8.33.1(eslint@9.28.0)(typescript@5.8.3) - '@typescript-eslint/utils': 8.33.1(eslint@9.28.0)(typescript@5.8.3) - '@typescript-eslint/visitor-keys': 8.33.1 - eslint: 9.28.0 - graphemer: 1.4.0 + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.54.0 + '@typescript-eslint/type-utils': 8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.54.0 + eslint: 9.39.2(jiti@2.6.1) ignore: 7.0.5 natural-compare: 1.4.0 - ts-api-utils: 2.1.0(typescript@5.8.3) - typescript: 5.8.3 + ts-api-utils: 2.4.0(typescript@5.9.3) + typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.33.1(eslint@9.28.0)(typescript@5.8.3)': + '@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@typescript-eslint/scope-manager': 8.33.1 - '@typescript-eslint/types': 8.33.1 - '@typescript-eslint/typescript-estree': 8.33.1(typescript@5.8.3) - '@typescript-eslint/visitor-keys': 8.33.1 - debug: 4.4.1 - eslint: 9.28.0 - typescript: 5.8.3 + '@typescript-eslint/scope-manager': 8.54.0 + '@typescript-eslint/types': 8.54.0 + '@typescript-eslint/typescript-estree': 8.54.0(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.54.0 + debug: 4.4.3 + eslint: 9.39.2(jiti@2.6.1) + typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.33.1(typescript@5.8.3)': + '@typescript-eslint/project-service@8.54.0(typescript@5.9.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.33.1(typescript@5.8.3) - '@typescript-eslint/types': 8.33.1 - debug: 4.4.1 - typescript: 5.8.3 + '@typescript-eslint/tsconfig-utils': 8.54.0(typescript@5.9.3) + '@typescript-eslint/types': 8.54.0 + debug: 4.4.3 + typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.33.1': + '@typescript-eslint/scope-manager@8.54.0': dependencies: - '@typescript-eslint/types': 8.33.1 - '@typescript-eslint/visitor-keys': 8.33.1 + '@typescript-eslint/types': 8.54.0 + '@typescript-eslint/visitor-keys': 8.54.0 - '@typescript-eslint/tsconfig-utils@8.33.1(typescript@5.8.3)': + '@typescript-eslint/tsconfig-utils@8.54.0(typescript@5.9.3)': dependencies: - typescript: 5.8.3 + typescript: 5.9.3 - '@typescript-eslint/type-utils@8.33.1(eslint@9.28.0)(typescript@5.8.3)': + '@typescript-eslint/type-utils@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@typescript-eslint/typescript-estree': 8.33.1(typescript@5.8.3) - '@typescript-eslint/utils': 8.33.1(eslint@9.28.0)(typescript@5.8.3) - debug: 4.4.1 - eslint: 9.28.0 - ts-api-utils: 2.1.0(typescript@5.8.3) - typescript: 5.8.3 + '@typescript-eslint/types': 8.54.0 + '@typescript-eslint/typescript-estree': 8.54.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + debug: 4.4.3 + eslint: 9.39.2(jiti@2.6.1) + ts-api-utils: 2.4.0(typescript@5.9.3) + typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/types@8.33.1': {} + '@typescript-eslint/types@8.54.0': {} - '@typescript-eslint/typescript-estree@8.33.1(typescript@5.8.3)': + '@typescript-eslint/typescript-estree@8.54.0(typescript@5.9.3)': dependencies: - '@typescript-eslint/project-service': 8.33.1(typescript@5.8.3) - '@typescript-eslint/tsconfig-utils': 8.33.1(typescript@5.8.3) - '@typescript-eslint/types': 8.33.1 - '@typescript-eslint/visitor-keys': 8.33.1 - debug: 4.4.1 - fast-glob: 3.3.3 - is-glob: 4.0.3 + '@typescript-eslint/project-service': 8.54.0(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.54.0(typescript@5.9.3) + '@typescript-eslint/types': 8.54.0 + '@typescript-eslint/visitor-keys': 8.54.0 + debug: 4.4.3 minimatch: 9.0.5 - semver: 7.7.2 - ts-api-utils: 2.1.0(typescript@5.8.3) - typescript: 5.8.3 + semver: 7.7.4 + tinyglobby: 0.2.15 + ts-api-utils: 2.4.0(typescript@5.9.3) + typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.33.1(eslint@9.28.0)(typescript@5.8.3)': + '@typescript-eslint/utils@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@eslint-community/eslint-utils': 4.7.0(eslint@9.28.0) - '@typescript-eslint/scope-manager': 8.33.1 - '@typescript-eslint/types': 8.33.1 - '@typescript-eslint/typescript-estree': 8.33.1(typescript@5.8.3) - eslint: 9.28.0 - typescript: 5.8.3 + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@2.6.1)) + '@typescript-eslint/scope-manager': 8.54.0 + '@typescript-eslint/types': 8.54.0 + '@typescript-eslint/typescript-estree': 8.54.0(typescript@5.9.3) + eslint: 9.39.2(jiti@2.6.1) + typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/visitor-keys@8.33.1': + '@typescript-eslint/visitor-keys@8.54.0': dependencies: - '@typescript-eslint/types': 8.33.1 - eslint-visitor-keys: 4.2.0 + '@typescript-eslint/types': 8.54.0 + eslint-visitor-keys: 4.2.1 - acorn-jsx@5.3.2(acorn@8.14.1): + acorn-jsx@5.3.2(acorn@8.15.0): dependencies: - acorn: 8.14.1 + acorn: 8.15.0 acorn-walk@8.3.4: dependencies: - acorn: 8.14.1 + acorn: 8.15.0 - acorn@8.14.1: {} + acorn@8.15.0: {} ajv@6.12.6: dependencies: @@ -3608,7 +3887,7 @@ snapshots: dependencies: type-fest: 1.4.0 - ansi-escapes@7.0.0: + ansi-escapes@7.3.0: dependencies: environment: 1.1.0 @@ -3620,7 +3899,7 @@ snapshots: ansi-regex@5.0.1: {} - ansi-regex@6.1.0: {} + ansi-regex@6.2.2: {} ansi-styles@2.2.1: {} @@ -3634,7 +3913,7 @@ snapshots: ansi-styles@5.2.0: {} - ansi-styles@6.2.1: {} + ansi-styles@6.2.3: {} any-observable@0.3.0(rxjs@6.6.7): optionalDependencies: @@ -3655,12 +3934,10 @@ snapshots: argparse@2.0.1: {} - async@3.2.6: {} - - atomically@2.0.3: + atomically@2.1.0: dependencies: - stubborn-fs: 1.2.5 - when-exit: 2.1.4 + stubborn-fs: 2.0.0 + when-exit: 2.1.5 auto-changelog@2.5.0: dependencies: @@ -3669,17 +3946,17 @@ snapshots: import-cwd: 3.0.0 node-fetch: 2.7.0 parse-github-url: 1.0.3 - semver: 7.7.2 + semver: 7.7.4 transitivePeerDependencies: - encoding - babel-jest@29.7.0(@babel/core@7.27.4): + babel-jest@29.7.0(@babel/core@7.29.0): dependencies: - '@babel/core': 7.27.4 + '@babel/core': 7.29.0 '@jest/transform': 29.7.0 '@types/babel__core': 7.20.5 babel-plugin-istanbul: 6.1.1 - babel-preset-jest: 29.6.3(@babel/core@7.27.4) + babel-preset-jest: 29.6.3(@babel/core@7.29.0) chalk: 4.1.2 graceful-fs: 4.2.11 slash: 3.0.0 @@ -3688,7 +3965,7 @@ snapshots: babel-plugin-istanbul@6.1.1: dependencies: - '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-plugin-utils': 7.28.6 '@istanbuljs/load-nyc-config': 1.1.0 '@istanbuljs/schema': 0.1.3 istanbul-lib-instrument: 5.2.1 @@ -3698,55 +3975,57 @@ snapshots: babel-plugin-jest-hoist@29.6.3: dependencies: - '@babel/template': 7.27.2 - '@babel/types': 7.27.6 + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 '@types/babel__core': 7.20.5 - '@types/babel__traverse': 7.20.7 - - babel-preset-current-node-syntax@1.1.0(@babel/core@7.27.4): - dependencies: - '@babel/core': 7.27.4 - '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.27.4) - '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.27.4) - '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.27.4) - '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.27.4) - '@babel/plugin-syntax-import-attributes': 7.27.1(@babel/core@7.27.4) - '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.27.4) - '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.27.4) - '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.27.4) - '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.27.4) - '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.27.4) - '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.27.4) - '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.27.4) - '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.27.4) - '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.27.4) - '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.27.4) - - babel-preset-jest@29.6.3(@babel/core@7.27.4): - dependencies: - '@babel/core': 7.27.4 + '@types/babel__traverse': 7.28.0 + + babel-preset-current-node-syntax@1.2.0(@babel/core@7.29.0): + dependencies: + '@babel/core': 7.29.0 + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.29.0) + '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.29.0) + '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.29.0) + '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.29.0) + '@babel/plugin-syntax-import-attributes': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.29.0) + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.29.0) + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.29.0) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.29.0) + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.29.0) + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.29.0) + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.29.0) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.29.0) + '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.29.0) + '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.29.0) + + babel-preset-jest@29.6.3(@babel/core@7.29.0): + dependencies: + '@babel/core': 7.29.0 babel-plugin-jest-hoist: 29.6.3 - babel-preset-current-node-syntax: 1.1.0(@babel/core@7.27.4) + babel-preset-current-node-syntax: 1.2.0(@babel/core@7.29.0) balanced-match@1.0.2: {} + baseline-browser-mapping@2.9.19: {} + boxen@8.0.1: dependencies: ansi-align: 3.0.1 camelcase: 8.0.0 - chalk: 5.4.1 + chalk: 5.6.2 cli-boxes: 3.0.0 string-width: 7.2.0 type-fest: 4.41.0 widest-line: 5.0.0 - wrap-ansi: 9.0.0 + wrap-ansi: 9.0.2 - brace-expansion@1.1.11: + brace-expansion@1.1.12: dependencies: balanced-match: 1.0.2 concat-map: 0.0.1 - brace-expansion@2.0.1: + brace-expansion@2.0.2: dependencies: balanced-match: 1.0.2 @@ -3754,12 +4033,13 @@ snapshots: dependencies: fill-range: 7.1.1 - browserslist@4.25.0: + browserslist@4.28.1: dependencies: - caniuse-lite: 1.0.30001721 - electron-to-chromium: 1.5.165 - node-releases: 2.0.19 - update-browserslist-db: 1.1.3(browserslist@4.25.0) + baseline-browser-mapping: 2.9.19 + caniuse-lite: 1.0.30001769 + electron-to-chromium: 1.5.286 + node-releases: 2.0.27 + update-browserslist-db: 1.2.3(browserslist@4.28.1) bs-logger@0.2.6: dependencies: @@ -3773,7 +4053,7 @@ snapshots: bundle-name@4.1.0: dependencies: - run-applescript: 7.0.0 + run-applescript: 7.1.0 callsites@3.1.0: {} @@ -3783,11 +4063,11 @@ snapshots: camelcase@8.0.0: {} - caniuse-lite@1.0.30001721: {} + caniuse-lite@1.0.30001769: {} - chalk-template@1.1.0: + chalk-template@1.1.2: dependencies: - chalk: 5.4.1 + chalk: 5.6.2 chalk@1.1.3: dependencies: @@ -3808,12 +4088,14 @@ snapshots: ansi-styles: 4.3.0 supports-color: 7.2.0 - chalk@5.4.1: {} + chalk@5.6.2: {} char-regex@1.0.2: {} chardet@0.7.0: {} + chardet@2.1.1: {} + ci-info@3.9.0: {} cjs-module-lexer@1.4.3: {} @@ -3858,7 +4140,7 @@ snapshots: code-point-at@1.1.0: {} - collect-v8-coverage@1.0.2: {} + collect-v8-coverage@1.0.3: {} color-convert@1.9.3: dependencies: @@ -3885,40 +4167,40 @@ snapshots: ini: 1.3.8 proto-list: 1.2.4 - configstore@7.0.0: + configstore@7.1.0: dependencies: - atomically: 2.0.3 + atomically: 2.1.0 dot-prop: 9.0.0 graceful-fs: 4.2.11 xdg-basedir: 5.1.0 convert-source-map@2.0.0: {} - cosmiconfig@8.3.6(typescript@5.8.3): + cosmiconfig@8.3.6(typescript@5.9.3): dependencies: import-fresh: 3.3.1 - js-yaml: 4.1.0 + js-yaml: 4.1.1 parse-json: 5.2.0 path-type: 4.0.0 optionalDependencies: - typescript: 5.8.3 + typescript: 5.9.3 - cosmiconfig@9.0.0(typescript@5.8.3): + cosmiconfig@9.0.0(typescript@5.9.3): dependencies: env-paths: 2.2.1 import-fresh: 3.3.1 - js-yaml: 4.1.0 + js-yaml: 4.1.1 parse-json: 5.2.0 optionalDependencies: - typescript: 5.8.3 + typescript: 5.9.3 - create-jest@29.7.0(@types/node@20.17.58)(ts-node@10.9.2(@types/node@20.17.58)(typescript@5.8.3)): + create-jest@29.7.0(@types/node@20.19.32)(ts-node@10.9.2(@types/node@20.19.32)(typescript@5.9.3)): dependencies: '@jest/types': 29.6.3 chalk: 4.1.2 exit: 0.1.2 graceful-fs: 4.2.11 - jest-config: 29.7.0(@types/node@20.17.58)(ts-node@10.9.2(@types/node@20.17.58)(typescript@5.8.3)) + jest-config: 29.7.0(@types/node@20.19.32)(ts-node@10.9.2(@types/node@20.19.32)(typescript@5.9.3)) jest-util: 29.7.0 prompts: 2.4.2 transitivePeerDependencies: @@ -3937,11 +4219,11 @@ snapshots: date-fns@1.30.1: {} - debug@4.4.1: + debug@4.4.3: dependencies: ms: 2.1.3 - dedent@1.6.0: {} + dedent@1.7.1: {} deep-extend@0.6.0: {} @@ -3949,29 +4231,30 @@ snapshots: deepmerge@4.3.1: {} - default-browser-id@5.0.0: {} + default-browser-id@5.0.1: {} - default-browser@5.2.1: + default-browser@5.5.0: dependencies: bundle-name: 4.1.0 - default-browser-id: 5.0.0 + default-browser-id: 5.0.1 define-lazy-prop@3.0.0: {} - del@8.0.0: + del@8.0.1: dependencies: globby: 14.1.0 is-glob: 4.0.3 is-path-cwd: 3.0.0 is-path-inside: 4.0.0 - p-map: 7.0.3 + p-map: 7.0.4 + presentable-error: 0.0.1 slash: 5.1.0 detect-newline@3.1.0: {} diff-sequences@29.6.3: {} - diff@4.0.2: {} + diff@4.0.4: {} dot-prop@9.0.0: dependencies: @@ -3979,17 +4262,13 @@ snapshots: eastasianwidth@0.2.0: {} - ejs@3.1.10: - dependencies: - jake: 10.9.2 - - electron-to-chromium@1.5.165: {} + electron-to-chromium@1.5.286: {} elegant-spinner@1.0.1: {} emittery@0.13.1: {} - emoji-regex@10.4.0: {} + emoji-regex@10.6.0: {} emoji-regex@8.0.0: {} @@ -3999,7 +4278,7 @@ snapshots: environment@1.1.0: {} - error-ex@1.3.2: + error-ex@1.3.4: dependencies: is-arrayish: 0.2.1 @@ -4015,39 +4294,38 @@ snapshots: escape-string-regexp@5.0.0: {} - eslint-scope@8.3.0: + eslint-scope@8.4.0: dependencies: esrecurse: 4.3.0 estraverse: 5.3.0 eslint-visitor-keys@3.4.3: {} - eslint-visitor-keys@4.2.0: {} + eslint-visitor-keys@4.2.1: {} - eslint@9.28.0: + eslint@9.39.2(jiti@2.6.1): dependencies: - '@eslint-community/eslint-utils': 4.7.0(eslint@9.28.0) - '@eslint-community/regexpp': 4.12.1 - '@eslint/config-array': 0.20.0 - '@eslint/config-helpers': 0.2.2 - '@eslint/core': 0.14.0 - '@eslint/eslintrc': 3.3.1 - '@eslint/js': 9.28.0 - '@eslint/plugin-kit': 0.3.1 - '@humanfs/node': 0.16.6 + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@2.6.1)) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.21.1 + '@eslint/config-helpers': 0.4.2 + '@eslint/core': 0.17.0 + '@eslint/eslintrc': 3.3.3 + '@eslint/js': 9.39.2 + '@eslint/plugin-kit': 0.4.1 + '@humanfs/node': 0.16.7 '@humanwhocodes/module-importer': 1.0.1 '@humanwhocodes/retry': 0.4.3 - '@types/estree': 1.0.7 - '@types/json-schema': 7.0.15 + '@types/estree': 1.0.8 ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.6 - debug: 4.4.1 + debug: 4.4.3 escape-string-regexp: 4.0.0 - eslint-scope: 8.3.0 - eslint-visitor-keys: 4.2.0 - espree: 10.3.0 - esquery: 1.6.0 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + esquery: 1.7.0 esutils: 2.0.3 fast-deep-equal: 3.1.3 file-entry-cache: 8.0.0 @@ -4061,18 +4339,20 @@ snapshots: minimatch: 3.1.2 natural-compare: 1.4.0 optionator: 0.9.4 + optionalDependencies: + jiti: 2.6.1 transitivePeerDependencies: - supports-color - espree@10.3.0: + espree@10.4.0: dependencies: - acorn: 8.14.1 - acorn-jsx: 5.3.2(acorn@8.14.1) - eslint-visitor-keys: 4.2.0 + acorn: 8.15.0 + acorn-jsx: 5.3.2(acorn@8.15.0) + eslint-visitor-keys: 4.2.1 esprima@4.0.1: {} - esquery@1.6.0: + esquery@1.7.0: dependencies: estraverse: 5.3.0 @@ -4084,7 +4364,7 @@ snapshots: esutils@2.0.3: {} - eventemitter3@5.0.1: {} + eventemitter3@5.0.4: {} execa@5.1.1: dependencies: @@ -4142,7 +4422,7 @@ snapshots: fast-levenshtein@2.0.6: {} - fastq@1.19.1: + fastq@1.20.1: dependencies: reusify: 1.1.0 @@ -4150,6 +4430,14 @@ snapshots: dependencies: bser: 2.1.1 + fd-package-json@2.0.0: + dependencies: + walk-up-path: 4.0.0 + + fdir@6.5.0(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + figures@1.7.0: dependencies: escape-string-regexp: 1.0.5 @@ -4167,10 +4455,6 @@ snapshots: dependencies: flat-cache: 4.0.1 - filelist@1.0.4: - dependencies: - minimatch: 5.1.6 - fill-range@7.1.1: dependencies: to-regex-range: 5.0.1 @@ -4199,10 +4483,14 @@ snapshots: cross-spawn: 7.0.6 signal-exit: 4.1.0 - fs-extra@11.3.0: + formatly@0.3.0: + dependencies: + fd-package-json: 2.0.0 + + fs-extra@11.3.3: dependencies: graceful-fs: 4.2.11 - jsonfile: 6.1.0 + jsonfile: 6.2.0 universalify: 2.0.1 fs.realpath@1.0.0: {} @@ -4216,7 +4504,7 @@ snapshots: get-caller-file@2.0.5: {} - get-east-asian-width@1.3.0: {} + get-east-asian-width@1.4.0: {} get-package-type@0.1.0: {} @@ -4234,7 +4522,7 @@ snapshots: dependencies: is-glob: 4.0.3 - glob@10.4.5: + glob@10.5.0: dependencies: foreground-child: 3.3.1 jackspeak: 3.4.3 @@ -4256,8 +4544,6 @@ snapshots: dependencies: ini: 4.1.1 - globals@11.12.0: {} - globals@14.0.0: {} globby@14.1.0: @@ -4273,8 +4559,6 @@ snapshots: graceful-fs@4.2.11: {} - graphemer@1.4.0: {} - handlebars@4.7.8: dependencies: minimist: 1.2.8 @@ -4316,6 +4600,10 @@ snapshots: dependencies: safer-buffer: 2.1.2 + iconv-lite@0.7.2: + dependencies: + safer-buffer: 2.1.2 + ignore-walk@7.0.0: dependencies: minimatch: 9.0.5 @@ -4346,7 +4634,7 @@ snapshots: indent-string@3.2.0: {} - index-to-position@1.1.0: {} + index-to-position@1.2.0: {} inflight@1.0.6: dependencies: @@ -4365,17 +4653,17 @@ snapshots: inquirer: 6.5.2 rxjs: 6.6.7 - inquirer@12.6.3(@types/node@20.17.58): + inquirer@12.11.1(@types/node@20.19.32): dependencies: - '@inquirer/core': 10.1.13(@types/node@20.17.58) - '@inquirer/prompts': 7.5.3(@types/node@20.17.58) - '@inquirer/type': 3.0.7(@types/node@20.17.58) - ansi-escapes: 4.3.2 + '@inquirer/ansi': 1.0.2 + '@inquirer/core': 10.3.2(@types/node@20.19.32) + '@inquirer/prompts': 7.10.1(@types/node@20.19.32) + '@inquirer/type': 3.0.10(@types/node@20.19.32) mute-stream: 2.0.0 - run-async: 3.0.0 + run-async: 4.0.6 rxjs: 7.8.2 optionalDependencies: - '@types/node': 20.17.58 + '@types/node': 20.19.32 inquirer@6.5.2: dependencies: @@ -4385,7 +4673,7 @@ snapshots: cli-width: 2.2.1 external-editor: 3.1.0 figures: 2.0.0 - lodash: 4.17.21 + lodash: 4.17.23 mute-stream: 0.0.7 run-async: 2.4.1 rxjs: 6.6.7 @@ -4401,7 +4689,7 @@ snapshots: cli-width: 3.0.0 external-editor: 3.1.0 figures: 3.2.0 - lodash: 4.17.21 + lodash: 4.17.23 mute-stream: 0.0.8 run-async: 2.4.1 rxjs: 6.6.7 @@ -4429,9 +4717,9 @@ snapshots: is-fullwidth-code-point@4.0.0: {} - is-fullwidth-code-point@5.0.0: + is-fullwidth-code-point@5.1.0: dependencies: - get-east-asian-width: 1.3.0 + get-east-asian-width: 1.4.0 is-generator-fn@2.1.0: {} @@ -4452,7 +4740,7 @@ snapshots: is-interactive@2.0.0: {} - is-npm@6.0.0: {} + is-npm@6.1.0: {} is-number@7.0.0: {} @@ -4492,8 +4780,8 @@ snapshots: istanbul-lib-instrument@5.2.1: dependencies: - '@babel/core': 7.27.4 - '@babel/parser': 7.27.5 + '@babel/core': 7.29.0 + '@babel/parser': 7.29.0 '@istanbuljs/schema': 0.1.3 istanbul-lib-coverage: 3.2.2 semver: 6.3.1 @@ -4502,11 +4790,11 @@ snapshots: istanbul-lib-instrument@6.0.3: dependencies: - '@babel/core': 7.27.4 - '@babel/parser': 7.27.5 + '@babel/core': 7.29.0 + '@babel/parser': 7.29.0 '@istanbuljs/schema': 0.1.3 istanbul-lib-coverage: 3.2.2 - semver: 7.7.2 + semver: 7.7.4 transitivePeerDependencies: - supports-color @@ -4518,13 +4806,13 @@ snapshots: istanbul-lib-source-maps@4.0.1: dependencies: - debug: 4.4.1 + debug: 4.4.3 istanbul-lib-coverage: 3.2.2 source-map: 0.6.1 transitivePeerDependencies: - supports-color - istanbul-reports@3.1.7: + istanbul-reports@3.2.0: dependencies: html-escaper: 2.0.2 istanbul-lib-report: 3.0.1 @@ -4535,13 +4823,6 @@ snapshots: optionalDependencies: '@pkgjs/parseargs': 0.11.0 - jake@10.9.2: - dependencies: - async: 3.2.6 - chalk: 4.1.2 - filelist: 1.0.4 - minimatch: 3.1.2 - jest-changed-files@29.7.0: dependencies: execa: 5.1.1 @@ -4554,10 +4835,10 @@ snapshots: '@jest/expect': 29.7.0 '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.17.58 + '@types/node': 20.19.32 chalk: 4.1.2 co: 4.6.0 - dedent: 1.6.0 + dedent: 1.7.1 is-generator-fn: 2.1.0 jest-each: 29.7.0 jest-matcher-utils: 29.7.0 @@ -4574,16 +4855,16 @@ snapshots: - babel-plugin-macros - supports-color - jest-cli@29.7.0(@types/node@20.17.58)(ts-node@10.9.2(@types/node@20.17.58)(typescript@5.8.3)): + jest-cli@29.7.0(@types/node@20.19.32)(ts-node@10.9.2(@types/node@20.19.32)(typescript@5.9.3)): dependencies: - '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@20.17.58)(typescript@5.8.3)) + '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@20.19.32)(typescript@5.9.3)) '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 chalk: 4.1.2 - create-jest: 29.7.0(@types/node@20.17.58)(ts-node@10.9.2(@types/node@20.17.58)(typescript@5.8.3)) + create-jest: 29.7.0(@types/node@20.19.32)(ts-node@10.9.2(@types/node@20.19.32)(typescript@5.9.3)) exit: 0.1.2 import-local: 3.2.0 - jest-config: 29.7.0(@types/node@20.17.58)(ts-node@10.9.2(@types/node@20.17.58)(typescript@5.8.3)) + jest-config: 29.7.0(@types/node@20.19.32)(ts-node@10.9.2(@types/node@20.19.32)(typescript@5.9.3)) jest-util: 29.7.0 jest-validate: 29.7.0 yargs: 17.7.2 @@ -4593,12 +4874,12 @@ snapshots: - supports-color - ts-node - jest-config@29.7.0(@types/node@20.17.58)(ts-node@10.9.2(@types/node@20.17.58)(typescript@5.8.3)): + jest-config@29.7.0(@types/node@20.19.32)(ts-node@10.9.2(@types/node@20.19.32)(typescript@5.9.3)): dependencies: - '@babel/core': 7.27.4 + '@babel/core': 7.29.0 '@jest/test-sequencer': 29.7.0 '@jest/types': 29.6.3 - babel-jest: 29.7.0(@babel/core@7.27.4) + babel-jest: 29.7.0(@babel/core@7.29.0) chalk: 4.1.2 ci-info: 3.9.0 deepmerge: 4.3.1 @@ -4618,8 +4899,8 @@ snapshots: slash: 3.0.0 strip-json-comments: 3.1.1 optionalDependencies: - '@types/node': 20.17.58 - ts-node: 10.9.2(@types/node@20.17.58)(typescript@5.8.3) + '@types/node': 20.19.32 + ts-node: 10.9.2(@types/node@20.19.32)(typescript@5.9.3) transitivePeerDependencies: - babel-plugin-macros - supports-color @@ -4648,7 +4929,7 @@ snapshots: '@jest/environment': 29.7.0 '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.17.58 + '@types/node': 20.19.32 jest-mock: 29.7.0 jest-util: 29.7.0 @@ -4658,7 +4939,7 @@ snapshots: dependencies: '@jest/types': 29.6.3 '@types/graceful-fs': 4.1.9 - '@types/node': 20.17.58 + '@types/node': 20.19.32 anymatch: 3.1.3 fb-watchman: 2.0.2 graceful-fs: 4.2.11 @@ -4684,7 +4965,7 @@ snapshots: jest-message-util@29.7.0: dependencies: - '@babel/code-frame': 7.27.1 + '@babel/code-frame': 7.29.0 '@jest/types': 29.6.3 '@types/stack-utils': 2.0.3 chalk: 4.1.2 @@ -4697,7 +4978,7 @@ snapshots: jest-mock@29.7.0: dependencies: '@jest/types': 29.6.3 - '@types/node': 20.17.58 + '@types/node': 20.19.32 jest-util: 29.7.0 jest-pnp-resolver@1.2.3(jest-resolve@29.7.0): @@ -4721,7 +5002,7 @@ snapshots: jest-pnp-resolver: 1.2.3(jest-resolve@29.7.0) jest-util: 29.7.0 jest-validate: 29.7.0 - resolve: 1.22.10 + resolve: 1.22.11 resolve.exports: 2.0.3 slash: 3.0.0 @@ -4732,7 +5013,7 @@ snapshots: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.17.58 + '@types/node': 20.19.32 chalk: 4.1.2 emittery: 0.13.1 graceful-fs: 4.2.11 @@ -4760,10 +5041,10 @@ snapshots: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.17.58 + '@types/node': 20.19.32 chalk: 4.1.2 cjs-module-lexer: 1.4.3 - collect-v8-coverage: 1.0.2 + collect-v8-coverage: 1.0.3 glob: 7.2.3 graceful-fs: 4.2.11 jest-haste-map: 29.7.0 @@ -4780,15 +5061,15 @@ snapshots: jest-snapshot@29.7.0: dependencies: - '@babel/core': 7.27.4 - '@babel/generator': 7.27.5 - '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.27.4) - '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.27.4) - '@babel/types': 7.27.6 + '@babel/core': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-syntax-typescript': 7.28.6(@babel/core@7.29.0) + '@babel/types': 7.29.0 '@jest/expect-utils': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - babel-preset-current-node-syntax: 1.1.0(@babel/core@7.27.4) + babel-preset-current-node-syntax: 1.2.0(@babel/core@7.29.0) chalk: 4.1.2 expect: 29.7.0 graceful-fs: 4.2.11 @@ -4799,14 +5080,14 @@ snapshots: jest-util: 29.7.0 natural-compare: 1.4.0 pretty-format: 29.7.0 - semver: 7.7.2 + semver: 7.7.4 transitivePeerDependencies: - supports-color jest-util@29.7.0: dependencies: '@jest/types': 29.6.3 - '@types/node': 20.17.58 + '@types/node': 20.19.32 chalk: 4.1.2 ci-info: 3.9.0 graceful-fs: 4.2.11 @@ -4825,7 +5106,7 @@ snapshots: dependencies: '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.17.58 + '@types/node': 20.19.32 ansi-escapes: 4.3.2 chalk: 4.1.2 emittery: 0.13.1 @@ -4834,31 +5115,33 @@ snapshots: jest-worker@29.7.0: dependencies: - '@types/node': 20.17.58 + '@types/node': 20.19.32 jest-util: 29.7.0 merge-stream: 2.0.0 supports-color: 8.1.1 - jest@29.7.0(@types/node@20.17.58)(ts-node@10.9.2(@types/node@20.17.58)(typescript@5.8.3)): + jest@29.7.0(@types/node@20.19.32)(ts-node@10.9.2(@types/node@20.19.32)(typescript@5.9.3)): dependencies: - '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@20.17.58)(typescript@5.8.3)) + '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@20.19.32)(typescript@5.9.3)) '@jest/types': 29.6.3 import-local: 3.2.0 - jest-cli: 29.7.0(@types/node@20.17.58)(ts-node@10.9.2(@types/node@20.17.58)(typescript@5.8.3)) + jest-cli: 29.7.0(@types/node@20.19.32)(ts-node@10.9.2(@types/node@20.19.32)(typescript@5.9.3)) transitivePeerDependencies: - '@types/node' - babel-plugin-macros - supports-color - ts-node + jiti@2.6.1: {} + js-tokens@4.0.0: {} - js-yaml@3.14.1: + js-yaml@3.14.2: dependencies: argparse: 1.0.10 esprima: 4.0.1 - js-yaml@4.1.0: + js-yaml@4.1.1: dependencies: argparse: 2.0.1 @@ -4874,7 +5157,7 @@ snapshots: json5@2.2.3: {} - jsonfile@6.1.0: + jsonfile@6.2.0: dependencies: universalify: 2.0.1 optionalDependencies: @@ -4886,7 +5169,24 @@ snapshots: kleur@3.0.3: {} - ky@1.8.1: {} + knip@5.83.0(@types/node@20.19.32)(typescript@5.9.3): + dependencies: + '@nodelib/fs.walk': 1.2.8 + '@types/node': 20.19.32 + fast-glob: 3.3.3 + formatly: 0.3.0 + jiti: 2.6.1 + js-yaml: 4.1.1 + minimist: 1.2.8 + oxc-resolver: 11.17.0 + picocolors: 1.1.1 + picomatch: 4.0.3 + smol-toml: 1.6.0 + strip-json-comments: 5.0.3 + typescript: 5.9.3 + zod: 4.3.6 + + ky@1.14.3: {} latest-version@9.0.0: dependencies: @@ -4905,16 +5205,16 @@ snapshots: lint-staged@15.5.2: dependencies: - chalk: 5.4.1 + chalk: 5.6.2 commander: 13.1.0 - debug: 4.4.1 + debug: 4.4.3 execa: 8.0.1 lilconfig: 3.1.3 listr2: 8.3.3 micromatch: 4.0.8 pidtree: 0.6.0 string-argv: 0.3.2 - yaml: 2.8.0 + yaml: 2.8.2 transitivePeerDependencies: - supports-color @@ -4950,10 +5250,10 @@ snapshots: dependencies: cli-truncate: 4.0.0 colorette: 2.0.20 - eventemitter3: 5.0.1 + eventemitter3: 5.0.4 log-update: 6.1.0 rfdc: 1.4.1 - wrap-ansi: 9.0.0 + wrap-ansi: 9.0.2 listr@0.14.3: dependencies: @@ -4984,7 +5284,7 @@ snapshots: lodash.zip@4.2.0: {} - lodash@4.17.21: {} + lodash@4.17.23: {} log-symbols@1.0.2: dependencies: @@ -4993,7 +5293,7 @@ snapshots: log-symbols@7.0.1: dependencies: is-unicode-supported: 2.1.0 - yoctocolors: 2.1.1 + yoctocolors: 2.1.2 log-update@2.3.0: dependencies: @@ -5003,11 +5303,11 @@ snapshots: log-update@6.1.0: dependencies: - ansi-escapes: 7.0.0 + ansi-escapes: 7.3.0 cli-cursor: 5.0.0 - slice-ansi: 7.1.0 - strip-ansi: 7.1.0 - wrap-ansi: 9.0.0 + slice-ansi: 7.1.2 + strip-ansi: 7.1.2 + wrap-ansi: 9.0.2 lru-cache@10.4.3: {} @@ -5017,7 +5317,7 @@ snapshots: make-dir@4.0.0: dependencies: - semver: 7.7.2 + semver: 7.7.4 make-error@1.3.6: {} @@ -5046,15 +5346,11 @@ snapshots: minimatch@3.1.2: dependencies: - brace-expansion: 1.1.11 - - minimatch@5.1.6: - dependencies: - brace-expansion: 2.0.1 + brace-expansion: 1.1.12 minimatch@9.0.5: dependencies: - brace-expansion: 2.0.1 + brace-expansion: 2.0.2 minimist@1.2.8: {} @@ -5082,22 +5378,22 @@ snapshots: node-int64@0.4.0: {} - node-releases@2.0.19: {} + node-releases@2.0.27: {} normalize-package-data@6.0.2: dependencies: hosted-git-info: 7.0.2 - semver: 7.7.2 + semver: 7.7.4 validate-npm-package-license: 3.0.4 normalize-path@3.0.0: {} - np@10.2.0(@types/node@20.17.58)(typescript@5.8.3): + np@10.3.0(@types/node@20.19.32)(typescript@5.9.3): dependencies: - chalk: 5.4.1 - chalk-template: 1.1.0 - cosmiconfig: 8.3.6(typescript@5.8.3) - del: 8.0.0 + chalk: 5.6.2 + chalk-template: 1.1.2 + cosmiconfig: 8.3.6(typescript@5.9.3) + del: 8.0.1 escape-goat: 4.0.0 escape-string-regexp: 5.0.0 execa: 8.0.1 @@ -5106,7 +5402,7 @@ snapshots: hosted-git-info: 8.1.0 ignore-walk: 7.0.0 import-local: 3.2.0 - inquirer: 12.6.3(@types/node@20.17.58) + inquirer: 12.11.1(@types/node@20.19.32) is-installed-globally: 1.0.0 is-interactive: 2.0.0 is-scoped: 3.0.0 @@ -5116,17 +5412,17 @@ snapshots: log-symbols: 7.0.1 meow: 13.2.0 new-github-release-url: 2.0.0 - npm-name: 8.0.0 + npm-name: 8.1.0 onetime: 7.0.0 - open: 10.1.2 + open: 10.2.0 p-memoize: 7.1.1 p-timeout: 6.1.4 + package-directory: 8.2.0 path-exists: 5.0.0 - pkg-dir: 8.0.0 read-package-up: 11.0.0 read-pkg: 9.0.1 rxjs: 7.8.2 - semver: 7.7.2 + semver: 7.7.4 symbol-observable: 4.0.0 terminal-link: 3.0.0 update-notifier: 7.3.1 @@ -5136,15 +5432,15 @@ snapshots: - zen-observable - zenObservable - npm-name@8.0.0: + npm-name@8.1.0: dependencies: is-scoped: 3.0.0 is-url-superb: 6.1.0 - ky: 1.8.1 + ky: 1.14.3 lodash.zip: 4.2.0 org-regex: 1.0.0 - p-map: 7.0.3 - registry-auth-token: 5.1.0 + p-map: 7.0.4 + registry-auth-token: 5.1.1 registry-url: 6.0.1 validate-npm-package-name: 5.0.1 @@ -5180,12 +5476,12 @@ snapshots: dependencies: mimic-function: 5.0.1 - open@10.1.2: + open@10.2.0: dependencies: - default-browser: 5.2.1 + default-browser: 5.5.0 define-lazy-prop: 3.0.0 is-inside-container: 1.0.0 - is-wsl: 3.1.0 + wsl-utils: 0.1.0 optionator@0.9.4: dependencies: @@ -5200,6 +5496,29 @@ snapshots: os-tmpdir@1.0.2: {} + oxc-resolver@11.17.0: + optionalDependencies: + '@oxc-resolver/binding-android-arm-eabi': 11.17.0 + '@oxc-resolver/binding-android-arm64': 11.17.0 + '@oxc-resolver/binding-darwin-arm64': 11.17.0 + '@oxc-resolver/binding-darwin-x64': 11.17.0 + '@oxc-resolver/binding-freebsd-x64': 11.17.0 + '@oxc-resolver/binding-linux-arm-gnueabihf': 11.17.0 + '@oxc-resolver/binding-linux-arm-musleabihf': 11.17.0 + '@oxc-resolver/binding-linux-arm64-gnu': 11.17.0 + '@oxc-resolver/binding-linux-arm64-musl': 11.17.0 + '@oxc-resolver/binding-linux-ppc64-gnu': 11.17.0 + '@oxc-resolver/binding-linux-riscv64-gnu': 11.17.0 + '@oxc-resolver/binding-linux-riscv64-musl': 11.17.0 + '@oxc-resolver/binding-linux-s390x-gnu': 11.17.0 + '@oxc-resolver/binding-linux-x64-gnu': 11.17.0 + '@oxc-resolver/binding-linux-x64-musl': 11.17.0 + '@oxc-resolver/binding-openharmony-arm64': 11.17.0 + '@oxc-resolver/binding-wasm32-wasi': 11.17.0 + '@oxc-resolver/binding-win32-arm64-msvc': 11.17.0 + '@oxc-resolver/binding-win32-ia32-msvc': 11.17.0 + '@oxc-resolver/binding-win32-x64-msvc': 11.17.0 + p-limit@2.3.0: dependencies: p-try: 2.2.0 @@ -5218,7 +5537,7 @@ snapshots: p-map@2.1.0: {} - p-map@7.0.3: {} + p-map@7.0.4: {} p-memoize@7.1.1: dependencies: @@ -5229,14 +5548,18 @@ snapshots: p-try@2.2.0: {} + package-directory@8.2.0: + dependencies: + find-up-simple: 1.0.1 + package-json-from-dist@1.0.1: {} package-json@10.0.1: dependencies: - ky: 1.8.1 - registry-auth-token: 5.1.0 + ky: 1.14.3 + registry-auth-token: 5.1.1 registry-url: 6.0.1 - semver: 7.7.2 + semver: 7.7.4 parent-module@1.0.1: dependencies: @@ -5246,15 +5569,15 @@ snapshots: parse-json@5.2.0: dependencies: - '@babel/code-frame': 7.27.1 - error-ex: 1.3.2 + '@babel/code-frame': 7.29.0 + error-ex: 1.3.4 json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 parse-json@8.3.0: dependencies: - '@babel/code-frame': 7.27.1 - index-to-position: 1.1.0 + '@babel/code-frame': 7.29.0 + index-to-position: 1.2.0 type-fest: 4.41.0 path-exists@4.0.0: {} @@ -5282,6 +5605,8 @@ snapshots: picomatch@2.3.1: {} + picomatch@4.0.3: {} + pidtree@0.6.0: {} pirates@4.0.7: {} @@ -5290,13 +5615,11 @@ snapshots: dependencies: find-up: 4.1.0 - pkg-dir@8.0.0: - dependencies: - find-up-simple: 1.0.1 - prelude-ls@1.2.1: {} - prettier@3.5.3: {} + presentable-error@0.0.1: {} + + prettier@3.8.1: {} pretty-format@29.7.0: dependencies: @@ -5313,7 +5636,7 @@ snapshots: punycode@2.3.1: {} - pupa@3.1.0: + pupa@3.3.0: dependencies: escape-goat: 4.0.0 @@ -5344,9 +5667,9 @@ snapshots: type-fest: 4.41.0 unicorn-magic: 0.1.0 - registry-auth-token@5.1.0: + registry-auth-token@5.1.1: dependencies: - '@pnpm/npm-conf': 2.3.1 + '@pnpm/npm-conf': 3.0.2 registry-url@6.0.1: dependencies: @@ -5364,7 +5687,7 @@ snapshots: resolve.exports@2.0.3: {} - resolve@1.22.10: + resolve@1.22.11: dependencies: is-core-module: 2.16.1 path-parse: 1.0.7 @@ -5391,13 +5714,13 @@ snapshots: rimraf@5.0.10: dependencies: - glob: 10.4.5 + glob: 10.5.0 - run-applescript@7.0.0: {} + run-applescript@7.1.0: {} run-async@2.4.1: {} - run-async@3.0.0: {} + run-async@4.0.6: {} run-parallel@1.2.0: dependencies: @@ -5417,7 +5740,7 @@ snapshots: semver@6.3.1: {} - semver@7.7.2: {} + semver@7.7.4: {} shebang-command@2.0.0: dependencies: @@ -5439,13 +5762,15 @@ snapshots: slice-ansi@5.0.0: dependencies: - ansi-styles: 6.2.1 + ansi-styles: 6.2.3 is-fullwidth-code-point: 4.0.0 - slice-ansi@7.1.0: + slice-ansi@7.1.2: dependencies: - ansi-styles: 6.2.1 - is-fullwidth-code-point: 5.0.0 + ansi-styles: 6.2.3 + is-fullwidth-code-point: 5.1.0 + + smol-toml@1.6.0: {} source-map-support@0.5.13: dependencies: @@ -5457,16 +5782,16 @@ snapshots: spdx-correct@3.2.0: dependencies: spdx-expression-parse: 3.0.1 - spdx-license-ids: 3.0.21 + spdx-license-ids: 3.0.22 spdx-exceptions@2.5.0: {} spdx-expression-parse@3.0.1: dependencies: spdx-exceptions: 2.5.0 - spdx-license-ids: 3.0.21 + spdx-license-ids: 3.0.22 - spdx-license-ids@3.0.21: {} + spdx-license-ids@3.0.22: {} sprintf-js@1.0.3: {} @@ -5502,13 +5827,13 @@ snapshots: dependencies: eastasianwidth: 0.2.0 emoji-regex: 9.2.2 - strip-ansi: 7.1.0 + strip-ansi: 7.1.2 string-width@7.2.0: dependencies: - emoji-regex: 10.4.0 - get-east-asian-width: 1.3.0 - strip-ansi: 7.1.0 + emoji-regex: 10.6.0 + get-east-asian-width: 1.4.0 + strip-ansi: 7.1.2 strip-ansi@3.0.1: dependencies: @@ -5526,9 +5851,9 @@ snapshots: dependencies: ansi-regex: 5.0.1 - strip-ansi@7.1.0: + strip-ansi@7.1.2: dependencies: - ansi-regex: 6.1.0 + ansi-regex: 6.2.2 strip-bom@4.0.0: {} @@ -5540,7 +5865,13 @@ snapshots: strip-json-comments@3.1.1: {} - stubborn-fs@1.2.5: {} + strip-json-comments@5.0.3: {} + + stubborn-fs@2.0.0: + dependencies: + stubborn-utils: 1.0.2 + + stubborn-utils@1.0.2: {} supports-color@2.0.0: {} @@ -5580,6 +5911,11 @@ snapshots: through@2.3.8: {} + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + tmp@0.0.33: dependencies: os-tmpdir: 1.0.2 @@ -5592,45 +5928,45 @@ snapshots: tr46@0.0.3: {} - ts-api-utils@2.1.0(typescript@5.8.3): + ts-api-utils@2.4.0(typescript@5.9.3): dependencies: - typescript: 5.8.3 + typescript: 5.9.3 - ts-jest@29.3.4(@babel/core@7.27.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.27.4))(jest@29.7.0(@types/node@20.17.58)(ts-node@10.9.2(@types/node@20.17.58)(typescript@5.8.3)))(typescript@5.8.3): + ts-jest@29.4.6(@babel/core@7.29.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.0))(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.32)(ts-node@10.9.2(@types/node@20.19.32)(typescript@5.9.3)))(typescript@5.9.3): dependencies: bs-logger: 0.2.6 - ejs: 3.1.10 fast-json-stable-stringify: 2.1.0 - jest: 29.7.0(@types/node@20.17.58)(ts-node@10.9.2(@types/node@20.17.58)(typescript@5.8.3)) - jest-util: 29.7.0 + handlebars: 4.7.8 + jest: 29.7.0(@types/node@20.19.32)(ts-node@10.9.2(@types/node@20.19.32)(typescript@5.9.3)) json5: 2.2.3 lodash.memoize: 4.1.2 make-error: 1.3.6 - semver: 7.7.2 + semver: 7.7.4 type-fest: 4.41.0 - typescript: 5.8.3 + typescript: 5.9.3 yargs-parser: 21.1.1 optionalDependencies: - '@babel/core': 7.27.4 + '@babel/core': 7.29.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - babel-jest: 29.7.0(@babel/core@7.27.4) + babel-jest: 29.7.0(@babel/core@7.29.0) + jest-util: 29.7.0 - ts-node@10.9.2(@types/node@20.17.58)(typescript@5.8.3): + ts-node@10.9.2(@types/node@20.19.32)(typescript@5.9.3): dependencies: '@cspotcode/source-map-support': 0.8.1 - '@tsconfig/node10': 1.0.11 + '@tsconfig/node10': 1.0.12 '@tsconfig/node12': 1.0.11 '@tsconfig/node14': 1.0.3 '@tsconfig/node16': 1.0.4 - '@types/node': 20.17.58 - acorn: 8.14.1 + '@types/node': 20.19.32 + acorn: 8.15.0 acorn-walk: 8.3.4 arg: 4.1.3 create-require: 1.1.1 - diff: 4.0.2 + diff: 4.0.4 make-error: 1.3.6 - typescript: 5.8.3 + typescript: 5.9.3 v8-compile-cache-lib: 3.0.1 yn: 3.1.1 @@ -5654,22 +5990,23 @@ snapshots: type-fest@4.41.0: {} - typescript-eslint@8.33.1(eslint@9.28.0)(typescript@5.8.3): + typescript-eslint@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.33.1(@typescript-eslint/parser@8.33.1(eslint@9.28.0)(typescript@5.8.3))(eslint@9.28.0)(typescript@5.8.3) - '@typescript-eslint/parser': 8.33.1(eslint@9.28.0)(typescript@5.8.3) - '@typescript-eslint/utils': 8.33.1(eslint@9.28.0)(typescript@5.8.3) - eslint: 9.28.0 - typescript: 5.8.3 + '@typescript-eslint/eslint-plugin': 8.54.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/parser': 8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.54.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + eslint: 9.39.2(jiti@2.6.1) + typescript: 5.9.3 transitivePeerDependencies: - supports-color - typescript@5.8.3: {} + typescript@5.9.3: {} uglify-js@3.19.3: optional: true - undici-types@6.19.8: {} + undici-types@6.21.0: {} unicorn-magic@0.1.0: {} @@ -5677,23 +6014,23 @@ snapshots: universalify@2.0.1: {} - update-browserslist-db@1.1.3(browserslist@4.25.0): + update-browserslist-db@1.2.3(browserslist@4.28.1): dependencies: - browserslist: 4.25.0 + browserslist: 4.28.1 escalade: 3.2.0 picocolors: 1.1.1 update-notifier@7.3.1: dependencies: boxen: 8.0.1 - chalk: 5.4.1 - configstore: 7.0.0 + chalk: 5.6.2 + configstore: 7.1.0 is-in-ci: 1.0.0 is-installed-globally: 1.0.0 - is-npm: 6.0.0 + is-npm: 6.1.0 latest-version: 9.0.0 - pupa: 3.1.0 - semver: 7.7.2 + pupa: 3.3.0 + semver: 7.7.4 xdg-basedir: 5.1.0 uri-js@4.4.1: @@ -5704,7 +6041,7 @@ snapshots: v8-to-istanbul@9.3.0: dependencies: - '@jridgewell/trace-mapping': 0.3.25 + '@jridgewell/trace-mapping': 0.3.31 '@types/istanbul-lib-coverage': 2.0.6 convert-source-map: 2.0.0 @@ -5715,6 +6052,8 @@ snapshots: validate-npm-package-name@5.0.1: {} + walk-up-path@4.0.0: {} + walker@1.0.8: dependencies: makeerror: 1.0.12 @@ -5726,7 +6065,7 @@ snapshots: tr46: 0.0.3 webidl-conversions: 3.0.1 - when-exit@2.1.4: {} + when-exit@2.1.5: {} which@2.0.2: dependencies: @@ -5759,15 +6098,15 @@ snapshots: wrap-ansi@8.1.0: dependencies: - ansi-styles: 6.2.1 + ansi-styles: 6.2.3 string-width: 5.1.2 - strip-ansi: 7.1.0 + strip-ansi: 7.1.2 - wrap-ansi@9.0.0: + wrap-ansi@9.0.2: dependencies: - ansi-styles: 6.2.1 + ansi-styles: 6.2.3 string-width: 7.2.0 - strip-ansi: 7.1.0 + strip-ansi: 7.1.2 wrappy@1.0.2: {} @@ -5776,13 +6115,17 @@ snapshots: imurmurhash: 0.1.4 signal-exit: 3.0.7 + wsl-utils@0.1.0: + dependencies: + is-wsl: 3.1.0 + xdg-basedir@5.1.0: {} y18n@5.0.8: {} yallist@3.1.1: {} - yaml@2.8.0: {} + yaml@2.8.2: {} yargs-parser@21.1.1: {} @@ -5800,6 +6143,8 @@ snapshots: yocto-queue@0.1.0: {} - yoctocolors-cjs@2.1.2: {} + yoctocolors-cjs@2.1.3: {} + + yoctocolors@2.1.2: {} - yoctocolors@2.1.1: {} + zod@4.3.6: {} diff --git a/rules/rules-best-practices.mdc b/rules/rules-best-practices.mdc deleted file mode 100644 index 089d2d4..0000000 --- a/rules/rules-best-practices.mdc +++ /dev/null @@ -1,47 +0,0 @@ ---- -description: Best practices and formatting guidelines for writing effective rules -globs: rules/*.mdc -alwaysApply: false ---- - -## Rule Structure - -``` ---- -description: Clear, one-line description -globs: # Empty for alwaysApply: true, glob pattern for alwaysApply: false -alwaysApply: boolean ---- - -## Main Points as headers - - Sub-points with specifics -``` - -## Frontmatter Guidelines - -- **`alwaysApply: true`** requires **empty `globs`** key - - Use `alwaysApply: true` with `globs:` (no value) for rules that apply to entire codebase - - Use `alwaysApply: false` with `globs: **/*.ts` for rules targeting specific file types/paths - -## File References - - - Rules or Code: @rules/filename.mdc - -## Code Examples - -```typescript -// ✅ DO: Good pattern -const correct = true; - -// ❌ DON'T: Anti-pattern -const incorrect = false; -``` - -## Guidelines - -- Start with overview, then specifics -- Write concisely - include only what's necessary -- Use bullet points -- Include both DO/DON'T examples -- Reference existing code -- Do not repeat yourself by cross-referencing other rule file \ No newline at end of file diff --git a/rules/rules-maintenance.mdc b/rules/rules-maintenance.mdc deleted file mode 100644 index 2454cee..0000000 --- a/rules/rules-maintenance.mdc +++ /dev/null @@ -1,25 +0,0 @@ ---- -description: Guidelines for maintaining and updating rules based on codebase evolution -globs: -alwaysApply: true ---- - -## Update Triggers - -- New patterns in 3+ files -- Repeated implementations -- Common errors preventable by rules -- Consistent library usage -- Emerging best practices - -## Analysis - -- Compare code vs existing rules -- Identify standardization opportunities -- Check external doc references -- Monitor error/test patterns - -## Rule Updates - -- **Add:** New tech/pattern in 3+ files, preventable bugs, repeated code review feedback -- **Modify:** Better examples exist, new edge cases, related rule changes, implementation shifts \ No newline at end of file diff --git a/commands/review.md b/skills/review/SKILL.md similarity index 94% rename from commands/review.md rename to skills/review/SKILL.md index f805479..243461b 100644 --- a/commands/review.md +++ b/skills/review/SKILL.md @@ -1,3 +1,8 @@ +--- +name: review +description: Code review before commit - reviews changes for logical, structural, or quality issues. +--- + # Code Review Before Commit Review all current changes (typically all files are staged). Detect logical, structural, or quality issues, categorize them by severity, and ensure the code is ready to be committed. diff --git a/src/api.ts b/src/api.ts index b271058..d4ffd9a 100644 --- a/src/api.ts +++ b/src/api.ts @@ -2,32 +2,18 @@ import { install as installInternal } from "./commands/install"; import { InstallOptions, InstallResult } from "./commands/install"; import { checkWorkspacesEnabled as checkWorkspacesEnabledInternal } from "./utils/config"; -/** - * Install AICM rules based on configuration - * @param options Installation options - * @returns Result of the install operation - */ export async function install( options: InstallOptions = {}, ): Promise { return installInternal(options); } -/** - * Check if workspaces mode is enabled without loading all rules/presets - * @param cwd Current working directory (optional, defaults to process.cwd()) - * @returns True if workspaces mode is enabled - */ export async function checkWorkspacesEnabled(cwd?: string): Promise { return checkWorkspacesEnabledInternal(cwd); } export type { InstallOptions, InstallResult } from "./commands/install"; -export type { - ResolvedConfig, - Config, - RuleFile, - CommandFile, - MCPServers, -} from "./utils/config"; +export type { ResolvedConfig, Config, MCPServers } from "./utils/config"; +export type { InstructionFile } from "./utils/instructions"; export type { HookFile, HooksJson, HookType, HookCommand } from "./utils/hooks"; +export type { SkillFile, AgentFile } from "./utils/config"; diff --git a/src/cli.ts b/src/cli.ts index 305a8d8..3721f4a 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,17 +1,15 @@ -#!/usr/bin/env node - import arg from "arg"; import chalk from "chalk"; import { initCommand } from "./commands/init"; import { installCommand } from "./commands/install"; import { listCommand } from "./commands/list"; import { cleanCommand } from "./commands/clean"; +import { log } from "./utils/log"; -// Define version from package.json // eslint-disable-next-line @typescript-eslint/no-require-imports const pkg = require("../package.json"); -export async function runCli() { +export async function runCli(): Promise { const args = arg( { "--help": Boolean, @@ -28,13 +26,11 @@ export async function runCli() { }, ); - // Show version if (args["--version"]) { - console.log(pkg.version); + log.plain(pkg.version); process.exit(0); } - // Show help if (args["--help"]) { showHelp(); process.exit(0); @@ -70,8 +66,8 @@ export async function runCli() { } } -function showHelp() { - console.log(` +function showHelp(): void { + log.plain(` ${chalk.bold("aicm")} - A CLI tool for managing AI IDE configurations ${chalk.bold("USAGE")} @@ -79,8 +75,8 @@ ${chalk.bold("USAGE")} ${chalk.bold("COMMANDS")} init Initialize a new aicm configuration file - install Install rules from configured sources - list List all configured rules and their status + install Install instructions from configured sources + list List all configured instructions and their status clean Remove all files and directories created by aicm ${chalk.bold("OPTIONS")} @@ -98,13 +94,13 @@ ${chalk.bold("EXAMPLES")} `); } -function logError(error: unknown, verbose?: boolean) { +function logError(error: unknown, verbose?: boolean): void { if (error instanceof Error) { - console.error(chalk.red(`Error: ${error.message}`)); + log.error(`Error: ${error.message}`); if (verbose && error.stack) { console.error(chalk.gray(error.stack)); } } else { - console.error(chalk.red(`Error: ${String(error)}`)); + log.error(`Error: ${String(error)}`); } } diff --git a/src/commands/clean.ts b/src/commands/clean.ts index 3c74782..b0721f5 100644 --- a/src/commands/clean.ts +++ b/src/commands/clean.ts @@ -1,127 +1,223 @@ import chalk from "chalk"; import fs from "fs-extra"; import path from "node:path"; +import { parse as parseToml, stringify as stringifyToml } from "smol-toml"; import { checkWorkspacesEnabled } from "../utils/config"; import { withWorkingDirectory } from "../utils/working-directory"; -import { removeRulesBlock } from "../utils/rules-file-writer"; +import { removeInstructionsBlock } from "../utils/instructions-file"; import { discoverPackagesWithAicm } from "../utils/workspace-discovery"; +import { log } from "../utils/log"; -export interface CleanOptions { - /** - * Base directory to use instead of process.cwd() - */ - cwd?: string; - /** - * Show verbose output - */ - verbose?: boolean; -} - -export interface CleanResult { +interface CleanResult { success: boolean; - error?: Error; cleanedCount: number; } function cleanFile(filePath: string, verbose: boolean): boolean { if (!fs.existsSync(filePath)) return false; - try { fs.removeSync(filePath); - if (verbose) console.log(chalk.gray(` Removed ${filePath}`)); + if (verbose) log.info(chalk.gray(` Removed ${filePath}`)); return true; } catch { - console.warn(chalk.yellow(`Warning: Failed to remove ${filePath}`)); + log.warn(`Warning: Failed to remove ${filePath}`); return false; } } -function cleanRulesBlock(filePath: string, verbose: boolean): boolean { +function cleanInstructionsBlock(filePath: string, verbose: boolean): boolean { if (!fs.existsSync(filePath)) return false; - try { const content = fs.readFileSync(filePath, "utf8"); - const cleanedContent = removeRulesBlock(content); - - if (content === cleanedContent) return false; + const cleaned = removeInstructionsBlock(content); + if (content === cleaned) return false; - if (cleanedContent.trim() === "") { + if (cleaned.trim() === "") { fs.removeSync(filePath); - if (verbose) console.log(chalk.gray(` Removed empty file ${filePath}`)); + if (verbose) log.info(chalk.gray(` Removed empty file ${filePath}`)); } else { - fs.writeFileSync(filePath, cleanedContent); + fs.writeFileSync(filePath, cleaned); if (verbose) - console.log(chalk.gray(` Cleaned rules block from ${filePath}`)); + log.info(chalk.gray(` Cleaned instructions block from ${filePath}`)); } return true; } catch { - console.warn(chalk.yellow(`Warning: Failed to clean ${filePath}`)); + log.warn(`Warning: Failed to clean ${filePath}`); return false; } } function cleanMcpServers(cwd: string, verbose: boolean): boolean { - const mcpPath = path.join(cwd, ".cursor", "mcp.json"); + const mcpPaths = [ + path.join(cwd, ".cursor", "mcp.json"), + path.join(cwd, ".mcp.json"), + ]; + let cleanedAny = false; + + for (const mcpPath of mcpPaths) { + if (!fs.existsSync(mcpPath)) continue; + try { + const content = fs.readJsonSync(mcpPath); + const servers = content.mcpServers; + if (!servers) continue; + + let hasChanges = false; + const userServers: Record = {}; + for (const [key, value] of Object.entries(servers)) { + if ( + typeof value === "object" && + value !== null && + (value as Record).aicm === true + ) { + hasChanges = true; + } else { + userServers[key] = value; + } + } + + if (!hasChanges) continue; + + if ( + Object.keys(userServers).length === 0 && + Object.keys(content).length === 1 + ) { + fs.removeSync(mcpPath); + if (verbose) log.info(chalk.gray(` Removed empty ${mcpPath}`)); + } else { + content.mcpServers = userServers; + fs.writeJsonSync(mcpPath, content, { spaces: 2 }); + if (verbose) + log.info(chalk.gray(` Cleaned aicm MCP servers from ${mcpPath}`)); + } + cleanedAny = true; + } catch { + log.warn(`Warning: Failed to clean MCP servers`); + } + } + + return cleanedAny; +} + +function cleanOpenCodeMcp(cwd: string, verbose: boolean): boolean { + const mcpPath = path.join(cwd, "opencode.json"); if (!fs.existsSync(mcpPath)) return false; try { const content = fs.readJsonSync(mcpPath); - const mcpServers = content.mcpServers; - - if (!mcpServers) return false; + const mcp = content.mcp as + | Record> + | undefined; + if (!mcp) return false; let hasChanges = false; - const newMcpServers: Record = {}; - - for (const [key, value] of Object.entries(mcpServers)) { + const userServers: Record = {}; + for (const [key, value] of Object.entries(mcp)) { if ( typeof value === "object" && value !== null && - "aicm" in value && - value.aicm === true + (value as Record).aicm === true ) { hasChanges = true; } else { - newMcpServers[key] = value; + userServers[key] = value; } } if (!hasChanges) return false; - // If no servers remain and no other properties, remove the file if ( - Object.keys(newMcpServers).length === 0 && + Object.keys(userServers).length === 0 && Object.keys(content).length === 1 ) { fs.removeSync(mcpPath); - if (verbose) console.log(chalk.gray(` Removed empty ${mcpPath}`)); + if (verbose) log.info(chalk.gray(` Removed empty ${mcpPath}`)); } else { - content.mcpServers = newMcpServers; + content.mcp = userServers; fs.writeJsonSync(mcpPath, content, { spaces: 2 }); if (verbose) - console.log(chalk.gray(` Cleaned aicm MCP servers from ${mcpPath}`)); + log.info(chalk.gray(` Cleaned aicm MCP servers from ${mcpPath}`)); } return true; } catch { - console.warn(chalk.yellow(`Warning: Failed to clean MCP servers`)); + log.warn(`Warning: Failed to clean OpenCode MCP servers`); return false; } } -function cleanHooks(cwd: string, verbose: boolean): boolean { +function cleanCodexMcp(cwd: string, verbose: boolean): boolean { + const mcpPath = path.join(cwd, ".codex", "config.toml"); + if (!fs.existsSync(mcpPath)) return false; + + try { + const rawContent = fs.readFileSync(mcpPath, "utf8"); + + // Detect aicm-managed servers from comment markers + const managedServerNames = new Set(); + const lines = rawContent.split("\n"); + for (let i = 0; i < lines.length; i++) { + if (lines[i].trim() === "# aicm:managed") { + const match = lines[i + 1]?.match(/^\[mcp_servers\.("?)(.+)\1\]$/); + if (match) managedServerNames.add(match[2]); + } + } + + const config = parseToml(rawContent) as Record; + const mcpServers = config.mcp_servers as + | Record> + | undefined; + if (!mcpServers) return false; + + // Also detect legacy aicm property marker + for (const [key, value] of Object.entries(mcpServers)) { + if ( + typeof value === "object" && + value !== null && + (value as Record).aicm === true + ) { + managedServerNames.add(key); + } + } + + if (managedServerNames.size === 0) return false; + + const userServers: Record> = {}; + for (const [key, value] of Object.entries(mcpServers)) { + if (!managedServerNames.has(key)) { + userServers[key] = value as Record; + } + } + + if ( + Object.keys(userServers).length === 0 && + Object.keys(config).length === 1 + ) { + fs.removeSync(mcpPath); + if (verbose) log.info(chalk.gray(` Removed empty ${mcpPath}`)); + } else { + config.mcp_servers = userServers; + const tomlContent = stringifyToml(config); + fs.writeFileSync(mcpPath, tomlContent); + if (verbose) + log.info(chalk.gray(` Cleaned aicm MCP servers from ${mcpPath}`)); + } + return true; + } catch { + log.warn(`Warning: Failed to clean Codex MCP servers`); + return false; + } +} + +function cleanCursorHooks(cwd: string, verbose: boolean): boolean { const hooksJsonPath = path.join(cwd, ".cursor", "hooks.json"); const hooksDir = path.join(cwd, ".cursor", "hooks", "aicm"); - let hasChanges = false; - // Clean hooks directory if (fs.existsSync(hooksDir)) { fs.removeSync(hooksDir); - if (verbose) console.log(chalk.gray(` Removed ${hooksDir}`)); + if (verbose) log.info(chalk.gray(` Removed ${hooksDir}`)); hasChanges = true; } - // Clean hooks.json if (fs.existsSync(hooksJsonPath)) { try { const content: { @@ -129,7 +225,6 @@ function cleanHooks(cwd: string, verbose: boolean): boolean { hooks?: Record>; } = fs.readJsonSync(hooksJsonPath); - // Filter out aicm-managed hooks (those pointing to hooks/aicm/) const userConfig: typeof content = { version: content.version || 1, hooks: {}, @@ -142,11 +237,7 @@ function cleanHooks(cwd: string, verbose: boolean): boolean { const userCommands = hookCommands.filter( (cmd) => !cmd.command || !cmd.command.includes("hooks/aicm/"), ); - - if (userCommands.length < hookCommands.length) { - removedAny = true; - } - + if (userCommands.length < hookCommands.length) removedAny = true; if (userCommands.length > 0) { userConfig.hooks![hookType] = userCommands; } @@ -157,157 +248,198 @@ function cleanHooks(cwd: string, verbose: boolean): boolean { if (removedAny) { const hasUserHooks = userConfig.hooks && Object.keys(userConfig.hooks).length > 0; - if (!hasUserHooks) { fs.removeSync(hooksJsonPath); - if (verbose) - console.log(chalk.gray(` Removed empty ${hooksJsonPath}`)); + if (verbose) log.info(chalk.gray(` Removed empty ${hooksJsonPath}`)); } else { fs.writeJsonSync(hooksJsonPath, userConfig, { spaces: 2 }); if (verbose) - console.log( - chalk.gray(` Cleaned aicm hooks from ${hooksJsonPath}`), - ); + log.info(chalk.gray(` Cleaned aicm hooks from ${hooksJsonPath}`)); } hasChanges = true; } } catch { - console.warn(chalk.yellow(`Warning: Failed to clean hooks.json`)); + log.warn(`Warning: Failed to clean hooks.json`); + } + } + + return hasChanges; +} + +function cleanClaudeCodeHooks(cwd: string, verbose: boolean): boolean { + const settingsPath = path.join(cwd, ".claude", "settings.json"); + const hooksDir = path.join(cwd, ".claude", "hooks", "aicm"); + let hasChanges = false; + + if (fs.existsSync(hooksDir)) { + fs.removeSync(hooksDir); + if (verbose) log.info(chalk.gray(` Removed ${hooksDir}`)); + hasChanges = true; + } + + if (fs.existsSync(settingsPath)) { + try { + const settings: Record = fs.readJsonSync(settingsPath); + const hooks = settings.hooks as Record | undefined; + + if (hooks) { + const userHooks: Record = {}; + let removedAny = false; + + for (const [eventName, matcherGroups] of Object.entries(hooks)) { + if (Array.isArray(matcherGroups)) { + const userGroups = matcherGroups.filter((group) => { + if (typeof group !== "object" || group === null) return true; + const g = group as Record; + if (!Array.isArray(g.hooks)) return true; + return !g.hooks.some( + (h: unknown) => + typeof h === "object" && + h !== null && + typeof (h as Record).command === "string" && + ((h as Record).command as string).includes( + "hooks/aicm/", + ), + ); + }); + if (userGroups.length < matcherGroups.length) removedAny = true; + if (userGroups.length > 0) userHooks[eventName] = userGroups; + } + } + + if (removedAny) { + if (Object.keys(userHooks).length > 0) { + settings.hooks = userHooks; + } else { + delete settings.hooks; + } + + if (Object.keys(settings).length === 0) { + fs.removeSync(settingsPath); + if (verbose) + log.info(chalk.gray(` Removed empty ${settingsPath}`)); + } else { + fs.writeJsonSync(settingsPath, settings, { spaces: 2 }); + if (verbose) + log.info(chalk.gray(` Cleaned aicm hooks from ${settingsPath}`)); + } + hasChanges = true; + } + } + } catch { + log.warn(`Warning: Failed to clean Claude Code settings.json`); } } return hasChanges; } -/** - * Clean aicm-managed skills from a skills directory - * Only removes skills that have .aicm.json (presence indicates aicm management) - */ function cleanSkills(cwd: string, verbose: boolean): number { let cleanedCount = 0; - - // Skills directories for each target const skillsDirs = [ + path.join(cwd, ".agents", "skills"), path.join(cwd, ".cursor", "skills"), path.join(cwd, ".claude", "skills"), - path.join(cwd, ".codex", "skills"), + path.join(cwd, ".opencode", "skills"), ]; - for (const skillsDir of skillsDirs) { - if (!fs.existsSync(skillsDir)) { - continue; - } - + for (const dir of skillsDirs) { + if (!fs.existsSync(dir)) continue; try { - const entries = fs.readdirSync(skillsDir, { withFileTypes: true }); - + const entries = fs.readdirSync(dir, { withFileTypes: true }); for (const entry of entries) { - if (!entry.isDirectory()) { + if (!entry.isDirectory()) continue; + const skillPath = path.join(dir, entry.name); + if (fs.existsSync(path.join(skillPath, ".aicm.json"))) { + fs.removeSync(skillPath); + if (verbose) log.info(chalk.gray(` Removed skill ${skillPath}`)); + cleanedCount++; continue; } - const skillPath = path.join(skillsDir, entry.name); - const metadataPath = path.join(skillPath, ".aicm.json"); - - // Only clean skills that have .aicm.json (presence indicates aicm management) - if (fs.existsSync(metadataPath)) { - fs.removeSync(skillPath); - if (verbose) { - console.log(chalk.gray(` Removed skill ${skillPath}`)); + // LEGACY(v0->v1): clean old namespaced skill layout (/skills/aicm//). + // Keep this block temporarily so `aicm clean` can remove pre-migration installs. + if ( + entry.name === "aicm" && + !fs.existsSync(path.join(skillPath, "SKILL.md")) + ) { + const namespacedEntries = fs.readdirSync(skillPath, { + withFileTypes: true, + }); + for (const namespacedEntry of namespacedEntries) { + if (!namespacedEntry.isDirectory()) continue; + const namespacedSkillPath = path.join( + skillPath, + namespacedEntry.name, + ); + if (fs.existsSync(path.join(namespacedSkillPath, ".aicm.json"))) { + fs.removeSync(namespacedSkillPath); + if (verbose) + log.info(chalk.gray(` Removed skill ${namespacedSkillPath}`)); + cleanedCount++; + } + } + if ( + fs.existsSync(skillPath) && + fs.readdirSync(skillPath).length === 0 + ) { + fs.removeSync(skillPath); + if (verbose) log.info(chalk.gray(` Removed ${skillPath}`)); } - cleanedCount++; } } - - // Remove the skills directory if it's now empty - const remainingEntries = fs.readdirSync(skillsDir); - if (remainingEntries.length === 0) { - fs.removeSync(skillsDir); - if (verbose) { - console.log(chalk.gray(` Removed empty directory ${skillsDir}`)); - } + if (fs.readdirSync(dir).length === 0) { + fs.removeSync(dir); + if (verbose) log.info(chalk.gray(` Removed empty directory ${dir}`)); } } catch { - console.warn( - chalk.yellow(`Warning: Failed to clean skills in ${skillsDir}`), - ); + log.warn(`Warning: Failed to clean skills in ${dir}`); } } return cleanedCount; } -/** - * Metadata file structure for tracking aicm-managed agents - */ -interface AgentsAicmMetadata { - managedAgents: string[]; // List of agent names (without path or extension) -} - -/** - * Clean aicm-managed agents from agents directories - * Only removes agents that are tracked in .aicm.json metadata file - */ function cleanAgents(cwd: string, verbose: boolean): number { let cleanedCount = 0; - - // Agents directories for each target const agentsDirs = [ + path.join(cwd, ".agents", "agents"), path.join(cwd, ".cursor", "agents"), path.join(cwd, ".claude", "agents"), + path.join(cwd, ".opencode", "agents"), ]; - for (const agentsDir of agentsDirs) { - const metadataPath = path.join(agentsDir, ".aicm.json"); - - if (!fs.existsSync(metadataPath)) { - continue; - } + for (const dir of agentsDirs) { + const metadataPath = path.join(dir, ".aicm.json"); + if (!fs.existsSync(metadataPath)) continue; try { - const metadata: AgentsAicmMetadata = fs.readJsonSync(metadataPath); - - // Remove all managed agents (names only) + const metadata = fs.readJsonSync(metadataPath) as { + managedAgents?: string[]; + }; for (const agentName of metadata.managedAgents || []) { - // Skip invalid names containing path separators (security check) if (agentName.includes("/") || agentName.includes("\\")) { - console.warn( - chalk.yellow( - `Warning: Skipping invalid agent name "${agentName}" (contains path separator)`, - ), + log.warn( + `Warning: Skipping invalid agent name "${agentName}" (contains path separator)`, ); continue; } - const fullPath = path.join(agentsDir, agentName + ".md"); + const fullPath = path.join(dir, agentName + ".md"); if (fs.existsSync(fullPath)) { fs.removeSync(fullPath); - if (verbose) { - console.log(chalk.gray(` Removed agent ${fullPath}`)); - } + if (verbose) log.info(chalk.gray(` Removed agent ${fullPath}`)); cleanedCount++; } } - - // Remove the metadata file fs.removeSync(metadataPath); - if (verbose) { - console.log(chalk.gray(` Removed ${metadataPath}`)); - } + if (verbose) log.info(chalk.gray(` Removed ${metadataPath}`)); - // Remove the agents directory if it's now empty - if (fs.existsSync(agentsDir)) { - const remainingEntries = fs.readdirSync(agentsDir); - if (remainingEntries.length === 0) { - fs.removeSync(agentsDir); - if (verbose) { - console.log(chalk.gray(` Removed empty directory ${agentsDir}`)); - } - } + if (fs.existsSync(dir) && fs.readdirSync(dir).length === 0) { + fs.removeSync(dir); + if (verbose) log.info(chalk.gray(` Removed empty directory ${dir}`)); } } catch { - console.warn( - chalk.yellow(`Warning: Failed to clean agents in ${agentsDir}`), - ); + log.warn(`Warning: Failed to clean agents in ${dir}`); } } @@ -316,159 +448,116 @@ function cleanAgents(cwd: string, verbose: boolean): number { function cleanEmptyDirectories(cwd: string, verbose: boolean): number { let cleanedCount = 0; - const dirsToCheck = [ - path.join(cwd, ".cursor", "rules"), - path.join(cwd, ".cursor", "commands"), - path.join(cwd, ".cursor", "assets"), path.join(cwd, ".cursor", "hooks"), path.join(cwd, ".cursor", "skills"), path.join(cwd, ".cursor", "agents"), path.join(cwd, ".cursor"), + path.join(cwd, ".agents", "skills"), + path.join(cwd, ".agents", "agents"), + path.join(cwd, ".agents", "instructions"), + path.join(cwd, ".agents"), + path.join(cwd, ".claude", "hooks"), path.join(cwd, ".claude", "skills"), path.join(cwd, ".claude", "agents"), path.join(cwd, ".claude"), - path.join(cwd, ".codex", "skills"), + path.join(cwd, ".opencode", "skills"), + path.join(cwd, ".opencode", "agents"), + path.join(cwd, ".opencode"), path.join(cwd, ".codex"), ]; for (const dir of dirsToCheck) { - if (fs.existsSync(dir)) { - try { - const contents = fs.readdirSync(dir); - if (contents.length === 0) { - fs.removeSync(dir); - if (verbose) - console.log(chalk.gray(` Removed empty directory ${dir}`)); - cleanedCount++; - } - } catch { - // Ignore errors when checking/removing empty directories + if (!fs.existsSync(dir)) continue; + try { + if (fs.readdirSync(dir).length === 0) { + fs.removeSync(dir); + if (verbose) log.info(chalk.gray(` Removed empty directory ${dir}`)); + cleanedCount++; } + } catch { + // ignore } } return cleanedCount; } -export async function cleanPackage( - options: CleanOptions = {}, +async function cleanPackage( + cwd: string, + verbose: boolean, ): Promise { - const cwd = options.cwd || process.cwd(); - const verbose = options.verbose || false; - return withWorkingDirectory(cwd, async () => { let cleanedCount = 0; - const filesToClean = [ - path.join(cwd, ".cursor", "rules", "aicm"), - path.join(cwd, ".cursor", "commands", "aicm"), - path.join(cwd, ".cursor", "assets", "aicm"), - path.join(cwd, ".aicm"), - ]; - - const rulesFilesToClean = [ - path.join(cwd, ".windsurfrules"), - path.join(cwd, "AGENTS.md"), - path.join(cwd, "CLAUDE.md"), - ]; - - // Clean directories and files - for (const file of filesToClean) { - if (cleanFile(file, verbose)) cleanedCount++; - } - - // Clean rules blocks from files - for (const file of rulesFilesToClean) { - if (cleanRulesBlock(file, verbose)) cleanedCount++; + if (cleanFile(path.join(cwd, ".agents", "instructions"), verbose)) + cleanedCount++; + + if (cleanInstructionsBlock(path.join(cwd, "AGENTS.md"), verbose)) + cleanedCount++; + + // For CLAUDE.md: if it's only the @AGENTS.md pointer created by aicm, remove entirely + const claudeMdPath = path.join(cwd, "CLAUDE.md"); + if (fs.existsSync(claudeMdPath)) { + const claudeContent = fs.readFileSync(claudeMdPath, "utf8"); + if (claudeContent.trim() === "@AGENTS.md") { + fs.removeSync(claudeMdPath); + if (verbose) + log.info(chalk.gray(` Removed pointer file ${claudeMdPath}`)); + cleanedCount++; + } else if (cleanInstructionsBlock(claudeMdPath, verbose)) { + cleanedCount++; + } } - // Clean MCP servers if (cleanMcpServers(cwd, verbose)) cleanedCount++; - - // Clean hooks - if (cleanHooks(cwd, verbose)) cleanedCount++; - - // Clean skills + if (cleanOpenCodeMcp(cwd, verbose)) cleanedCount++; + if (cleanCodexMcp(cwd, verbose)) cleanedCount++; + if (cleanCursorHooks(cwd, verbose)) cleanedCount++; + if (cleanClaudeCodeHooks(cwd, verbose)) cleanedCount++; cleanedCount += cleanSkills(cwd, verbose); - - // Clean agents cleanedCount += cleanAgents(cwd, verbose); - - // Clean empty directories cleanedCount += cleanEmptyDirectories(cwd, verbose); - return { - success: true, - cleanedCount, - }; + return { success: true, cleanedCount }; }); } -export async function cleanWorkspaces( +async function cleanWorkspaces( cwd: string, - verbose: boolean = false, + verbose: boolean, ): Promise { - if (verbose) console.log(chalk.blue("🔍 Discovering packages...")); - const packages = await discoverPackagesWithAicm(cwd); - - if (verbose && packages.length > 0) { - console.log( - chalk.blue(`Found ${packages.length} packages with aicm configurations.`), - ); - } - let totalCleaned = 0; - // Clean all discovered packages for (const pkg of packages) { - if (verbose) - console.log(chalk.blue(`Cleaning package: ${pkg.relativePath}`)); - - const result = await cleanPackage({ - cwd: pkg.absolutePath, - verbose, - }); - + if (verbose) log.info(chalk.blue(`Cleaning package: ${pkg.relativePath}`)); + const result = await cleanPackage(pkg.absolutePath, verbose); totalCleaned += result.cleanedCount; } - // Always clean root directory (for merged artifacts like mcp.json and commands) const rootPackage = packages.find((p) => p.absolutePath === cwd); if (!rootPackage) { - if (verbose) - console.log(chalk.blue(`Cleaning root workspace artifacts...`)); - const rootResult = await cleanPackage({ cwd, verbose }); + const rootResult = await cleanPackage(cwd, verbose); totalCleaned += rootResult.cleanedCount; } - return { - success: true, - cleanedCount: totalCleaned, - }; + return { success: true, cleanedCount: totalCleaned }; } -export async function clean(options: CleanOptions = {}): Promise { - const cwd = options.cwd || process.cwd(); - const verbose = options.verbose || false; +export async function cleanCommand(verbose?: boolean): Promise { + const cwd = process.cwd(); + const v = verbose || false; const shouldUseWorkspaces = await checkWorkspacesEnabled(cwd); - - if (shouldUseWorkspaces) { - return cleanWorkspaces(cwd, verbose); - } - - return cleanPackage(options); -} - -export async function cleanCommand(verbose?: boolean): Promise { - const result = await clean({ verbose }); + const result = shouldUseWorkspaces + ? await cleanWorkspaces(cwd, v) + : await cleanPackage(cwd, v); if (result.cleanedCount === 0) { - console.log("Nothing to clean."); + log.info("Nothing to clean."); } else { - console.log( + log.info( chalk.green( `Successfully cleaned ${result.cleanedCount} file(s)/director(y/ies).`, ), diff --git a/src/commands/init.ts b/src/commands/init.ts index 99db98a..bfa60e0 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -1,54 +1,56 @@ import fs from "fs-extra"; -import path from "path"; +import path from "node:path"; import chalk from "chalk"; +import { log } from "../utils/log"; -const defaultConfig = { +const DEFAULT_CONFIG = { rootDir: "./", - targets: ["cursor"], + targets: ["cursor", "claude-code"], }; export function initCommand(): void { const configPath = path.join(process.cwd(), "aicm.json"); if (fs.existsSync(configPath)) { - console.log(chalk.yellow("Configuration file already exists!")); + log.info(chalk.yellow("Configuration file already exists!")); return; } - try { - // Create standard directory structure - const dirs = ["rules", "commands", "assets", "hooks"]; - for (const dir of dirs) { - const dirPath = path.join(process.cwd(), dir); - if (!fs.existsSync(dirPath)) { - fs.mkdirSync(dirPath, { recursive: true }); - } - } - - // Create placeholder file in rules directory - const rulesReadmePath = path.join(process.cwd(), "rules", ".gitkeep"); - if (!fs.existsSync(rulesReadmePath)) { - fs.writeFileSync(rulesReadmePath, "# Place your .mdc rule files here\n"); + // Create optional directories + const dirs = ["skills", "agents", "hooks"]; + for (const dir of dirs) { + const dirPath = path.join(process.cwd(), dir); + if (!fs.existsSync(dirPath)) { + fs.mkdirSync(dirPath, { recursive: true }); } + } - fs.writeJsonSync(configPath, defaultConfig, { spaces: 2 }); - console.log(`Configuration file location: ${chalk.blue(configPath)}`); - console.log(`\nCreated directory structure:`); - console.log(` - ${chalk.blue("rules/")} for rule files (.mdc)`); - console.log(` - ${chalk.blue("commands/")} for command files (.md)`); - console.log(` - ${chalk.blue("assets/")} for auxiliary files`); - console.log(` - ${chalk.blue("hooks/")} for hook scripts`); - console.log(`\nNext steps:`); - console.log( - ` 1. Add your rule files to ${chalk.blue("rules/")} directory`, + // Create placeholder AGENTS.src.md + const srcPath = path.join(process.cwd(), "AGENTS.src.md"); + if (!fs.existsSync(srcPath)) { + fs.writeFileSync( + srcPath, + "## Project Instructions\n\n- Add your instructions here.\n", ); - console.log( - ` 2. Edit ${chalk.blue("aicm.json")} to configure presets if needed`, - ); - console.log( - ` 3. Run ${chalk.blue("npx aicm install")} to install rules & mcps`, - ); - } catch (error) { - console.error(chalk.red("Error creating configuration file:"), error); } + + fs.writeJsonSync(configPath, DEFAULT_CONFIG, { spaces: 2 }); + log.info(`Configuration file location: ${chalk.blue(configPath)}`); + log.info(`\nCreated files:`); + log.info( + ` - ${chalk.blue("AGENTS.src.md")} — your project instructions (source)`, + ); + log.info(` - ${chalk.blue("skills/")} for skill directories`); + log.info(` - ${chalk.blue("agents/")} for agent definitions`); + log.info(` - ${chalk.blue("hooks/")} for hook scripts`); + log.info(`\nNext steps:`); + log.info( + ` 1. Edit ${chalk.blue("AGENTS.src.md")} with your project's conventions`, + ); + log.info( + ` 2. Edit ${chalk.blue("aicm.json")} to configure presets if needed`, + ); + log.info( + ` 3. Run ${chalk.blue("npx aicm install")} to generate AGENTS.md & CLAUDE.md`, + ); } diff --git a/src/commands/install-workspaces.ts b/src/commands/install-workspaces.ts index ceffa03..cc1aad1 100644 --- a/src/commands/install-workspaces.ts +++ b/src/commands/install-workspaces.ts @@ -2,11 +2,9 @@ import chalk from "chalk"; import path from "node:path"; import { ResolvedConfig, - CommandFile, SkillFile, AgentFile, MCPServers, - SupportedTarget, } from "../utils/config"; import { HookFile, @@ -21,220 +19,85 @@ import { installPackage, InstallOptions, InstallResult, - writeCommandsToTargets, - writeAssetsToTargets, writeSkillsToTargets, - writeAgentsToTargets, - warnPresetCommandCollisions, + writeSubagentsToTargets, warnPresetSkillCollisions, warnPresetAgentCollisions, - dedupeCommandsForInstall, dedupeSkillsForInstall, dedupeAgentsForInstall, writeMcpServersToFile, + writeMcpServersToOpenCode, + writeMcpServersToCodex, } from "./install"; +import { log } from "../utils/log"; -function mergeWorkspaceCommands( - packages: Array<{ - relativePath: string; - config: ResolvedConfig; - }>, -): CommandFile[] { - const commands: CommandFile[] = []; - const seenPresetCommands = new Set(); +type PkgInfo = { relativePath: string; config: ResolvedConfig }; +function collectTargets( + packages: PkgInfo[], + key: keyof ResolvedConfig["config"]["targets"], +): string[] { + const targets = new Set(); for (const pkg of packages) { - const hasCursorTarget = pkg.config.config.targets.includes("cursor"); - if (!hasCursorTarget) { - continue; - } - - for (const command of pkg.config.commands ?? []) { - if (command.presetName) { - const presetKey = `${command.presetName}::${command.name}`; - if (seenPresetCommands.has(presetKey)) { - continue; - } - seenPresetCommands.add(presetKey); - } - - commands.push(command); - } + for (const t of pkg.config.config.targets[key]) targets.add(t); } - - return commands; -} - -function collectWorkspaceCommandTargets( - packages: Array<{ - relativePath: string; - config: ResolvedConfig; - }>, -): SupportedTarget[] { - const targets = new Set(); - - for (const pkg of packages) { - if (pkg.config.config.targets.includes("cursor")) { - targets.add("cursor"); - } - } - return Array.from(targets); } -/** - * Merge skills from multiple workspace packages - * Skills are merged flat (not namespaced by preset) - * Dedupes preset skills that appear in multiple packages - */ -function mergeWorkspaceSkills( - packages: Array<{ - relativePath: string; - config: ResolvedConfig; - }>, -): SkillFile[] { +function mergeWorkspaceSkills(packages: PkgInfo[]): SkillFile[] { const skills: SkillFile[] = []; - const seenPresetSkills = new Set(); - + const seen = new Set(); for (const pkg of packages) { - // Skills are supported by cursor, claude, and codex targets - const hasSkillsTarget = - pkg.config.config.targets.includes("cursor") || - pkg.config.config.targets.includes("claude") || - pkg.config.config.targets.includes("codex"); - - if (!hasSkillsTarget) { - continue; - } - + if (pkg.config.config.targets.skills.length === 0) continue; for (const skill of pkg.config.skills ?? []) { if (skill.presetName) { - // Dedupe preset skills by preset+name combination - const presetKey = `${skill.presetName}::${skill.name}`; - if (seenPresetSkills.has(presetKey)) { - continue; - } - seenPresetSkills.add(presetKey); + const key = `${skill.presetName}::${skill.name}`; + if (seen.has(key)) continue; + seen.add(key); } - skills.push(skill); } } - return skills; } -/** - * Collect all targets that support skills from workspace packages - */ -function collectWorkspaceSkillTargets( - packages: Array<{ - relativePath: string; - config: ResolvedConfig; - }>, -): SupportedTarget[] { - const targets = new Set(); - - for (const pkg of packages) { - for (const target of pkg.config.config.targets) { - // Skills are supported by cursor, claude, and codex - if (target === "cursor" || target === "claude" || target === "codex") { - targets.add(target as SupportedTarget); - } - } - } - - return Array.from(targets); -} - -/** - * Merge agents from multiple workspace packages - * Agents are merged flat (not namespaced by preset) - * Dedupes preset agents that appear in multiple packages - */ -function mergeWorkspaceAgents( - packages: Array<{ - relativePath: string; - config: ResolvedConfig; - }>, -): AgentFile[] { +function mergeWorkspaceAgents(packages: PkgInfo[]): AgentFile[] { const agents: AgentFile[] = []; - const seenPresetAgents = new Set(); - + const seen = new Set(); for (const pkg of packages) { - // Agents are supported by cursor and claude targets - const hasAgentsTarget = - pkg.config.config.targets.includes("cursor") || - pkg.config.config.targets.includes("claude"); - - if (!hasAgentsTarget) { - continue; - } - + if (pkg.config.config.targets.agents.length === 0) continue; for (const agent of pkg.config.agents ?? []) { if (agent.presetName) { - // Dedupe preset agents by preset+name combination - const presetKey = `${agent.presetName}::${agent.name}`; - if (seenPresetAgents.has(presetKey)) { - continue; - } - seenPresetAgents.add(presetKey); + const key = `${agent.presetName}::${agent.name}`; + if (seen.has(key)) continue; + seen.add(key); } - agents.push(agent); } } - return agents; } -/** - * Collect all targets that support agents from workspace packages - */ -function collectWorkspaceAgentTargets( - packages: Array<{ - relativePath: string; - config: ResolvedConfig; - }>, -): SupportedTarget[] { - const targets = new Set(); - - for (const pkg of packages) { - for (const target of pkg.config.config.targets) { - // Agents are supported by cursor and claude - if (target === "cursor" || target === "claude") { - targets.add(target as SupportedTarget); - } - } - } - - return Array.from(targets); -} - interface MergeConflict { key: string; packages: string[]; chosen: string; } -function mergeWorkspaceMcpServers( - packages: Array<{ relativePath: string; config: ResolvedConfig }>, -): { merged: MCPServers; conflicts: MergeConflict[] } { +function mergeWorkspaceMcpServers(packages: PkgInfo[]): { + merged: MCPServers; + conflicts: MergeConflict[]; +} { const merged: MCPServers = {}; const info: Record< string, - { - configs: Set; - packages: string[]; - chosen: string; - } + { configs: Set; packages: string[]; chosen: string } > = {}; for (const pkg of packages) { for (const [key, value] of Object.entries(pkg.config.mcpServers)) { if (value === false) continue; const json = JSON.stringify(value); - if (!info[key]) { info[key] = { configs: new Set([json]), @@ -246,13 +109,11 @@ function mergeWorkspaceMcpServers( info[key].configs.add(json); info[key].chosen = pkg.relativePath; } - merged[key] = value; } } const conflicts: MergeConflict[] = []; - for (const [key, data] of Object.entries(info)) { if (data.configs.size > 1) { conflicts.push({ key, packages: data.packages, chosen: data.chosen }); @@ -262,60 +123,43 @@ function mergeWorkspaceMcpServers( return { merged, conflicts }; } -/** - * Merge hooks from multiple workspace packages - */ -function mergeWorkspaceHooks( - packages: Array<{ relativePath: string; config: ResolvedConfig }>, -): { merged: HooksJson; hookFiles: HookFile[] } { - const allHooksConfigs: HooksJson[] = []; - const allHookFiles: HookFile[] = []; +function mergeWorkspaceHooks(packages: PkgInfo[]): { + merged: HooksJson; + hookFiles: HookFile[]; +} { + const allConfigs: HooksJson[] = []; + const allFiles: HookFile[] = []; for (const pkg of packages) { - // Collect hooks configs - if (pkg.config.hooks) { - allHooksConfigs.push(pkg.config.hooks); - } - - // Collect hook files - allHookFiles.push(...pkg.config.hookFiles); + if (pkg.config.hooks) allConfigs.push(pkg.config.hooks); + allFiles.push(...pkg.config.hookFiles); } - // Merge hooks configs - const merged = mergeHooksConfigs(allHooksConfigs); - - // Dedupe hook files by basename with MD5 checking - const dedupedHookFiles = dedupeHookFiles(allHookFiles); - - return { merged, hookFiles: dedupedHookFiles }; + return { + merged: mergeHooksConfigs(allConfigs), + hookFiles: dedupeHookFiles(allFiles), + }; } -/** - * Install aicm configurations for all packages in a workspace - */ async function installWorkspacesPackages( packages: Array<{ relativePath: string; absolutePath: string; config: ResolvedConfig; }>, - options: InstallOptions = {}, + options: InstallOptions, ): Promise<{ success: boolean; packages: Array<{ path: string; success: boolean; error?: Error; - installedRuleCount: number; - installedCommandCount: number; - installedAssetCount: number; + installedInstructionCount: number; installedHookCount: number; installedSkillCount: number; installedAgentCount: number; }>; - totalRuleCount: number; - totalCommandCount: number; - totalAssetCount: number; + totalInstructionCount: number; totalHookCount: number; totalSkillCount: number; totalAgentCount: number; @@ -324,45 +168,32 @@ async function installWorkspacesPackages( path: string; success: boolean; error?: Error; - installedRuleCount: number; - installedCommandCount: number; - installedAssetCount: number; + installedInstructionCount: number; installedHookCount: number; installedSkillCount: number; installedAgentCount: number; }> = []; - let totalRuleCount = 0; - let totalCommandCount = 0; - let totalAssetCount = 0; - let totalHookCount = 0; - let totalSkillCount = 0; - let totalAgentCount = 0; + let totalInstructions = 0, + totalHooks = 0, + totalSkills = 0, + totalAgents = 0; - // Install packages sequentially for now (can be parallelized later) for (const pkg of packages) { - const packagePath = pkg.absolutePath; - try { const result = await installPackage({ ...options, - cwd: packagePath, + cwd: pkg.absolutePath, config: pkg.config, }); - - totalRuleCount += result.installedRuleCount; - totalCommandCount += result.installedCommandCount; - totalAssetCount += result.installedAssetCount; - totalHookCount += result.installedHookCount; - totalSkillCount += result.installedSkillCount; - totalAgentCount += result.installedAgentCount; - + totalInstructions += result.installedInstructionCount; + totalHooks += result.installedHookCount; + totalSkills += result.installedSkillCount; + totalAgents += result.installedAgentCount; results.push({ path: pkg.relativePath, success: result.success, error: result.error, - installedRuleCount: result.installedRuleCount, - installedCommandCount: result.installedCommandCount, - installedAssetCount: result.installedAssetCount, + installedInstructionCount: result.installedInstructionCount, installedHookCount: result.installedHookCount, installedSkillCount: result.installedSkillCount, installedAgentCount: result.installedAgentCount, @@ -372,9 +203,7 @@ async function installWorkspacesPackages( path: pkg.relativePath, success: false, error: error instanceof Error ? error : new Error(String(error)), - installedRuleCount: 0, - installedCommandCount: 0, - installedAssetCount: 0, + installedInstructionCount: 0, installedHookCount: 0, installedSkillCount: 0, installedAgentCount: 0, @@ -382,23 +211,16 @@ async function installWorkspacesPackages( } } - const failedPackages = results.filter((r) => !r.success); - return { - success: failedPackages.length === 0, + success: results.every((r) => r.success), packages: results, - totalRuleCount, - totalCommandCount, - totalAssetCount, - totalHookCount, - totalSkillCount, - totalAgentCount, + totalInstructionCount: totalInstructions, + totalHookCount: totalHooks, + totalSkillCount: totalSkills, + totalAgentCount: totalAgents, }; } -/** - * Install rules across multiple packages in a workspace - */ export async function installWorkspaces( cwd: string, installOnCI: boolean, @@ -406,37 +228,28 @@ export async function installWorkspaces( dryRun: boolean = false, ): Promise { return withWorkingDirectory(cwd, async () => { - if (verbose) { - console.log(chalk.blue("🔍 Discovering packages...")); - } + if (verbose) log.info(chalk.blue("🔍 Discovering packages...")); const allPackages = await discoverPackagesWithAicm(cwd); const packages = allPackages.filter((pkg) => { - if (pkg.config.config.skipInstall === true) { - return false; - } - + if (pkg.config.config.skipInstall === true) return false; const isRoot = pkg.relativePath === "."; if (!isRoot) return true; - - // For root directories, only keep if it has rules, commands, skills, agents, or presets - const hasRules = pkg.config.rules && pkg.config.rules.length > 0; - const hasCommands = pkg.config.commands && pkg.config.commands.length > 0; + const hasInstructions = + pkg.config.instructions && pkg.config.instructions.length > 0; const hasSkills = pkg.config.skills && pkg.config.skills.length > 0; const hasAgents = pkg.config.agents && pkg.config.agents.length > 0; const hasPresets = pkg.config.config.presets && pkg.config.config.presets.length > 0; - return hasRules || hasCommands || hasSkills || hasAgents || hasPresets; + return hasInstructions || hasSkills || hasAgents || hasPresets; }); if (packages.length === 0) { return { success: false, error: new Error("No packages with aicm configurations found"), - installedRuleCount: 0, - installedCommandCount: 0, - installedAssetCount: 0, + installedInstructionCount: 0, installedHookCount: 0, installedSkillCount: 0, installedAgentCount: 0, @@ -445,16 +258,15 @@ export async function installWorkspaces( } if (verbose) { - console.log( + log.info( chalk.blue( `Found ${packages.length} packages with aicm configurations:`, ), ); - packages.forEach((pkg) => { - console.log(chalk.gray(` - ${pkg.relativePath}`)); - }); - - console.log(chalk.blue(`📦 Installing configurations...`)); + packages.forEach((pkg) => + log.info(chalk.gray(` - ${pkg.relativePath}`)), + ); + log.info(chalk.blue("📦 Installing configurations...")); } const result = await installWorkspacesPackages(packages, { @@ -463,73 +275,48 @@ export async function installWorkspaces( dryRun, }); - const workspaceCommands = mergeWorkspaceCommands(packages); - const workspaceCommandTargets = collectWorkspaceCommandTargets(packages); - - if (workspaceCommands.length > 0) { - warnPresetCommandCollisions(workspaceCommands); - } - - if ( - !dryRun && - workspaceCommands.length > 0 && - workspaceCommandTargets.length > 0 - ) { - const dedupedWorkspaceCommands = - dedupeCommandsForInstall(workspaceCommands); - - // Collect all assets from packages - const allAssets = packages.flatMap((pkg) => pkg.config.assets ?? []); - - // Copy assets to root - writeAssetsToTargets(allAssets, workspaceCommandTargets); - - writeCommandsToTargets(dedupedWorkspaceCommands, workspaceCommandTargets); - } - - // Merge and write skills for workspace const workspaceSkills = mergeWorkspaceSkills(packages); - const workspaceSkillTargets = collectWorkspaceSkillTargets(packages); - - if (workspaceSkills.length > 0) { - warnPresetSkillCollisions(workspaceSkills); - } - - if ( - !dryRun && - workspaceSkills.length > 0 && - workspaceSkillTargets.length > 0 - ) { - const dedupedWorkspaceSkills = dedupeSkillsForInstall(workspaceSkills); - writeSkillsToTargets(dedupedWorkspaceSkills, workspaceSkillTargets); + const skillTargets = collectTargets(packages, "skills"); + if (workspaceSkills.length > 0) warnPresetSkillCollisions(workspaceSkills); + if (!dryRun && workspaceSkills.length > 0 && skillTargets.length > 0) { + writeSkillsToTargets( + dedupeSkillsForInstall(workspaceSkills), + skillTargets, + cwd, + ); } - // Merge and write agents for workspace const workspaceAgents = mergeWorkspaceAgents(packages); - const workspaceAgentTargets = collectWorkspaceAgentTargets(packages); - - if (workspaceAgents.length > 0) { - warnPresetAgentCollisions(workspaceAgents); - } - - if ( - !dryRun && - workspaceAgents.length > 0 && - workspaceAgentTargets.length > 0 - ) { - const dedupedWorkspaceAgents = dedupeAgentsForInstall(workspaceAgents); - writeAgentsToTargets(dedupedWorkspaceAgents, workspaceAgentTargets); + const agentTargets = collectTargets(packages, "agents"); + if (workspaceAgents.length > 0) warnPresetAgentCollisions(workspaceAgents); + if (!dryRun && workspaceAgents.length > 0 && agentTargets.length > 0) { + writeSubagentsToTargets( + dedupeAgentsForInstall(workspaceAgents), + agentTargets, + cwd, + ); } const { merged: rootMcp, conflicts } = mergeWorkspaceMcpServers(packages); - - const hasCursorTarget = packages.some((p) => - p.config.config.targets.includes("cursor"), - ); - - if (!dryRun && hasCursorTarget && Object.keys(rootMcp).length > 0) { - const mcpPath = path.join(cwd, ".cursor", "mcp.json"); - writeMcpServersToFile(rootMcp, mcpPath); + const mcpTargets = collectTargets(packages, "mcp"); + if (!dryRun && mcpTargets.length > 0 && Object.keys(rootMcp).length > 0) { + for (const target of mcpTargets) { + const mcpPath = path.isAbsolute(target) + ? target + : path.join(cwd, target); + const basename = path.basename(target); + + if (basename === "opencode.json") { + writeMcpServersToOpenCode(rootMcp, mcpPath); + } else if ( + target === ".codex/config.toml" || + basename === "config.toml" + ) { + writeMcpServersToCodex(rootMcp, mcpPath); + } else { + writeMcpServersToFile(rootMcp, mcpPath); + } + } } for (const conflict of conflicts) { @@ -538,120 +325,63 @@ export async function installWorkspaces( ); } - // Merge and write hooks for workspace const { merged: rootHooks, hookFiles: rootHookFiles } = mergeWorkspaceHooks(packages); + const hookTargets = collectTargets(packages, "hooks"); + const hasCursorTarget = hookTargets.some((target) => { + const resolved = path.isAbsolute(target) + ? target + : path.join(cwd, target); + return path.basename(resolved) === ".cursor"; + }); + const hasHooksContent = + rootHooks.hooks && Object.keys(rootHooks.hooks).length > 0; - const hasHooks = rootHooks.hooks && Object.keys(rootHooks.hooks).length > 0; - - if (!dryRun && hasCursorTarget && (hasHooks || rootHookFiles.length > 0)) { + if ( + !dryRun && + hasCursorTarget && + (hasHooksContent || rootHookFiles.length > 0) + ) { writeHooksToCursor(rootHooks, rootHookFiles, cwd); } if (verbose) { result.packages.forEach((pkg) => { if (pkg.success) { - const summaryParts = [`${pkg.installedRuleCount} rules`]; - - if (pkg.installedCommandCount > 0) { - summaryParts.push( - `${pkg.installedCommandCount} command${ - pkg.installedCommandCount === 1 ? "" : "s" - }`, + const parts = [ + `${pkg.installedInstructionCount} instruction${pkg.installedInstructionCount === 1 ? "" : "s"}`, + ]; + if (pkg.installedHookCount > 0) + parts.push( + `${pkg.installedHookCount} hook${pkg.installedHookCount === 1 ? "" : "s"}`, ); - } - - if (pkg.installedHookCount > 0) { - summaryParts.push( - `${pkg.installedHookCount} hook${ - pkg.installedHookCount === 1 ? "" : "s" - }`, + if (pkg.installedSkillCount > 0) + parts.push( + `${pkg.installedSkillCount} skill${pkg.installedSkillCount === 1 ? "" : "s"}`, ); - } - - if (pkg.installedSkillCount > 0) { - summaryParts.push( - `${pkg.installedSkillCount} skill${ - pkg.installedSkillCount === 1 ? "" : "s" - }`, + if (pkg.installedAgentCount > 0) + parts.push( + `${pkg.installedAgentCount} agent${pkg.installedAgentCount === 1 ? "" : "s"}`, ); - } - - if (pkg.installedAgentCount > 0) { - summaryParts.push( - `${pkg.installedAgentCount} agent${ - pkg.installedAgentCount === 1 ? "" : "s" - }`, - ); - } - - console.log( - chalk.green(`✅ ${pkg.path} (${summaryParts.join(", ")})`), - ); + log.info(chalk.green(`✅ ${pkg.path} (${parts.join(", ")})`)); } else { - console.log(chalk.red(`❌ ${pkg.path}: ${pkg.error}`)); + log.info(chalk.red(`❌ ${pkg.path}: ${pkg.error}`)); } }); } - const failedPackages = result.packages.filter((r) => !r.success); - - if (failedPackages.length > 0) { - console.log(chalk.yellow(`Installation completed with errors`)); - if (verbose) { - const commandSummary = - result.totalCommandCount > 0 - ? `, ${result.totalCommandCount} command${ - result.totalCommandCount === 1 ? "" : "s" - } total` - : ""; - const hookSummary = - result.totalHookCount > 0 - ? `, ${result.totalHookCount} hook${ - result.totalHookCount === 1 ? "" : "s" - } total` - : ""; - const skillSummary = - result.totalSkillCount > 0 - ? `, ${result.totalSkillCount} skill${ - result.totalSkillCount === 1 ? "" : "s" - } total` - : ""; - const agentSummary = - result.totalAgentCount > 0 - ? `, ${result.totalAgentCount} agent${ - result.totalAgentCount === 1 ? "" : "s" - } total` - : ""; - - console.log( - chalk.green( - `Successfully installed: ${ - result.packages.length - failedPackages.length - }/${result.packages.length} packages (${result.totalRuleCount} rule${ - result.totalRuleCount === 1 ? "" : "s" - } total${commandSummary}${hookSummary}${skillSummary}${agentSummary})`, - ), - ); - console.log( - chalk.red( - `Failed packages: ${failedPackages.map((p) => p.path).join(", ")}`, - ), - ); - } - - const errorDetails = failedPackages + const failed = result.packages.filter((r) => !r.success); + if (failed.length > 0) { + log.info(chalk.yellow("Installation completed with errors")); + const errorDetails = failed .map((p) => `${p.path}: ${p.error}`) .join("; "); - return { success: false, error: new Error( - `Package installation failed for ${failedPackages.length} package(s): ${errorDetails}`, + `Package installation failed for ${failed.length} package(s): ${errorDetails}`, ), - installedRuleCount: result.totalRuleCount, - installedCommandCount: result.totalCommandCount, - installedAssetCount: result.totalAssetCount, + installedInstructionCount: result.totalInstructionCount, installedHookCount: result.totalHookCount, installedSkillCount: result.totalSkillCount, installedAgentCount: result.totalAgentCount, @@ -661,9 +391,7 @@ export async function installWorkspaces( return { success: true, - installedRuleCount: result.totalRuleCount, - installedCommandCount: result.totalCommandCount, - installedAssetCount: result.totalAssetCount, + installedInstructionCount: result.totalInstructionCount, installedHookCount: result.totalHookCount, installedSkillCount: result.totalSkillCount, installedAgentCount: result.totalAgentCount, diff --git a/src/commands/install.ts b/src/commands/install.ts index 51c9e6b..c3c5e23 100644 --- a/src/commands/install.ts +++ b/src/commands/install.ts @@ -1,788 +1,536 @@ import chalk from "chalk"; import fs from "fs-extra"; import path from "node:path"; +import { parse as parseToml, stringify as stringifyToml } from "smol-toml"; import { loadConfig, - extractNamespaceFromPresetPath, ResolvedConfig, - RuleFile, - CommandFile, - AssetFile, SkillFile, AgentFile, MCPServers, - SupportedTarget, detectWorkspacesFromPackageJson, } from "../utils/config"; import { - HookFile, HooksJson, + HookFile, countHooks, writeHooksToCursor, + writeHooksToClaudeCode, } from "../utils/hooks"; +import { InstructionFile } from "../utils/instructions"; +import { writeInstructionsFile } from "../utils/instructions-file"; import { withWorkingDirectory } from "../utils/working-directory"; import { isCIEnvironment } from "../utils/is-ci"; -import { - parseRuleFrontmatter, - generateRulesFileContent, - writeRulesFile, -} from "../utils/rules-file-writer"; import { installWorkspaces } from "./install-workspaces"; +import { log } from "../utils/log"; export interface InstallOptions { - /** - * Base directory to use instead of process.cwd() - */ cwd?: string; - /** - * Custom config object to use instead of loading from file - */ config?: ResolvedConfig; - /** - * allow installation on CI environments - */ installOnCI?: boolean; - /** - * Show verbose output during installation - */ verbose?: boolean; - /** - * Perform a dry run without writing any files - */ dryRun?: boolean; } -/** - * Result of the install operation - */ export interface InstallResult { - /** - * Whether the operation was successful - */ success: boolean; - /** - * Error object if the operation failed - */ error?: Error; - /** - * Number of rules installed - */ - installedRuleCount: number; - /** - * Number of commands installed - */ - installedCommandCount: number; - /** - * Number of assets installed - */ - installedAssetCount: number; - /** - * Number of hooks installed - */ + installedInstructionCount: number; installedHookCount: number; - /** - * Number of skills installed - */ installedSkillCount: number; - /** - * Number of agents installed - */ installedAgentCount: number; - /** - * Number of packages installed - */ packagesCount: number; } -/** - * Rewrite asset references from source paths to installation paths - * Only rewrites the ../assets/ pattern - everything else is preserved - * - * @param content - The file content to rewrite - * @param presetName - The preset name if this file is from a preset - * @param fileInstallDepth - The depth of the file's installation directory relative to .cursor/ - * For example: .cursor/commands/aicm/file.md has depth 2 (commands, aicm) - * .cursor/rules/aicm/preset/file.mdc has depth 3 (rules, aicm, preset) - */ -function rewriteAssetReferences( - content: string, - presetName?: string, - fileInstallDepth: number = 2, -): string { - // Calculate the relative path from the file to .cursor/assets/aicm/ - // We need to go up fileInstallDepth levels to reach .cursor/, then down to assets/aicm/ - const upLevels = "../".repeat(fileInstallDepth); - - // If this is from a preset, include the preset namespace in the asset path - let assetBasePath = "assets/aicm/"; - if (presetName) { - const namespace = extractNamespaceFromPresetPath(presetName); - assetBasePath = path.posix.join("assets", "aicm", ...namespace) + "/"; - } - - const targetPath = upLevels + assetBasePath; - - // Replace ../assets/ with the calculated target path - // Handles both forward slashes and backslashes for cross-platform compatibility - return content.replace(/\.\.[\\/]assets[\\/]/g, targetPath); +function resolveTargetPath(targetPath: string, cwd: string): string { + return path.isAbsolute(targetPath) + ? targetPath + : path.resolve(cwd, targetPath); } -function getTargetPaths(): Record { - const projectDir = process.cwd(); - - return { - cursor: path.join(projectDir, ".cursor", "rules", "aicm"), - assetsAicm: path.join(projectDir, ".cursor", "assets", "aicm"), - aicm: path.join(projectDir, ".aicm"), - }; +function formatPresetLabel(presetName?: string): string | null { + if (!presetName) return null; + if (presetName.startsWith("@")) return presetName; + const normalized = presetName.replace(/\\/g, "/"); + const parts = normalized.split("/").filter(Boolean); + return parts[parts.length - 1] ?? presetName; } -function writeCursorRules(rules: RuleFile[], cursorRulesDir: string): void { - fs.emptyDirSync(cursorRulesDir); - - for (const rule of rules) { - let rulePath; - - const ruleNameParts = rule.name.split(path.sep).filter(Boolean); +function buildInstructionsContent(instructions: InstructionFile[]): { + content: string; + progressiveFiles: Array<{ relativePath: string; content: string }>; +} { + const lines: string[] = []; + const progressive: Array<{ + title: string; + description: string; + relativePath: string; + content: string; + }> = []; + + let currentPreset: string | null = null; + + for (const instruction of instructions) { + const presetLabel = formatPresetLabel(instruction.presetName); + if (presetLabel !== currentPreset) { + if (lines.length > 0) lines.push(""); + if (presetLabel) { + lines.push(``); + } + currentPreset = presetLabel; + } - if (rule.presetName) { - // For rules from presets, create a namespaced directory structure - const namespace = extractNamespaceFromPresetPath(rule.presetName); - // Path will be: cursorRulesDir/namespace/rule-name.mdc - rulePath = path.join(cursorRulesDir, ...namespace, ...ruleNameParts); + if (instruction.inline) { + if (lines.length > 0 && lines[lines.length - 1] !== "") { + lines.push(""); + } + lines.push(instruction.content.trim()); } else { - // For local rules, maintain the original flat structure - rulePath = path.join(cursorRulesDir, ...ruleNameParts); + const relativePath = path.posix.join( + ".agents", + "instructions", + `${instruction.name}.md`, + ); + progressive.push({ + title: instruction.name, + description: instruction.description, + relativePath, + content: instruction.content, + }); } + } - const ruleFile = rulePath + ".mdc"; - fs.ensureDirSync(path.dirname(ruleFile)); - - // Calculate the depth for asset path rewriting - // cursorRulesDir is .cursor/rules/aicm (depth 2 from .cursor) - // Add namespace depth if present - let fileInstallDepth = 2; // rules, aicm - if (rule.presetName) { - const namespace = extractNamespaceFromPresetPath(rule.presetName); - fileInstallDepth += namespace.length; + if (progressive.length > 0) { + if (lines.length > 0) lines.push(""); + for (const item of progressive) { + lines.push( + `- [${item.title}](${item.relativePath}): ${item.description}`, + ); } - // Add any subdirectories in the rule name - fileInstallDepth += ruleNameParts.length - 1; // -1 because the last part is the filename - - // Rewrite asset references before writing - const content = rewriteAssetReferences( - rule.content, - rule.presetName, - fileInstallDepth, - ); - fs.writeFileSync(ruleFile, content); } + + return { + content: lines.join("\n").trim(), + progressiveFiles: progressive.map((item) => ({ + relativePath: item.relativePath, + content: item.content, + })), + }; } -function writeCursorCommands( - commands: CommandFile[], - cursorCommandsDir: string, +function writeInstructionsToTargets( + instructions: InstructionFile[], + targetFiles: string[], + cwd: string, ): void { - fs.removeSync(cursorCommandsDir); - - for (const command of commands) { - const commandNameParts = command.name - .replace(/\\/g, "/") - .split("/") - .filter(Boolean); - const commandPath = path.join(cursorCommandsDir, ...commandNameParts); - const commandFile = commandPath + ".md"; - fs.ensureDirSync(path.dirname(commandFile)); - - // Calculate the depth for asset path rewriting - // cursorCommandsDir is .cursor/commands/aicm (depth 2 from .cursor) - // Commands are NOT namespaced by preset, but we still need to account for subdirectories - let fileInstallDepth = 2; // commands, aicm - // Add any subdirectories in the command name - fileInstallDepth += commandNameParts.length - 1; // -1 because the last part is the filename - - // Rewrite asset references before writing - const content = rewriteAssetReferences( - command.content, - command.presetName, - fileInstallDepth, - ); - fs.writeFileSync(commandFile, content); + if (instructions.length === 0) return; + + const { content, progressiveFiles } = buildInstructionsContent(instructions); + if (!content) return; + + const hasAgentsMd = targetFiles.includes("AGENTS.md"); + const hasClaudeMd = targetFiles.includes("CLAUDE.md"); + + for (const targetFile of targetFiles) { + // When both AGENTS.md and CLAUDE.md are targets, only write full content to AGENTS.md + // CLAUDE.md gets a pointer (@AGENTS.md) if it doesn't already exist + if (hasAgentsMd && hasClaudeMd && targetFile === "CLAUDE.md") { + const resolvedPath = resolveTargetPath(targetFile, cwd); + if (!fs.existsSync(resolvedPath)) { + fs.ensureDirSync(path.dirname(resolvedPath)); + fs.writeFileSync(resolvedPath, "@AGENTS.md\n"); + } + // If CLAUDE.md already exists, leave it untouched + continue; + } + + const resolvedPath = resolveTargetPath(targetFile, cwd); + writeInstructionsFile(content, resolvedPath); + } + + for (const file of progressiveFiles) { + const resolvedPath = resolveTargetPath(file.relativePath, cwd); + fs.ensureDirSync(path.dirname(resolvedPath)); + fs.writeFileSync(resolvedPath, file.content); } } -/** - * Write rules to a shared directory and update the given rules file - */ -function writeRulesForFile( - rules: RuleFile[], - assets: AssetFile[], - ruleDir: string, - rulesFile: string, +export function writeMcpServersToFile( + mcpServers: MCPServers, + mcpPath: string, ): void { - fs.emptyDirSync(ruleDir); - - const ruleFiles = rules.map((rule) => { - let rulePath; + fs.ensureDirSync(path.dirname(mcpPath)); - const ruleNameParts = rule.name.split(path.sep).filter(Boolean); + const existingConfig: Record = fs.existsSync(mcpPath) + ? fs.readJsonSync(mcpPath) + : {}; - if (rule.presetName) { - // For rules from presets, create a namespaced directory structure - const namespace = extractNamespaceFromPresetPath(rule.presetName); - // Path will be: ruleDir/namespace/rule-name.md - rulePath = path.join(ruleDir, ...namespace, ...ruleNameParts); - } else { - // For local rules, maintain the original flat structure - rulePath = path.join(ruleDir, ...ruleNameParts); + const existingServers = + (existingConfig?.mcpServers as Record>) ?? + {}; + + // Keep only user-defined servers (those without aicm: true) + const userServers: Record = {}; + for (const [key, value] of Object.entries(existingServers)) { + if ( + typeof value === "object" && + value !== null && + (value as Record).aicm !== true + ) { + userServers[key] = value; } + } - // For windsurf/codex/claude, assets are installed at the same namespace level as rules - // Example: .aicm/my-preset/rule.md and .aicm/my-preset/asset.json - // So we need to remove the 'assets/' part from the path - // ../assets/file.json -> ../file.json - // ../../assets/file.json -> ../../file.json - const content = rule.content.replace(/(\.\.[/\\])assets[/\\]/g, "$1"); - - const physicalRulePath = rulePath + ".md"; - fs.ensureDirSync(path.dirname(physicalRulePath)); - fs.writeFileSync(physicalRulePath, content); - - const relativeRuleDir = path.basename(ruleDir); - - // For the rules file, maintain the same structure - let windsurfPath; - if (rule.presetName) { - const namespace = extractNamespaceFromPresetPath(rule.presetName); - windsurfPath = - path.join(relativeRuleDir, ...namespace, ...ruleNameParts) + ".md"; - } else { - windsurfPath = path.join(relativeRuleDir, ...ruleNameParts) + ".md"; + // Mark aicm servers and filter out canceled (false) ones + const aicmServers: Record = {}; + for (const [key, value] of Object.entries(mcpServers)) { + if (value !== false) { + aicmServers[key] = { ...value, aicm: true }; } + } - // Normalize to POSIX style for cross-platform compatibility - const windsurfPathPosix = windsurfPath.replace(/\\/g, "/"); - - return { - name: rule.name, - path: windsurfPathPosix, - metadata: parseRuleFrontmatter(content), - }; - }); + const mergedConfig = { + ...existingConfig, + mcpServers: { ...userServers, ...aicmServers }, + }; - const rulesContent = generateRulesFileContent(ruleFiles); - writeRulesFile(rulesContent, path.join(process.cwd(), rulesFile)); + fs.writeJsonSync(mcpPath, mergedConfig, { spaces: 2 }); } -export function writeAssetsToTargets( - assets: AssetFile[], - targets: SupportedTarget[], +export function writeMcpServersToOpenCode( + mcpServers: MCPServers, + mcpPath: string, ): void { - const targetPaths = getTargetPaths(); + fs.ensureDirSync(path.dirname(mcpPath)); - for (const target of targets) { - let targetDir: string; - - switch (target) { - case "cursor": - targetDir = targetPaths.assetsAicm; - break; - case "windsurf": - case "codex": - case "claude": - targetDir = targetPaths.aicm; - break; - default: - continue; - } + const existingConfig: Record = fs.existsSync(mcpPath) + ? fs.readJsonSync(mcpPath) + : {}; - for (const asset of assets) { - let assetPath; - if (asset.presetName) { - const namespace = extractNamespaceFromPresetPath(asset.presetName); - assetPath = path.join(targetDir, ...namespace, asset.name); - } else { - assetPath = path.join(targetDir, asset.name); - } + const existingMcp = + (existingConfig?.mcp as Record>) ?? {}; - fs.ensureDirSync(path.dirname(assetPath)); - fs.writeFileSync(assetPath, asset.content); + // Keep only user-defined servers (those without aicm marker) + const userServers: Record = {}; + for (const [key, value] of Object.entries(existingMcp)) { + if ( + typeof value === "object" && + value !== null && + (value as Record).aicm !== true + ) { + userServers[key] = value; } } -} - -/** - * Write all collected rules to their respective IDE targets - */ -function writeRulesToTargets( - rules: RuleFile[], - assets: AssetFile[], - targets: SupportedTarget[], -): void { - const targetPaths = getTargetPaths(); - for (const target of targets) { - switch (target) { - case "cursor": - if (rules.length > 0) { - writeCursorRules(rules, targetPaths.cursor); - } - break; - case "windsurf": - if (rules.length > 0) { - writeRulesForFile(rules, assets, targetPaths.aicm, ".windsurfrules"); - } - break; - case "codex": - if (rules.length > 0) { - writeRulesForFile(rules, assets, targetPaths.aicm, "AGENTS.md"); - } - break; - case "claude": - if (rules.length > 0) { - writeRulesForFile(rules, assets, targetPaths.aicm, "CLAUDE.md"); - } - break; + // Convert aicm MCP format to OpenCode format + const aicmServers: Record = {}; + for (const [key, value] of Object.entries(mcpServers)) { + if (value === false) continue; + + if (value.url) { + aicmServers[key] = { + type: "remote", + url: value.url, + enabled: true, + ...(value.env ? { environment: value.env } : {}), + aicm: true, + }; + } else if (value.command) { + const command = [value.command, ...(value.args || [])]; + aicmServers[key] = { + type: "local", + command, + enabled: true, + ...(value.env ? { environment: value.env } : {}), + aicm: true, + }; } } - // Write assets after rules so they don't get wiped by emptyDirSync - writeAssetsToTargets(assets, targets); + const mergedConfig = { + ...existingConfig, + mcp: { ...userServers, ...aicmServers }, + }; + + fs.writeJsonSync(mcpPath, mergedConfig, { spaces: 2 }); } -export function writeCommandsToTargets( - commands: CommandFile[], - targets: SupportedTarget[], +export function writeMcpServersToCodex( + mcpServers: MCPServers, + mcpPath: string, ): void { - const projectDir = process.cwd(); - const cursorRoot = path.join(projectDir, ".cursor"); + fs.ensureDirSync(path.dirname(mcpPath)); - for (const target of targets) { - if (target === "cursor") { - const commandsDir = path.join(cursorRoot, "commands", "aicm"); + // Read existing TOML config + let existingConfig: Record = {}; + const managedServerNames = new Set(); + if (fs.existsSync(mcpPath)) { + try { + const rawContent = fs.readFileSync(mcpPath, "utf8"); + + // Detect aicm-managed servers from comment markers + const lines = rawContent.split("\n"); + for (let i = 0; i < lines.length; i++) { + if (lines[i].trim() === "# aicm:managed") { + const match = lines[i + 1]?.match(/^\[mcp_servers\.("?)(.+)\1\]$/); + if (match) managedServerNames.add(match[2]); + } + } - writeCursorCommands(commands, commandsDir); + existingConfig = parseToml(rawContent) as Record; + } catch { + // If we can't parse, start fresh + existingConfig = {}; } - // Other targets do not support commands yet } -} - -/** - * Metadata file written inside each installed skill to track aicm management - * The presence of .aicm.json indicates the skill is managed by aicm - */ -interface SkillAicmMetadata { - source: "local" | "preset"; - presetName?: string; -} -/** - * Get the skills installation path for a target - * Returns null for targets that don't support skills - */ -function getSkillsTargetPath(target: SupportedTarget): string | null { - const projectDir = process.cwd(); - - switch (target) { - case "cursor": - return path.join(projectDir, ".cursor", "skills"); - case "claude": - return path.join(projectDir, ".claude", "skills"); - case "codex": - return path.join(projectDir, ".codex", "skills"); - case "windsurf": - // Windsurf does not support skills - return null; - default: - return null; + const existingServers = + (existingConfig.mcp_servers as Record>) ?? + {}; + + // Keep only user-defined servers (skip comment-marked and legacy aicm:true) + const userServers: Record> = {}; + for (const [key, value] of Object.entries(existingServers)) { + if (managedServerNames.has(key)) continue; + if ( + typeof value === "object" && + value !== null && + (value as Record).aicm === true + ) { + continue; + } + userServers[key] = value as Record; } -} -/** - * Write a single skill to the target directory - * Copies the entire skill directory and writes .aicm.json metadata - */ -function writeSkillToTarget(skill: SkillFile, targetSkillsDir: string): void { - const skillTargetPath = path.join(targetSkillsDir, skill.name); + // Convert aicm MCP format to Codex TOML format + const aicmNames: string[] = []; + const aicmServers: Record> = {}; + for (const [key, value] of Object.entries(mcpServers)) { + if (value === false) continue; + aicmNames.push(key); - // Remove existing skill directory if it exists (to ensure clean install) - if (fs.existsSync(skillTargetPath)) { - fs.removeSync(skillTargetPath); + if (value.url) { + aicmServers[key] = { + url: value.url, + ...(value.env ? { env: value.env } : {}), + }; + } else if (value.command) { + aicmServers[key] = { + command: value.command, + ...(value.args && value.args.length > 0 ? { args: value.args } : {}), + ...(value.env ? { env: value.env } : {}), + }; + } } - // Copy the entire skill directory - fs.copySync(skill.sourcePath, skillTargetPath); - - // Write .aicm.json metadata file - // The presence of this file indicates the skill is managed by aicm - const metadata: SkillAicmMetadata = { - source: skill.source, + const mergedConfig = { + ...existingConfig, + mcp_servers: { ...userServers, ...aicmServers }, }; - if (skill.presetName) { - metadata.presetName = skill.presetName; + let tomlContent = stringifyToml(mergedConfig); + + // Add comment markers before aicm-managed server sections + for (const name of aicmNames) { + const bare = `[mcp_servers.${name}]`; + const quoted = `[mcp_servers."${name}"]`; + if (tomlContent.includes(bare)) { + tomlContent = tomlContent.replace(bare, `# aicm:managed\n${bare}`); + } else if (tomlContent.includes(quoted)) { + tomlContent = tomlContent.replace(quoted, `# aicm:managed\n${quoted}`); + } } - const metadataPath = path.join(skillTargetPath, ".aicm.json"); - fs.writeJsonSync(metadataPath, metadata, { spaces: 2 }); + fs.writeFileSync(mcpPath, tomlContent); } -/** - * Write skills to all supported target directories - */ -export function writeSkillsToTargets( - skills: SkillFile[], - targets: SupportedTarget[], +function writeMcpServersToTargets( + mcpServers: MCPServers, + targets: string[], + cwd: string, ): void { - if (skills.length === 0) return; - + if (!mcpServers || Object.keys(mcpServers).length === 0) return; for (const target of targets) { - const targetSkillsDir = getSkillsTargetPath(target); - - if (!targetSkillsDir) { - // Target doesn't support skills - continue; - } + const resolvedPath = resolveTargetPath(target, cwd); + const basename = path.basename(target); - // Ensure the skills directory exists - fs.ensureDirSync(targetSkillsDir); - - for (const skill of skills) { - writeSkillToTarget(skill, targetSkillsDir); + if (basename === "opencode.json") { + writeMcpServersToOpenCode(mcpServers, resolvedPath); + } else if (target === ".codex/config.toml" || basename === "config.toml") { + writeMcpServersToCodex(mcpServers, resolvedPath); + } else { + // Default: Cursor (.cursor/mcp.json) and Claude Code (.mcp.json) format + writeMcpServersToFile(mcpServers, resolvedPath); } } } -/** - * Warn about skill name collisions from different presets - */ -export function warnPresetSkillCollisions(skills: SkillFile[]): void { - const collisions = new Map< - string, - { presets: Set; lastPreset: string } - >(); - - for (const skill of skills) { - if (!skill.presetName) continue; +export function writeSkillsToTargets( + skills: SkillFile[], + targetDirs: string[], + cwd: string, +): void { + if (skills.length === 0) return; - const entry = collisions.get(skill.name); - if (entry) { - entry.presets.add(skill.presetName); - entry.lastPreset = skill.presetName; - } else { - collisions.set(skill.name, { - presets: new Set([skill.presetName]), - lastPreset: skill.presetName, - }); + for (const targetDir of targetDirs) { + const resolvedDir = resolveTargetPath(targetDir, cwd); + fs.ensureDirSync(resolvedDir); + + // LEGACY(v0->v1): remove old namespaced skill layout (/skills/aicm/). + // Keep this migration block temporarily to auto-clean historical installs. + const legacyNamespacedDir = path.join(resolvedDir, "aicm"); + if ( + fs.existsSync(legacyNamespacedDir) && + fs.statSync(legacyNamespacedDir).isDirectory() && + !fs.existsSync(path.join(legacyNamespacedDir, "SKILL.md")) + ) { + fs.removeSync(legacyNamespacedDir); } - } - for (const [skillName, { presets, lastPreset }] of collisions) { - if (presets.size > 1) { - const presetList = Array.from(presets).sort().join(", "); - console.warn( - chalk.yellow( - `Warning: multiple presets provide the "${skillName}" skill (${presetList}). Using definition from ${lastPreset}.`, - ), - ); + for (const skill of skills) { + const skillTargetPath = path.join(resolvedDir, skill.name); + if (fs.existsSync(skillTargetPath)) fs.removeSync(skillTargetPath); + fs.copySync(skill.sourcePath, skillTargetPath); + + const metadata: Record = { source: skill.source }; + if (skill.presetName) metadata.presetName = skill.presetName; + fs.writeJsonSync(path.join(skillTargetPath, ".aicm.json"), metadata, { + spaces: 2, + }); } } } -/** - * Dedupe skills by name (last one wins) - */ -export function dedupeSkillsForInstall(skills: SkillFile[]): SkillFile[] { - const unique = new Map(); - for (const skill of skills) { - unique.set(skill.name, skill); - } - return Array.from(unique.values()); -} - -/** - * Get the agents installation path for a target - * Returns null for targets that don't support agents - */ -function getAgentsTargetPath(target: SupportedTarget): string | null { - const projectDir = process.cwd(); - - switch (target) { - case "cursor": - return path.join(projectDir, ".cursor", "agents"); - case "claude": - return path.join(projectDir, ".claude", "agents"); - case "codex": - case "windsurf": - // Codex and Windsurf do not support agents - return null; - default: - return null; - } -} - -/** - * Metadata file written to the agents directory to track aicm-managed agents - */ -interface AgentsAicmMetadata { - managedAgents: string[]; // List of agent names (without path or extension) -} - -/** - * Write agents to all supported target directories - * Similar to skills, agents are written directly to the agents directory - * with a .aicm.json metadata file tracking which agents are managed - */ -export function writeAgentsToTargets( +export function writeSubagentsToTargets( agents: AgentFile[], - targets: SupportedTarget[], + targetDirs: string[], + cwd: string, ): void { if (agents.length === 0) return; - for (const target of targets) { - const targetAgentsDir = getAgentsTargetPath(target); - - if (!targetAgentsDir) { - // Target doesn't support agents - continue; - } - - // Ensure the agents directory exists + for (const targetDir of targetDirs) { + const targetAgentsDir = resolveTargetPath(targetDir, cwd); fs.ensureDirSync(targetAgentsDir); - // Read existing metadata to clean up old managed agents const metadataPath = path.join(targetAgentsDir, ".aicm.json"); if (fs.existsSync(metadataPath)) { - try { - const existingMetadata: AgentsAicmMetadata = - fs.readJsonSync(metadataPath); - // Remove previously managed agents - for (const agentName of existingMetadata.managedAgents || []) { - // Skip invalid names containing path separators - if (agentName.includes("/") || agentName.includes("\\")) { - console.warn( - chalk.yellow( - `Warning: Skipping invalid agent name "${agentName}" (contains path separator)`, - ), - ); - continue; - } - const fullPath = path.join(targetAgentsDir, agentName + ".md"); - if (fs.existsSync(fullPath)) { - fs.removeSync(fullPath); - } + const existing = fs.readJsonSync(metadataPath) as { + managedAgents?: string[]; + }; + + for (const agentName of existing.managedAgents || []) { + if (agentName.includes("/") || agentName.includes("\\")) { + log.warn( + `Warning: Skipping invalid agent name "${agentName}" (contains path separator)`, + ); + continue; } - } catch { - // Ignore errors reading metadata + const fullPath = path.join(targetAgentsDir, agentName + ".md"); + if (fs.existsSync(fullPath)) fs.removeSync(fullPath); } } const managedAgents: string[] = []; - for (const agent of agents) { - // Use base name only const agentName = path.basename(agent.name, path.extname(agent.name)); - const agentFile = path.join(targetAgentsDir, agentName + ".md"); - - fs.writeFileSync(agentFile, agent.content); + fs.writeFileSync( + path.join(targetAgentsDir, agentName + ".md"), + agent.content, + ); managedAgents.push(agentName); } - // Write metadata file to track managed agents - const metadata: AgentsAicmMetadata = { - managedAgents, - }; - fs.writeJsonSync(metadataPath, metadata, { spaces: 2 }); + fs.writeJsonSync(metadataPath, { managedAgents }, { spaces: 2 }); } } -/** - * Warn about agent name collisions from different presets - */ -export function warnPresetAgentCollisions(agents: AgentFile[]): void { - const collisions = new Map< - string, - { presets: Set; lastPreset: string } - >(); - - for (const agent of agents) { - if (!agent.presetName) continue; - - const entry = collisions.get(agent.name); +export function warnPresetSkillCollisions(skills: SkillFile[]): void { + const seen = new Map; last: string }>(); + for (const skill of skills) { + if (!skill.presetName) continue; + const entry = seen.get(skill.name); if (entry) { - entry.presets.add(agent.presetName); - entry.lastPreset = agent.presetName; + entry.presets.add(skill.presetName); + entry.last = skill.presetName; } else { - collisions.set(agent.name, { - presets: new Set([agent.presetName]), - lastPreset: agent.presetName, + seen.set(skill.name, { + presets: new Set([skill.presetName]), + last: skill.presetName, }); } } - for (const [agentName, { presets, lastPreset }] of collisions) { + for (const [name, { presets, last }] of seen) { if (presets.size > 1) { - const presetList = Array.from(presets).sort().join(", "); + const list = Array.from(presets).sort().join(", "); console.warn( chalk.yellow( - `Warning: multiple presets provide the "${agentName}" agent (${presetList}). Using definition from ${lastPreset}.`, + `Warning: multiple presets provide the "${name}" skill (${list}). Using definition from ${last}.`, ), ); } } } -/** - * Dedupe agents by name (last one wins) - */ -export function dedupeAgentsForInstall(agents: AgentFile[]): AgentFile[] { - const unique = new Map(); +export function warnPresetAgentCollisions(agents: AgentFile[]): void { + const seen = new Map; last: string }>(); for (const agent of agents) { - unique.set(agent.name, agent); - } - return Array.from(unique.values()); -} - -export function warnPresetCommandCollisions(commands: CommandFile[]): void { - const collisions = new Map< - string, - { presets: Set; lastPreset: string } - >(); - - for (const command of commands) { - if (!command.presetName) continue; - - const entry = collisions.get(command.name); + if (!agent.presetName) continue; + const entry = seen.get(agent.name); if (entry) { - entry.presets.add(command.presetName); - entry.lastPreset = command.presetName; + entry.presets.add(agent.presetName); + entry.last = agent.presetName; } else { - collisions.set(command.name, { - presets: new Set([command.presetName]), - lastPreset: command.presetName, + seen.set(agent.name, { + presets: new Set([agent.presetName]), + last: agent.presetName, }); } } - for (const [commandName, { presets, lastPreset }] of collisions) { + for (const [name, { presets, last }] of seen) { if (presets.size > 1) { - const presetList = Array.from(presets).sort().join(", "); + const list = Array.from(presets).sort().join(", "); console.warn( chalk.yellow( - `Warning: multiple presets provide the "${commandName}" command (${presetList}). Using definition from ${lastPreset}.`, + `Warning: multiple presets provide the "${name}" agent (${list}). Using definition from ${last}.`, ), ); } } } -export function dedupeCommandsForInstall( - commands: CommandFile[], -): CommandFile[] { - const unique = new Map(); - for (const command of commands) { - unique.set(command.name, command); - } +export function dedupeSkillsForInstall(skills: SkillFile[]): SkillFile[] { + const unique = new Map(); + for (const skill of skills) unique.set(skill.name, skill); return Array.from(unique.values()); } -/** - * Write MCP servers configuration to IDE targets - */ -function writeMcpServersToTargets( - mcpServers: MCPServers, - targets: SupportedTarget[], - cwd: string, -): void { - if (!mcpServers || Object.keys(mcpServers).length === 0) return; - - for (const target of targets) { - if (target === "cursor") { - const mcpPath = path.join(cwd, ".cursor", "mcp.json"); - writeMcpServersToFile(mcpServers, mcpPath); - } - // Windsurf and Codex do not support project mcpServers, so skip - } +export function dedupeAgentsForInstall(agents: AgentFile[]): AgentFile[] { + const unique = new Map(); + for (const agent of agents) unique.set(agent.name, agent); + return Array.from(unique.values()); } -/** - * Write hooks to IDE targets - */ function writeHooksToTargets( hooksConfig: HooksJson, hookFiles: HookFile[], - targets: SupportedTarget[], + targets: string[], cwd: string, ): void { - const hasHooks = - hooksConfig.hooks && Object.keys(hooksConfig.hooks).length > 0; - - if (!hasHooks && hookFiles.length === 0) { - return; - } + const hookCount = countHooks(hooksConfig); + if (hookCount === 0 && hookFiles.length === 0) return; for (const target of targets) { - if (target === "cursor") { - writeHooksToCursor(hooksConfig, hookFiles, cwd); + const targetPath = resolveTargetPath(target, cwd); + if (path.basename(targetPath) === ".cursor") { + writeHooksToCursor(hooksConfig, hookFiles, path.dirname(targetPath)); + } else if (path.basename(targetPath) === ".claude") { + writeHooksToClaudeCode(hooksConfig, hookFiles, path.dirname(targetPath)); } - // Other targets do not support hooks yet } } -/** - * Write MCP servers configuration to a specific file - */ -export function writeMcpServersToFile( - mcpServers: MCPServers, - mcpPath: string, -): void { - fs.ensureDirSync(path.dirname(mcpPath)); - - const existingConfig: Record = fs.existsSync(mcpPath) - ? fs.readJsonSync(mcpPath) - : {}; - - const existingMcpServers = existingConfig?.mcpServers ?? {}; - - // Filter out any existing aicm-managed servers (with aicm: true) - // This removes stale aicm servers that are no longer in the configuration - const userMcpServers: Record = {}; - - for (const [key, value] of Object.entries(existingMcpServers)) { - if (typeof value === "object" && value !== null && value.aicm !== true) { - userMcpServers[key] = value; - } - } - - // Mark new aicm servers as managed and filter out canceled servers - const aicmMcpServers: Record = {}; - - for (const [key, value] of Object.entries(mcpServers)) { - if (value !== false) { - aicmMcpServers[key] = { - ...value, - aicm: true, - }; - } - } - - // Merge user servers with aicm servers (aicm servers override user servers with same key) - const mergedMcpServers = { - ...userMcpServers, - ...aicmMcpServers, - }; - - const mergedConfig = { - ...existingConfig, - mcpServers: mergedMcpServers, - }; - - fs.writeJsonSync(mcpPath, mergedConfig, { spaces: 2 }); -} - -/** - * Install rules for a single package (used within workspaces and standalone installs) - */ export async function installPackage( options: InstallOptions = {}, ): Promise { @@ -801,9 +549,7 @@ export async function installPackage( return { success: false, error: new Error("Configuration file not found"), - installedRuleCount: 0, - installedCommandCount: 0, - installedAssetCount: 0, + installedInstructionCount: 0, installedHookCount: 0, installedSkillCount: 0, installedAgentCount: 0, @@ -813,9 +559,7 @@ export async function installPackage( const { config, - rules, - commands, - assets, + instructions, skills, agents, mcpServers, @@ -826,9 +570,7 @@ export async function installPackage( if (config.skipInstall === true) { return { success: true, - installedRuleCount: 0, - installedCommandCount: 0, - installedAssetCount: 0, + installedInstructionCount: 0, installedHookCount: 0, installedSkillCount: 0, installedAgentCount: 0, @@ -836,9 +578,6 @@ export async function installPackage( }; } - warnPresetCommandCollisions(commands); - const commandsToInstall = dedupeCommandsForInstall(commands); - warnPresetSkillCollisions(skills); const skillsToInstall = dedupeSkillsForInstall(skills); @@ -847,66 +586,43 @@ export async function installPackage( try { if (!options.dryRun) { - writeRulesToTargets(rules, assets, config.targets as SupportedTarget[]); - - writeCommandsToTargets( - commandsToInstall, - config.targets as SupportedTarget[], + writeInstructionsToTargets( + instructions, + config.targets.instructions, + cwd, ); - writeSkillsToTargets( - skillsToInstall, - config.targets as SupportedTarget[], - ); - - writeAgentsToTargets( - agentsToInstall, - config.targets as SupportedTarget[], - ); + writeSkillsToTargets(skillsToInstall, config.targets.skills, cwd); + writeSubagentsToTargets(agentsToInstall, config.targets.agents, cwd); if (mcpServers && Object.keys(mcpServers).length > 0) { - writeMcpServersToTargets( - mcpServers, - config.targets as SupportedTarget[], - cwd, - ); + writeMcpServersToTargets(mcpServers, config.targets.mcp, cwd); } - if (hooks && (countHooks(hooks) > 0 || hookFiles.length > 0)) { - writeHooksToTargets( - hooks, - hookFiles, - config.targets as SupportedTarget[], - cwd, - ); + const hooksCount = countHooks(hooks); + if (hooksCount > 0 || hookFiles.length > 0) { + writeHooksToTargets(hooks, hookFiles, config.targets.hooks, cwd); } } - const uniqueRuleCount = new Set(rules.map((rule) => rule.name)).size; - const uniqueCommandCount = new Set( - commandsToInstall.map((command) => command.name), + const uniqueInstructionCount = new Set( + instructions.map((i) => `${i.presetName ?? "local"}::${i.name}`), ).size; const uniqueHookCount = countHooks(hooks); - const uniqueSkillCount = skillsToInstall.length; - const uniqueAgentCount = agentsToInstall.length; return { success: true, - installedRuleCount: uniqueRuleCount, - installedCommandCount: uniqueCommandCount, - installedAssetCount: assets.length, + installedInstructionCount: uniqueInstructionCount, installedHookCount: uniqueHookCount, - installedSkillCount: uniqueSkillCount, - installedAgentCount: uniqueAgentCount, + installedSkillCount: skillsToInstall.length, + installedAgentCount: agentsToInstall.length, packagesCount: 1, }; } catch (error) { return { success: false, error: error instanceof Error ? error : new Error(String(error)), - installedRuleCount: 0, - installedCommandCount: 0, - installedAssetCount: 0, + installedInstructionCount: 0, installedHookCount: 0, installedSkillCount: 0, installedAgentCount: 0, @@ -916,24 +632,17 @@ export async function installPackage( }); } -/** - * Core implementation of the rule installation logic - */ export async function install( options: InstallOptions = {}, ): Promise { const cwd = options.cwd || process.cwd(); - const installOnCI = options.installOnCI === true; // Default to false if not specified - - const inCI = isCIEnvironment(); - if (inCI && !installOnCI) { - console.log(chalk.yellow("Detected CI environment, skipping install.")); + const installOnCI = options.installOnCI === true; + if (isCIEnvironment() && !installOnCI) { + log.info(chalk.yellow("Detected CI environment, skipping install.")); return { success: true, - installedRuleCount: 0, - installedCommandCount: 0, - installedAssetCount: 0, + installedInstructionCount: 0, installedHookCount: 0, installedSkillCount: 0, installedAgentCount: 0, @@ -955,7 +664,7 @@ export async function install( (!resolvedConfig && detectWorkspacesFromPackageJson(cwd)); if (shouldUseWorkspaces) { - return await installWorkspaces( + return installWorkspaces( cwd, installOnCI, options.verbose, @@ -967,9 +676,6 @@ export async function install( }); } -/** - * CLI command wrapper for install - */ export async function installCommand( installOnCI?: boolean, verbose?: boolean, @@ -979,71 +685,57 @@ export async function installCommand( if (!result.success) { throw result.error ?? new Error("Installation failed with unknown error"); - } else { - const ruleCount = result.installedRuleCount; - const commandCount = result.installedCommandCount; - const hookCount = result.installedHookCount; - const skillCount = result.installedSkillCount; - const agentCount = result.installedAgentCount; - const ruleMessage = - ruleCount > 0 ? `${ruleCount} rule${ruleCount === 1 ? "" : "s"}` : null; - const commandMessage = - commandCount > 0 - ? `${commandCount} command${commandCount === 1 ? "" : "s"}` - : null; - const hookMessage = - hookCount > 0 ? `${hookCount} hook${hookCount === 1 ? "" : "s"}` : null; - const skillMessage = - skillCount > 0 - ? `${skillCount} skill${skillCount === 1 ? "" : "s"}` - : null; - const agentMessage = - agentCount > 0 - ? `${agentCount} agent${agentCount === 1 ? "" : "s"}` - : null; - const countsParts: string[] = []; - if (ruleMessage) { - countsParts.push(ruleMessage); - } - if (commandMessage) { - countsParts.push(commandMessage); - } - if (hookMessage) { - countsParts.push(hookMessage); - } - if (skillMessage) { - countsParts.push(skillMessage); - } - if (agentMessage) { - countsParts.push(agentMessage); - } - const countsMessage = - countsParts.length > 0 - ? countsParts.join(", ").replace(/, ([^,]*)$/, " and $1") - : "0 rules"; - - if (dryRun) { - if (result.packagesCount > 1) { - console.log( - `Dry run: validated ${countsMessage} across ${result.packagesCount} packages`, - ); - } else { - console.log(`Dry run: validated ${countsMessage}`); - } - } else if ( - ruleCount === 0 && - commandCount === 0 && - hookCount === 0 && - skillCount === 0 && - agentCount === 0 - ) { - console.log("No rules, commands, hooks, skills, or agents installed"); - } else if (result.packagesCount > 1) { - console.log( - `Successfully installed ${countsMessage} across ${result.packagesCount} packages`, + } + + const { + installedInstructionCount, + installedHookCount, + installedSkillCount, + installedAgentCount, + } = result; + const parts: string[] = []; + if (installedInstructionCount > 0) + parts.push( + `${installedInstructionCount} instruction${installedInstructionCount === 1 ? "" : "s"}`, + ); + if (installedHookCount > 0) + parts.push( + `${installedHookCount} hook${installedHookCount === 1 ? "" : "s"}`, + ); + if (installedSkillCount > 0) + parts.push( + `${installedSkillCount} skill${installedSkillCount === 1 ? "" : "s"}`, + ); + if (installedAgentCount > 0) + parts.push( + `${installedAgentCount} agent${installedAgentCount === 1 ? "" : "s"}`, + ); + + const countsMessage = + parts.length > 0 + ? parts.join(", ").replace(/, ([^,]*)$/, " and $1") + : "0 instructions"; + + if (dryRun) { + if (result.packagesCount > 1) { + log.info( + `Dry run: validated ${countsMessage} across ${result.packagesCount} packages`, ); } else { - console.log(`Successfully installed ${countsMessage}`); - } + log.info(`Dry run: validated ${countsMessage}`); + } + } else if ( + installedInstructionCount === 0 && + installedHookCount === 0 && + installedSkillCount === 0 && + installedAgentCount === 0 + ) { + log.info("No instructions, hooks, skills, or agents installed"); + } else if (result.packagesCount > 1) { + log.info( + `Successfully installed ${countsMessage} across ${result.packagesCount} packages`, + ); + } else { + log.info(`Successfully installed ${countsMessage}`); } } diff --git a/src/commands/list.ts b/src/commands/list.ts index 5dd07d0..0401449 100644 --- a/src/commands/list.ts +++ b/src/commands/list.ts @@ -1,53 +1,30 @@ import chalk from "chalk"; import { loadConfig } from "../utils/config"; +import { log } from "../utils/log"; export async function listCommand(): Promise { const config = await loadConfig(); if (!config) { - console.log(chalk.red("Configuration file not found!")); - console.log(`Run ${chalk.blue("npx aicm init")} to create one.`); + log.info(chalk.red("Configuration file not found!")); + log.info(`Run ${chalk.blue("npx aicm init")} to create one.`); return; } - const hasRules = config.rules && config.rules.length > 0; - const hasCommands = config.commands && config.commands.length > 0; - - if (!hasRules && !hasCommands) { - console.log(chalk.yellow("No rules or commands defined in configuration.")); - console.log( - `Edit your ${chalk.blue("aicm.json")} file to add rules or commands.`, - ); + if (!config.instructions || config.instructions.length === 0) { + log.info(chalk.yellow("No instructions defined in configuration.")); + log.info(`Edit your ${chalk.blue("aicm.json")} file to add instructions.`); return; } - if (hasRules) { - console.log(chalk.blue("Configured Rules:")); - console.log(chalk.dim("─".repeat(50))); - - for (const rule of config.rules) { - console.log( - `${chalk.bold(rule.name)} - ${rule.sourcePath} ${ - rule.presetName ? `[${rule.presetName}]` : "" - }`, - ); - } - } + log.info(chalk.blue("Configured Instructions:")); + log.info(chalk.dim("─".repeat(50))); - if (hasCommands) { - if (hasRules) { - console.log(); - } - - console.log(chalk.blue("Configured Commands:")); - console.log(chalk.dim("─".repeat(50))); - - for (const command of config.commands) { - console.log( - `${chalk.bold(command.name)} - ${command.sourcePath} ${ - command.presetName ? `[${command.presetName}]` : "" - }`, - ); - } + for (const instruction of config.instructions) { + log.info( + `${chalk.bold(instruction.name)} - ${instruction.sourcePath} ${ + instruction.presetName ? `[${instruction.presetName}]` : "" + }`, + ); } } diff --git a/src/types/declarations.d.ts b/src/types/declarations.d.ts index b8705f2..f26f995 100644 --- a/src/types/declarations.d.ts +++ b/src/types/declarations.d.ts @@ -1,3 +1,2 @@ -// Declare modules that don't have TypeScript definitions declare module "args"; declare module "mock-fs"; diff --git a/src/utils/config.ts b/src/utils/config.ts index 6716d27..a4c0899 100644 --- a/src/utils/config.ts +++ b/src/utils/config.ts @@ -1,31 +1,23 @@ +/** + * Configuration loading and validation. + * + * Loads aicm.json via cosmiconfig, validates structure, + * and resolves all referenced resources (instructions, skills, agents, hooks, presets). + */ + import fs from "fs-extra"; import path from "node:path"; -import { cosmiconfig, CosmiconfigResult } from "cosmiconfig"; import fg from "fast-glob"; +import { cosmiconfig } from "cosmiconfig"; +import { InstructionFile, loadInstructionsFromPath } from "./instructions"; +import { TargetsConfig, validateTargetsInput, resolveTargets } from "./targets"; import { + HookFile, + HooksJson, loadHooksFromDirectory, mergeHooksConfigs, - HooksJson, - HookFile, } from "./hooks"; - -export interface RawConfig { - rootDir?: string; - targets?: string[]; - presets?: string[]; - mcpServers?: MCPServers; - workspaces?: boolean; - skipInstall?: boolean; -} - -export interface Config { - rootDir?: string; - targets: string[]; - presets?: string[]; - mcpServers?: MCPServers; - workspaces?: boolean; - skipInstall?: boolean; -} +import { loadPresetRecursively, mergePresetMcpServers } from "./preset-loader"; export type MCPServer = | { @@ -46,44 +38,37 @@ export interface MCPServers { [serverName: string]: MCPServer; } -export interface ManagedFile { +export interface SkillFile { name: string; - content: string; sourcePath: string; source: "local" | "preset"; presetName?: string; } -export interface AssetFile { +export interface AgentFile { name: string; - content: Buffer; + content: string; sourcePath: string; source: "local" | "preset"; presetName?: string; } -export type RuleFile = ManagedFile; +export type { HookFile, HooksJson } from "./hooks"; -export type CommandFile = ManagedFile; - -export interface SkillFile { - name: string; // skill directory name - sourcePath: string; // absolute path to source skill directory - source: "local" | "preset"; - presetName?: string; -} - -export type AgentFile = ManagedFile; - -export interface RuleCollection { - [target: string]: RuleFile[]; +export interface Config { + rootDir?: string; + instructionsFile?: string; + instructionsDir?: string; + targets: TargetsConfig; + presets?: string[]; + mcpServers?: MCPServers; + workspaces?: boolean; + skipInstall?: boolean; } export interface ResolvedConfig { config: Config; - rules: RuleFile[]; - commands: CommandFile[]; - assets: AssetFile[]; + instructions: InstructionFile[]; skills: SkillFile[]; agents: AgentFile[]; mcpServers: MCPServers; @@ -91,8 +76,10 @@ export interface ResolvedConfig { hookFiles: HookFile[]; } -export const ALLOWED_CONFIG_KEYS = [ +const ALLOWED_CONFIG_KEYS = [ "rootDir", + "instructionsFile", + "instructionsDir", "targets", "presets", "mcpServers", @@ -100,66 +87,34 @@ export const ALLOWED_CONFIG_KEYS = [ "skipInstall", ] as const; -export const SUPPORTED_TARGETS = [ - "cursor", - "windsurf", - "codex", - "claude", -] as const; -export type SupportedTarget = (typeof SUPPORTED_TARGETS)[number]; +const DEFAULT_INSTRUCTIONS_FILE = "AGENTS.src.md"; -export function detectWorkspacesFromPackageJson(cwd: string): boolean { - try { - const packageJsonPath = path.join(cwd, "package.json"); - if (!fs.existsSync(packageJsonPath)) { - return false; - } +/** + * Auto-detect instruction source under a root directory. + * Detects both AGENTS.src.md (single file) and instructions/ (directory) when present. + */ +function autoDetectInstructions(rootPath: string): { + instructionsFile?: string; + instructionsDir?: string; +} { + const detected: { instructionsFile?: string; instructionsDir?: string } = {}; - const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8")); - return Boolean(packageJson.workspaces); - } catch { - return false; + if (fs.existsSync(path.join(rootPath, DEFAULT_INSTRUCTIONS_FILE))) { + detected.instructionsFile = DEFAULT_INSTRUCTIONS_FILE; } -} - -export function resolveWorkspaces( - config: unknown, - configFilePath: string, - cwd: string, -): boolean { - const hasConfigWorkspaces = - typeof config === "object" && config !== null && "workspaces" in config; - - if (hasConfigWorkspaces) { - if (typeof config.workspaces === "boolean") { - return config.workspaces; - } - - throw new Error( - `workspaces must be a boolean in config at ${configFilePath}`, - ); + if (fs.existsSync(path.join(rootPath, "instructions"))) { + detected.instructionsDir = "instructions"; } - return detectWorkspacesFromPackageJson(cwd); -} - -export function applyDefaults(config: RawConfig, workspaces: boolean): Config { - return { - rootDir: config.rootDir, - targets: config.targets || ["cursor"], - presets: config.presets || [], - mcpServers: config.mcpServers || {}, - workspaces, - skipInstall: config.skipInstall || false, - }; + return detected; } -export function validateConfig( +function validateConfig( config: unknown, configFilePath: string, cwd: string, isWorkspaceMode: boolean = false, -): asserts config is Config { +): void { if (typeof config !== "object" || config === null) { throw new Error(`Config is not an object at ${configFilePath}`); } @@ -177,589 +132,278 @@ export function validateConfig( ); } - // Validate rootDir - const hasRootDir = "rootDir" in config && typeof config.rootDir === "string"; + const obj = config as Record; + const hasRootDir = typeof obj.rootDir === "string"; + const hasExplicitFile = typeof obj.instructionsFile === "string"; + const hasExplicitDir = typeof obj.instructionsDir === "string"; const hasPresets = - "presets" in config && - Array.isArray(config.presets) && - config.presets.length > 0; + Array.isArray(obj.presets) && (obj.presets as unknown[]).length > 0; - if (hasRootDir) { - const rootPath = path.resolve(cwd, config.rootDir as string); + // Resolve the effective instruction source (explicit or auto-detected) + const baseDir = hasRootDir ? path.resolve(cwd, obj.rootDir as string) : cwd; + let resolvedFile: string | undefined; + let resolvedDir: string | undefined; + if (hasExplicitFile) { + resolvedFile = obj.instructionsFile as string; + } + if (hasExplicitDir) { + resolvedDir = obj.instructionsDir as string; + } + if (!hasExplicitFile && !hasExplicitDir && hasRootDir) { + const detected = autoDetectInstructions( + path.resolve(cwd, obj.rootDir as string), + ); + resolvedFile = detected.instructionsFile; + resolvedDir = detected.instructionsDir; + } + + const hasInstructions = + resolvedFile !== undefined || resolvedDir !== undefined; + + // Validate explicit paths exist + if (hasExplicitFile) { + const filePath = path.resolve(baseDir, obj.instructionsFile as string); + if (!fs.existsSync(filePath)) { + throw new Error(`Instructions file does not exist: ${filePath}`); + } + } + if (hasExplicitDir) { + const dirPath = path.resolve(baseDir, resolvedDir as string); + if (!fs.existsSync(dirPath)) { + throw new Error(`Instructions path does not exist: ${dirPath}`); + } + } + + if (hasRootDir) { + const rootPath = path.resolve(cwd, obj.rootDir as string); if (!fs.existsSync(rootPath)) { throw new Error(`Root directory does not exist: ${rootPath}`); } - if (!fs.statSync(rootPath).isDirectory()) { throw new Error(`Root path is not a directory: ${rootPath}`); } - // Check for at least one valid subdirectory or file - const hasRules = fs.existsSync(path.join(rootPath, "rules")); - const hasCommands = fs.existsSync(path.join(rootPath, "commands")); + const hasResolvedFile = resolvedFile + ? fs.existsSync(path.resolve(rootPath, resolvedFile)) + : false; + const hasResolvedDir = resolvedDir + ? fs.existsSync(path.resolve(rootPath, resolvedDir)) + : false; + const hasInstructionsSource = hasResolvedFile || hasResolvedDir; const hasHooks = fs.existsSync(path.join(rootPath, "hooks.json")); const hasSkills = fs.existsSync(path.join(rootPath, "skills")); const hasAgents = fs.existsSync(path.join(rootPath, "agents")); - // In workspace mode, root config doesn't need these directories - // since packages will have their own configurations if ( !isWorkspaceMode && - !hasRules && - !hasCommands && + !hasInstructionsSource && !hasHooks && !hasSkills && !hasAgents && !hasPresets ) { throw new Error( - `Root directory must contain at least one of: rules/, commands/, skills/, agents/, hooks.json, or have presets configured`, + `Root directory must contain at least one of: instructions, skills/, agents/, hooks.json, or have presets configured`, ); } - } else if (!isWorkspaceMode && !hasPresets) { - // If no rootDir specified and not in workspace mode, must have presets + } else if (!isWorkspaceMode && !hasPresets && !hasInstructions) { throw new Error( - `At least one of rootDir or presets must be specified in config at ${configFilePath}`, + `At least one of rootDir, instructionsFile, instructionsDir, or presets must be specified in config at ${configFilePath}`, ); } - if ("targets" in config) { - if (!Array.isArray(config.targets)) { - throw new Error( - `targets must be an array in config at ${configFilePath}`, - ); - } - - if (config.targets.length === 0) { - throw new Error( - `targets must not be empty in config at ${configFilePath}`, - ); - } - - for (const target of config.targets) { - if (!SUPPORTED_TARGETS.includes(target as SupportedTarget)) { - throw new Error( - `Unsupported target: ${target}. Supported targets: ${SUPPORTED_TARGETS.join(", ")}`, - ); - } - } - } -} - -export async function loadRulesFromDirectory( - directoryPath: string, - source: "local" | "preset", - presetName?: string, -): Promise { - const rules: RuleFile[] = []; - - if (!fs.existsSync(directoryPath)) { - return rules; - } - - const pattern = path.join(directoryPath, "**/*.mdc").replace(/\\/g, "/"); - const filePaths = await fg(pattern, { - onlyFiles: true, - absolute: true, - }); - - for (const filePath of filePaths) { - const content = await fs.readFile(filePath, "utf8"); - - // Preserve directory structure by using relative path from source directory - const relativePath = path.relative(directoryPath, filePath); - const ruleName = relativePath.replace(/\.mdc$/, "").replace(/\\/g, "/"); - - rules.push({ - name: ruleName, - content, - sourcePath: filePath, - source, - presetName, - }); - } - - return rules; -} - -export async function loadCommandsFromDirectory( - directoryPath: string, - source: "local" | "preset", - presetName?: string, -): Promise { - const commands: CommandFile[] = []; - - if (!fs.existsSync(directoryPath)) { - return commands; + if ("targets" in obj) { + validateTargetsInput(obj.targets, configFilePath); } - - const pattern = path.join(directoryPath, "**/*.md").replace(/\\/g, "/"); - const filePaths = await fg(pattern, { - onlyFiles: true, - absolute: true, - }); - - filePaths.sort(); - - for (const filePath of filePaths) { - const content = await fs.readFile(filePath, "utf8"); - const relativePath = path.relative(directoryPath, filePath); - const commandName = relativePath.replace(/\.md$/, "").replace(/\\/g, "/"); - - commands.push({ - name: commandName, - content, - sourcePath: filePath, - source, - presetName, - }); - } - - return commands; } -export async function loadAssetsFromDirectory( - directoryPath: string, - source: "local" | "preset", - presetName?: string, -): Promise { - const assets: AssetFile[] = []; - - if (!fs.existsSync(directoryPath)) { - return assets; - } - - // Find all files except .mdc files and hidden files - const pattern = path.join(directoryPath, "**/*").replace(/\\/g, "/"); - const filePaths = await fg(pattern, { - onlyFiles: true, - absolute: true, - ignore: ["**/*.mdc", "**/.*"], - }); - - for (const filePath of filePaths) { - const content = await fs.readFile(filePath); - // Preserve directory structure by using relative path from source directory - const relativePath = path.relative(directoryPath, filePath); - // Keep extension for assets - const assetName = relativePath.replace(/\\/g, "/"); - - assets.push({ - name: assetName, - content, - sourcePath: filePath, - source, - presetName, - }); +export function detectWorkspacesFromPackageJson(cwd: string): boolean { + try { + const pkgPath = path.join(cwd, "package.json"); + if (!fs.existsSync(pkgPath)) return false; + const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8")); + return Boolean(pkg.workspaces); + } catch { + return false; } - - return assets; } -/** - * Load skills from a skills/ directory - * Each direct subdirectory containing a SKILL.md file is considered a skill - */ -export async function loadSkillsFromDirectory( - directoryPath: string, - source: "local" | "preset", - presetName?: string, -): Promise { - const skills: SkillFile[] = []; - - if (!fs.existsSync(directoryPath)) { - return skills; - } - - // Get all direct subdirectories - const entries = await fs.readdir(directoryPath, { withFileTypes: true }); - - for (const entry of entries) { - if (!entry.isDirectory()) { - continue; - } - - const skillPath = path.join(directoryPath, entry.name); - const skillMdPath = path.join(skillPath, "SKILL.md"); +function resolveWorkspacesFlag( + config: unknown, + configFilePath: string, + cwd: string, +): boolean { + const hasWorkspaces = + typeof config === "object" && config !== null && "workspaces" in config; - // Only include directories that contain a SKILL.md file - if (!fs.existsSync(skillMdPath)) { - continue; + if (hasWorkspaces) { + if (typeof (config as Record).workspaces === "boolean") { + return (config as Record).workspaces as boolean; } - - skills.push({ - name: entry.name, - sourcePath: skillPath, - source, - presetName, - }); - } - - return skills; -} - -/** - * Load agents from an agents/ directory - * Agents are markdown files (.md) with YAML frontmatter - */ -export async function loadAgentsFromDirectory( - directoryPath: string, - source: "local" | "preset", - presetName?: string, -): Promise { - const agents: AgentFile[] = []; - - if (!fs.existsSync(directoryPath)) { - return agents; - } - - const pattern = path.join(directoryPath, "**/*.md").replace(/\\/g, "/"); - const filePaths = await fg(pattern, { - onlyFiles: true, - absolute: true, - }); - - filePaths.sort(); - - for (const filePath of filePaths) { - const content = await fs.readFile(filePath, "utf8"); - const relativePath = path.relative(directoryPath, filePath); - const agentName = relativePath.replace(/\.md$/, "").replace(/\\/g, "/"); - - agents.push({ - name: agentName, - content, - sourcePath: filePath, - source, - presetName, - }); + throw new Error( + `workspaces must be a boolean in config at ${configFilePath}`, + ); } - return agents; + return detectWorkspacesFromPackageJson(cwd); } -/** - * Extract namespace from preset path for directory structure - * Handles both npm packages and local paths consistently - */ -export function extractNamespaceFromPresetPath(presetPath: string): string[] { - // Special case: npm package names always use forward slashes, regardless of platform - if (presetPath.startsWith("@")) { - // For scoped packages like @scope/package/subdir, create nested directories - return presetPath.split("/"); - } - - // Always split by forward slash since JSON config files use forward slashes on all platforms - const parts = presetPath.split(path.posix.sep); - return parts.filter( - (part) => part.length > 0 && part !== "." && part !== "..", - ); +interface RawConfig { + rootDir?: string; + instructionsFile?: string; + instructionsDir?: string; + targets?: string[]; + presets?: string[]; + mcpServers?: MCPServers; + workspaces?: boolean; + skipInstall?: boolean; } -export function resolvePresetPath( - presetPath: string, +function applyDefaults( + raw: RawConfig, + workspaces: boolean, cwd: string, -): string | null { - // Support specifying aicm.json directory and load the config from it - if (!presetPath.endsWith(".json")) { - presetPath = path.join(presetPath, "aicm.json"); - } +): Config { + let instructionsFile = raw.instructionsFile; + let instructionsDir = raw.instructionsDir; - // Support local or absolute paths - const absolutePath = path.isAbsolute(presetPath) - ? presetPath - : path.resolve(cwd, presetPath); - - if (fs.existsSync(absolutePath)) { - return absolutePath; + // Auto-detect when neither field is explicitly set + if (!instructionsFile && !instructionsDir && raw.rootDir) { + const detected = autoDetectInstructions(path.resolve(cwd, raw.rootDir)); + instructionsFile = detected.instructionsFile; + instructionsDir = detected.instructionsDir; } - try { - // Support npm packages - const resolvedPath = require.resolve(presetPath, { - paths: [cwd, __dirname], - }); - return fs.existsSync(resolvedPath) ? resolvedPath : null; - } catch { - return null; - } + return { + rootDir: raw.rootDir, + instructionsFile, + instructionsDir, + targets: resolveTargets(raw.targets), + presets: raw.presets || [], + mcpServers: raw.mcpServers || {}, + workspaces, + skipInstall: raw.skipInstall || false, + }; } -export async function loadPreset( - presetPath: string, - cwd: string, -): Promise<{ - config: Config; - rootDir: string; - resolvedPath: string; -}> { - const resolvedPresetPath = resolvePresetPath(presetPath, cwd); - - if (!resolvedPresetPath) { - throw new Error( - `Preset not found: "${presetPath}". Make sure the package is installed or the path is correct.`, - ); - } - - let presetConfig: Config; +async function loadConfigFile(searchFrom?: string) { + const explorer = cosmiconfig("aicm", { + searchPlaces: ["aicm.json", "package.json"], + }); try { - const content = await fs.readFile(resolvedPresetPath, "utf8"); - presetConfig = JSON.parse(content); + return await explorer.search(searchFrom); } catch (error) { throw new Error( - `Failed to load preset "${presetPath}": ${error instanceof Error ? error.message : "Unknown error"}`, - ); - } - - const presetDir = path.dirname(resolvedPresetPath); - const presetRootDir = path.resolve(presetDir, presetConfig.rootDir || "./"); - - // Check if preset has content or inherits from other presets - const hasRules = fs.existsSync(path.join(presetRootDir, "rules")); - const hasCommands = fs.existsSync(path.join(presetRootDir, "commands")); - const hasHooks = fs.existsSync(path.join(presetRootDir, "hooks.json")); - const hasAssets = fs.existsSync(path.join(presetRootDir, "assets")); - const hasSkills = fs.existsSync(path.join(presetRootDir, "skills")); - const hasAgents = fs.existsSync(path.join(presetRootDir, "agents")); - const hasNestedPresets = - Array.isArray(presetConfig.presets) && presetConfig.presets.length > 0; - - const hasAnyContent = - hasRules || - hasCommands || - hasHooks || - hasAssets || - hasSkills || - hasAgents || - hasNestedPresets; - - if (!hasAnyContent) { - throw new Error( - `Preset "${presetPath}" must have at least one of: rules/, commands/, skills/, agents/, hooks.json, assets/, or presets`, + `Failed to load configuration: ${error instanceof Error ? error.message : "Unknown error"}`, ); } - - return { - config: presetConfig, - rootDir: presetRootDir, - resolvedPath: resolvedPresetPath, - }; } /** - * Result of recursively loading a preset and its dependencies + * Check if workspaces mode is enabled without loading all resources. */ -interface PresetLoadResult { - rules: RuleFile[]; - commands: CommandFile[]; - assets: AssetFile[]; - skills: SkillFile[]; - agents: AgentFile[]; - mcpServers: MCPServers; - hooksConfigs: HooksJson[]; - hookFiles: HookFile[]; -} - -/** - * Recursively load a preset and all its dependencies - * @param presetPath The original preset path (used for namespacing) - * @param cwd The current working directory for resolving paths - * @param visited Set of already visited preset paths (by resolved absolute path) for cycle detection - */ -async function loadPresetRecursively( - presetPath: string, - cwd: string, - visited: Set, -): Promise { - const preset = await loadPreset(presetPath, cwd); - const presetRootDir = preset.rootDir; - const presetDir = path.dirname(preset.resolvedPath); - - // Check for circular dependency - if (visited.has(preset.resolvedPath)) { - throw new Error( - `Circular preset dependency detected: "${presetPath}" has already been loaded`, - ); - } - visited.add(preset.resolvedPath); - - const result: PresetLoadResult = { - rules: [], - commands: [], - assets: [], - skills: [], - agents: [], - mcpServers: {}, - hooksConfigs: [], - hookFiles: [], - }; - - // Load entities from this preset's rootDir - const presetRulesPath = path.join(presetRootDir, "rules"); - if (fs.existsSync(presetRulesPath)) { - const presetRules = await loadRulesFromDirectory( - presetRulesPath, - "preset", - presetPath, - ); - result.rules.push(...presetRules); - } +export async function checkWorkspacesEnabled(cwd?: string): Promise { + const workingDir = cwd || process.cwd(); + const result = await loadConfigFile(workingDir); - const presetCommandsPath = path.join(presetRootDir, "commands"); - if (fs.existsSync(presetCommandsPath)) { - const presetCommands = await loadCommandsFromDirectory( - presetCommandsPath, - "preset", - presetPath, - ); - result.commands.push(...presetCommands); + if (!result?.config) { + return detectWorkspacesFromPackageJson(workingDir); } - const presetHooksFile = path.join(presetRootDir, "hooks.json"); - if (fs.existsSync(presetHooksFile)) { - const { config: presetHooksConfig, files: presetHookFiles } = - await loadHooksFromDirectory(presetRootDir, "preset", presetPath); - result.hooksConfigs.push(presetHooksConfig); - result.hookFiles.push(...presetHookFiles); - } + return resolveWorkspacesFlag(result.config, result.filepath, workingDir); +} - const presetAssetsPath = path.join(presetRootDir, "assets"); - if (fs.existsSync(presetAssetsPath)) { - const presetAssets = await loadAssetsFromDirectory( - presetAssetsPath, - "preset", - presetPath, - ); - result.assets.push(...presetAssets); - } +/** + * Load and fully resolve the configuration, including all instructions, + * skills, agents, hooks, and presets. + */ +export async function loadConfig(cwd?: string): Promise { + const workingDir = cwd || process.cwd(); + const configResult = await loadConfigFile(workingDir); - const presetSkillsPath = path.join(presetRootDir, "skills"); - if (fs.existsSync(presetSkillsPath)) { - const presetSkills = await loadSkillsFromDirectory( - presetSkillsPath, - "preset", - presetPath, - ); - result.skills.push(...presetSkills); + if (!configResult?.config) { + return null; } - const presetAgentsPath = path.join(presetRootDir, "agents"); - if (fs.existsSync(presetAgentsPath)) { - const presetAgents = await loadAgentsFromDirectory( - presetAgentsPath, - "preset", - presetPath, - ); - result.agents.push(...presetAgents); - } + const raw = configResult.config; + const isWorkspaces = resolveWorkspacesFlag( + raw, + configResult.filepath, + workingDir, + ); - // Add MCP servers from this preset - if (preset.config.mcpServers) { - result.mcpServers = { ...preset.config.mcpServers }; - } + validateConfig(raw, configResult.filepath, workingDir, isWorkspaces); - // Recursively load nested presets - if (preset.config.presets && preset.config.presets.length > 0) { - for (const nestedPresetPath of preset.config.presets) { - const nestedResult = await loadPresetRecursively( - nestedPresetPath, - presetDir, // Use preset's directory as cwd for relative paths - visited, - ); + const config = applyDefaults(raw, isWorkspaces, workingDir); - // Merge results from nested preset - result.rules.push(...nestedResult.rules); - result.commands.push(...nestedResult.commands); - result.assets.push(...nestedResult.assets); - result.skills.push(...nestedResult.skills); - result.agents.push(...nestedResult.agents); - result.hooksConfigs.push(...nestedResult.hooksConfigs); - result.hookFiles.push(...nestedResult.hookFiles); - - // Merge MCP servers (current preset takes precedence over nested) - result.mcpServers = mergePresetMcpServers( - result.mcpServers, - nestedResult.mcpServers, - ); - } - } + const { instructions, skills, agents, mcpServers, hooks, hookFiles } = + await loadAllResources(config, workingDir); - return result; + return { config, instructions, skills, agents, mcpServers, hooks, hookFiles }; } -export async function loadAllRules( +// ---------- Resource loading ---------- + +async function loadAllResources( config: Config, cwd: string, ): Promise<{ - rules: RuleFile[]; - commands: CommandFile[]; - assets: AssetFile[]; + instructions: InstructionFile[]; skills: SkillFile[]; agents: AgentFile[]; mcpServers: MCPServers; hooks: HooksJson; hookFiles: HookFile[]; }> { - const allRules: RuleFile[] = []; - const allCommands: CommandFile[] = []; - const allAssets: AssetFile[] = []; + const allInstructions: InstructionFile[] = []; const allSkills: SkillFile[] = []; const allAgents: AgentFile[] = []; const allHookFiles: HookFile[] = []; - const allHooksConfigs: HooksJson[] = []; let mergedMcpServers: MCPServers = { ...config.mcpServers }; + const allHooksConfigs: HooksJson[] = []; + + // Load local instructions (single file and/or directory) + const basePath = config.rootDir ? path.resolve(cwd, config.rootDir) : cwd; + if (config.instructionsFile) { + const instructionsFilePath = path.resolve( + basePath, + config.instructionsFile, + ); + const local = await loadInstructionsFromPath(instructionsFilePath, "local"); + allInstructions.push(...local); + } + if (config.instructionsDir) { + const instructionsDirPath = path.resolve(basePath, config.instructionsDir); + const local = await loadInstructionsFromPath(instructionsDirPath, "local"); + allInstructions.push(...local); + } - // Load local files from rootDir only if specified + // Load local skills, agents, hooks from rootDir if (config.rootDir) { const rootPath = path.resolve(cwd, config.rootDir); - // Load rules from rules/ subdirectory - const rulesPath = path.join(rootPath, "rules"); - if (fs.existsSync(rulesPath)) { - const localRules = await loadRulesFromDirectory(rulesPath, "local"); - allRules.push(...localRules); - } - - // Load commands from commands/ subdirectory - const commandsPath = path.join(rootPath, "commands"); - if (fs.existsSync(commandsPath)) { - const localCommands = await loadCommandsFromDirectory( - commandsPath, - "local", - ); - allCommands.push(...localCommands); - } - - // Load hooks from hooks.json (sibling to hooks/ directory) const hooksFilePath = path.join(rootPath, "hooks.json"); if (fs.existsSync(hooksFilePath)) { - const { config: localHooksConfig, files: localHookFiles } = - await loadHooksFromDirectory(rootPath, "local"); - allHooksConfigs.push(localHooksConfig); - allHookFiles.push(...localHookFiles); - } - - // Load assets from assets/ subdirectory - const assetsPath = path.join(rootPath, "assets"); - if (fs.existsSync(assetsPath)) { - const localAssets = await loadAssetsFromDirectory(assetsPath, "local"); - allAssets.push(...localAssets); + const { config: hooksConfig, files } = await loadHooksFromDirectory( + rootPath, + "local", + ); + allHooksConfigs.push(hooksConfig); + allHookFiles.push(...files); } - // Load skills from skills/ subdirectory const skillsPath = path.join(rootPath, "skills"); if (fs.existsSync(skillsPath)) { - const localSkills = await loadSkillsFromDirectory(skillsPath, "local"); - allSkills.push(...localSkills); + const skills = await loadSkillsFromDirectory(skillsPath, "local"); + allSkills.push(...skills); } - // Load agents from agents/ subdirectory const agentsPath = path.join(rootPath, "agents"); if (fs.existsSync(agentsPath)) { - const localAgents = await loadAgentsFromDirectory(agentsPath, "local"); - allAgents.push(...localAgents); + const agents = await loadAgentsFromDirectory(agentsPath, "local"); + allAgents.push(...agents); } } @@ -768,35 +412,27 @@ export async function loadAllRules( const visited = new Set(); for (const presetPath of config.presets) { - const presetResult = await loadPresetRecursively( - presetPath, - cwd, - visited, - ); - - allRules.push(...presetResult.rules); - allCommands.push(...presetResult.commands); - allAssets.push(...presetResult.assets); - allSkills.push(...presetResult.skills); - allAgents.push(...presetResult.agents); - allHooksConfigs.push(...presetResult.hooksConfigs); - allHookFiles.push(...presetResult.hookFiles); - - // Merge MCP servers (local config takes precedence) + const result = await loadPresetRecursively(presetPath, cwd, visited); + allInstructions.push(...result.instructions); + allSkills.push(...result.skills); + allAgents.push(...result.agents); + allHooksConfigs.push(...result.hooksConfigs); + allHookFiles.push(...result.hookFiles); mergedMcpServers = mergePresetMcpServers( mergedMcpServers, - presetResult.mcpServers, + result.mcpServers, ); } } - // Merge all hooks configurations - const mergedHooks = mergeHooksConfigs(allHooksConfigs); + // Merge hooks configs + const mergedHooks: HooksJson = + allHooksConfigs.length > 0 + ? mergeHooksConfigs(allHooksConfigs) + : { version: 1, hooks: {} }; return { - rules: allRules, - commands: allCommands, - assets: allAssets, + instructions: allInstructions, skills: allSkills, agents: allAgents, mcpServers: mergedMcpServers, @@ -805,123 +441,59 @@ export async function loadAllRules( }; } -/** - * Merge preset MCP servers with local config MCP servers - * Local config takes precedence over preset config - */ -function mergePresetMcpServers( - configMcpServers: MCPServers, - presetMcpServers: MCPServers, -): MCPServers { - const newMcpServers = { ...configMcpServers }; - - for (const [serverName, serverConfig] of Object.entries(presetMcpServers)) { - // Cancel if set to false in config - if ( - Object.prototype.hasOwnProperty.call(newMcpServers, serverName) && - newMcpServers[serverName] === false - ) { - delete newMcpServers[serverName]; - continue; - } - // Only add if not already defined in config (local config takes precedence) - if (!Object.prototype.hasOwnProperty.call(newMcpServers, serverName)) { - newMcpServers[serverName] = serverConfig; - } - } - - return newMcpServers; -} - -export async function loadConfigFile( - searchFrom?: string, -): Promise { - const explorer = cosmiconfig("aicm", { - searchPlaces: ["aicm.json", "package.json"], - }); - - try { - const result = await explorer.search(searchFrom); - return result; - } catch (error) { - throw new Error( - `Failed to load configuration: ${error instanceof Error ? error.message : "Unknown error"}`, - ); - } -} - -/** - * Check if workspaces mode is enabled without loading all rules/presets - * This is useful for commands that only need to know the workspace setting - */ -export async function checkWorkspacesEnabled(cwd?: string): Promise { - const workingDir = cwd || process.cwd(); +async function loadSkillsFromDirectory( + directoryPath: string, + source: "local" | "preset", + presetName?: string, +): Promise { + if (!fs.existsSync(directoryPath)) return []; - const configResult = await loadConfigFile(workingDir); + const entries = await fs.readdir(directoryPath, { withFileTypes: true }); + const skills: SkillFile[] = []; - if (!configResult?.config) { - return detectWorkspacesFromPackageJson(workingDir); + for (const entry of entries) { + if (!entry.isDirectory()) continue; + const skillPath = path.join(directoryPath, entry.name); + if (!fs.existsSync(path.join(skillPath, "SKILL.md"))) continue; + skills.push({ + name: entry.name, + sourcePath: skillPath, + source, + presetName, + }); } - return resolveWorkspaces( - configResult.config, - configResult.filepath, - workingDir, - ); + return skills; } -export async function loadConfig(cwd?: string): Promise { - const workingDir = cwd || process.cwd(); - - const configResult = await loadConfigFile(workingDir); - - if (!configResult?.config) { - return null; - } - - const config = configResult.config; - const isWorkspaces = resolveWorkspaces( - config, - configResult.filepath, - workingDir, - ); - - validateConfig(config, configResult.filepath, workingDir, isWorkspaces); - - const configWithDefaults = applyDefaults(config, isWorkspaces); - - const { - rules, - commands, - assets, - skills, - agents, - mcpServers, - hooks, - hookFiles, - } = await loadAllRules(configWithDefaults, workingDir); +export { loadSkillsFromDirectory, loadAgentsFromDirectory }; - return { - config: configWithDefaults, - rules, - commands, - assets, - skills, - agents, - mcpServers, - hooks, - hookFiles, - }; -} +async function loadAgentsFromDirectory( + directoryPath: string, + source: "local" | "preset", + presetName?: string, +): Promise { + if (!fs.existsSync(directoryPath)) return []; -export function saveConfig(config: Config, cwd?: string): boolean { - const workingDir = cwd || process.cwd(); - const configPath = path.join(workingDir, "aicm.json"); + const pattern = path.join(directoryPath, "**/*.md").replace(/\\/g, "/"); + const filePaths = await fg(pattern, { onlyFiles: true, absolute: true }); + filePaths.sort(); - try { - fs.writeFileSync(configPath, JSON.stringify(config, null, 2)); - return true; - } catch { - return false; + const agents: AgentFile[] = []; + for (const filePath of filePaths) { + const content = await fs.readFile(filePath, "utf8"); + const relativePath = path + .relative(directoryPath, filePath) + .replace(/\\/g, "/"); + const agentName = relativePath.replace(/\.md$/, ""); + agents.push({ + name: agentName, + content, + sourcePath: filePath, + source, + presetName, + }); } + + return agents; } diff --git a/src/utils/git.ts b/src/utils/git.ts new file mode 100644 index 0000000..be1dfc1 --- /dev/null +++ b/src/utils/git.ts @@ -0,0 +1,104 @@ +/** + * Git clone operations (shallow clone and sparse checkout). + */ + +import { execFile as execFileCb } from "child_process"; +import { promisify } from "util"; + +const execFile = promisify(execFileCb); + +/** 60 seconds -- generous to handle slow networks and large repos. */ +const GIT_TIMEOUT_MS = 60_000; + +/** + * Shallow clone (--depth 1) of a git repo. Downloads all blobs for the + * latest commit -- suitable for small repos. + */ +export async function shallowClone( + url: string, + destPath: string, + ref?: string, +): Promise { + const args = ["clone", "--depth", "1"]; + if (ref) args.push("--branch", ref); + args.push(url, destPath); + + try { + await execFile("git", args, { timeout: GIT_TIMEOUT_MS }); + } catch (error) { + throw wrapGitError(error, url); + } +} + +/** + * Sparse checkout clone. Uses --filter=blob:none so only tree/commit objects + * are fetched initially, then materializes only the specified `paths`. + * Requires git 2.25+. + */ +export async function sparseClone( + url: string, + destPath: string, + paths: string[], + ref?: string, +): Promise { + const cloneArgs = ["clone", "--filter=blob:none", "--sparse", "--depth", "1"]; + if (ref) cloneArgs.push("--branch", ref); + cloneArgs.push(url, destPath); + + try { + await execFile("git", cloneArgs, { timeout: GIT_TIMEOUT_MS }); + } catch (error) { + throw wrapGitError(error, url); + } + + try { + await execFile("git", ["sparse-checkout", "set", ...paths], { + cwd: destPath, + timeout: GIT_TIMEOUT_MS, + }); + } catch (error) { + throw wrapGitError(error, url, true); + } +} + +function wrapGitError( + error: unknown, + url: string, + isSparseCheckout?: boolean, +): Error { + const message = + error instanceof Error ? error.message : String(error ?? "Unknown error"); + const lowerMessage = message.toLowerCase(); + + if (lowerMessage.includes("timed out") || lowerMessage.includes("timeout")) { + return new Error( + `Git operation timed out for "${url}". The repository may be too large or the network is slow.`, + ); + } + + if ( + lowerMessage.includes("authentication failed") || + lowerMessage.includes("could not read username") || + lowerMessage.includes("permission denied") || + lowerMessage.includes("repository not found") + ) { + return new Error( + `Git authentication failed for "${url}". For private repositories, set GITHUB_TOKEN or run "gh auth login".`, + ); + } + + if (lowerMessage.includes("enoent")) { + return new Error( + `Git is not installed or not found in PATH. Please install git to use GitHub presets.`, + ); + } + + if (isSparseCheckout && lowerMessage.includes("sparse-checkout")) { + return new Error( + `Sparse checkout failed for "${url}". Your git version may not support sparse checkout (requires git 2.25+). ` + + `Try updating git or use a smaller repository.`, + ); + } + + return new Error(`Git operation failed for "${url}": ${message}`); +} diff --git a/src/utils/github.ts b/src/utils/github.ts new file mode 100644 index 0000000..0dd28a4 --- /dev/null +++ b/src/utils/github.ts @@ -0,0 +1,91 @@ +/** + * GitHub API helpers for preflight checks and sparse checkout. + */ + +import { execSync } from "child_process"; + +/** Repos larger than this (in KB) trigger sparse checkout instead of shallow clone. */ +export const SPARSE_CHECKOUT_THRESHOLD_KB = 50 * 1024; // 50 MB + +/** + * Resolve a GitHub token. Checks GITHUB_TOKEN, GH_TOKEN, then `gh auth token`. + */ +export function getGitHubToken(): string | null { + if (process.env.GITHUB_TOKEN) return process.env.GITHUB_TOKEN; + if (process.env.GH_TOKEN) return process.env.GH_TOKEN; + + try { + const token = execSync("gh auth token", { + encoding: "utf8", + stdio: ["pipe", "pipe", "pipe"], + }).trim(); + return token || null; + } catch { + return null; + } +} + +function buildHeaders(token?: string | null): Record { + const headers: Record = { + Accept: "application/vnd.github.v3+json", + "User-Agent": "aicm", + }; + if (token) headers["Authorization"] = `Bearer ${token}`; + return headers; +} + +/** Fetch repo size in KB via the GitHub REST API. Returns null on failure. */ +export async function fetchRepoSize( + owner: string, + repo: string, + token?: string | null, +): Promise { + const url = `https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`; + + try { + const response = await fetch(url, { headers: buildHeaders(token) }); + if (!response.ok) return null; + const data = (await response.json()) as { size?: number }; + return typeof data.size === "number" ? data.size : null; + } catch { + return null; + } +} + +/** + * Fetch a single file's content from GitHub via the contents API. + * Only works for files under 1 MB (GitHub API limitation). + */ +export async function fetchFileContent( + owner: string, + repo: string, + filePath: string, + ref?: string, + token?: string | null, +): Promise { + const encodedPath = filePath + .split("/") + .map((s) => encodeURIComponent(s)) + .join("/"); + let url = `https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/contents/${encodedPath}`; + if (ref) url += `?ref=${encodeURIComponent(ref)}`; + + try { + const response = await fetch(url, { headers: buildHeaders(token) }); + if (!response.ok) return null; + + const data = (await response.json()) as { + type?: string; + content?: string; + encoding?: string; + }; + if (data.type !== "file" || !data.content) return null; + + if (data.encoding === "base64") { + return Buffer.from(data.content, "base64").toString("utf8"); + } + return data.content; + } catch { + return null; + } +} diff --git a/src/utils/hooks.ts b/src/utils/hooks.ts index 3b30cec..25ed090 100644 --- a/src/utils/hooks.ts +++ b/src/utils/hooks.ts @@ -1,7 +1,10 @@ +/** + * Hook loading, merging, and writing to Cursor and Claude Code targets. + */ + import crypto from "node:crypto"; import fs from "fs-extra"; import path from "node:path"; -import { extractNamespaceFromPresetPath } from "./config"; export type HookType = | "beforeShellExecution" @@ -19,78 +22,68 @@ export interface HookCommand { export interface HooksJson { version: number; - hooks: { - [K in HookType]?: HookCommand[]; - }; + hooks: { [K in HookType]?: HookCommand[] }; } export interface HookFile { - name: string; // Namespaced path for installation (e.g., "preset-name/script.sh" or "script.sh") - basename: string; // Original basename (e.g., "script.sh") + name: string; + basename: string; content: Buffer; sourcePath: string; source: "local" | "preset"; presetName?: string; } -/** - * Validate that a command path points to a file within the hooks directory - * Commands should be relative paths starting with ./hooks/ - */ -function validateHookPath( - commandPath: string, - rootDir: string, - hooksDir: string, -): { valid: boolean; relativePath?: string } { - if (!commandPath.startsWith("./") && !commandPath.startsWith("../")) { - return { valid: false }; - } - - // Resolve path relative to rootDir (where hooks.json is located) - const resolvedPath = path.resolve(rootDir, commandPath); - const relativePath = path.relative(hooksDir, resolvedPath); +// ---------- Namespace extraction ---------- - // Check if the file is within hooks directory (not using .. to escape) - if (relativePath.startsWith("..") || path.isAbsolute(relativePath)) { - return { valid: false }; +function extractNamespaceFromPresetPath(presetPath: string): string[] { + if (presetPath.startsWith("@")) { + return presetPath.split("/"); } - - return { valid: true, relativePath }; + const parts = presetPath.split(path.posix.sep); + return parts.filter((p) => p.length > 0 && p !== "." && p !== ".."); } -/** - * Load all files from the hooks directory - */ +// ---------- Loading ---------- + async function loadAllFilesFromDirectory( dir: string, baseDir: string = dir, ): Promise> { const files: Array<{ relativePath: string; absolutePath: string }> = []; - const entries = await fs.readdir(dir, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(dir, entry.name); - if (entry.isDirectory()) { - // Recursively load files from subdirectories - const subFiles = await loadAllFilesFromDirectory(fullPath, baseDir); - files.push(...subFiles); + files.push(...(await loadAllFilesFromDirectory(fullPath, baseDir))); } else if (entry.isFile() && entry.name !== "hooks.json") { - // Skip hooks.json, collect all other files - const relativePath = path.relative(baseDir, fullPath); - files.push({ relativePath, absolutePath: fullPath }); + files.push({ + relativePath: path.relative(baseDir, fullPath), + absolutePath: fullPath, + }); } } return files; } -/** - * Load hooks configuration from a root directory - * The directory should contain hooks.json (sibling to hooks/ directory) - * All files in the hooks/ subdirectory are copied during installation - */ +function validateHookPath( + commandPath: string, + rootDir: string, + hooksDir: string, +): { valid: boolean; relativePath?: string } { + if (!commandPath.startsWith("./") && !commandPath.startsWith("../")) { + return { valid: false }; + } + const resolvedPath = path.resolve(rootDir, commandPath); + const relativePath = path.relative(hooksDir, resolvedPath); + if (relativePath.startsWith("..") || path.isAbsolute(relativePath)) { + return { valid: false }; + } + return { valid: true, relativePath }; +} + export async function loadHooksFromDirectory( rootDir: string, source: "local" | "preset", @@ -100,45 +93,35 @@ export async function loadHooksFromDirectory( const hooksDir = path.join(rootDir, "hooks"); if (!fs.existsSync(hooksFilePath)) { - return { - config: { version: 1, hooks: {} }, - files: [], - }; + return { config: { version: 1, hooks: {} }, files: [] }; } const content = await fs.readFile(hooksFilePath, "utf8"); const hooksConfig: HooksJson = JSON.parse(content); - // Load all files from hooks/ subdirectory const hookFiles: HookFile[] = []; + if (fs.existsSync(hooksDir)) { const allFiles = await loadAllFilesFromDirectory(hooksDir); - // Create a map of all files for validation - const filePathMap = new Map(); - for (const file of allFiles) { - filePathMap.set(file.relativePath, file.absolutePath); - } - - // Validate that all referenced commands point to files within hooks directory + // Validate commands if (hooksConfig.hooks) { for (const hookType of Object.keys(hooksConfig.hooks) as HookType[]) { - const hookCommands = hooksConfig.hooks[hookType]; - - if (hookCommands && Array.isArray(hookCommands)) { - for (const hookCommand of hookCommands) { - const commandPath = hookCommand.command; - - if (commandPath && typeof commandPath === "string") { + const commands = hooksConfig.hooks[hookType]; + if (commands && Array.isArray(commands)) { + for (const hookCommand of commands) { + if ( + hookCommand.command && + typeof hookCommand.command === "string" + ) { const validation = validateHookPath( - commandPath, + hookCommand.command, rootDir, hooksDir, ); - if (!validation.valid || !validation.relativePath) { console.warn( - `Warning: Hook command "${commandPath}" in hooks.json must reference a file within the hooks/ directory. Skipping.`, + `Warning: Hook command "${hookCommand.command}" in hooks.json must reference a file within the hooks/ directory. Skipping.`, ); } } @@ -147,22 +130,18 @@ export async function loadHooksFromDirectory( } } - // Copy all files from the hooks/ directory for (const file of allFiles) { const fileContent = await fs.readFile(file.absolutePath); const basename = path.basename(file.absolutePath); - // Namespace preset files let namespacedPath: string; if (source === "preset" && presetName) { const namespace = extractNamespaceFromPresetPath(presetName); - // Use posix paths for consistent cross-platform behavior namespacedPath = path.posix.join( ...namespace, file.relativePath.split(path.sep).join(path.posix.sep), ); } else { - // For local files, use the relative path as-is namespacedPath = file.relativePath.split(path.sep).join(path.posix.sep); } @@ -177,7 +156,6 @@ export async function loadHooksFromDirectory( } } - // Rewrite the config to use namespaced file names const rewrittenConfig = rewriteHooksConfigForNamespace( hooksConfig, hookFiles, @@ -187,30 +165,23 @@ export async function loadHooksFromDirectory( return { config: rewrittenConfig, files: hookFiles }; } -/** - * Rewrite hooks config to use the namespaced names from the hook files - */ function rewriteHooksConfigForNamespace( hooksConfig: HooksJson, hookFiles: HookFile[], rootDir: string, ): HooksJson { - // Create a map from sourcePath to the hookFile const sourcePathToFile = new Map(); for (const hookFile of hookFiles) { sourcePathToFile.set(hookFile.sourcePath, hookFile); } - const rewritten: HooksJson = { - version: hooksConfig.version, - hooks: {}, - }; + const rewritten: HooksJson = { version: hooksConfig.version, hooks: {} }; if (hooksConfig.hooks) { for (const hookType of Object.keys(hooksConfig.hooks) as HookType[]) { - const hookCommands = hooksConfig.hooks[hookType]; - if (hookCommands && Array.isArray(hookCommands)) { - rewritten.hooks[hookType] = hookCommands + const commands = hooksConfig.hooks[hookType]; + if (commands && Array.isArray(commands)) { + rewritten.hooks[hookType] = commands .map((hookCommand) => { const commandPath = hookCommand.command; if ( @@ -218,19 +189,14 @@ function rewriteHooksConfigForNamespace( typeof commandPath === "string" && (commandPath.startsWith("./") || commandPath.startsWith("../")) ) { - // Resolve path relative to rootDir (where hooks.json is) - const resolvedPath = path.resolve(rootDir, commandPath); - const hookFile = sourcePathToFile.get(resolvedPath); - if (hookFile) { - // Use the namespaced name - return { command: hookFile.name }; - } - // File was invalid or not found, filter it out + const resolved = path.resolve(rootDir, commandPath); + const matched = sourcePathToFile.get(resolved); + if (matched) return { command: matched.name }; return null; } return hookCommand; }) - .filter((cmd): cmd is HookCommand => cmd !== null); + .filter((entry): entry is HookCommand => entry !== null); } } } @@ -238,34 +204,19 @@ function rewriteHooksConfigForNamespace( return rewritten; } -/** - * Merge multiple hooks configurations into one - * Later configurations override earlier ones for the same hook type - */ export function mergeHooksConfigs(configs: HooksJson[]): HooksJson { - const merged: HooksJson = { - version: 1, - hooks: {}, - }; + const merged: HooksJson = { version: 1, hooks: {} }; for (const config of configs) { - // Use the latest version - if (config.version) { - merged.version = config.version; - } - - // Merge hooks - concatenate arrays for each hook type + if (config.version) merged.version = config.version; if (config.hooks) { for (const hookType of Object.keys(config.hooks) as HookType[]) { - const hookCommands = config.hooks[hookType]; - if (hookCommands && Array.isArray(hookCommands)) { - if (!merged.hooks[hookType]) { - merged.hooks[hookType] = []; - } - // Concatenate commands (later configs add to the list) + const commands = config.hooks[hookType]; + if (commands && Array.isArray(commands)) { + if (!merged.hooks[hookType]) merged.hooks[hookType] = []; merged.hooks[hookType] = [ ...(merged.hooks[hookType] || []), - ...hookCommands, + ...commands, ]; } } @@ -275,64 +226,23 @@ export function mergeHooksConfigs(configs: HooksJson[]): HooksJson { return merged; } -/** - * Rewrite command paths to point to the managed hooks directory (hooks/aicm/) - * At this point, paths are already namespaced filenames from loadHooksFromDirectory - */ -export function rewriteHooksConfigToManagedDir( - hooksConfig: HooksJson, -): HooksJson { - const rewritten: HooksJson = { - version: hooksConfig.version, - hooks: {}, - }; - - if (hooksConfig.hooks) { - for (const hookType of Object.keys(hooksConfig.hooks) as HookType[]) { - const hookCommands = hooksConfig.hooks[hookType]; - if (hookCommands && Array.isArray(hookCommands)) { - rewritten.hooks[hookType] = hookCommands.map((hookCommand) => { - const commandPath = hookCommand.command; - if (commandPath && typeof commandPath === "string") { - return { command: `./hooks/aicm/${commandPath}` }; - } - return hookCommand; - }); - } - } - } - - return rewritten; -} - -/** - * Count the number of hook entries in a hooks configuration - */ export function countHooks(hooksConfig: HooksJson): number { let count = 0; if (hooksConfig.hooks) { for (const hookType of Object.keys(hooksConfig.hooks) as HookType[]) { - const hookCommands = hooksConfig.hooks[hookType]; - if (hookCommands && Array.isArray(hookCommands)) { - count += hookCommands.length; - } + const commands = hooksConfig.hooks[hookType]; + if (commands && Array.isArray(commands)) count += commands.length; } } return count; } -/** - * Dedupe hook files by namespaced path, warn on content conflicts - * Presets are namespaced with directories, so same basename from different presets won't collide - */ export function dedupeHookFiles(hookFiles: HookFile[]): HookFile[] { const fileMap = new Map(); for (const hookFile of hookFiles) { - const namespacedPath = hookFile.name; - - if (fileMap.has(namespacedPath)) { - const existing = fileMap.get(namespacedPath)!; + if (fileMap.has(hookFile.name)) { + const existing = fileMap.get(hookFile.name)!; const existingHash = crypto .createHash("md5") .update(existing.content) @@ -343,29 +253,46 @@ export function dedupeHookFiles(hookFiles: HookFile[]): HookFile[] { .digest("hex"); if (existingHash !== currentHash) { - const sourceInfo = hookFile.presetName + const src = hookFile.presetName ? `preset "${hookFile.presetName}"` : hookFile.source; - const existingSourceInfo = existing.presetName + const existSrc = existing.presetName ? `preset "${existing.presetName}"` : existing.source; - console.warn( - `Warning: Hook file "${namespacedPath}" has different content from ${existingSourceInfo} and ${sourceInfo}. Using last occurrence.`, + `Warning: Hook file "${hookFile.name}" has different content from ${existSrc} and ${src}. Using last occurrence.`, ); } - // Last writer wins - fileMap.set(namespacedPath, hookFile); - } else { - fileMap.set(namespacedPath, hookFile); } + fileMap.set(hookFile.name, hookFile); } return Array.from(fileMap.values()); } +function rewriteHooksConfigToManagedDir(hooksConfig: HooksJson): HooksJson { + const rewritten: HooksJson = { version: hooksConfig.version, hooks: {} }; + + if (hooksConfig.hooks) { + for (const hookType of Object.keys(hooksConfig.hooks) as HookType[]) { + const commands = hooksConfig.hooks[hookType]; + if (commands && Array.isArray(commands)) { + rewritten.hooks[hookType] = commands.map((hookCommand) => { + if (hookCommand.command && typeof hookCommand.command === "string") { + return { command: `./hooks/aicm/${hookCommand.command}` }; + } + return hookCommand; + }); + } + } + } + + return rewritten; +} + /** - * Write hooks configuration and files to Cursor target + * Write hooks to Cursor target (.cursor/hooks.json + .cursor/hooks/aicm/). + * Preserves user-defined hooks while replacing aicm-managed ones. */ export function writeHooksToCursor( hooksConfig: HooksJson, @@ -376,23 +303,17 @@ export function writeHooksToCursor( const hooksJsonPath = path.join(cursorRoot, "hooks.json"); const hooksDir = path.join(cursorRoot, "hooks", "aicm"); - // Dedupe hook files - const dedupedHookFiles = dedupeHookFiles(hookFiles); - - // Create hooks directory and clean it + const dedupedFiles = dedupeHookFiles(hookFiles); fs.emptyDirSync(hooksDir); - // Copy hook files to managed directory - for (const hookFile of dedupedHookFiles) { + for (const hookFile of dedupedFiles) { const targetPath = path.join(hooksDir, hookFile.name); fs.ensureDirSync(path.dirname(targetPath)); fs.writeFileSync(targetPath, hookFile.content); } - // Rewrite paths to point to managed directory const finalConfig = rewriteHooksConfigToManagedDir(hooksConfig); - // Read existing hooks.json and preserve user hooks let existingConfig: HooksJson | null = null; if (fs.existsSync(hooksJsonPath)) { try { @@ -402,7 +323,7 @@ export function writeHooksToCursor( } } - // Extract user hooks (non-aicm managed) + // Extract user-defined hooks (not managed by aicm) const userHooks: HooksJson = { version: 1, hooks: {} }; if (existingConfig?.hooks) { for (const hookType of Object.keys(existingConfig.hooks) as HookType[]) { @@ -411,43 +332,174 @@ export function writeHooksToCursor( const userCommands = commands.filter( (cmd) => !cmd.command?.includes("hooks/aicm/"), ); - if (userCommands.length > 0) { - userHooks.hooks[hookType] = userCommands; - } + if (userCommands.length > 0) userHooks.hooks[hookType] = userCommands; } } } - // Merge user hooks with aicm hooks - const mergedConfig: HooksJson = { - version: finalConfig.version, - hooks: {}, - }; + // Merge: user hooks first, then aicm hooks + const mergedConfig: HooksJson = { version: finalConfig.version, hooks: {} }; - // Add user hooks first if (userHooks.hooks) { for (const hookType of Object.keys(userHooks.hooks) as HookType[]) { const commands = userHooks.hooks[hookType]; - if (commands) { - mergedConfig.hooks[hookType] = [...commands]; - } + if (commands) mergedConfig.hooks[hookType] = [...commands]; } } - // Then add aicm hooks if (finalConfig.hooks) { for (const hookType of Object.keys(finalConfig.hooks) as HookType[]) { const commands = finalConfig.hooks[hookType]; if (commands) { - if (!mergedConfig.hooks[hookType]) { - mergedConfig.hooks[hookType] = []; - } + if (!mergedConfig.hooks[hookType]) mergedConfig.hooks[hookType] = []; mergedConfig.hooks[hookType]!.push(...commands); } } } - // Write hooks.json fs.ensureDirSync(path.dirname(hooksJsonPath)); fs.writeJsonSync(hooksJsonPath, mergedConfig, { spaces: 2 }); } + +// ---------- Claude Code hooks ---------- +// Claude Code uses a different hooks format in .claude/settings.json: +// hooks are grouped by event name with matcher patterns, not by aicm HookType. + +const AICM_TO_CLAUDE_CODE_HOOK_MAP: Partial> = { + beforeShellExecution: "PreToolUse", + afterShellExecution: "PostToolUse", + beforeMCPExecution: "PreToolUse", + afterMCPExecution: "PostToolUse", + beforeReadFile: "PreToolUse", + afterFileEdit: "PostToolUse", + beforeSubmitPrompt: "UserPromptSubmit", + stop: "Stop", +}; + +const AICM_TO_CLAUDE_CODE_MATCHER_MAP: Partial< + Record +> = { + beforeShellExecution: "Bash", + afterShellExecution: "Bash", + beforeMCPExecution: "mcp__.*", + afterMCPExecution: "mcp__.*", + beforeReadFile: "Read", + afterFileEdit: "Edit|Write", + beforeSubmitPrompt: undefined, + stop: undefined, +}; + +function convertHooksToClaudeCodeFormat( + hooksConfig: HooksJson, +): Record< + string, + Array<{ matcher?: string; hooks: Array<{ type: string; command: string }> }> +> { + const claudeHooks: Record< + string, + Array<{ matcher?: string; hooks: Array<{ type: string; command: string }> }> + > = {}; + + if (!hooksConfig.hooks) return claudeHooks; + + for (const hookType of Object.keys(hooksConfig.hooks) as HookType[]) { + const commands = hooksConfig.hooks[hookType]; + if (!commands || commands.length === 0) continue; + + const claudeEvent = AICM_TO_CLAUDE_CODE_HOOK_MAP[hookType]; + if (!claudeEvent) continue; + + const matcher = AICM_TO_CLAUDE_CODE_MATCHER_MAP[hookType]; + const handlers = commands.map((hookCommand) => ({ + type: "command" as const, + command: hookCommand.command, + })); + + if (!claudeHooks[claudeEvent]) claudeHooks[claudeEvent] = []; + + const group: { + matcher?: string; + hooks: Array<{ type: string; command: string }>; + } = { hooks: handlers }; + if (matcher) group.matcher = matcher; + + claudeHooks[claudeEvent].push(group); + } + + return claudeHooks; +} + +/** + * Write hooks to Claude Code target (.claude/settings.json + .claude/hooks/aicm/). + * Converts aicm hook types to Claude Code event/matcher format. + */ +export function writeHooksToClaudeCode( + hooksConfig: HooksJson, + hookFiles: HookFile[], + cwd: string, +): void { + const claudeRoot = path.join(cwd, ".claude"); + const settingsPath = path.join(claudeRoot, "settings.json"); + const hooksDir = path.join(claudeRoot, "hooks", "aicm"); + + const dedupedFiles = dedupeHookFiles(hookFiles); + fs.emptyDirSync(hooksDir); + + for (const hookFile of dedupedFiles) { + const targetPath = path.join(hooksDir, hookFile.name); + fs.ensureDirSync(path.dirname(targetPath)); + fs.writeFileSync(targetPath, hookFile.content); + } + + const rewrittenConfig = rewriteHooksConfigToManagedDir(hooksConfig); + const claudeHooks = convertHooksToClaudeCodeFormat(rewrittenConfig); + + let existingSettings: Record = {}; + if (fs.existsSync(settingsPath)) { + try { + existingSettings = fs.readJsonSync(settingsPath); + } catch { + existingSettings = {}; + } + } + + const existingHooks = + (existingSettings.hooks as Record | undefined) ?? {}; + const userHooks: Record = {}; + + for (const [eventName, matcherGroups] of Object.entries(existingHooks)) { + if (Array.isArray(matcherGroups)) { + const userGroups = matcherGroups.filter((group) => { + if (typeof group !== "object" || group === null) return true; + const g = group as Record; + if (!Array.isArray(g.hooks)) return true; + return !g.hooks.some( + (h: unknown) => + typeof h === "object" && + h !== null && + typeof (h as Record).command === "string" && + ((h as Record).command as string).includes( + "hooks/aicm/", + ), + ); + }); + if (userGroups.length > 0) userHooks[eventName] = userGroups; + } + } + + const mergedHooks: Record = { ...userHooks }; + for (const [eventName, groups] of Object.entries(claudeHooks)) { + if (!mergedHooks[eventName]) mergedHooks[eventName] = []; + mergedHooks[eventName].push(...groups); + } + + const mergedSettings: Record = { ...existingSettings }; + if (Object.keys(mergedHooks).length > 0) { + mergedSettings.hooks = mergedHooks; + } else { + delete mergedSettings.hooks; + } + + fs.ensureDirSync(path.dirname(settingsPath)); + fs.writeJsonSync(settingsPath, mergedSettings, { spaces: 2 }); +} diff --git a/src/utils/install-cache.ts b/src/utils/install-cache.ts new file mode 100644 index 0000000..8fa4e16 --- /dev/null +++ b/src/utils/install-cache.ts @@ -0,0 +1,99 @@ +/** + * Install cache for GitHub presets. + * + * Stores metadata at ~/.aicm/install-cache.json and cloned repos under + * ~/.aicm/repos/{owner}/{repo}/ to avoid re-downloading on every install. + */ + +import fs from "fs-extra"; +import path from "node:path"; +import os from "node:os"; + +const CURRENT_CACHE_VERSION = 1; + +export interface InstallCache { + version: number; + entries: Record; +} + +export interface InstallCacheEntry { + /** Full GitHub URL as specified in presets */ + url: string; + /** Branch or tag, if specified */ + ref?: string; + /** Commit SHA at time of clone */ + sha?: string; + /** Sub-path within repo where aicm.json lives */ + subpath?: string; + /** ISO 8601 timestamp */ + cachedAt: string; + /** Absolute path to the cached repo on disk */ + cachePath: string; +} + +function getAicmCacheDir(): string { + return path.join(os.homedir(), ".aicm"); +} + +export function getInstallCachePath(): string { + return path.join(getAicmCacheDir(), "install-cache.json"); +} + +export function getRepoCachePath(owner: string, repo: string): string { + return path.join(getAicmCacheDir(), "repos", owner, repo); +} + +function createEmptyCache(): InstallCache { + return { version: CURRENT_CACHE_VERSION, entries: {} }; +} + +export async function readInstallCache(): Promise { + const cachePath = getInstallCachePath(); + + try { + if (!fs.existsSync(cachePath)) { + return createEmptyCache(); + } + + const content = await fs.readFile(cachePath, "utf8"); + const data = JSON.parse(content) as InstallCache; + + if (data.version !== CURRENT_CACHE_VERSION) { + return createEmptyCache(); + } + + return data; + } catch { + return createEmptyCache(); + } +} + +export async function writeInstallCache(cache: InstallCache): Promise { + const cachePath = getInstallCachePath(); + await fs.ensureDir(path.dirname(cachePath)); + await fs.writeFile(cachePath, JSON.stringify(cache, null, 2)); +} + +export async function getCacheEntry( + key: string, +): Promise { + const cache = await readInstallCache(); + return cache.entries[key] ?? null; +} + +export async function setCacheEntry( + key: string, + entry: InstallCacheEntry, +): Promise { + const cache = await readInstallCache(); + cache.entries[key] = entry; + await writeInstallCache(cache); +} + +export function isCacheValid(entry: InstallCacheEntry): boolean { + return fs.existsSync(entry.cachePath); +} + +export function buildCacheKey(owner: string, repo: string): string { + return `https://github.com/${owner}/${repo}`; +} diff --git a/src/utils/instructions-file.ts b/src/utils/instructions-file.ts new file mode 100644 index 0000000..d9c8a32 --- /dev/null +++ b/src/utils/instructions-file.ts @@ -0,0 +1,63 @@ +/** + * Writing instruction content to target files (AGENTS.md, CLAUDE.md, etc.) + * with marker-based sections that can be updated on reinstall. + */ + +import fs from "fs-extra"; +import path from "node:path"; + +const BEGIN_MARKER = ""; +const END_MARKER = ""; +const WARNING = + ""; + +function createInstructionsBlock(content: string): string { + return `${BEGIN_MARKER}\n${WARNING}\n\n${content}\n\n${END_MARKER}`; +} + +/** + * Write instructions content to a target file, preserving any + * user content outside the AICM markers. + */ +export function writeInstructionsFile( + instructionsContent: string, + filePath: string, +): void { + const block = createInstructionsBlock(instructionsContent); + let fileContent: string; + + if (fs.existsSync(filePath)) { + const existing = fs.readFileSync(filePath, "utf8"); + + if (existing.includes(BEGIN_MARKER) && existing.includes(END_MARKER)) { + const before = existing.split(BEGIN_MARKER)[0]; + const after = existing.split(END_MARKER)[1]; + fileContent = before + block + after; + } else if (existing.trim() === "") { + fileContent = block; + } else { + let separator = ""; + if (!existing.endsWith("\n")) separator += "\n"; + if (!existing.endsWith("\n\n")) separator += "\n"; + fileContent = existing + separator + block; + } + } else { + fileContent = block; + } + + fs.ensureDirSync(path.dirname(filePath)); + fs.writeFileSync(filePath, fileContent); +} + +/** + * Remove the AICM instructions block from file content. + */ +export function removeInstructionsBlock(content: string): string { + if (content.includes(BEGIN_MARKER) && content.includes(END_MARKER)) { + const before = content.split(BEGIN_MARKER)[0]; + const afterParts = content.split(END_MARKER); + const after = afterParts.slice(1).join(END_MARKER); + return (before + after).trim(); + } + return content; +} diff --git a/src/utils/instructions.ts b/src/utils/instructions.ts new file mode 100644 index 0000000..7ac169e --- /dev/null +++ b/src/utils/instructions.ts @@ -0,0 +1,130 @@ +/** + * Instruction file loading and frontmatter parsing. + */ + +import fs from "fs-extra"; +import path from "node:path"; +import fg from "fast-glob"; + +export interface InstructionFile { + name: string; + content: string; + sourcePath: string; + source: "local" | "preset"; + presetName?: string; + description: string; + inline: boolean; +} + +interface InstructionMetadata { + description: string; + inline: boolean; +} + +const FRONTMATTER_REGEX = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/; + +/** + * Parse YAML-like frontmatter from an instruction file. + * Returns null when no frontmatter is present. + */ +function parseFrontmatter(content: string): { + metadata: InstructionMetadata; + body: string; +} | null { + const match = FRONTMATTER_REGEX.exec(content); + if (!match) { + return null; + } + + const raw = match[1]; + const body = content.slice(match[0].length); + const metadata: Partial = {}; + + for (const line of raw.split("\n")) { + const trimmed = line.trim(); + if (!trimmed) continue; + const [key, ...rest] = trimmed.split(":"); + if (!key) continue; + const value = rest.join(":").trim(); + if (key === "description") { + metadata.description = value.replace(/^['"]|['"]$/g, ""); + } else if (key === "inline") { + metadata.inline = value === "true"; + } + } + + if (!metadata.description) { + throw new Error("Instruction file frontmatter requires description"); + } + + return { + metadata: { + description: metadata.description, + inline: metadata.inline ?? false, + }, + body: body.trim(), + }; +} + +/** + * Load instruction files from a path (file or directory). + */ +export async function loadInstructionsFromPath( + instructionsPath: string, + source: "local" | "preset", + presetName?: string, +): Promise { + if (!fs.existsSync(instructionsPath)) { + return []; + } + + const stats = fs.statSync(instructionsPath); + const files: string[] = []; + + if (stats.isFile()) { + files.push(instructionsPath); + } else { + const pattern = path.join(instructionsPath, "**/*.md").replace(/\\/g, "/"); + const matched = await fg(pattern, { onlyFiles: true, absolute: true }); + files.push(...matched); + } + + files.sort(); + + const isSingleFile = stats.isFile(); + const instructions: InstructionFile[] = []; + for (const filePath of files) { + const content = await fs.readFile(filePath, "utf8"); + const parsed = parseFrontmatter(content); + + if (!parsed && !isSingleFile) { + throw new Error( + `Instruction file missing frontmatter: ${filePath}. ` + + `Directory-based instructions require frontmatter with at least a "description" field.`, + ); + } + + const body = parsed ? parsed.body : content.trim(); + const metadata = parsed + ? parsed.metadata + : { description: "", inline: true }; + + const baseDir = isSingleFile + ? path.dirname(instructionsPath) + : instructionsPath; + const relativePath = path.relative(baseDir, filePath).replace(/\\/g, "/"); + const name = relativePath.replace(/\.md$/, ""); + + instructions.push({ + name, + content: body, + sourcePath: filePath, + source, + presetName, + description: metadata.description, + inline: metadata.inline, + }); + } + + return instructions; +} diff --git a/src/utils/is-ci.ts b/src/utils/is-ci.ts index 54eb5f6..de0816f 100644 --- a/src/utils/is-ci.ts +++ b/src/utils/is-ci.ts @@ -1,6 +1,12 @@ import { env } from "node:process"; -export const isCIEnvironment = () => - (env.CI !== "0" && env.CI !== "false" && "CI" in env) || - "CONTINUOUS_INTEGRATION" in env || - Object.keys(env).some((key) => key.startsWith("CI_")); +/** + * Detect whether the current process is running in a CI environment. + */ +export function isCIEnvironment(): boolean { + return ( + (env.CI !== "0" && env.CI !== "false" && "CI" in env) || + "CONTINUOUS_INTEGRATION" in env || + Object.keys(env).some((key) => key.startsWith("CI_")) + ); +} diff --git a/src/utils/log.ts b/src/utils/log.ts new file mode 100644 index 0000000..dd9d87a --- /dev/null +++ b/src/utils/log.ts @@ -0,0 +1,24 @@ +import chalk from "chalk"; + +/** + * Thin logging utility to centralize console output. + * All user-facing output should go through this module. + */ +export const log = { + info(message: string): void { + console.log(message); + }, + + warn(message: string): void { + console.warn(chalk.yellow(message)); + }, + + error(message: string): void { + console.error(chalk.red(message)); + }, + + /** Print a message without any formatting */ + plain(message: string): void { + console.log(message); + }, +}; diff --git a/src/utils/preset-loader.ts b/src/utils/preset-loader.ts new file mode 100644 index 0000000..88b4af1 --- /dev/null +++ b/src/utils/preset-loader.ts @@ -0,0 +1,405 @@ +/** + * Recursive preset loading: resolves local, npm, and GitHub presets. + */ + +import fs from "fs-extra"; +import path from "node:path"; +import { InstructionFile, loadInstructionsFromPath } from "./instructions"; +import { + MCPServers, + SkillFile, + AgentFile, + HooksJson, + HookFile, + loadSkillsFromDirectory, + loadAgentsFromDirectory, +} from "./config"; +import { + parsePresetSource, + GitHubPresetSource, + isGitHubPreset, +} from "./preset-source"; +import { + getGitHubToken, + fetchRepoSize, + fetchFileContent, + SPARSE_CHECKOUT_THRESHOLD_KB, +} from "./github"; +import { shallowClone, sparseClone } from "./git"; +import { + getCacheEntry, + setCacheEntry, + isCacheValid, + getRepoCachePath, + buildCacheKey, +} from "./install-cache"; +import { loadHooksFromDirectory } from "./hooks"; + +export interface PresetLoadResult { + instructions: InstructionFile[]; + skills: SkillFile[]; + agents: AgentFile[]; + mcpServers: MCPServers; + hooksConfigs: HooksJson[]; + hookFiles: HookFile[]; +} + +interface RawPresetConfig { + rootDir?: string; + instructionsFile?: string; + instructionsDir?: string; + presets?: string[]; + mcpServers?: MCPServers; +} + +async function resolvePresetPath( + presetPath: string, + cwd: string, +): Promise { + const source = parsePresetSource(presetPath); + + if (source.type === "github") { + return resolveGitHubPreset(source); + } + + if (!presetPath.endsWith(".json")) { + presetPath = path.join(presetPath, "aicm.json"); + } + + const absolutePath = path.isAbsolute(presetPath) + ? presetPath + : path.resolve(cwd, presetPath); + + if (fs.existsSync(absolutePath)) return absolutePath; + + try { + const resolved = require.resolve(presetPath, { paths: [cwd, __dirname] }); + return fs.existsSync(resolved) ? resolved : null; + } catch { + return null; + } +} + +async function resolveGitHubPreset( + source: GitHubPresetSource, +): Promise { + const { owner, repo, ref, subpath } = source; + const cacheKey = buildCacheKey(owner, repo); + + const cached = await getCacheEntry(cacheKey); + if (cached && isCacheValid(cached)) { + const aicmJsonPath = subpath + ? path.join(cached.cachePath, subpath, "aicm.json") + : path.join(cached.cachePath, "aicm.json"); + if (fs.existsSync(aicmJsonPath)) return aicmJsonPath; + } + + const token = getGitHubToken(); + const repoSizeKB = await fetchRepoSize(owner, repo, token); + const destPath = getRepoCachePath(owner, repo); + + if (fs.existsSync(destPath)) await fs.remove(destPath); + await fs.ensureDir(path.dirname(destPath)); + + const useSparse = + repoSizeKB !== null && repoSizeKB > SPARSE_CHECKOUT_THRESHOLD_KB; + + if (useSparse) { + const sparsePaths = await determineSparseCheckoutPaths( + owner, + repo, + subpath, + ref, + token, + ); + try { + await sparseClone(source.cloneUrl, destPath, sparsePaths, ref); + } catch { + if (fs.existsSync(destPath)) await fs.remove(destPath); + await shallowClone(source.cloneUrl, destPath, ref); + } + } else { + await shallowClone(source.cloneUrl, destPath, ref); + } + + const aicmJsonPath = subpath + ? path.join(destPath, subpath, "aicm.json") + : path.join(destPath, "aicm.json"); + + if (!fs.existsSync(aicmJsonPath)) { + const location = subpath ? `${subpath}/` : "root of "; + throw new Error( + `No aicm.json found at ${location}${owner}/${repo}. ` + + `Make sure the repository contains an aicm.json configuration file at the specified path.`, + ); + } + + await setCacheEntry(cacheKey, { + url: source.raw, + ref, + subpath, + cachedAt: new Date().toISOString(), + cachePath: destPath, + }); + + return aicmJsonPath; +} + +async function determineSparseCheckoutPaths( + owner: string, + repo: string, + subpath: string | undefined, + ref: string | undefined, + token: string | null, +): Promise { + const configPath = subpath + ? path.posix.join(subpath, "aicm.json") + : "aicm.json"; + + const content = await fetchFileContent(owner, repo, configPath, ref, token); + + if (!content) return subpath ? [subpath] : ["."]; + + try { + const config = JSON.parse(content) as { rootDir?: string }; + const rootDir = config.rootDir || "."; + + if (subpath) { + const resolvedRoot = path.posix.normalize( + path.posix.join(subpath, rootDir), + ); + if ( + !resolvedRoot.startsWith(subpath) && + resolvedRoot !== subpath.replace(/\/$/, "") + ) { + throw new Error( + `rootDir "${rootDir}" in preset escapes the specified subpath "${subpath}". ` + + `rootDir must reference a path within the preset's directory.`, + ); + } + + const paths = [path.posix.join(subpath, "aicm.json")]; + const rootPath = path.posix.normalize(path.posix.join(subpath, rootDir)); + if (rootPath !== subpath && rootPath !== path.posix.join(subpath, ".")) { + paths.push(rootPath); + } else { + paths.push(subpath); + } + return [...new Set(paths)]; + } + + const paths = ["aicm.json"]; + if (rootDir !== "." && rootDir !== "./") paths.push(rootDir); + return paths.length > 0 ? paths : ["."]; + } catch (error) { + if ( + error instanceof Error && + error.message.includes("rootDir") && + error.message.includes("escapes") + ) { + throw error; + } + return subpath ? [subpath] : ["."]; + } +} + +// ---------- Preset loading ---------- + +async function loadPreset( + presetPath: string, + cwd: string, +): Promise<{ + config: RawPresetConfig; + rootDir: string; + resolvedPath: string; +}> { + const resolvedPresetPath = await resolvePresetPath(presetPath, cwd); + + if (!resolvedPresetPath) { + const hint = isGitHubPreset(presetPath) + ? "Make sure the GitHub URL is correct and the repository contains an aicm.json." + : "Make sure the package is installed or the path is correct."; + throw new Error(`Preset not found: "${presetPath}". ${hint}`); + } + + let presetConfig: RawPresetConfig; + try { + const content = await fs.readFile(resolvedPresetPath, "utf8"); + presetConfig = JSON.parse(content); + } catch (error) { + throw new Error( + `Failed to load preset "${presetPath}": ${error instanceof Error ? error.message : "Unknown error"}`, + ); + } + + const presetDir = path.dirname(resolvedPresetPath); + const presetRootDir = path.resolve(presetDir, presetConfig.rootDir || "./"); + + // Auto-detect instruction sources under presetRootDir when not explicitly set + if ( + typeof presetConfig.instructionsFile !== "string" && + typeof presetConfig.instructionsDir !== "string" + ) { + if (fs.existsSync(path.join(presetRootDir, "AGENTS.src.md"))) { + presetConfig.instructionsFile = "AGENTS.src.md"; + } + if (fs.existsSync(path.join(presetRootDir, "instructions"))) { + presetConfig.instructionsDir = "instructions"; + } + } + + const hasInstructionsFile = presetConfig.instructionsFile + ? fs.existsSync(path.resolve(presetRootDir, presetConfig.instructionsFile)) + : false; + const hasInstructionsDir = presetConfig.instructionsDir + ? fs.existsSync(path.resolve(presetRootDir, presetConfig.instructionsDir)) + : false; + const hasInstructions = hasInstructionsFile || hasInstructionsDir; + const hasHooks = fs.existsSync(path.join(presetRootDir, "hooks.json")); + const hasSkills = fs.existsSync(path.join(presetRootDir, "skills")); + const hasAgents = fs.existsSync(path.join(presetRootDir, "agents")); + const hasNestedPresets = + Array.isArray(presetConfig.presets) && presetConfig.presets.length > 0; + + if ( + !hasInstructions && + !hasHooks && + !hasSkills && + !hasAgents && + !hasNestedPresets + ) { + throw new Error( + `Preset "${presetPath}" must have at least one of: instructionsFile/instructionsDir, skills/, agents/, hooks.json, or presets`, + ); + } + + return { + config: presetConfig, + rootDir: presetRootDir, + resolvedPath: resolvedPresetPath, + }; +} + +export async function loadPresetRecursively( + presetPath: string, + cwd: string, + visited: Set, +): Promise { + const preset = await loadPreset(presetPath, cwd); + const presetRootDir = preset.rootDir; + const presetDir = path.dirname(preset.resolvedPath); + + if (visited.has(preset.resolvedPath)) { + throw new Error( + `Circular preset dependency detected: "${presetPath}" has already been loaded`, + ); + } + visited.add(preset.resolvedPath); + + const result: PresetLoadResult = { + instructions: [], + skills: [], + agents: [], + mcpServers: {}, + hooksConfigs: [], + hookFiles: [], + }; + + if (preset.config.instructionsFile) { + const instructionsPath = path.resolve( + presetRootDir, + preset.config.instructionsFile, + ); + result.instructions.push( + ...(await loadInstructionsFromPath( + instructionsPath, + "preset", + presetPath, + )), + ); + } + if (preset.config.instructionsDir) { + const instructionsPath = path.resolve( + presetRootDir, + preset.config.instructionsDir, + ); + result.instructions.push( + ...(await loadInstructionsFromPath( + instructionsPath, + "preset", + presetPath, + )), + ); + } + + if (fs.existsSync(path.join(presetRootDir, "hooks.json"))) { + const { config: hooksConfig, files } = await loadHooksFromDirectory( + presetRootDir, + "preset", + presetPath, + ); + result.hooksConfigs.push(hooksConfig); + result.hookFiles.push(...files); + } + + const skillsPath = path.join(presetRootDir, "skills"); + if (fs.existsSync(skillsPath)) { + result.skills.push( + ...(await loadSkillsFromDirectory(skillsPath, "preset", presetPath)), + ); + } + + const agentsPath = path.join(presetRootDir, "agents"); + if (fs.existsSync(agentsPath)) { + result.agents.push( + ...(await loadAgentsFromDirectory(agentsPath, "preset", presetPath)), + ); + } + + if (preset.config.mcpServers) { + result.mcpServers = { ...preset.config.mcpServers }; + } + + if (preset.config.presets && preset.config.presets.length > 0) { + for (const nestedPresetPath of preset.config.presets) { + const nested = await loadPresetRecursively( + nestedPresetPath, + presetDir, + visited, + ); + result.instructions.push(...nested.instructions); + result.skills.push(...nested.skills); + result.agents.push(...nested.agents); + result.hooksConfigs.push(...nested.hooksConfigs); + result.hookFiles.push(...nested.hookFiles); + result.mcpServers = mergePresetMcpServers( + result.mcpServers, + nested.mcpServers, + ); + } + } + + return result; +} + +export function mergePresetMcpServers( + configServers: MCPServers, + presetServers: MCPServers, +): MCPServers { + const merged = { ...configServers }; + + for (const [name, config] of Object.entries(presetServers)) { + if ( + Object.prototype.hasOwnProperty.call(merged, name) && + merged[name] === false + ) { + delete merged[name]; + continue; + } + if (!Object.prototype.hasOwnProperty.call(merged, name)) { + merged[name] = config; + } + } + + return merged; +} diff --git a/src/utils/preset-source.ts b/src/utils/preset-source.ts new file mode 100644 index 0000000..06fcc73 --- /dev/null +++ b/src/utils/preset-source.ts @@ -0,0 +1,110 @@ +/** + * Preset source detection and GitHub URL parsing. + * + * Classifies a preset string into one of three source types: + * - "github" -- full GitHub URL (https://github.com/...) + * - "local" -- filesystem path (relative or absolute) + * - "npm" -- npm package name (everything else) + */ + +export interface GitHubPresetSource { + type: "github"; + raw: string; + owner: string; + repo: string; + ref?: string; + subpath?: string; + cloneUrl: string; +} + +interface LocalPresetSource { + type: "local"; + raw: string; +} + +interface NpmPresetSource { + type: "npm"; + raw: string; +} + +export type PresetSource = + | GitHubPresetSource + | LocalPresetSource + | NpmPresetSource; + +const GITHUB_URL_PREFIX = "https://github.com/"; +const WINDOWS_DRIVE_RE = /^[a-zA-Z]:[/\\]/; + +/** + * Classify a preset string into a source type. + */ +export function parsePresetSource(input: string): PresetSource { + if (input.startsWith(GITHUB_URL_PREFIX)) { + return parseGitHubUrl(input); + } + + if ( + input.startsWith(".") || + input.startsWith("/") || + WINDOWS_DRIVE_RE.test(input) + ) { + return { type: "local", raw: input }; + } + + return { type: "npm", raw: input }; +} + +/** + * Parse a full GitHub URL into its components. + * + * Supported formats: + * https://github.com/owner/repo + * https://github.com/owner/repo/tree/ref + * https://github.com/owner/repo/tree/ref/sub/path + */ +export function parseGitHubUrl(input: string): GitHubPresetSource { + if (!input.startsWith(GITHUB_URL_PREFIX)) { + throw new Error(`Not a GitHub URL: "${input}"`); + } + + const rest = input.slice(GITHUB_URL_PREFIX.length).replace(/\/+$/, ""); + const segments = rest.split("/").filter(Boolean); + + if (segments.length < 2) { + throw new Error( + `Invalid GitHub URL: "${input}". Expected format: https://github.com/owner/repo`, + ); + } + + const owner = segments[0]; + const repo = segments[1]; + const cloneUrl = `https://github.com/${owner}/${repo}.git`; + + if (segments.length === 2) { + return { type: "github", raw: input, owner, repo, cloneUrl }; + } + + if (segments[2] !== "tree") { + throw new Error( + `Invalid GitHub URL: "${input}". Only /tree/ URLs are supported (e.g. https://github.com/owner/repo/tree/main/path).`, + ); + } + + if (segments.length < 4) { + throw new Error( + `Invalid GitHub URL: "${input}". Missing branch/tag after /tree/.`, + ); + } + + const ref = segments[3]; + const subpath = segments.length > 4 ? segments.slice(4).join("/") : undefined; + + return { type: "github", raw: input, owner, repo, ref, subpath, cloneUrl }; +} + +/** + * Check whether a preset string is a GitHub URL. + */ +export function isGitHubPreset(input: string): boolean { + return input.startsWith(GITHUB_URL_PREFIX); +} diff --git a/src/utils/rules-file-writer.ts b/src/utils/rules-file-writer.ts deleted file mode 100644 index b193a53..0000000 --- a/src/utils/rules-file-writer.ts +++ /dev/null @@ -1,218 +0,0 @@ -import fs from "fs-extra"; -import path from "path"; - -export type RuleMetadata = Record; - -/** - * Parse YAML frontmatter blocks from a rule file and return a flat metadata object - */ -export function parseRuleFrontmatter(content: string): RuleMetadata { - const metadata: RuleMetadata = {}; - // Support both LF and CRLF line endings - const frontmatterRegex = /^---\r?\n([\s\S]*?)\r?\n---/gm; - let match: RegExpExecArray | null; - - while ((match = frontmatterRegex.exec(content)) !== null) { - const lines = match[1].split("\n"); - for (const line of lines) { - const trimmed = line.trim(); - if (!trimmed) continue; - const [key, ...rest] = trimmed.split(":"); - if (!key) continue; - const raw = rest.join(":").trim(); - - if (raw === "") { - metadata[key] = ""; - } else if (raw === "true" || raw === "false") { - metadata[key] = raw === "true"; - } else if (raw.startsWith("[") && raw.endsWith("]")) { - try { - const parsed = JSON.parse(raw.replace(/'/g, '"')); - metadata[key] = parsed; - } catch { - metadata[key] = raw; - } - } else if ( - (raw.startsWith('"') && raw.endsWith('"')) || - (raw.startsWith("'") && raw.endsWith("'")) - ) { - metadata[key] = raw.slice(1, -1); - } else { - metadata[key] = raw; - } - } - } - - return metadata; -} - -const RULES_BEGIN = ""; -const RULES_END = ""; -const WARNING = - ""; - -/** - * Remove the rules block from the content - */ -export function removeRulesBlock(content: string): string { - // Check if our markers exist - if (content.includes(RULES_BEGIN) && content.includes(RULES_END)) { - const parts = content.split(RULES_BEGIN); - const beforeMarker = parts[0]; - const afterParts = parts[1].split(RULES_END); - const afterMarker = afterParts.slice(1).join(RULES_END); // In case RULES_END appears multiple times (unlikely but safe) - - return (beforeMarker + afterMarker).trim(); - } - - return content; -} - -/** - * Create a formatted block of content with rules markers - */ -function createRulesBlock(rulesContent: string): string { - return `${RULES_BEGIN} -${WARNING} - -${rulesContent} - -${RULES_END}`; -} - -/** - * Write rules to the .windsurfrules file - * This will update the content between the RULES_BEGIN and RULES_END markers - * If the file doesn't exist, it will create it - * If the markers don't exist, it will append them to the existing content - */ -export function writeRulesFile( - rulesContent: string, - rulesFilePath: string = path.join(process.cwd(), ".windsurfrules"), -): void { - let fileContent: string; - const formattedRulesBlock = createRulesBlock(rulesContent); - - // Check if file exists - if (fs.existsSync(rulesFilePath)) { - const existingContent = fs.readFileSync(rulesFilePath, "utf8"); - - // Check if our markers exist - if ( - existingContent.includes(RULES_BEGIN) && - existingContent.includes(RULES_END) - ) { - // Replace content between markers - const beforeMarker = existingContent.split(RULES_BEGIN)[0]; - const afterMarker = existingContent.split(RULES_END)[1]; - fileContent = beforeMarker + formattedRulesBlock + afterMarker; - } else { - // Preserve the existing content and append markers - // Ensure there's proper spacing between existing content and markers - let separator = ""; - if (!existingContent.endsWith("\n")) { - separator += "\n"; - } - - // Add an extra line if the file doesn't already end with multiple newlines - if (!existingContent.endsWith("\n\n")) { - separator += "\n"; - } - - // Create the new file content with preserved original content - fileContent = existingContent + separator + formattedRulesBlock; - } - } else { - // Create new file with markers and content - fileContent = formattedRulesBlock; - } - - fs.writeFileSync(rulesFilePath, fileContent); -} - -/** - * Generate the rules file content based on rule files - */ -export function generateRulesFileContent( - ruleFiles: { - name: string; - path: string; - metadata: Record; - }[], -): string { - const alwaysRules: string[] = []; - const autoAttachedRules: Array<{ path: string; glob: string }> = []; - const agentRequestedRules: string[] = []; - const manualRules: string[] = []; - - ruleFiles.forEach(({ path, metadata }) => { - // Determine rule type based on metadata - if ( - metadata.type === "always" || - metadata.alwaysApply === true || - metadata.alwaysApply === "true" - ) { - alwaysRules.push(path); - } else if (metadata.type === "auto-attached" || metadata.globs) { - const globPattern: string | undefined = - typeof metadata.globs === "string" || Array.isArray(metadata.globs) - ? Array.isArray(metadata.globs) - ? metadata.globs.join(", ") - : metadata.globs - : undefined; - if (globPattern !== undefined) { - autoAttachedRules.push({ path, glob: globPattern }); - } - } else if (metadata.type === "agent-requested" || metadata.description) { - agentRequestedRules.push(path); - } else { - // Default to manual inclusion - manualRules.push(path); - } - }); - - // Generate the content - let content = ""; - - // Always rules - if (alwaysRules.length > 0) { - content += - "The following rules always apply to all files in the project:\n"; - alwaysRules.forEach((rule) => { - content += `- ${rule}\n`; - }); - content += "\n"; - } - - // Auto Attached rules - if (autoAttachedRules.length > 0) { - content += - "The following rules are automatically attached to matching glob patterns:\n"; - autoAttachedRules.forEach((rule) => { - content += `- [${rule.glob}] ${rule.path}\n`; - }); - content += "\n"; - } - - // Agent Requested rules - if (agentRequestedRules.length > 0) { - content += - "The following rules can be loaded when relevant. Check each file's description:\n"; - agentRequestedRules.forEach((rule) => { - content += `- ${rule}\n`; - }); - content += "\n"; - } - - // Manual rules - if (manualRules.length > 0) { - content += - "The following rules are only included when explicitly referenced:\n"; - manualRules.forEach((rule) => { - content += `- ${rule}\n`; - }); - content += "\n"; - } - - return content.trim(); -} diff --git a/src/utils/targets.ts b/src/utils/targets.ts new file mode 100644 index 0000000..af55e0c --- /dev/null +++ b/src/utils/targets.ts @@ -0,0 +1,119 @@ +/** + * Target resolution: maps target preset names + * into fully resolved target paths for each resource type. + */ + +export interface TargetsConfig { + skills: string[]; + agents: string[]; + instructions: string[]; + mcp: string[]; + hooks: string[]; +} + +interface TargetPreset { + instructions: string[]; + skills: string[]; + agents: string[]; + mcp: string[]; + hooks: string[]; +} + +const BUILT_IN_PRESETS: Record = { + cursor: { + instructions: ["AGENTS.md"], + skills: [".cursor/skills"], + agents: [".cursor/agents"], + mcp: [".cursor/mcp.json"], + hooks: [".cursor"], + }, + "claude-code": { + instructions: ["CLAUDE.md"], + skills: [".claude/skills"], + agents: [".claude/agents"], + mcp: [".mcp.json"], + hooks: [".claude"], + }, + opencode: { + instructions: ["AGENTS.md"], + skills: [".opencode/skills"], + agents: [".opencode/agents"], + mcp: ["opencode.json"], + hooks: [], + }, + codex: { + instructions: ["AGENTS.md"], + skills: [".agents/skills"], + agents: [], + mcp: [".codex/config.toml"], + hooks: [], + }, +}; + +const DEFAULT_PRESET_NAMES = ["cursor", "claude-code"]; + +export function validateTargetsInput( + targets: unknown, + configFilePath: string, +): void { + if (!Array.isArray(targets)) { + throw new Error( + `targets must be an array of preset names in config at ${configFilePath}. ` + + `Available presets: ${Object.keys(BUILT_IN_PRESETS).join(", ")}`, + ); + } + + for (const item of targets) { + if (typeof item !== "string") { + throw new Error( + `targets array entries must be strings in config at ${configFilePath}`, + ); + } + } + + validatePresetNames(targets as string[], configFilePath); +} + +function validatePresetNames(names: string[], configFilePath: string): void { + const available = Object.keys(BUILT_IN_PRESETS); + for (const name of names) { + if (!available.includes(name)) { + throw new Error( + `Unknown target preset "${name}" in config at ${configFilePath}. Available presets: ${available.join(", ")}`, + ); + } + } +} + +function mergePresetTargets(presetNames: string[]): TargetsConfig { + const merged: TargetsConfig = { + instructions: [], + skills: [], + agents: [], + mcp: [], + hooks: [], + }; + + for (const name of presetNames) { + const preset = BUILT_IN_PRESETS[name]; + if (!preset) continue; + + for (const key of Object.keys(merged) as (keyof TargetsConfig)[]) { + for (const value of preset[key]) { + if (!merged[key].includes(value)) { + merged[key].push(value); + } + } + } + } + + return merged; +} + +export function resolveTargets(targets: string[] | undefined): TargetsConfig { + if (targets === undefined || targets.length === 0) { + return mergePresetTargets(DEFAULT_PRESET_NAMES); + } + + return mergePresetTargets(targets); +} diff --git a/src/utils/working-directory.ts b/src/utils/working-directory.ts index 4a456bb..8321116 100644 --- a/src/utils/working-directory.ts +++ b/src/utils/working-directory.ts @@ -1,6 +1,6 @@ /** - * Helper function to execute a function within a specific working directory - * and ensure the original directory is always restored + * Execute an async function within a specific working directory, + * restoring the original cwd when done. */ export async function withWorkingDirectory( targetDir: string, diff --git a/src/utils/workspace-discovery.ts b/src/utils/workspace-discovery.ts index fea28e0..22658b6 100644 --- a/src/utils/workspace-discovery.ts +++ b/src/utils/workspace-discovery.ts @@ -1,64 +1,52 @@ +/** + * Workspace package discovery using git ls-files. + */ + import { execSync } from "child_process"; -import path from "path"; +import path from "node:path"; import { loadConfig, ResolvedConfig } from "./config"; -/** - * Discover all packages with aicm configurations using git ls-files - */ -export function findAicmFiles(rootDir: string): string[] { +export interface DiscoveredPackage { + relativePath: string; + absolutePath: string; + config: ResolvedConfig; +} + +function findAicmFiles(rootDir: string): string[] { try { const output = execSync( "git ls-files --cached --others --exclude-standard aicm.json **/aicm.json", - { - cwd: rootDir, - encoding: "utf8", - }, + { cwd: rootDir, encoding: "utf8" }, ); - return output .trim() .split("\n") .filter(Boolean) .map((file: string) => path.resolve(rootDir, file)); } catch { - // Fallback to manual search if git is not available return []; } } -/** - * Discover all packages with aicm configurations - */ export async function discoverPackagesWithAicm( rootDir: string, -): Promise< - Array<{ relativePath: string; absolutePath: string; config: ResolvedConfig }> -> { +): Promise { const aicmFiles = findAicmFiles(rootDir); - const packages: Array<{ - relativePath: string; - absolutePath: string; - config: ResolvedConfig; - }> = []; + const packages: DiscoveredPackage[] = []; for (const aicmFile of aicmFiles) { const packageDir = path.dirname(aicmFile); - const relativePath = path.relative(rootDir, packageDir); - - // Normalize to forward slashes for cross-platform compatibility - const normalizedRelativePath = relativePath.replace(/\\/g, "/"); + const relativePath = path.relative(rootDir, packageDir).replace(/\\/g, "/"); const config = await loadConfig(packageDir); - if (config) { packages.push({ - relativePath: normalizedRelativePath || ".", + relativePath: relativePath || ".", absolutePath: packageDir, config, }); } } - // Sort packages by relativePath for deterministic order return packages.sort((a, b) => a.relativePath.localeCompare(b.relativePath)); } diff --git a/tests/E2E_TESTS.md b/tests/E2E_TESTS.md index 9155d84..331d89b 100644 --- a/tests/E2E_TESTS.md +++ b/tests/E2E_TESTS.md @@ -40,20 +40,17 @@ tests/fixtures/e2e/list-with-multiple-rules/ ## Key Principles 1. **Always use fixtures for test state** - - Store initial test state in the `tests/fixtures` directory - Create a dedicated fixture directory for each test scenario - Include `.gitkeep` files in empty fixture directories - **Never** create test files on-the-fly with `fs.writeFileSync()` or similar methods 2. **Fixture Directory Structure** - - Name fixtures descriptively based on test scenario - Follow the established pattern in existing fixtures - Each fixture should be self-contained and independent 3. **Using Fixtures in Tests** - - Use `setupFromFixture(fixtureName)` to initialize test state - Verify fixture content exists before running tests - For empty starting states, use the `init-empty` fixture diff --git a/tests/e2e/agents.test.ts b/tests/e2e/agents.test.ts index 10f17a8..b7b5d80 100644 --- a/tests/e2e/agents.test.ts +++ b/tests/e2e/agents.test.ts @@ -88,7 +88,7 @@ describe("agents installation", () => { expect(stdout).toContain("Successfully installed 1 agent"); - // Verify agent is installed to both targets + // Verify agent is installed to both targets (cursor + claude-code) expect(fileExists(".cursor/agents/multi-agent.md")).toBe(true); expect(fileExists(".claude/agents/multi-agent.md")).toBe(true); }); diff --git a/tests/e2e/api.test.ts b/tests/e2e/api.test.ts index 27725f1..881b53f 100644 --- a/tests/e2e/api.test.ts +++ b/tests/e2e/api.test.ts @@ -3,7 +3,7 @@ import fs from "fs-extra"; import { install } from "../../src/api"; import { setupFromFixture } from "./helpers"; -test("install rules", async () => { +test("install instructions", async () => { const testDir = await setupFromFixture("single-rule"); const result = await install({ @@ -12,22 +12,18 @@ test("install rules", async () => { }); expect(result.success).toBe(true); - expect(result.installedRuleCount).toBe(1); - expect(result.installedCommandCount).toBe(0); + expect(result.installedInstructionCount).toBe(1); + expect(result.installedSkillCount).toBe(0); + expect(result.installedAgentCount).toBe(0); + expect(result.installedHookCount).toBe(0); expect(result.packagesCount).toBe(1); - // Check that rule was installed - const ruleFile = path.join( - testDir, - ".cursor", - "rules", - "aicm", - "test-rule.mdc", - ); - expect(fs.existsSync(ruleFile)).toBe(true); + // Check that instruction was installed + const agentsFile = path.join(testDir, "AGENTS.md"); + expect(fs.existsSync(agentsFile)).toBe(true); - const ruleContent = fs.readFileSync(ruleFile, "utf8"); - expect(ruleContent).toContain("Test Rule"); + const agentsContent = fs.readFileSync(agentsFile, "utf8"); + expect(agentsContent).toContain("Test Instruction"); // Check that MCP config was installed const mcpFile = path.join(testDir, ".cursor", "mcp.json"); @@ -53,8 +49,10 @@ test("handle missing config", async () => { expect(result.success).toBe(false); expect(result.error).toBeInstanceOf(Error); expect(result.error?.message).toBe("Configuration file not found"); - expect(result.installedRuleCount).toBe(0); - expect(result.installedCommandCount).toBe(0); + expect(result.installedInstructionCount).toBe(0); + expect(result.installedSkillCount).toBe(0); + expect(result.installedAgentCount).toBe(0); + expect(result.installedHookCount).toBe(0); expect(result.packagesCount).toBe(0); }); @@ -68,16 +66,12 @@ test("dry run API", async () => { }); expect(result.success).toBe(true); - expect(result.installedRuleCount).toBe(1); - expect(result.installedCommandCount).toBe(0); + expect(result.installedInstructionCount).toBe(1); + expect(result.installedSkillCount).toBe(0); + expect(result.installedAgentCount).toBe(0); + expect(result.installedHookCount).toBe(0); expect(result.packagesCount).toBe(1); - const ruleFile = path.join( - testDir, - ".cursor", - "rules", - "aicm", - "test-rule.mdc", - ); - expect(fs.existsSync(ruleFile)).toBe(false); + const agentsFile = path.join(testDir, "AGENTS.md"); + expect(fs.existsSync(agentsFile)).toBe(false); }); diff --git a/tests/e2e/assets.test.ts b/tests/e2e/assets.test.ts deleted file mode 100644 index 9f39d57..0000000 --- a/tests/e2e/assets.test.ts +++ /dev/null @@ -1,182 +0,0 @@ -import { - setupFromFixture, - runCommand, - fileExists, - readTestFile, - getDirectoryStructure, -} from "./helpers"; - -describe("assetsDir functionality", () => { - test("installs assets to .cursor/assets/aicm/ for cursor target", async () => { - await setupFromFixture("assets-dir-basic"); - - const { code, stdout } = await runCommand("install --ci"); - expect(code).toBe(0); - expect(stdout).toContain("Successfully installed"); - - // Verify assets are in .cursor/assets/aicm/ - expect(fileExists(".cursor/assets/aicm/schema.json")).toBe(true); - expect(fileExists(".cursor/assets/aicm/examples/response.json")).toBe(true); - - // Verify asset contents - const schemaContent = readTestFile(".cursor/assets/aicm/schema.json"); - expect(schemaContent).toContain('"type": "object"'); - - const exampleContent = readTestFile( - ".cursor/assets/aicm/examples/response.json", - ); - expect(exampleContent).toContain('"id": "123"'); - }); - - test("rules preserve relative paths to assets", async () => { - await setupFromFixture("assets-dir-basic"); - - const { code } = await runCommand("install --ci"); - expect(code).toBe(0); - - // Read the installed rule - const ruleContent = readTestFile(".cursor/rules/aicm/api.mdc"); - - // Verify relative paths are rewritten to point to assets/aicm/ - expect(ruleContent).toContain( - "[schema.json](../../assets/aicm/schema.json)", - ); - expect(ruleContent).toContain("`../../assets/aicm/examples/response.json`"); - }); - - test("commands preserve relative paths to assets", async () => { - await setupFromFixture("assets-dir-basic"); - - const { code } = await runCommand("install --ci"); - expect(code).toBe(0); - - // Read the installed command - const commandContent = readTestFile(".cursor/commands/aicm/generate.md"); - - // Verify relative paths are rewritten to point to assets/aicm/ - expect(commandContent).toContain( - "[this schema](../../assets/aicm/schema.json)", - ); - expect(commandContent).toContain( - "Check the example at ../../assets/aicm/examples/response.json", - ); - }); - - test("hook scripts can be stored in assetsDir", async () => { - await setupFromFixture("assets-dir-hooks"); - - const { code } = await runCommand("install --ci"); - expect(code).toBe(0); - - // Hook files are now stored in the hooks/ directory - expect(fileExists(".cursor/hooks/aicm/validate.sh")).toBe(true); - expect(fileExists(".cursor/hooks/aicm/helper.js")).toBe(true); - - // Verify hooks.json points to the correct location - const hooksJson = JSON.parse(readTestFile(".cursor/hooks.json")); - expect(hooksJson.hooks.beforeShellExecution).toHaveLength(2); - expect(hooksJson.hooks.beforeShellExecution[0].command).toBe( - "./hooks/aicm/validate.sh", - ); - expect(hooksJson.hooks.beforeShellExecution[1].command).toBe( - "./hooks/aicm/helper.js", - ); - - // Verify hook script content is preserved - const validateContent = readTestFile(".cursor/hooks/aicm/validate.sh"); - expect(validateContent).toContain("./helper.js"); - expect(validateContent).toContain("Validation complete"); - - // Verify both hook files maintain their relative paths to each other - const helperContent = readTestFile(".cursor/hooks/aicm/helper.js"); - expect(helperContent).toContain("Helper executed"); - }); - - test("assets installed to .aicm/ for windsurf/codex/claude targets", async () => { - await setupFromFixture("assets-dir-multitarget"); - - const { code } = await runCommand("install --ci"); - expect(code).toBe(0); - - // Verify assets are in .aicm/ for non-cursor targets - expect(fileExists(".aicm/config.yaml")).toBe(true); - expect(fileExists(".aicm/data.json")).toBe(true); - - // Verify asset contents - const configContent = readTestFile(".aicm/config.yaml"); - expect(configContent).toContain("version: 1.0"); - - const dataContent = readTestFile(".aicm/data.json"); - expect(dataContent).toContain('"id": 1'); - }); - - test("assets installed to both .cursor/assets/aicm/ and .aicm/ for multi-target", async () => { - await setupFromFixture("assets-dir-multitarget"); - - const { code } = await runCommand("install --ci"); - expect(code).toBe(0); - - // Verify assets exist in both locations - expect(fileExists(".cursor/assets/aicm/config.yaml")).toBe(true); - expect(fileExists(".cursor/assets/aicm/data.json")).toBe(true); - expect(fileExists(".aicm/config.yaml")).toBe(true); - expect(fileExists(".aicm/data.json")).toBe(true); - - // Verify rule references are rewritten to point to .cursor/assets/aicm/ - const ruleContent = readTestFile(".cursor/rules/aicm/example.mdc"); - expect(ruleContent).toContain( - "[config file](../../assets/aicm/config.yaml)", - ); - expect(ruleContent).toContain("`../../assets/aicm/data.json`"); - }); - - test("windsurf target receives assets in .aicm/", async () => { - await setupFromFixture("assets-dir-multitarget"); - - const { code } = await runCommand("install --ci"); - expect(code).toBe(0); - - // Read windsurf rules file - const windsurfContent = readTestFile(".windsurfrules"); - - // Should reference .aicm/ directory - expect(windsurfContent).toContain("aicm/example.md"); - - // Verify assets are in .aicm/ (same directory as rules for windsurf) - expect(fileExists(".aicm/config.yaml")).toBe(true); - expect(fileExists(".aicm/data.json")).toBe(true); - expect(fileExists(".aicm/example.md")).toBe(true); - }); - - test("preserves directory structure in assets", async () => { - await setupFromFixture("assets-dir-basic"); - - const { code } = await runCommand("install --ci"); - expect(code).toBe(0); - - // Check directory structure is preserved - const structure = getDirectoryStructure(".cursor/assets/aicm"); - - expect(structure).toContain(".cursor/assets/aicm/examples/"); - expect(structure).toContain(".cursor/assets/aicm/examples/response.json"); - expect(structure).toContain(".cursor/assets/aicm/schema.json"); - - // Verify subdirectory structure maintained - expect(fileExists(".cursor/assets/aicm/examples/response.json")).toBe(true); - }); - - test("installs successfully without assetsDir", async () => { - // Use a fixture that doesn't have assetsDir - await setupFromFixture("commands-basic"); - - const { code, stdout } = await runCommand("install --ci"); - expect(code).toBe(0); - - // Should install commands successfully - expect(stdout).toContain("Successfully installed 2 commands"); - expect(fileExists(".cursor/commands/aicm/test.md")).toBe(true); - - // No assets directory should be created - expect(fileExists(".cursor/assets")).toBe(false); - }); -}); diff --git a/tests/e2e/ci.test.ts b/tests/e2e/ci.test.ts index 373505e..6341f7d 100644 --- a/tests/e2e/ci.test.ts +++ b/tests/e2e/ci.test.ts @@ -8,13 +8,7 @@ import { } from "./helpers"; describe("CI", () => { - const ruleName = "test-rule"; - const cursorRulePath = path.join( - ".cursor", - "rules", - "aicm", - `${ruleName}.mdc`, - ); + const agentsPath = "AGENTS.md"; test("should skip install on CI by default", async () => { await setupFromFixture("single-rule-clean"); @@ -24,7 +18,7 @@ describe("CI", () => { }); expect(stdout).toContain("Detected CI environment, skipping install"); - expect(fileExists(cursorRulePath)).toBe(false); + expect(fileExists(agentsPath)).toBe(false); }); test("should install when --ci flag is used in CI", async () => { @@ -35,12 +29,12 @@ describe("CI", () => { }); expect(code).toBe(0); - expect(stdout).toContain("Successfully installed 1 rule"); - expect(fileExists(cursorRulePath)).toBe(true); + expect(stdout).toContain("Successfully installed 1 instruction"); + expect(fileExists(agentsPath)).toBe(true); - // Verify rule content - const ruleContent = readTestFile(cursorRulePath); - expect(ruleContent).toContain("Test Rule"); + // Verify instruction content + const agentsContent = readTestFile(agentsPath); + expect(agentsContent).toContain("Test Instruction"); // Verify MCP config was installed const mcpPath = path.join(".cursor", "mcp.json"); @@ -64,12 +58,12 @@ describe("CI", () => { }); expect(code).toBe(0); - expect(stdout).toContain("Successfully installed 1 rule"); - expect(fileExists(cursorRulePath)).toBe(true); + expect(stdout).toContain("Successfully installed 1 instruction"); + expect(fileExists(agentsPath)).toBe(true); - // Verify rule content - const ruleContent = readTestFile(cursorRulePath); - expect(ruleContent).toContain("Test Rule"); + // Verify instruction content + const agentsContent = readTestFile(agentsPath); + expect(agentsContent).toContain("Test Instruction"); }); test("should install normally when not in CI environment with --ci flag", async () => { @@ -80,11 +74,11 @@ describe("CI", () => { }); expect(code).toBe(0); - expect(stdout).toContain("Successfully installed 1 rule"); - expect(fileExists(cursorRulePath)).toBe(true); + expect(stdout).toContain("Successfully installed 1 instruction"); + expect(fileExists(agentsPath)).toBe(true); - // Verify rule content - const ruleContent = readTestFile(cursorRulePath); - expect(ruleContent).toContain("Test Rule"); + // Verify instruction content + const agentsContent = readTestFile(agentsPath); + expect(agentsContent).toContain("Test Instruction"); }); }); diff --git a/tests/e2e/claude.test.ts b/tests/e2e/claude.test.ts deleted file mode 100644 index ad9af65..0000000 --- a/tests/e2e/claude.test.ts +++ /dev/null @@ -1,79 +0,0 @@ -import path from "path"; -import { - setupFromFixture, - runCommand, - fileExists, - readTestFile, - writeTestFile, -} from "./helpers"; - -describe("claude integration", () => { - test("should install rules and update CLAUDE.md", async () => { - await setupFromFixture("claude-basic"); - - const { stdout, code } = await runCommand("install --ci"); - - expect(code).toBe(0); - expect(stdout).toContain("Successfully installed 4 rules"); - - // Check that rules were installed - expect(fileExists(path.join(".aicm", "always-rule.md"))).toBe(true); - expect(fileExists(path.join(".aicm", "opt-in-rule.md"))).toBe(true); - expect(fileExists(path.join(".aicm", "file-pattern-rule.md"))).toBe(true); - expect(fileExists(path.join(".aicm", "manual-rule.md"))).toBe(true); - - // Check that CLAUDE.md was created/updated - expect(fileExists("CLAUDE.md")).toBe(true); - const claudeContent = readTestFile("CLAUDE.md"); - expect(claudeContent).toContain(""); - expect(claudeContent).toContain(""); - expect(claudeContent).toContain( - "The following rules always apply to all files in the project:", - ); - expect(claudeContent).toContain( - "The following rules can be loaded when relevant. Check each file's description:", - ); - expect(claudeContent).toContain( - "The following rules are only included when explicitly referenced:", - ); - expect(claudeContent).toContain("- .aicm/always-rule.md"); - expect(claudeContent).toContain("- [*.ts] .aicm/file-pattern-rule.md"); - expect(claudeContent).toContain("- .aicm/opt-in-rule.md"); - expect(claudeContent).toContain("- .aicm/manual-rule.md"); - }); - - test("should append markers to existing file without markers", async () => { - await setupFromFixture("claude-no-markers"); - - // Create existing CLAUDE.md file without markers - const existingContent = - "# Existing Claude Rules\n\nThese are some existing rules."; - writeTestFile("CLAUDE.md", existingContent); - - const { stdout, code } = await runCommand("install --ci"); - - expect(code).toBe(0); - expect(stdout).toContain("Successfully installed 1 rule"); - - // Check that rule was installed - expect(fileExists(path.join(".aicm", "no-marker-rule.md"))).toBe(true); - - // Check that CLAUDE.md was updated with markers - const claudeContent = readTestFile("CLAUDE.md"); - expect(claudeContent).toContain("# Existing Claude Rules"); - expect(claudeContent).toContain("These are some existing rules."); - expect(claudeContent).toContain(""); - expect(claudeContent).toContain(""); - expect(claudeContent).toContain( - "The following rules are only included when explicitly referenced:", - ); - expect(claudeContent).toContain("- .aicm/no-marker-rule.md"); - - // Verify that existing content comes before AICM markers - const beginMarkerIndex = claudeContent.indexOf(""); - const existingContentIndex = claudeContent.indexOf( - "# Existing Claude Rules", - ); - expect(existingContentIndex).toBeLessThan(beginMarkerIndex); - }); -}); diff --git a/tests/e2e/clean.test.ts b/tests/e2e/clean.test.ts index f66064b..14654af 100644 --- a/tests/e2e/clean.test.ts +++ b/tests/e2e/clean.test.ts @@ -13,9 +13,7 @@ test("clean removes installed artifacts", async () => { await runCommand("install --ci"); // Verify installed - expect( - fileExists(path.join(".cursor", "rules", "aicm", "test-rule.mdc")), - ).toBe(true); + expect(fileExists("AGENTS.md")).toBe(true); // Run clean const { stdout, code } = await runCommand("clean"); @@ -24,9 +22,8 @@ test("clean removes installed artifacts", async () => { expect(stdout).toContain("Successfully cleaned"); // Verify removed - expect(fileExists(path.join(".cursor", "rules", "aicm"))).toBe(false); - expect(fileExists(path.join(".cursor", "commands", "aicm"))).toBe(false); - expect(fileExists(path.join(".aicm"))).toBe(false); + expect(fileExists("AGENTS.md")).toBe(false); + expect(fileExists(path.join(".agents"))).toBe(false); // Verify MCP cleaned const mcpPath = path.join(".cursor", "mcp.json"); @@ -45,9 +42,7 @@ test("clean removes workspace artifacts", async () => { await runCommand("install --ci"); // Check artifacts in packages (based on fixture structure) - expect( - fileExists(path.join("packages", "backend", ".cursor", "rules", "aicm")), - ).toBe(true); + expect(fileExists(path.join("packages", "backend", "AGENTS.md"))).toBe(true); expect(fileExists(path.join(".cursor", "mcp.json"))).toBe(true); // Clean @@ -56,9 +51,7 @@ test("clean removes workspace artifacts", async () => { expect(code).toBe(0); // Verify removed in packages - expect( - fileExists(path.join("packages", "backend", ".cursor", "rules", "aicm")), - ).toBe(false); + expect(fileExists(path.join("packages", "backend", "AGENTS.md"))).toBe(false); // Verify root MCP cleaned const mcpPath = path.join(".cursor", "mcp.json"); @@ -92,9 +85,7 @@ test("clean removes empty directories and mcp.json", async () => { await runCommand("install --ci"); // Verify installed - expect( - fileExists(path.join(".cursor", "rules", "aicm", "test-rule.mdc")), - ).toBe(true); + expect(fileExists("AGENTS.md")).toBe(true); expect(fileExists(path.join(".cursor", "mcp.json"))).toBe(true); // Run clean @@ -103,9 +94,8 @@ test("clean removes empty directories and mcp.json", async () => { expect(code).toBe(0); // Verify everything is removed including empty parent directories - expect(fileExists(path.join(".cursor", "rules", "aicm"))).toBe(false); - expect(fileExists(path.join(".cursor", "rules"))).toBe(false); - expect(fileExists(path.join(".cursor", "commands"))).toBe(false); + expect(fileExists("AGENTS.md")).toBe(false); + expect(fileExists(path.join(".agents"))).toBe(false); expect(fileExists(path.join(".cursor", "mcp.json"))).toBe(false); expect(fileExists(path.join(".cursor"))).toBe(false); }); diff --git a/tests/e2e/codex.test.ts b/tests/e2e/codex.test.ts deleted file mode 100644 index e8f1d0f..0000000 --- a/tests/e2e/codex.test.ts +++ /dev/null @@ -1,77 +0,0 @@ -import path from "path"; -import { - setupFromFixture, - runCommand, - fileExists, - readTestFile, - writeTestFile, -} from "./helpers"; - -describe("codex integration", () => { - test("should install rules and update AGENTS.md", async () => { - await setupFromFixture("codex-basic"); - - const { stdout, code } = await runCommand("install --ci"); - - expect(code).toBe(0); - expect(stdout).toContain("Successfully installed 3 rules"); - - // Check that rules were installed - expect(fileExists(path.join(".aicm", "always-rule.md"))).toBe(true); - expect(fileExists(path.join(".aicm", "opt-in-rule.md"))).toBe(true); - expect(fileExists(path.join(".aicm", "file-pattern-rule.md"))).toBe(true); - - // Check that AGENTS.md was created/updated - expect(fileExists("AGENTS.md")).toBe(true); - const agentsContent = readTestFile("AGENTS.md"); - expect(agentsContent).toContain(""); - expect(agentsContent).toContain(""); - expect(agentsContent).toContain( - "The following rules always apply to all files in the project:", - ); - expect(agentsContent).toContain( - "The following rules can be loaded when relevant. Check each file's description:", - ); - expect(agentsContent).toContain( - "The following rules are only included when explicitly referenced:", - ); - expect(agentsContent).toContain("- .aicm/always-rule.md"); - expect(agentsContent).toContain("- .aicm/file-pattern-rule.md"); - expect(agentsContent).toContain("- .aicm/opt-in-rule.md"); - }); - - test("should append markers to existing file without markers", async () => { - await setupFromFixture("codex-no-markers"); - - // Create existing AGENTS.md file without markers - const existingContent = - "# Existing Codex Rules\n\nThese are some existing rules."; - writeTestFile("AGENTS.md", existingContent); - - const { stdout, code } = await runCommand("install --ci"); - - expect(code).toBe(0); - expect(stdout).toContain("Successfully installed 1 rule"); - - // Check that rule was installed - expect(fileExists(path.join(".aicm", "no-marker-rule.md"))).toBe(true); - - // Check that AGENTS.md was updated with markers - const agentsContent = readTestFile("AGENTS.md"); - expect(agentsContent).toContain("# Existing Codex Rules"); - expect(agentsContent).toContain("These are some existing rules."); - expect(agentsContent).toContain(""); - expect(agentsContent).toContain(""); - expect(agentsContent).toContain( - "The following rules are only included when explicitly referenced:", - ); - expect(agentsContent).toContain("- .aicm/no-marker-rule.md"); - - // Verify that existing content comes before AICM markers - const beginMarkerIndex = agentsContent.indexOf(""); - const existingContentIndex = agentsContent.indexOf( - "# Existing Codex Rules", - ); - expect(existingContentIndex).toBeLessThan(beginMarkerIndex); - }); -}); diff --git a/tests/e2e/commands.test.ts b/tests/e2e/commands.test.ts deleted file mode 100644 index 067d441..0000000 --- a/tests/e2e/commands.test.ts +++ /dev/null @@ -1,127 +0,0 @@ -import { - getDirectoryStructure, - readTestFile, - runCommand, - setupFromFixture, -} from "./helpers"; - -describe("command installation", () => { - test("installs local commands", async () => { - await setupFromFixture("commands-basic"); - - const { stdout } = await runCommand("install --ci"); - - expect(stdout).toContain("Successfully installed 2 commands"); - - const structure = getDirectoryStructure(".cursor/commands"); - - expect(structure).toEqual([ - ".cursor/commands/aicm/", - ".cursor/commands/aicm/build/", - ".cursor/commands/aicm/build/release.md", - ".cursor/commands/aicm/test.md", - ]); - - expect(readTestFile(".cursor/commands/aicm/test.md")).toContain( - "Run the unit test suite.", - ); - expect(readTestFile(".cursor/commands/aicm/build/release.md")).toContain( - "Build the project for release.", - ); - }); - - test("installs preset commands without preset namespace", async () => { - await setupFromFixture("commands-preset"); - - const { stdout } = await runCommand("install --ci"); - - expect(stdout).toContain("Successfully installed 2 commands"); - - const structure = getDirectoryStructure(".cursor/commands"); - - expect(structure).toEqual([ - ".cursor/commands/aicm/", - ".cursor/commands/aicm/local/", - ".cursor/commands/aicm/local/custom.md", - ".cursor/commands/aicm/test/", - ".cursor/commands/aicm/test/run-tests.md", - ]); - - expect(readTestFile(".cursor/commands/aicm/local/custom.md")).toContain( - "project-specific", - ); - expect(readTestFile(".cursor/commands/aicm/test/run-tests.md")).toContain( - "shared test suite", - ); - }); - - test("warns when presets provide the same command", async () => { - await setupFromFixture("commands-collision"); - - const { stdout, stderr } = await runCommand("install --ci"); - - expect(stdout).toContain("Successfully installed 1 command"); - expect(stderr).toContain( - 'Warning: multiple presets provide the "shared/run" command', - ); - expect(stderr).toContain("Using definition from ./preset-b"); - - expect(readTestFile(".cursor/commands/aicm/shared/run.md")).toContain( - "Preset B version", - ); - }); - - test("installs commands for each workspace package", async () => { - await setupFromFixture("commands-workspace"); - - const { stdout } = await runCommand("install --ci --verbose"); - - expect(stdout).toContain( - "Successfully installed 2 commands across 2 packages", - ); - - const rootStructure = getDirectoryStructure(".cursor/commands"); - - expect(rootStructure).toEqual([ - ".cursor/commands/aicm/", - ".cursor/commands/aicm/test-a.md", - ".cursor/commands/aicm/test-b.md", - ]); - - expect(readTestFile(".cursor/commands/aicm/test-a.md")).toContain( - "Package A Command", - ); - expect(readTestFile(".cursor/commands/aicm/test-b.md")).toContain( - "Package B Command", - ); - - expect(readTestFile("package-a/.cursor/commands/aicm/test-a.md")).toContain( - "Package A Command", - ); - expect(readTestFile("package-b/.cursor/commands/aicm/test-b.md")).toContain( - "Package B Command", - ); - }); - - test("dedupes preset commands when joining workspace commands", async () => { - await setupFromFixture("commands-workspace-preset"); - - const { stdout } = await runCommand("install --ci --verbose"); - - expect(stdout).toContain( - "Successfully installed 2 commands across 2 packages", - ); - - const structure = getDirectoryStructure(".cursor/commands"); - - expect(structure).toEqual([ - ".cursor/commands/aicm/", - ".cursor/commands/aicm/test/", - ".cursor/commands/aicm/test/run-tests.md", - ]); - - expect(readTestFile(".cursor/commands/aicm/test/run-tests.md")).toContain( - "Run shared workspace tests.", - ); - }); -}); diff --git a/tests/e2e/git-preset.test.ts b/tests/e2e/git-preset.test.ts new file mode 100644 index 0000000..6d27b88 --- /dev/null +++ b/tests/e2e/git-preset.test.ts @@ -0,0 +1,133 @@ +import fs from "fs-extra"; +import path from "path"; +import os from "os"; +import { execSync } from "child_process"; +import { shallowClone } from "../../src/utils/git"; +import { + setCacheEntry, + getRepoCachePath, + buildCacheKey, +} from "../../src/utils/install-cache"; +import { + setupTestDir, + runCommand, + readTestFile, + fileExists, + testDir, +} from "./helpers"; + +/** + * Helper to create a local bare git repo with aicm preset content. + * Returns the file:// URL to the bare repo. + */ +async function createBareGitRepo( + content: Record, +): Promise<{ bareUrl: string; cleanup: () => Promise }> { + const tempBase = await fs.mkdtemp(path.join(os.tmpdir(), "aicm-git-test-")); + const workDir = path.join(tempBase, "work"); + const bareDir = path.join(tempBase, "bare.git"); + + // Create a work directory with content + await fs.ensureDir(workDir); + + for (const [filePath, fileContent] of Object.entries(content)) { + const fullPath = path.join(workDir, filePath); + await fs.ensureDir(path.dirname(fullPath)); + await fs.writeFile(fullPath, fileContent); + } + + // Initialize git repo, add content, and create a bare clone + execSync("git init", { cwd: workDir, stdio: "pipe" }); + execSync("git add -A", { cwd: workDir, stdio: "pipe" }); + execSync( + 'git -c user.email="test@test.com" -c user.name="Test" commit -m "init"', + { + cwd: workDir, + stdio: "pipe", + }, + ); + execSync(`git clone --bare "${workDir}" "${bareDir}"`, { + stdio: "pipe", + }); + + const bareUrl = `file://${bareDir}`; + + return { + bareUrl, + cleanup: async () => { + await fs.remove(tempBase); + }, + }; +} + +describe("full e2e: aicm install with GitHub preset", () => { + let tempHome: string; + let originalHomedir: () => string; + + beforeEach(async () => { + tempHome = await fs.mkdtemp(path.join(os.tmpdir(), "aicm-e2e-home-")); + originalHomedir = os.homedir; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (os as any).homedir = () => tempHome; + }); + + afterEach(async () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (os as any).homedir = originalHomedir; + await fs.remove(tempHome); + }); + + test("installs instructions from a GitHub preset via cache", async () => { + await setupTestDir(); + + // 1. Create a local bare git repo with valid preset content + const { bareUrl, cleanup } = await createBareGitRepo({ + "aicm.json": JSON.stringify({ + rootDir: "./", + instructions: "instructions", + }), + "instructions/best-practices.md": + "---\ndescription: Best practices from GitHub preset\ninline: true\n---\nAlways write tests.", + }); + + try { + // 2. Clone it into the cache directory where resolveGitHubPreset would put it + const owner = "testorg"; + const repo = "shared-preset"; + const cachePath = getRepoCachePath(owner, repo); + await shallowClone(bareUrl, cachePath); + + // 3. Write the cache entry so resolveGitHubPreset finds it + await setCacheEntry(buildCacheKey(owner, repo), { + url: `https://github.com/${owner}/${repo}`, + cachedAt: new Date().toISOString(), + cachePath, + }); + + // 4. Create the project's aicm.json referencing the GitHub URL as a preset + fs.writeFileSync( + path.join(testDir, "aicm.json"), + JSON.stringify({ + presets: [`https://github.com/${owner}/${repo}`], + }), + ); + + // 5. Initialize git (required by workspace detection) + execSync("git init", { cwd: testDir, stdio: "pipe" }); + + // 6. Run aicm install, passing HOME so the child process finds our cache + const { stdout } = await runCommand("install --ci", testDir, { + env: { HOME: tempHome, USERPROFILE: tempHome }, + }); + + expect(stdout).toContain("Successfully installed 1 instruction"); + + // 7. Verify the instruction was written to the default target (AGENTS.md) + expect(fileExists("AGENTS.md")).toBe(true); + const content = readTestFile("AGENTS.md"); + expect(content).toContain("Always write tests."); + } finally { + await cleanup(); + } + }); +}); diff --git a/tests/e2e/helpers.ts b/tests/e2e/helpers.ts index 27b8eec..dd835bf 100644 --- a/tests/e2e/helpers.ts +++ b/tests/e2e/helpers.ts @@ -16,12 +16,12 @@ interface ExecError extends Error { /** * The root directory of the project */ -export const projectRoot = path.resolve(__dirname, "../../"); +const projectRoot = path.resolve(__dirname, "../../"); /** * Temporary test directory for tests */ -export const testRootDir = path.join(projectRoot, "tmp-test"); +const testRootDir = path.join(projectRoot, "tmp-test"); /** * Current test directory (will be set per test) @@ -31,7 +31,7 @@ export let testDir = testRootDir; /** * E2E test fixtures directory for */ -export const e2eFixturesDir = path.join(projectRoot, "tests/fixtures"); +const e2eFixturesDir = path.join(projectRoot, "tests/fixtures"); /** * Sanitize a filename to be safe for directories @@ -244,20 +244,6 @@ export function readTestFile( return fs.readFileSync(fullPath, "utf8"); } -/** - * Write a file to the test directory - */ -export function writeTestFile( - filePath: string, - content: string, - testDirOverride?: string, -): void { - const workingDir = testDirOverride || testDir; - const fullPath = path.join(workingDir, filePath); - fs.ensureDirSync(path.dirname(fullPath)); - fs.writeFileSync(fullPath, content); -} - /** * Get the structure of files in the test directory */ @@ -315,27 +301,3 @@ export async function setupFromFixture(fixtureName: string): Promise { return testDir; } - -/** - * Run npm install for a specific package in the test directory - * @param packageName The npm package to install - */ -export async function runNpmInstall( - packageName: string, - testDirOverride?: string, -): Promise<{ stdout: string; stderr: string; code: number }> { - const workingDir = testDirOverride || testDir; - - try { - const command = `npm install --no-save ${packageName}`; - const { stdout, stderr } = await execPromise(command, { cwd: workingDir }); - return { stdout, stderr, code: 0 }; - } catch (error: unknown) { - const execError = error as ExecError; - return { - stdout: execError.stdout || "", - stderr: execError.stderr || "", - code: execError.code || 1, - }; - } -} diff --git a/tests/e2e/hooks.test.ts b/tests/e2e/hooks.test.ts index 098d52b..63e18a6 100644 --- a/tests/e2e/hooks.test.ts +++ b/tests/e2e/hooks.test.ts @@ -192,7 +192,7 @@ describe("hooks installation", () => { // Should succeed but install 0 hooks expect(stdout).toContain( - "No rules, commands, hooks, skills, or agents installed", + "No instructions, hooks, skills, or agents installed", ); }); diff --git a/tests/e2e/init.test.ts b/tests/e2e/init.test.ts index 8e95cb9..4b70515 100644 --- a/tests/e2e/init.test.ts +++ b/tests/e2e/init.test.ts @@ -19,13 +19,25 @@ test("should create default config file", async () => { expect(fileExists("aicm.json")).toBe(true); const config = JSON.parse(readTestFile("aicm.json")); - expect(config).toEqual({ rootDir: "./", targets: ["cursor"] }); + expect(config).toEqual({ + rootDir: "./", + targets: ["cursor", "claude-code"], + }); }); test("should not overwrite existing config", async () => { await setupFromFixture("no-config"); - const customConfig = { rootDir: "./", targets: ["windsurf"] }; + const customConfig = { + rootDir: "./", + targets: { + skills: [".cursor/skills"], + agents: [".agents/agents"], + instructions: ["CLAUDE.md"], + mcp: [".mcp.json"], + hooks: [".cursor"], + }, + }; fs.writeJsonSync(path.join(testDir, "aicm.json"), customConfig); const { stdout, code } = await runCommand("init"); diff --git a/tests/e2e/install.test.ts b/tests/e2e/install.test.ts index a4485e9..6ec4afd 100644 --- a/tests/e2e/install.test.ts +++ b/tests/e2e/install.test.ts @@ -7,23 +7,19 @@ import { readTestFile, } from "./helpers"; -test("single rule", async () => { +test("single instruction", async () => { await setupFromFixture("single-rule"); const { stdout, code } = await runCommand("install --ci"); expect(code).toBe(0); - expect(stdout).toContain("Successfully installed 1 rule"); + expect(stdout).toContain("Successfully installed 1 instruction"); - // Check that rule was installed - expect( - fileExists(path.join(".cursor", "rules", "aicm", "test-rule.mdc")), - ).toBe(true); - - const ruleContent = readTestFile( - path.join(".cursor", "rules", "aicm", "test-rule.mdc"), - ); - expect(ruleContent).toContain("Test Rule"); + // Check that instructions were installed + expect(fileExists("AGENTS.md")).toBe(true); + const agentsContent = readTestFile("AGENTS.md"); + expect(agentsContent).toContain(""); + expect(agentsContent).toContain("Test Instruction"); // Check that MCP config was installed const mcpPath = path.join(".cursor", "mcp.json"); @@ -48,14 +44,14 @@ test("show error when no config file exists", async () => { expect(stderr).toMatch(/config|configuration|not found/i); }); -test("no rules prints message", async () => { +test("no instructions prints message", async () => { await setupFromFixture("empty-rules"); const { stdout, code } = await runCommand("install --ci"); expect(code).toBe(0); expect(stdout).toContain( - "No rules, commands, hooks, skills, or agents installed", + "No instructions, hooks, skills, or agents installed", ); }); @@ -68,129 +64,6 @@ test("unknown config keys throw error", async () => { expect(stderr).toMatch(/Invalid configuration/); }); -test("handle missing rule files", async () => { - await setupFromFixture("missing-rules"); - - const { stdout, code } = await runCommand("install --ci"); - - expect(code).toBe(0); - expect(stdout).toContain("Successfully installed 2 rules"); - - // Check that existing rule was installed - expect( - fileExists(path.join(".cursor", "rules", "aicm", "existing-rule.mdc")), - ).toBe(true); - expect( - fileExists(path.join(".cursor", "rules", "aicm", "broken-reference.mdc")), - ).toBe(true); -}); - -test("multiple rules from rootDir", async () => { - await setupFromFixture("multiple-rules"); - - const { stdout, code } = await runCommand("install --ci"); - - expect(code).toBe(0); - expect(stdout).toContain("Successfully installed 3 rules"); - - // Check that all rules were installed - expect(fileExists(path.join(".cursor", "rules", "aicm", "rule1.mdc"))).toBe( - true, - ); - expect(fileExists(path.join(".cursor", "rules", "aicm", "rule2.mdc"))).toBe( - true, - ); - expect( - fileExists(path.join(".cursor", "rules", "aicm", "subdir", "rule3.mdc")), - ).toBe(true); - - // Verify content - const rule1Content = readTestFile( - path.join(".cursor", "rules", "aicm", "rule1.mdc"), - ); - expect(rule1Content).toContain("Rule 1"); - - const rule3Content = readTestFile( - path.join(".cursor", "rules", "aicm", "subdir", "rule3.mdc"), - ); - expect(rule3Content).toContain("Rule 3"); -}); - -test("multiple targets", async () => { - await setupFromFixture("multiple-targets"); - - const { stdout, code } = await runCommand("install --ci"); - - expect(code).toBe(0); - expect(stdout).toContain("Successfully installed 2 rules"); - - // Check Cursor installation - expect( - fileExists(path.join(".cursor", "rules", "aicm", "multi-target-rule.mdc")), - ).toBe(true); - - // Check Windsurf installation - expect(fileExists(path.join(".aicm", "multi-target-rule.md"))).toBe(true); - - // Check .windsurfrules file - const windsurfRulesContent = readTestFile(".windsurfrules"); - expect(windsurfRulesContent).toContain(".aicm/multi-target-rule.md"); -}); - -test("clean stale Cursor rules", async () => { - await setupFromFixture("cursor-cleanup"); - - const staleRulePath = path.join(".cursor", "rules", "aicm", "stale-rule.mdc"); - const newRulePath = path.join(".cursor", "rules", "aicm", "fresh-rule.mdc"); - - // Verify stale rule exists before installation - expect(fileExists(staleRulePath)).toBe(true); - - const { stdout, code } = await runCommand("install --ci"); - - expect(code).toBe(0); - expect(stdout).toContain("Successfully installed 1 rule"); - - // Stale rule should be removed, new rule should exist - expect(fileExists(staleRulePath)).toBe(false); - expect(fileExists(newRulePath)).toBe(true); -}); - -test("clean stale Windsurf rules", async () => { - await setupFromFixture("windsurf-cleanup"); - - // Verify initial state - expect(fileExists(path.join(".aicm", "stale-windsurf-rule.md"))).toBe(true); - const oldFreshContent = readTestFile( - path.join(".aicm", "fresh-windsurf-rule.md"), - ); - expect(oldFreshContent).toContain("This is OLD fresh Windsurf content"); - - let windsurfRulesContent = readTestFile(".windsurfrules"); - expect(windsurfRulesContent).toContain("- .aicm/stale-windsurf-rule.md"); - - const { stdout, code } = await runCommand("install --ci"); - - expect(code).toBe(0); - expect(stdout).toContain("Successfully installed 1 rule"); - - // Stale rule should be removed - expect(fileExists(path.join(".aicm", "stale-windsurf-rule.md"))).toBe(false); - - // Fresh rule should be updated - expect(fileExists(path.join(".aicm", "fresh-windsurf-rule.md"))).toBe(true); - const freshRuleContent = readTestFile( - path.join(".aicm", "fresh-windsurf-rule.md"), - ); - expect(freshRuleContent).toContain("This is fresh Windsurf content."); - expect(freshRuleContent).not.toContain("This is OLD fresh Windsurf content"); - - // .windsurfrules should be updated - windsurfRulesContent = readTestFile(".windsurfrules"); - expect(windsurfRulesContent).not.toContain("- .aicm/stale-windsurf-rule.md"); - expect(windsurfRulesContent).toContain("- .aicm/fresh-windsurf-rule.md"); -}); - test("preserve existing mcp configuration", async () => { await setupFromFixture("mcp-preserve-existing"); @@ -205,10 +78,10 @@ test("preserve existing mcp configuration", async () => { const { code } = await runCommand("install --ci"); expect(code).toBe(0); - // Check that rule was installed - expect( - fileExists(path.join(".cursor", "rules", "aicm", "test-rule.mdc")), - ).toBe(true); + // Check that instructions were installed + expect(fileExists("AGENTS.md")).toBe(true); + const agentsContent = readTestFile("AGENTS.md"); + expect(agentsContent).toContain("Test Instruction"); // Verify MCP config preservation and addition const finalMcpConfig = JSON.parse(readTestFile(mcpPath)); @@ -317,44 +190,16 @@ test("do not install canceled mcp servers", async () => { expect(finalMcpConfig.mcpServers["canceled-server"]).toBeUndefined(); }); -test("rules directory with subdirectories", async () => { - await setupFromFixture("rule-subdirs"); - - const { stdout, code } = await runCommand("install --ci"); - - expect(code).toBe(0); - expect(stdout).toContain("Successfully installed 1 rule"); - - // Check that rule was installed in subdirectory - expect( - fileExists( - path.join(".cursor", "rules", "aicm", "subdir", "nested-rule.mdc"), - ), - ).toBe(true); - - const installedContent = readTestFile( - path.join(".cursor", "rules", "aicm", "subdir", "nested-rule.mdc"), - ); - expect(installedContent).toContain("Nested Rule Content"); - - // Check that directory structure is preserved - expect(fileExists(path.join(".cursor", "rules", "aicm", "subdir"))).toBe( - true, - ); -}); - test("no mcp servers", async () => { await setupFromFixture("no-mcp"); const { stdout, code } = await runCommand("install --ci"); expect(code).toBe(0); - expect(stdout).toContain("Successfully installed 2 rules"); + expect(stdout).toContain("Successfully installed 1 instruction"); - // Check that rule was installed - expect( - fileExists(path.join(".cursor", "rules", "aicm", "no-mcp-rule.mdc")), - ).toBe(true); + // Check that instructions were installed + expect(fileExists("AGENTS.md")).toBe(true); // Check that no MCP config was created const mcpPath = path.join(".cursor", "mcp.json"); @@ -369,9 +214,7 @@ test("dry run does not write files", async () => { expect(code).toBe(0); expect(stdout).toContain("Dry run"); - expect( - fileExists(path.join(".cursor", "rules", "aicm", "test-rule.mdc")), - ).toBe(false); + expect(fileExists("AGENTS.md")).toBe(false); const mcpPath = path.join(".cursor", "mcp.json"); expect(fileExists(mcpPath)).toBe(false); @@ -384,12 +227,12 @@ test("skip installation when skipInstall is true", async () => { expect(code).toBe(0); expect(stdout).toContain( - "No rules, commands, hooks, skills, or agents installed", + "No instructions, hooks, skills, or agents installed", ); - // Check that no rules were installed - expect(fileExists(path.join(".cursor", "rules", "aicm"))).toBe(false); - expect(fileExists(path.join(".cursor"))).toBe(false); + // Check that no instructions were installed + expect(fileExists("AGENTS.md")).toBe(false); + expect(fileExists(path.join(".agents"))).toBe(false); // Check that no MCP config was created const mcpPath = path.join(".cursor", "mcp.json"); diff --git a/tests/e2e/instructions.test.ts b/tests/e2e/instructions.test.ts new file mode 100644 index 0000000..8cf1212 --- /dev/null +++ b/tests/e2e/instructions.test.ts @@ -0,0 +1,130 @@ +import path from "path"; +import { + setupFromFixture, + runCommand, + fileExists, + readTestFile, +} from "./helpers"; + +describe("instructions installation", () => { + test("auto-detects instructions directory", async () => { + await setupFromFixture("instructions-basic"); + + const { stdout, code } = await runCommand("install --ci"); + + expect(code).toBe(0); + expect(stdout).toContain("Successfully installed 2 instructions"); + + const agentsContent = readTestFile("AGENTS.md"); + expect(agentsContent).toContain("General Instructions"); + expect(agentsContent).toContain("Testing Instructions"); + }); + + test("auto-detects AGENTS.src.md", async () => { + await setupFromFixture("instructions-single-file"); + + const { stdout, code } = await runCommand("install --ci"); + + expect(code).toBe(0); + expect(stdout).toContain("Successfully installed 1 instruction"); + + const agentsContent = readTestFile("AGENTS.md"); + expect(agentsContent).toContain("Single File Instructions"); + }); + + test("auto-detects both AGENTS.src.md and instructions directory", async () => { + await setupFromFixture("instructions-both-auto-detect"); + + const { stdout, code } = await runCommand("install --ci"); + + expect(code).toBe(0); + expect(stdout).toContain("Successfully installed 2 instructions"); + + const agentsContent = readTestFile("AGENTS.md"); + expect(agentsContent).toContain("Single File Instructions"); + expect(agentsContent).toContain("General Instructions"); + }); + + test("progressive disclosure writes links and files", async () => { + await setupFromFixture("instructions-progressive"); + + const { stdout, code } = await runCommand("install --ci"); + + expect(code).toBe(0); + expect(stdout).toContain("Successfully installed 2 instructions"); + + const agentsContent = readTestFile("AGENTS.md"); + expect(agentsContent).not.toContain( + "The following instructions are available:", + ); + expect(agentsContent).toContain( + "- [testing](.agents/instructions/testing.md): How to run tests", + ); + + const referencedPath = path.join(".agents", "instructions", "testing.md"); + expect(fileExists(referencedPath)).toBe(true); + expect(readTestFile(referencedPath)).toContain("Testing Instructions"); + }); + + test("writes instructions to AGENTS.md and creates CLAUDE.md pointer when both targets active", async () => { + await setupFromFixture("instructions-multitarget"); + + const { stdout, code } = await runCommand("install --ci"); + + expect(code).toBe(0); + expect(stdout).toContain("Successfully installed 1 instruction"); + + // AGENTS.md should have the full instructions + expect(fileExists("AGENTS.md")).toBe(true); + const agentsContent = readTestFile("AGENTS.md"); + expect(agentsContent).toContain(""); + + // CLAUDE.md should be created with @AGENTS.md pointer + expect(fileExists("CLAUDE.md")).toBe(true); + const claudeContent = readTestFile("CLAUDE.md"); + expect(claudeContent.trim()).toBe("@AGENTS.md"); + }); + + test("instructionsFile allows custom filename", async () => { + await setupFromFixture("instructions-file-explicit"); + + const { stdout, code } = await runCommand("install --ci"); + + expect(code).toBe(0); + expect(stdout).toContain("Successfully installed 1 instruction"); + + const agentsContent = readTestFile("AGENTS.md"); + expect(agentsContent).toContain("Custom Instructions File"); + expect(agentsContent).toContain( + "These instructions come from a custom-named file.", + ); + }); + + test("loads both explicit instructionsFile and instructions directory", async () => { + await setupFromFixture("instructions-both-explicit"); + + const { stdout, code } = await runCommand("install --ci"); + + expect(code).toBe(0); + expect(stdout).toContain("Successfully installed 2 instructions"); + + const agentsContent = readTestFile("AGENTS.md"); + expect(agentsContent).toContain("Explicit File Instructions"); + expect(agentsContent).toContain("Explicit Directory Instructions"); + }); + + test("merges instructions from multiple presets with separators", async () => { + await setupFromFixture("instructions-preset"); + + const { stdout, code } = await runCommand("install --ci"); + + expect(code).toBe(0); + expect(stdout).toContain("Successfully installed 2 instructions"); + + const agentsContent = readTestFile("AGENTS.md"); + expect(agentsContent).toContain(""); + expect(agentsContent).toContain(""); + expect(agentsContent).toContain("Preset A Instructions"); + expect(agentsContent).toContain("Preset B Instructions"); + }); +}); diff --git a/tests/e2e/list.test.ts b/tests/e2e/list.test.ts index 0493718..eb0d738 100644 --- a/tests/e2e/list.test.ts +++ b/tests/e2e/list.test.ts @@ -1,19 +1,19 @@ import { setupFromFixture, runCommand, runCommandRaw } from "./helpers"; -test("should list all rules in the config", async () => { +test("should list all instructions in the config", async () => { await setupFromFixture("list-multiple-rules"); const { stdout } = await runCommand("list"); - expect(stdout).toContain("rule1"); - expect(stdout).toContain("rule2"); - expect(stdout).toContain("rule3"); + expect(stdout).toContain("instruction1"); + expect(stdout).toContain("instruction2"); + expect(stdout).toContain("instruction3"); }); -test("should show message when no rules exist", async () => { +test("should show message when no instructions exist", async () => { await setupFromFixture("list-no-rules"); const { stdout, stderr } = await runCommandRaw("list"); - expect(stdout + stderr).toMatch(/no rules|empty|not found/i); + expect(stdout + stderr).toMatch(/no instructions|empty|not found/i); }); diff --git a/tests/e2e/presets.test.ts b/tests/e2e/presets.test.ts index bca468e..ebbbcd0 100644 --- a/tests/e2e/presets.test.ts +++ b/tests/e2e/presets.test.ts @@ -8,102 +8,30 @@ import { testDir, } from "./helpers"; -test("install rules from a preset file", async () => { +test("install instructions from a preset file", async () => { await setupFromFixture("presets-from-file"); const { stdout, code } = await runCommand("install --ci"); expect(code).toBe(0); - expect(stdout).toContain("Successfully installed 2 rules"); - - // Check that rules from preset were installed with preset namespace - expect( - fileExists( - path.join( - ".cursor", - "rules", - "aicm", - "company-preset-full.json", - "typescript.mdc", - ), - ), - ).toBe(true); - expect( - fileExists( - path.join( - ".cursor", - "rules", - "aicm", - "company-preset-full.json", - "react.mdc", - ), - ), - ).toBe(true); - - const typescriptRuleContent = readTestFile( - path.join( - ".cursor", - "rules", - "aicm", - "company-preset-full.json", - "typescript.mdc", - ), - ); - expect(typescriptRuleContent).toContain("TypeScript Best Practices"); - - const reactRuleContent = readTestFile( - path.join( - ".cursor", - "rules", - "aicm", - "company-preset-full.json", - "react.mdc", - ), - ); - expect(reactRuleContent).toContain("React Best Practices"); + expect(stdout).toContain("Successfully installed 2 instructions"); + + const agentsContent = readTestFile("AGENTS.md"); + expect(agentsContent).toContain("TypeScript Best Practices"); + expect(agentsContent).toContain("React Best Practices"); }); -test("merge rules from presets with main configuration", async () => { +test("merge instructions from presets with main configuration", async () => { await setupFromFixture("presets-merged"); const { stdout, code } = await runCommand("install --ci"); expect(code).toBe(0); - expect(stdout).toContain("Successfully installed 2 rules"); - - // Check that preset rule was installed with preset namespace - expect( - fileExists( - path.join( - ".cursor", - "rules", - "aicm", - "company-preset.json", - "preset-rule.mdc", - ), - ), - ).toBe(true); - - // Check that local rule was installed in the main namespace - expect( - fileExists(path.join(".cursor", "rules", "aicm", "local-rule.mdc")), - ).toBe(true); - - const presetRuleContent = readTestFile( - path.join( - ".cursor", - "rules", - "aicm", - "company-preset.json", - "preset-rule.mdc", - ), - ); - expect(presetRuleContent).toContain("Preset Rule"); - - const localRuleContent = readTestFile( - path.join(".cursor", "rules", "aicm", "local-rule.mdc"), - ); - expect(localRuleContent).toContain("Local Rule"); + expect(stdout).toContain("Successfully installed 3 instructions"); + + const agentsContent = readTestFile("AGENTS.md"); + expect(agentsContent).toContain("Preset Instruction"); + expect(agentsContent).toContain("Local Instruction"); // Check that MCP config was installed const mcpPath = path.join(".cursor", "mcp.json"); @@ -123,36 +51,13 @@ test("handle npm package presets", async () => { const { stdout, code } = await runCommand("install --ci"); expect(code).toBe(0); - expect(stdout).toContain("Successfully installed 1 rule"); - - // Check that npm package rule was installed with npm package namespace - expect( - fileExists( - path.join( - ".cursor", - "rules", - "aicm", - "@company", - "ai-rules", - "npm-rule.mdc", - ), - ), - ).toBe(true); - - const npmRuleContent = readTestFile( - path.join( - ".cursor", - "rules", - "aicm", - "@company", - "ai-rules", - "npm-rule.mdc", - ), - ); - expect(npmRuleContent).toContain("NPM Package Rule"); + expect(stdout).toContain("Successfully installed 1 instruction"); + + const agentsContent = readTestFile("AGENTS.md"); + expect(agentsContent).toContain("NPM Package Instruction"); }); -test("install rules from sibling preset using ../ path", async () => { +test("install instructions from sibling preset using ../ path", async () => { await setupFromFixture("presets-sibling"); // Run from the project/ subdirectory which has the main config @@ -160,29 +65,10 @@ test("install rules from sibling preset using ../ path", async () => { const { stdout, code } = await runCommand("install --ci", projectDir); expect(code).toBe(0); - expect(stdout).toContain("Successfully installed 1 rule"); - - // Check that rule from sibling preset was installed with sibling-preset namespace - // This specifically tests that ../sibling-preset is correctly parsed as namespace ["sibling-preset"] - // and NOT ["../sibling-preset"] which would create wrong directory structure - expect( - fileExists( - path.join( - ".cursor", - "rules", - "aicm", - "sibling-preset", - "sibling-rule.mdc", - ), - projectDir, - ), - ).toBe(true); - - const ruleContent = readTestFile( - path.join(".cursor", "rules", "aicm", "sibling-preset", "sibling-rule.mdc"), - projectDir, - ); - expect(ruleContent).toContain("Sibling Preset Rule"); + expect(stdout).toContain("Successfully installed 1 instruction"); + + const agentsContent = readTestFile("AGENTS.md", projectDir); + expect(agentsContent).toContain("Sibling Preset Instruction"); }); test("handle errors with missing preset files", async () => { @@ -194,179 +80,57 @@ test("handle errors with missing preset files", async () => { expect(stderr).toContain("Preset not found"); }); -test("install rules from preset only (no rootDir)", async () => { +test("install instructions from preset only (no rootDir)", async () => { await setupFromFixture("presets-only"); const { stdout, code } = await runCommand("install --ci"); expect(code).toBe(0); - expect(stdout).toContain("Successfully installed 1 rule"); - - // Check that rules from preset were installed with preset namespace - expect( - fileExists( - path.join(".cursor", "rules", "aicm", "preset.json", "typescript.mdc"), - ), - ).toBe(true); - - const typescriptRuleContent = readTestFile( - path.join(".cursor", "rules", "aicm", "preset.json", "typescript.mdc"), - ); - expect(typescriptRuleContent).toContain( - "TypeScript Best Practices (Preset Only)", - ); + expect(stdout).toContain("Successfully installed 1 instruction"); + + const agentsContent = readTestFile("AGENTS.md"); + expect(agentsContent).toContain("TypeScript Best Practices (Preset Only)"); }); -test("install rules from preset only without picking up user's app directories", async () => { +test("install instructions from preset only without picking up user's app directories", async () => { await setupFromFixture("presets-only-with-app-commands"); const { stdout, code } = await runCommand("install --ci"); expect(code).toBe(0); - expect(stdout).toContain("Successfully installed 1 rule"); - - // Check that rules from preset were installed - expect( - fileExists( - path.join(".cursor", "rules", "aicm", "preset.json", "typescript.mdc"), - ), - ).toBe(true); - - const typescriptRuleContent = readTestFile( - path.join(".cursor", "rules", "aicm", "preset.json", "typescript.mdc"), - ); - expect(typescriptRuleContent).toContain("TypeScript Best Practices (Preset)"); - - // Check that user's app commands directory was NOT picked up - // Since there's no rootDir, the commands/ directory should be ignored - expect( - fileExists(path.join(".cursor", "commands", "user-app-command.md")), - ).toBe(false); - - // The command should not be installed anywhere in .cursor - expect(stdout).not.toContain("user-app-command"); -}); + expect(stdout).toContain("Successfully installed 1 instruction"); -test("preset commands and rules correctly reference assets with namespace paths", async () => { - await setupFromFixture("preset-commands-assets"); + const agentsContent = readTestFile("AGENTS.md"); + expect(agentsContent).toContain("TypeScript Best Practices (Preset)"); - const { stdout, code } = await runCommand("install --ci"); - - expect(code).toBe(0); - expect(stdout).toContain("Successfully installed"); - - // Check that assets are installed with namespace - expect( - fileExists( - path.join(".cursor", "assets", "aicm", "my-preset", "schema.json"), - ), - ).toBe(true); - - // Commands are NOT namespaced by preset (unlike rules) - expect(fileExists(path.join(".cursor", "commands", "aicm", "setup.md"))).toBe( - true, - ); - - // Check that rule is installed with namespace - expect( - fileExists( - path.join(".cursor", "rules", "aicm", "my-preset", "preset-rule.mdc"), - ), - ).toBe(true); - - // Read the installed command and verify asset paths are correctly rewritten - const commandContent = readTestFile( - path.join(".cursor", "commands", "aicm", "setup.md"), - ); - - // The command is at .cursor/commands/aicm/setup.md - // The asset is at .cursor/assets/aicm/my-preset/schema.json - // Original path in preset: ../assets/schema.json - // After rewriting: ../../assets/aicm/my-preset/schema.json - expect(commandContent).toContain( - "[schema.json](../../assets/aicm/my-preset/schema.json)", - ); - expect(commandContent).toContain("`../../assets/aicm/my-preset/schema.json`"); - expect(commandContent).toContain( - "Check the file at ../../assets/aicm/my-preset/schema.json for more details", - ); - - // Read the installed rule and verify asset paths are correctly rewritten - const ruleContent = readTestFile( - path.join(".cursor", "rules", "aicm", "my-preset", "preset-rule.mdc"), - ); - - // The rule is at .cursor/rules/aicm/my-preset/preset-rule.mdc - // The asset is at .cursor/assets/aicm/my-preset/schema.json - // Original path in preset: ../assets/schema.json - // After rewriting: ../../../assets/aicm/my-preset/schema.json - expect(ruleContent).toContain( - "[schema.json](../../../assets/aicm/my-preset/schema.json)", - ); + // Check that user's app directories were NOT picked up + expect(fileExists(path.join(".agents", "skills"))).toBe(false); + expect(fileExists(path.join(".agents", "agents"))).toBe(false); }); -test("install rules from recursively inherited presets", async () => { +test("install instructions from recursively inherited presets", async () => { await setupFromFixture("presets-recursive"); const { stdout, code } = await runCommand("install --ci"); expect(code).toBe(0); - expect(stdout).toContain("Successfully installed 2 rules"); - - // Check that rule from preset-a was installed with preset-a namespace - expect( - fileExists(path.join(".cursor", "rules", "aicm", "preset-a", "rule-a.mdc")), - ).toBe(true); - - // Check that rule from preset-b was installed with preset-b namespace - // (nested preset inherits from ../preset-b relative to preset-a) - expect( - fileExists(path.join(".cursor", "rules", "aicm", "preset-b", "rule-b.mdc")), - ).toBe(true); - - const ruleAContent = readTestFile( - path.join(".cursor", "rules", "aicm", "preset-a", "rule-a.mdc"), - ); - expect(ruleAContent).toContain("Rule A"); - - const ruleBContent = readTestFile( - path.join(".cursor", "rules", "aicm", "preset-b", "rule-b.mdc"), - ); - expect(ruleBContent).toContain("Rule B"); + expect(stdout).toContain("Successfully installed 2 instructions"); + + const agentsContent = readTestFile("AGENTS.md"); + expect(agentsContent).toContain("Instruction A"); + expect(agentsContent).toContain("Instruction B"); }); -test("install rules from inherits-only preset (no own content)", async () => { +test("install instructions from inherits-only preset (no own content)", async () => { await setupFromFixture("presets-inherits-only"); const { stdout, code } = await runCommand("install --ci"); expect(code).toBe(0); - expect(stdout).toContain("Successfully installed 1 rule"); - - // The wrapper preset has no content, but inherits from content-preset - // The rule should be installed with the content-preset namespace - expect( - fileExists( - path.join( - ".cursor", - "rules", - "aicm", - "content-preset", - "inherited-rule.mdc", - ), - ), - ).toBe(true); - - const ruleContent = readTestFile( - path.join( - ".cursor", - "rules", - "aicm", - "content-preset", - "inherited-rule.mdc", - ), - ); - expect(ruleContent).toContain("Inherited Rule"); + expect(stdout).toContain("Successfully installed 1 instruction"); + + const agentsContent = readTestFile("AGENTS.md"); + expect(agentsContent).toContain("Inherited Instruction"); }); test("detect circular preset dependencies", async () => { diff --git a/tests/e2e/readme-example.test.ts b/tests/e2e/readme-example.test.ts deleted file mode 100644 index cdb566f..0000000 --- a/tests/e2e/readme-example.test.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { setupFromFixture, runCommand, fileExists } from "./helpers"; -import path from "path"; - -test("should install rules from preset as shown in the README", async () => { - await setupFromFixture("presets-npm"); - - const { stdout } = await runCommand("install --ci"); - - expect(stdout).toContain("Successfully installed 1 rule"); - - expect( - fileExists( - path.join( - ".cursor", - "rules", - "aicm", - "@company", - "ai-rules", - "npm-rule.mdc", - ), - ), - ).toBe(true); -}); diff --git a/tests/e2e/skills.test.ts b/tests/e2e/skills.test.ts index 048188a..ef90e19 100644 --- a/tests/e2e/skills.test.ts +++ b/tests/e2e/skills.test.ts @@ -94,15 +94,13 @@ describe("skills installation", () => { expect(stdout).toContain("Successfully installed 1 skill"); - // Verify skill is installed to all targets + // Verify skill is installed to both targets (cursor uses .cursor/skills, claude-code uses .claude/skills) expect(fileExists(".cursor/skills/multi-skill/SKILL.md")).toBe(true); expect(fileExists(".claude/skills/multi-skill/SKILL.md")).toBe(true); - expect(fileExists(".codex/skills/multi-skill/SKILL.md")).toBe(true); // Verify each target has .aicm.json expect(fileExists(".cursor/skills/multi-skill/.aicm.json")).toBe(true); expect(fileExists(".claude/skills/multi-skill/.aicm.json")).toBe(true); - expect(fileExists(".codex/skills/multi-skill/.aicm.json")).toBe(true); }); test("clean removes aicm-managed skills", async () => { @@ -121,6 +119,34 @@ describe("skills installation", () => { expect(fileExists(".cursor/skills/code-review")).toBe(false); }); + // LEGACY(v0->v1): remove with namespaced-skill migration cleanup in install/clean. + test("install migrates legacy namespaced skills to flat layout", async () => { + await setupFromFixture("skills-legacy-namespaced"); + + expect(fileExists(".agents/skills/aicm/legacy-skill/SKILL.md")).toBe(true); + expect(fileExists(".claude/skills/aicm/legacy-skill/SKILL.md")).toBe(true); + + await runCommand("install --ci"); + + expect(fileExists(".agents/skills/aicm")).toBe(true); + expect(fileExists(".claude/skills/aicm")).toBe(false); + expect(fileExists(".cursor/skills/current-skill/SKILL.md")).toBe(true); + expect(fileExists(".claude/skills/current-skill/SKILL.md")).toBe(true); + }); + + // LEGACY(v0->v1): remove with namespaced-skill migration cleanup in install/clean. + test("clean removes legacy namespaced skills", async () => { + await setupFromFixture("skills-legacy-namespaced"); + + expect(fileExists(".agents/skills/aicm/legacy-skill/SKILL.md")).toBe(true); + expect(fileExists(".claude/skills/aicm/legacy-skill/SKILL.md")).toBe(true); + + await runCommand("clean --verbose"); + + expect(fileExists(".agents/skills/aicm")).toBe(false); + expect(fileExists(".claude/skills/aicm")).toBe(false); + }); + test("clean preserves non-aicm skills", async () => { await setupFromFixture("skills-basic"); diff --git a/tests/e2e/target-merge.test.ts b/tests/e2e/target-merge.test.ts new file mode 100644 index 0000000..d1a1cd7 --- /dev/null +++ b/tests/e2e/target-merge.test.ts @@ -0,0 +1,224 @@ +import path from "path"; +import { + setupFromFixture, + runCommand, + fileExists, + readTestFile, +} from "./helpers"; + +describe("target merge scenarios", () => { + test("cursor + claude-code: AGENTS.md full content, CLAUDE.md pointer", async () => { + await setupFromFixture("target-merge-cursor-claude"); + + const { code } = await runCommand("install --ci"); + expect(code).toBe(0); + + // Instructions: AGENTS.md gets full content + expect(fileExists("AGENTS.md")).toBe(true); + const agentsContent = readTestFile("AGENTS.md"); + expect(agentsContent).toContain(""); + expect(agentsContent).toContain("General Instructions"); + + // CLAUDE.md gets @AGENTS.md pointer + expect(fileExists("CLAUDE.md")).toBe(true); + expect(readTestFile("CLAUDE.md").trim()).toBe("@AGENTS.md"); + + // Skills in both + expect(fileExists(".cursor/skills/test-skill/SKILL.md")).toBe(true); + expect(fileExists(".claude/skills/test-skill/SKILL.md")).toBe(true); + + // Agents in both + expect(fileExists(".cursor/agents/test-agent.md")).toBe(true); + expect(fileExists(".claude/agents/test-agent.md")).toBe(true); + + // MCP in both + expect(fileExists(".cursor/mcp.json")).toBe(true); + expect(fileExists(".mcp.json")).toBe(true); + + // Hooks in both + expect(fileExists(".cursor/hooks.json")).toBe(true); + expect(fileExists(path.join(".claude", "settings.json"))).toBe(true); + }); + + test("existing CLAUDE.md is left untouched", async () => { + await setupFromFixture("target-merge-existing-claude"); + + const { code } = await runCommand("install --ci"); + expect(code).toBe(0); + + expect(fileExists("AGENTS.md")).toBe(true); + const agentsContent = readTestFile("AGENTS.md"); + expect(agentsContent).toContain("General Instructions"); + + // CLAUDE.md preserved with original user content + const claudeContent = readTestFile("CLAUDE.md"); + expect(claudeContent).toContain("My Custom Claude Config"); + expect(claudeContent).not.toContain("@AGENTS.md"); + }); + + test("cursor + opencode: deduplicates AGENTS.md, adds opencode paths", async () => { + await setupFromFixture("target-merge-cursor-opencode"); + + const { code } = await runCommand("install --ci"); + expect(code).toBe(0); + + // Instructions: AGENTS.md only (deduplicated) + expect(fileExists("AGENTS.md")).toBe(true); + expect(fileExists("CLAUDE.md")).toBe(false); + + // Skills in both + expect(fileExists(".cursor/skills/test-skill/SKILL.md")).toBe(true); + expect(fileExists(".opencode/skills/test-skill/SKILL.md")).toBe(true); + + // Agents in both + expect(fileExists(".cursor/agents/test-agent.md")).toBe(true); + expect(fileExists(".opencode/agents/test-agent.md")).toBe(true); + + // MCP: cursor + opencode format + expect(fileExists(".cursor/mcp.json")).toBe(true); + expect(fileExists("opencode.json")).toBe(true); + const opencodeMcp = JSON.parse(readTestFile("opencode.json")); + expect(opencodeMcp.mcp["test-mcp"].type).toBe("local"); + expect(opencodeMcp.mcp["test-mcp"].command).toEqual([ + "npx", + "-y", + "test-mcp-server", + ]); + expect(opencodeMcp.mcp["test-mcp"].enabled).toBe(true); + expect(opencodeMcp.mcp["test-mcp"].environment).toEqual({ + TEST_KEY: "test-value", + }); + + // Hooks: cursor only (opencode has no hooks) + expect(fileExists(".cursor/hooks.json")).toBe(true); + expect(fileExists(path.join(".claude", "settings.json"))).toBe(false); + }); + + test("cursor + codex: writes .codex/config.toml and deduplicates paths", async () => { + await setupFromFixture("target-merge-cursor-codex"); + + const { code } = await runCommand("install --ci"); + expect(code).toBe(0); + + // Instructions: AGENTS.md only (deduplicated, both use AGENTS.md) + expect(fileExists("AGENTS.md")).toBe(true); + expect(fileExists("CLAUDE.md")).toBe(false); + + // Skills: cursor and codex paths are both installed + expect(fileExists(".cursor/skills/test-skill/SKILL.md")).toBe(true); + expect(fileExists(".agents/skills/test-skill/SKILL.md")).toBe(true); + + // Agents: .cursor/agents only (codex has no agents) + expect(fileExists(".cursor/agents/test-agent.md")).toBe(true); + + // MCP: .cursor/mcp.json + .codex/config.toml + expect(fileExists(".cursor/mcp.json")).toBe(true); + + const codexTomlPath = path.join(".codex", "config.toml"); + expect(fileExists(codexTomlPath)).toBe(true); + + // Validate TOML structure + const codexToml = readTestFile(codexTomlPath); + expect(codexToml).toContain("[mcp_servers.test-mcp]"); + expect(codexToml).toContain('command = "npx"'); + expect(codexToml).toContain('"-y"'); + expect(codexToml).toContain('"test-mcp-server"'); + expect(codexToml).toContain("# aicm:managed"); + expect(codexToml).not.toContain("aicm = true"); + expect(codexToml).toContain('TEST_KEY = "test-value"'); + + // Hooks: .cursor only (codex has no hooks) + expect(fileExists(".cursor/hooks.json")).toBe(true); + + // Should NOT have claude or opencode paths + expect(fileExists(".mcp.json")).toBe(false); + expect(fileExists("opencode.json")).toBe(false); + expect(fileExists(path.join(".claude", "skills"))).toBe(false); + }); + + test("clean removes codex .codex/config.toml", async () => { + await setupFromFixture("target-merge-cursor-codex"); + + await runCommand("install --ci"); + expect(fileExists(path.join(".codex", "config.toml"))).toBe(true); + + await runCommand("clean --verbose"); + expect(fileExists(path.join(".codex", "config.toml"))).toBe(false); + }); + + test("all targets: merges everything correctly", async () => { + await setupFromFixture("target-merge-all"); + + const { code } = await runCommand("install --ci"); + expect(code).toBe(0); + + // Instructions: AGENTS.md full content, CLAUDE.md pointer + expect(fileExists("AGENTS.md")).toBe(true); + expect(readTestFile("CLAUDE.md").trim()).toBe("@AGENTS.md"); + + // Skills: .cursor/skills, .claude/skills, .opencode/skills, .agents/skills (codex) + expect(fileExists(".cursor/skills/test-skill/SKILL.md")).toBe(true); + expect(fileExists(".agents/skills/test-skill/SKILL.md")).toBe(true); + expect(fileExists(".claude/skills/test-skill/SKILL.md")).toBe(true); + expect(fileExists(".opencode/skills/test-skill/SKILL.md")).toBe(true); + + // Agents: .cursor, .claude, .opencode + expect(fileExists(".cursor/agents/test-agent.md")).toBe(true); + expect(fileExists(".claude/agents/test-agent.md")).toBe(true); + expect(fileExists(".opencode/agents/test-agent.md")).toBe(true); + + // MCP: all four formats + expect(fileExists(".cursor/mcp.json")).toBe(true); + expect(fileExists(".mcp.json")).toBe(true); + expect(fileExists("opencode.json")).toBe(true); + expect(fileExists(path.join(".codex", "config.toml"))).toBe(true); + + // Codex TOML format + const codexToml = readTestFile(path.join(".codex", "config.toml")); + expect(codexToml).toContain("test-mcp"); + expect(codexToml).toContain("npx"); + + // Hooks: cursor and claude only + expect(fileExists(".cursor/hooks.json")).toBe(true); + expect(fileExists(path.join(".claude", "settings.json"))).toBe(true); + }); + + test("clean after all-targets install removes generated files", async () => { + await setupFromFixture("target-merge-all"); + + await runCommand("install --ci"); + + // Verify files exist + expect(fileExists("AGENTS.md")).toBe(true); + expect(fileExists("CLAUDE.md")).toBe(true); + expect(fileExists("opencode.json")).toBe(true); + expect(fileExists(path.join(".codex", "config.toml"))).toBe(true); + + // Clean + const { code } = await runCommand("clean --verbose"); + expect(code).toBe(0); + + // All generated files removed + expect(fileExists("AGENTS.md")).toBe(false); + expect(fileExists("CLAUDE.md")).toBe(false); + expect(fileExists(".cursor/mcp.json")).toBe(false); + expect(fileExists(".mcp.json")).toBe(false); + expect(fileExists("opencode.json")).toBe(false); + expect(fileExists(path.join(".codex", "config.toml"))).toBe(false); + expect(fileExists(".cursor/skills/test-skill")).toBe(false); + expect(fileExists(".agents/skills/test-skill")).toBe(false); + expect(fileExists(".opencode/skills/test-skill")).toBe(false); + expect(fileExists(".cursor/agents/test-agent.md")).toBe(false); + expect(fileExists(".opencode/agents/test-agent.md")).toBe(false); + }); + + test("clean removes @AGENTS.md-only CLAUDE.md files", async () => { + await setupFromFixture("target-merge-cursor-claude"); + + await runCommand("install --ci"); + expect(readTestFile("CLAUDE.md").trim()).toBe("@AGENTS.md"); + + await runCommand("clean --verbose"); + expect(fileExists("CLAUDE.md")).toBe(false); + }); +}); diff --git a/tests/e2e/target-presets.test.ts b/tests/e2e/target-presets.test.ts new file mode 100644 index 0000000..f1d63a3 --- /dev/null +++ b/tests/e2e/target-presets.test.ts @@ -0,0 +1,139 @@ +import path from "path"; +import { + setupFromFixture, + runCommand, + runFailedCommand, + fileExists, + readTestFile, + getDirectoryStructure, +} from "./helpers"; + +describe("target presets", () => { + test("cursor preset installs to cursor-specific paths", async () => { + await setupFromFixture("target-presets-cursor"); + + const { stdout, code } = await runCommand("install --ci"); + + expect(code).toBe(0); + expect(stdout).toContain("Successfully installed 1 instruction"); + + // Instructions should go to AGENTS.md (cursor preset) + expect(fileExists("AGENTS.md")).toBe(true); + const agentsContent = readTestFile("AGENTS.md"); + expect(agentsContent).toContain(""); + expect(agentsContent).toContain("General Instructions"); + + // Should NOT create CLAUDE.md (not using claude-code preset) + expect(fileExists("CLAUDE.md")).toBe(false); + }); + + test("both presets: AGENTS.md gets full content, CLAUDE.md gets pointer", async () => { + await setupFromFixture("target-presets-both"); + + const { stdout, code } = await runCommand("install --ci"); + + expect(code).toBe(0); + expect(stdout).toContain("Successfully installed 1 instruction"); + + // Instructions should go to AGENTS.md with full content + expect(fileExists("AGENTS.md")).toBe(true); + const agentsContent = readTestFile("AGENTS.md"); + expect(agentsContent).toContain(""); + expect(agentsContent).toContain("General Instructions"); + + // CLAUDE.md should be created with @AGENTS.md pointer + expect(fileExists("CLAUDE.md")).toBe(true); + const claudeContent = readTestFile("CLAUDE.md"); + expect(claudeContent.trim()).toBe("@AGENTS.md"); + }); + + test("invalid preset name throws error", async () => { + await setupFromFixture("target-presets-invalid"); + + const { stderr, code } = await runFailedCommand("install --ci"); + + expect(code).not.toBe(0); + expect(stderr).toContain("Unknown target preset"); + expect(stderr).toContain("nonexistent-preset"); + }); + + test("claude-code preset installs hooks to .claude/settings.json", async () => { + await setupFromFixture("target-presets-hooks-claude"); + + const { stdout, code } = await runCommand("install --ci"); + + expect(code).toBe(0); + expect(stdout).toContain("Successfully installed 3 hooks"); + + // Check .claude/settings.json was created + const settingsPath = path.join(".claude", "settings.json"); + expect(fileExists(settingsPath)).toBe(true); + + const settings = JSON.parse(readTestFile(settingsPath)); + expect(settings.hooks).toBeDefined(); + + // beforeShellExecution -> PreToolUse with Bash matcher + expect(settings.hooks.PreToolUse).toBeDefined(); + const preToolUseGroups = settings.hooks.PreToolUse; + const bashGroup = preToolUseGroups.find( + (g: { matcher?: string }) => g.matcher === "Bash", + ); + expect(bashGroup).toBeDefined(); + expect(bashGroup.hooks).toEqual([ + { type: "command", command: "./hooks/aicm/audit.sh" }, + ]); + + // afterFileEdit -> PostToolUse with Edit|Write matcher + expect(settings.hooks.PostToolUse).toBeDefined(); + const postToolUseGroups = settings.hooks.PostToolUse; + const editGroup = postToolUseGroups.find( + (g: { matcher?: string }) => g.matcher === "Edit|Write", + ); + expect(editGroup).toBeDefined(); + expect(editGroup.hooks).toEqual([ + { type: "command", command: "./hooks/aicm/format.js" }, + ]); + + // stop -> Stop (no matcher) + expect(settings.hooks.Stop).toBeDefined(); + const stopGroups = settings.hooks.Stop; + const stopGroup = stopGroups.find((g: { matcher?: string }) => !g.matcher); + expect(stopGroup).toBeDefined(); + expect(stopGroup.hooks).toEqual([ + { type: "command", command: "./hooks/aicm/cleanup.sh" }, + ]); + + // Check hook files were copied + const structure = getDirectoryStructure(path.join(".claude", "hooks")); + expect(structure).toContain(".claude/hooks/aicm/audit.sh"); + expect(structure).toContain(".claude/hooks/aicm/format.js"); + expect(structure).toContain(".claude/hooks/aicm/cleanup.sh"); + }); + + test("both presets install hooks to both cursor and claude-code", async () => { + await setupFromFixture("target-presets-hooks-both"); + + const { stdout, code } = await runCommand("install --ci"); + + expect(code).toBe(0); + expect(stdout).toContain("Successfully installed 2 hooks"); + + // Check Cursor hooks.json was created + expect(fileExists(".cursor/hooks.json")).toBe(true); + const hooksJson = JSON.parse(readTestFile(".cursor/hooks.json")); + expect(hooksJson.hooks.beforeShellExecution).toEqual([ + { command: "./hooks/aicm/audit.sh" }, + ]); + + // Check Claude Code settings.json was created + const settingsPath = path.join(".claude", "settings.json"); + expect(fileExists(settingsPath)).toBe(true); + const settings = JSON.parse(readTestFile(settingsPath)); + expect(settings.hooks).toBeDefined(); + expect(settings.hooks.PreToolUse).toBeDefined(); + + // Both should have hook files + expect(fileExists(".cursor/hooks/aicm/audit.sh")).toBe(true); + expect(fileExists(".claude/hooks/aicm/audit.sh")).toBe(true); + }); +}); diff --git a/tests/e2e/windsurf.test.ts b/tests/e2e/windsurf.test.ts deleted file mode 100644 index 3a59f7d..0000000 --- a/tests/e2e/windsurf.test.ts +++ /dev/null @@ -1,96 +0,0 @@ -import path from "path"; -import { - setupFromFixture, - runCommand, - fileExists, - readTestFile, -} from "./helpers"; - -test("should install rules to .aicm directory and update .windsurfrules", async () => { - await setupFromFixture("windsurf-basic"); - - const { stdout, code } = await runCommand("install --ci"); - - expect(code).toBe(0); - expect(stdout).toContain("Successfully installed 3 rules"); - - expect(fileExists(path.join(".aicm", "always-rule.md"))).toBe(true); - expect(fileExists(path.join(".aicm", "opt-in-rule.md"))).toBe(true); - expect(fileExists(path.join(".aicm", "file-pattern-rule.md"))).toBe(true); - - const alwaysRuleContent = readTestFile(path.join(".aicm", "always-rule.md")); - expect(alwaysRuleContent).toContain("Development Standards"); - - const optInRuleContent = readTestFile(path.join(".aicm", "opt-in-rule.md")); - expect(optInRuleContent).toContain("E2E Testing Best Practices"); - - const filePatternRuleContent = readTestFile( - path.join(".aicm", "file-pattern-rule.md"), - ); - expect(filePatternRuleContent).toContain("TypeScript Best Practices"); - - expect(fileExists(".windsurfrules")).toBe(true); - const windsurfRulesContent = readTestFile(".windsurfrules"); - - expect(windsurfRulesContent).toContain(""); - expect(windsurfRulesContent).toContain(""); - - expect(windsurfRulesContent).toContain( - "The following rules can be loaded when relevant. Check each file's description:", - ); - expect(windsurfRulesContent).toContain("- .aicm/always-rule.md"); - expect(windsurfRulesContent).toContain("- .aicm/opt-in-rule.md"); - expect(windsurfRulesContent).toContain("- .aicm/file-pattern-rule.md"); -}); - -test("should update existing .windsurfrules file", async () => { - await setupFromFixture("windsurf-existing"); - - const { stdout, code } = await runCommand("install --ci"); - - expect(code).toBe(0); - expect(stdout).toContain("Successfully installed 1 rule"); - - const windsurfRulesContent = readTestFile(".windsurfrules"); - - expect(windsurfRulesContent).toContain("Manually written rules"); - expect(windsurfRulesContent).toContain(""); - expect(windsurfRulesContent).toContain(""); - - expect(windsurfRulesContent).toContain(""); - expect(windsurfRulesContent).toContain(""); - expect(windsurfRulesContent).toContain(".aicm/new-rule.md"); -}); - -test("should clean stale windsurf rules", async () => { - await setupFromFixture("windsurf-cleanup"); - - // Verify initial state - expect(fileExists(path.join(".aicm", "stale-windsurf-rule.md"))).toBe(true); - const oldFreshContent = readTestFile( - path.join(".aicm", "fresh-windsurf-rule.md"), - ); - expect(oldFreshContent).toContain("This is OLD fresh Windsurf content"); - - let windsurfRulesContent = readTestFile(".windsurfrules"); - expect(windsurfRulesContent).toContain("- .aicm/stale-windsurf-rule.md"); - - const { stdout, code } = await runCommand("install --ci"); - - expect(code).toBe(0); - expect(stdout).toContain("Successfully installed 1 rule"); - - expect(fileExists(path.join(".aicm", "stale-windsurf-rule.md"))).toBe(false); - - expect(fileExists(path.join(".aicm", "fresh-windsurf-rule.md"))).toBe(true); - const freshRuleContent = readTestFile( - path.join(".aicm", "fresh-windsurf-rule.md"), - ); - expect(freshRuleContent).toContain("This is fresh Windsurf content."); - expect(freshRuleContent).not.toContain("This is OLD fresh Windsurf content"); - - // .windsurfrules should be updated - windsurfRulesContent = readTestFile(".windsurfrules"); - expect(windsurfRulesContent).not.toContain("- .aicm/stale-windsurf-rule.md"); - expect(windsurfRulesContent).toContain("- .aicm/fresh-windsurf-rule.md"); -}); diff --git a/tests/e2e/workspaces.test.ts b/tests/e2e/workspaces.test.ts index f304641..f381fcd 100644 --- a/tests/e2e/workspaces.test.ts +++ b/tests/e2e/workspaces.test.ts @@ -7,7 +7,7 @@ import { readTestFile, } from "./helpers"; -test("discover and install rules from multiple packages", async () => { +test("discover and install instructions from multiple packages", async () => { await setupFromFixture("workspaces-npm-basic"); const { stdout, code } = await runCommand("install --ci --verbose"); @@ -18,61 +18,29 @@ test("discover and install rules from multiple packages", async () => { expect(stdout).toContain("- packages/backend"); expect(stdout).toContain("- packages/frontend"); expect(stdout).toContain("📦 Installing configurations..."); - expect(stdout).toContain("✅ packages/backend (1 rules)"); - expect(stdout).toContain("✅ packages/frontend (1 rules)"); - expect(stdout).toContain("Successfully installed 2 rules across 2 packages"); + expect(stdout).toContain("✅ packages/backend (1 instruction)"); + expect(stdout).toContain("✅ packages/frontend (1 instruction)"); + expect(stdout).toContain( + "Successfully installed 2 instructions across 2 packages", + ); - // Check that rules were installed in both packages - expect( - fileExists( - path.join( - "packages", - "frontend", - ".cursor", - "rules", - "aicm", - "frontend-rule.mdc", - ), - ), - ).toBe(true); + // Check that instructions were installed in both packages + expect(fileExists(path.join("packages", "frontend", "AGENTS.md"))).toBe(true); + expect(fileExists(path.join("packages", "backend", "AGENTS.md"))).toBe(true); - expect( - fileExists( - path.join( - "packages", - "backend", - ".cursor", - "rules", - "aicm", - "backend-rule.mdc", - ), - ), - ).toBe(true); - - // Verify rule content - const frontendRule = readTestFile( - path.join( - "packages", - "frontend", - ".cursor", - "rules", - "aicm", - "frontend-rule.mdc", - ), + // Verify instruction content + const frontendAgents = readTestFile( + path.join("packages", "frontend", "AGENTS.md"), ); - expect(frontendRule).toContain("Frontend Development Rules"); - - const backendRule = readTestFile( - path.join( - "packages", - "backend", - ".cursor", - "rules", - "aicm", - "backend-rule.mdc", - ), + expect(frontendAgents).toContain("Frontend Development Instructions"); + + const backendAgents = readTestFile( + path.join("packages", "backend", "AGENTS.md"), ); - expect(backendRule).toContain("Backend Development Rules"); + expect(backendAgents).toContain("Backend Development Instructions"); + + // Workspace mode should not merge package instructions to root + expect(fileExists("AGENTS.md")).toBe(false); }); test("show error when no packages found in workspaces", async () => { @@ -92,11 +60,9 @@ test("install normally when workspaces is enabled on single package", async () = expect(code).toBe(0); expect(stdout).toContain("Found 1 packages with aicm configurations:"); expect(stdout).toContain("- ."); - expect(stdout).toContain("Successfully installed 1 rule"); + expect(stdout).toContain("Successfully installed 1 instruction"); - expect( - fileExists(path.join(".cursor", "rules", "aicm", "local-rule.mdc")), - ).toBe(true); + expect(fileExists(path.join("AGENTS.md"))).toBe(true); }); test("handle partial configurations (some packages with configs, some without)", async () => { @@ -111,44 +77,27 @@ test("handle partial configurations (some packages with configs, some without)", expect(stdout).toContain("- packages/also-with-config"); expect(stdout).not.toContain("- packages/without-config"); expect(stdout).toContain("📦 Installing configurations..."); - expect(stdout).toContain("✅ packages/with-config (1 rules)"); - expect(stdout).toContain("✅ packages/also-with-config (1 rules)"); - expect(stdout).toContain("Successfully installed 2 rules across 2 packages"); - - // Check that rules were installed only in packages with configs - expect( - fileExists( - path.join( - "packages", - "with-config", - ".cursor", - "rules", - "aicm", - "package-one-rule.mdc", - ), - ), - ).toBe(true); + expect(stdout).toContain("✅ packages/with-config (1 instruction)"); + expect(stdout).toContain("✅ packages/also-with-config (1 instruction)"); + expect(stdout).toContain( + "Successfully installed 2 instructions across 2 packages", + ); + // Check that instructions were installed only in packages with configs + expect(fileExists(path.join("packages", "with-config", "AGENTS.md"))).toBe( + true, + ); expect( - fileExists( - path.join( - "packages", - "also-with-config", - ".cursor", - "rules", - "aicm", - "package-three-rule.mdc", - ), - ), + fileExists(path.join("packages", "also-with-config", "AGENTS.md")), ).toBe(true); - // Verify no rules were installed in the package without config - expect(fileExists(path.join("packages", "without-config", ".cursor"))).toBe( + // Verify no instructions were installed in the package without config + expect(fileExists(path.join("packages", "without-config", "AGENTS.md"))).toBe( false, ); }); -test("discover and install rules from deeply nested workspaces structure", async () => { +test("discover and install instructions from deeply nested workspaces structure", async () => { await setupFromFixture("workspaces-npm-nested"); const { stdout, code } = await runCommand("install --ci --verbose"); @@ -160,46 +109,20 @@ test("discover and install rules from deeply nested workspaces structure", async expect(stdout).toContain("- packages/ui"); expect(stdout).toContain("- tools/build"); expect(stdout).toContain("📦 Installing configurations..."); - expect(stdout).toContain("✅ apps/web (1 rules)"); - expect(stdout).toContain("✅ packages/ui (1 rules)"); - expect(stdout).toContain("✅ tools/build (1 rules)"); - expect(stdout).toContain("Successfully installed 3 rules across 3 packages"); - - // Check that rules were installed in all nested packages - expect( - fileExists( - path.join("apps", "web", ".cursor", "rules", "aicm", "web-app-rule.mdc"), - ), - ).toBe(true); - - expect( - fileExists( - path.join( - "packages", - "ui", - ".cursor", - "rules", - "aicm", - "ui-components-rule.mdc", - ), - ), - ).toBe(true); + expect(stdout).toContain("✅ apps/web (1 instruction)"); + expect(stdout).toContain("✅ packages/ui (1 instruction)"); + expect(stdout).toContain("✅ tools/build (1 instruction)"); + expect(stdout).toContain( + "Successfully installed 3 instructions across 3 packages", + ); - expect( - fileExists( - path.join( - "tools", - "build", - ".cursor", - "rules", - "aicm", - "build-tools-rule.mdc", - ), - ), - ).toBe(true); + // Check that instructions were installed in all nested packages + expect(fileExists(path.join("apps", "web", "AGENTS.md"))).toBe(true); + expect(fileExists(path.join("packages", "ui", "AGENTS.md"))).toBe(true); + expect(fileExists(path.join("tools", "build", "AGENTS.md"))).toBe(true); }); -test("discover and install rules from Bazel workspaces", async () => { +test("discover and install instructions from Bazel workspaces", async () => { await setupFromFixture("workspaces-bazel-basic"); const { stdout, code } = await runCommand("install --ci --verbose"); @@ -210,39 +133,18 @@ test("discover and install rules from Bazel workspaces", async () => { expect(stdout).toContain("- services/api"); expect(stdout).toContain("- services/worker"); expect(stdout).toContain("📦 Installing configurations..."); - expect(stdout).toContain("✅ services/api (1 rules)"); - expect(stdout).toContain("✅ services/worker (1 rules)"); - expect(stdout).toContain("Successfully installed 2 rules across 2 packages"); - - // Check that rules were installed in both Bazel services - expect( - fileExists( - path.join( - "services", - "api", - ".cursor", - "rules", - "aicm", - "api-service-rule.mdc", - ), - ), - ).toBe(true); + expect(stdout).toContain("✅ services/api (1 instruction)"); + expect(stdout).toContain("✅ services/worker (1 instruction)"); + expect(stdout).toContain( + "Successfully installed 2 instructions across 2 packages", + ); - expect( - fileExists( - path.join( - "services", - "worker", - ".cursor", - "rules", - "aicm", - "worker-service-rule.mdc", - ), - ), - ).toBe(true); + // Check that instructions were installed in both Bazel services + expect(fileExists(path.join("services", "api", "AGENTS.md"))).toBe(true); + expect(fileExists(path.join("services", "worker", "AGENTS.md"))).toBe(true); }); -test("discover and install rules from mixed workspaces + Bazel structure", async () => { +test("discover and install instructions from mixed workspaces + Bazel structure", async () => { await setupFromFixture("workspaces-mixed"); const { stdout, code } = await runCommand("install --ci --verbose"); @@ -253,31 +155,18 @@ test("discover and install rules from mixed workspaces + Bazel structure", async expect(stdout).toContain("- frontend"); expect(stdout).toContain("- backend-service"); expect(stdout).toContain("📦 Installing configurations..."); - expect(stdout).toContain("✅ frontend (1 rules)"); - expect(stdout).toContain("✅ backend-service (1 rules)"); - expect(stdout).toContain("Successfully installed 2 rules across 2 packages"); - - // Check that rules were installed in both mixed package types - expect( - fileExists( - path.join("frontend", ".cursor", "rules", "aicm", "frontend-rule.mdc"), - ), - ).toBe(true); + expect(stdout).toContain("✅ frontend (1 instruction)"); + expect(stdout).toContain("✅ backend-service (1 instruction)"); + expect(stdout).toContain( + "Successfully installed 2 instructions across 2 packages", + ); - expect( - fileExists( - path.join( - "backend-service", - ".cursor", - "rules", - "aicm", - "backend-service-rule.mdc", - ), - ), - ).toBe(true); + // Check that instructions were installed in both mixed package types + expect(fileExists(path.join("frontend", "AGENTS.md"))).toBe(true); + expect(fileExists(path.join("backend-service", "AGENTS.md"))).toBe(true); }); -test("handle package missing rules gracefully", async () => { +test("handle package missing instructions gracefully", async () => { await setupFromFixture("workspaces-error-scenarios"); const { stdout, code } = await runCommand("install --ci --verbose"); @@ -288,18 +177,14 @@ test("handle package missing rules gracefully", async () => { expect(stdout).toContain("- valid-package"); expect(stdout).toContain("- missing-rule"); expect(stdout).toContain("📦 Installing configurations..."); - expect(stdout).toContain("✅ valid-package (1 rules)"); - expect(stdout).toContain("✅ missing-rule (0 rules)"); + expect(stdout).toContain("✅ valid-package (1 instruction)"); + expect(stdout).toContain("✅ missing-rule (0 instructions)"); // Check that the valid package still installed successfully - expect( - fileExists( - path.join("valid-package", ".cursor", "rules", "aicm", "valid-rule.mdc"), - ), - ).toBe(true); + expect(fileExists(path.join("valid-package", "AGENTS.md"))).toBe(true); // Check that the error package did not install anything - expect(fileExists(path.join("missing-rule", ".cursor"))).toBe(false); + expect(fileExists(path.join("missing-rule", "AGENTS.md"))).toBe(false); }); test("work quietly by default without verbose flag", async () => { @@ -311,8 +196,10 @@ test("work quietly by default without verbose flag", async () => { expect(stdout).not.toContain("🔍 Discovering packages..."); expect(stdout).not.toContain("Found 2 packages with aicm configurations:"); expect(stdout).not.toContain("📦 Installing configurations..."); - expect(stdout).not.toContain("✅ packages/backend (1 rules)"); - expect(stdout).toContain("Successfully installed 2 rules across 2 packages"); + expect(stdout).not.toContain("✅ packages/backend (1 instruction)"); + expect(stdout).toContain( + "Successfully installed 2 instructions across 2 packages", + ); }); test("automatically detect workspaces from package.json", async () => { @@ -326,61 +213,30 @@ test("automatically detect workspaces from package.json", async () => { expect(stdout).toContain("- packages/backend"); expect(stdout).toContain("- packages/frontend"); expect(stdout).toContain("📦 Installing configurations..."); - expect(stdout).toContain("✅ packages/backend (1 rules)"); - expect(stdout).toContain("✅ packages/frontend (1 rules)"); - expect(stdout).toContain("Successfully installed 2 rules across 2 packages"); + expect(stdout).toContain("✅ packages/backend (1 instruction)"); + expect(stdout).toContain("✅ packages/frontend (1 instruction)"); + expect(stdout).toContain( + "Successfully installed 2 instructions across 2 packages", + ); - // Check that rules were installed in both packages - expect( - fileExists( - path.join( - "packages", - "frontend", - ".cursor", - "rules", - "aicm", - "frontend-rule.mdc", - ), - ), - ).toBe(true); + // Check that instructions were installed in both packages + expect(fileExists(path.join("packages", "frontend", "AGENTS.md"))).toBe(true); + expect(fileExists(path.join("packages", "backend", "AGENTS.md"))).toBe(true); - expect( - fileExists( - path.join( - "packages", - "backend", - ".cursor", - "rules", - "aicm", - "backend-rule.mdc", - ), - ), - ).toBe(true); + // Verify instruction content + const frontendAgents = readTestFile( + path.join("packages", "frontend", "AGENTS.md"), + ); + expect(frontendAgents).toContain( + "Frontend Development Instructions (Auto-detected)", + ); - // Verify rule content - const frontendRule = readTestFile( - path.join( - "packages", - "frontend", - ".cursor", - "rules", - "aicm", - "frontend-rule.mdc", - ), + const backendAgents = readTestFile( + path.join("packages", "backend", "AGENTS.md"), ); - expect(frontendRule).toContain("Frontend Development Rules (Auto-detected)"); - - const backendRule = readTestFile( - path.join( - "packages", - "backend", - ".cursor", - "rules", - "aicm", - "backend-rule.mdc", - ), + expect(backendAgents).toContain( + "Backend Development Instructions (Auto-detected)", ); - expect(backendRule).toContain("Backend Development Rules (Auto-detected)"); }); test("explicit workspaces: false overrides auto-detection from package.json", async () => { @@ -392,21 +248,17 @@ test("explicit workspaces: false overrides auto-detection from package.json", as expect(stdout).not.toContain("🔍 Discovering packages..."); expect(stdout).not.toContain("Found"); expect(stdout).not.toContain("📦 Installing configurations..."); - expect(stdout).toContain("Successfully installed 1 rule"); + expect(stdout).toContain("Successfully installed 1 instruction"); - // Check that rule was installed in root directory, not as workspace - expect( - fileExists(path.join(".cursor", "rules", "aicm", "main-rule.mdc")), - ).toBe(true); + // Check that instruction was installed in root directory, not as workspace + expect(fileExists(path.join("AGENTS.md"))).toBe(true); // Check that no workspace packages were processed expect(fileExists(path.join("packages", "frontend", ".cursor"))).toBe(false); // Verify rule content - const mainRule = readTestFile( - path.join(".cursor", "rules", "aicm", "main-rule.mdc"), - ); - expect(mainRule).toContain("Main Rule (Explicit False)"); + const rootAgents = readTestFile("AGENTS.md"); + expect(rootAgents).toContain("Main Instruction (Explicit False)"); }); test("automatically detect workspaces when no root config file exists", async () => { @@ -420,61 +272,30 @@ test("automatically detect workspaces when no root config file exists", async () expect(stdout).toContain("- packages/backend"); expect(stdout).toContain("- packages/frontend"); expect(stdout).toContain("📦 Installing configurations..."); - expect(stdout).toContain("✅ packages/backend (1 rules)"); - expect(stdout).toContain("✅ packages/frontend (1 rules)"); - expect(stdout).toContain("Successfully installed 2 rules across 2 packages"); + expect(stdout).toContain("✅ packages/backend (1 instruction)"); + expect(stdout).toContain("✅ packages/frontend (1 instruction)"); + expect(stdout).toContain( + "Successfully installed 2 instructions across 2 packages", + ); - // Check that rules were installed in both packages - expect( - fileExists( - path.join( - "packages", - "frontend", - ".cursor", - "rules", - "aicm", - "frontend-rule.mdc", - ), - ), - ).toBe(true); + // Check that instructions were installed in both packages + expect(fileExists(path.join("packages", "frontend", "AGENTS.md"))).toBe(true); + expect(fileExists(path.join("packages", "backend", "AGENTS.md"))).toBe(true); - expect( - fileExists( - path.join( - "packages", - "backend", - ".cursor", - "rules", - "aicm", - "backend-rule.mdc", - ), - ), - ).toBe(true); + // Verify instruction content + const frontendAgents = readTestFile( + path.join("packages", "frontend", "AGENTS.md"), + ); + expect(frontendAgents).toContain( + "Frontend Development Instructions (No Config)", + ); - // Verify rule content - const frontendRule = readTestFile( - path.join( - "packages", - "frontend", - ".cursor", - "rules", - "aicm", - "frontend-rule.mdc", - ), + const backendAgents = readTestFile( + path.join("packages", "backend", "AGENTS.md"), ); - expect(frontendRule).toContain("Frontend Development Rules (No Config)"); - - const backendRule = readTestFile( - path.join( - "packages", - "backend", - ".cursor", - "rules", - "aicm", - "backend-rule.mdc", - ), + expect(backendAgents).toContain( + "Backend Development Instructions (No Config)", ); - expect(backendRule).toContain("Backend Development Rules (No Config)"); // Verify that no root config file exists expect(fileExists("aicm.json")).toBe(false); @@ -491,64 +312,29 @@ test("allow empty root config in workspace mode", async () => { expect(stdout).toContain("- packages/backend"); expect(stdout).toContain("- packages/frontend"); expect(stdout).toContain("📦 Installing configurations..."); - expect(stdout).toContain("✅ packages/backend (1 rules)"); - expect(stdout).toContain("✅ packages/frontend (1 rules)"); - expect(stdout).toContain("Successfully installed 2 rules across 2 packages"); - - // Check that rules were installed in both packages - expect( - fileExists( - path.join( - "packages", - "frontend", - ".cursor", - "rules", - "aicm", - "frontend-rule.mdc", - ), - ), - ).toBe(true); + expect(stdout).toContain("✅ packages/backend (1 instruction)"); + expect(stdout).toContain("✅ packages/frontend (1 instruction)"); + expect(stdout).toContain( + "Successfully installed 2 instructions across 2 packages", + ); - expect( - fileExists( - path.join( - "packages", - "backend", - ".cursor", - "rules", - "aicm", - "backend-rule.mdc", - ), - ), - ).toBe(true); + // Check that instructions were installed in both packages + expect(fileExists(path.join("packages", "frontend", "AGENTS.md"))).toBe(true); + expect(fileExists(path.join("packages", "backend", "AGENTS.md"))).toBe(true); - // Verify rule content - const frontendRule = readTestFile( - path.join( - "packages", - "frontend", - ".cursor", - "rules", - "aicm", - "frontend-rule.mdc", - ), + // Verify instruction content + const frontendAgents = readTestFile( + path.join("packages", "frontend", "AGENTS.md"), ); - expect(frontendRule).toContain( - "Frontend Development Rules (Empty Root Config)", + expect(frontendAgents).toContain( + "Frontend Development Instructions (Empty Root Config)", ); - const backendRule = readTestFile( - path.join( - "packages", - "backend", - ".cursor", - "rules", - "aicm", - "backend-rule.mdc", - ), + const backendAgents = readTestFile( + path.join("packages", "backend", "AGENTS.md"), ); - expect(backendRule).toContain( - "Backend Development Rules (Empty Root Config)", + expect(backendAgents).toContain( + "Backend Development Instructions (Empty Root Config)", ); // Verify that root config file exists but has no rootDir or presets @@ -616,39 +402,23 @@ test("skip installation for packages with skipInstall: true", async () => { expect(stdout).toContain("- packages/regular-package"); expect(stdout).not.toContain("- packages/preset-package"); expect(stdout).toContain("📦 Installing configurations..."); - expect(stdout).toContain("✅ packages/regular-package (1 rules)"); + expect(stdout).toContain("✅ packages/regular-package (1 instruction)"); expect(stdout).not.toContain("✅ packages/preset-package"); - expect(stdout).toContain("Successfully installed 1 rule"); + expect(stdout).toContain("Successfully installed 1 instruction"); - // Check that rules were installed only in the regular package + // Check that instructions were installed only in the regular package expect( - fileExists( - path.join( - "packages", - "regular-package", - ".cursor", - "rules", - "aicm", - "regular-rule.mdc", - ), - ), + fileExists(path.join("packages", "regular-package", "AGENTS.md")), ).toBe(true); - // Check that no rules were installed in the preset package - expect(fileExists(path.join("packages", "preset-package", ".cursor"))).toBe( + // Check that no instructions were installed in the preset package + expect(fileExists(path.join("packages", "preset-package", "AGENTS.md"))).toBe( false, ); - // Verify rule content in regular package - const regularRule = readTestFile( - path.join( - "packages", - "regular-package", - ".cursor", - "rules", - "aicm", - "regular-rule.mdc", - ), + // Verify instruction content in regular package + const regularAgents = readTestFile( + path.join("packages", "regular-package", "AGENTS.md"), ); - expect(regularRule).toContain("Regular Package Rule"); + expect(regularAgents).toContain("Regular Package Instruction"); }); diff --git a/tests/fixtures/agents-multitarget/aicm.json b/tests/fixtures/agents-multitarget/aicm.json index 0f0d4b0..457fbca 100644 --- a/tests/fixtures/agents-multitarget/aicm.json +++ b/tests/fixtures/agents-multitarget/aicm.json @@ -1,4 +1,4 @@ { "rootDir": "./", - "targets": ["cursor", "claude"] + "targets": ["cursor", "claude-code"] } diff --git a/tests/fixtures/agents-workspace/aicm.json b/tests/fixtures/agents-workspace/aicm.json index 3933c35..f46310b 100644 --- a/tests/fixtures/agents-workspace/aicm.json +++ b/tests/fixtures/agents-workspace/aicm.json @@ -1,3 +1,4 @@ { - "workspaces": true + "workspaces": true, + "targets": ["cursor"] } diff --git a/tests/fixtures/assets-basic/aicm.json b/tests/fixtures/assets-basic/aicm.json deleted file mode 100644 index 2ae91d4..0000000 --- a/tests/fixtures/assets-basic/aicm.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "rootDir": "./", - "targets": ["cursor", "windsurf", "codex"] -} diff --git a/tests/fixtures/assets-basic/rules/example.txt b/tests/fixtures/assets-basic/rules/example.txt deleted file mode 100644 index 3ab1d1b..0000000 --- a/tests/fixtures/assets-basic/rules/example.txt +++ /dev/null @@ -1,2 +0,0 @@ -This is an example asset. - diff --git a/tests/fixtures/assets-basic/rules/shared.txt b/tests/fixtures/assets-basic/rules/shared.txt deleted file mode 100644 index e39d682..0000000 --- a/tests/fixtures/assets-basic/rules/shared.txt +++ /dev/null @@ -1,2 +0,0 @@ -This is a shared asset. - diff --git a/tests/fixtures/assets-basic/rules/subdir/local.txt b/tests/fixtures/assets-basic/rules/subdir/local.txt deleted file mode 100644 index 2be9ede..0000000 --- a/tests/fixtures/assets-basic/rules/subdir/local.txt +++ /dev/null @@ -1,2 +0,0 @@ -Local asset. - diff --git a/tests/fixtures/assets-basic/rules/subdir/nested.mdc b/tests/fixtures/assets-basic/rules/subdir/nested.mdc deleted file mode 100644 index 494dd4c..0000000 --- a/tests/fixtures/assets-basic/rules/subdir/nested.mdc +++ /dev/null @@ -1,7 +0,0 @@ ---- -description: Nested rule ---- -# Nested Rule - -See [Shared](../shared.txt) -See [Local](./local.txt) diff --git a/tests/fixtures/assets-basic/rules/test.mdc b/tests/fixtures/assets-basic/rules/test.mdc deleted file mode 100644 index 57e2a0c..0000000 --- a/tests/fixtures/assets-basic/rules/test.mdc +++ /dev/null @@ -1,8 +0,0 @@ ---- -description: Test rule with asset -globs: "*.ts" ---- -# Test Rule - -See [Example](./example.txt) -See [Missing](./missing.txt) diff --git a/tests/fixtures/assets-commands/commands/generate-schema.md b/tests/fixtures/assets-commands/commands/generate-schema.md deleted file mode 100644 index 6dde834..0000000 --- a/tests/fixtures/assets-commands/commands/generate-schema.md +++ /dev/null @@ -1,3 +0,0 @@ -# Generate Schema - -Use the schema defined in [Schema Template](../rules/schema.json) to generate the response. diff --git a/tests/fixtures/assets-commands/rules/rule.mdc b/tests/fixtures/assets-commands/rules/rule.mdc deleted file mode 100644 index 73bbadb..0000000 --- a/tests/fixtures/assets-commands/rules/rule.mdc +++ /dev/null @@ -1,7 +0,0 @@ ---- -description: schema -globs: *.json ---- -# Schema Rule - -Rule content diff --git a/tests/fixtures/assets-commands/rules/schema.json b/tests/fixtures/assets-commands/rules/schema.json deleted file mode 100644 index bba55dc..0000000 --- a/tests/fixtures/assets-commands/rules/schema.json +++ /dev/null @@ -1 +0,0 @@ -{ "type": "object" } diff --git a/tests/fixtures/assets-dir-basic/assets/examples/response.json b/tests/fixtures/assets-dir-basic/assets/examples/response.json deleted file mode 100644 index 7f030ad..0000000 --- a/tests/fixtures/assets-dir-basic/assets/examples/response.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "id": "123", - "name": "Example Response" -} diff --git a/tests/fixtures/assets-dir-basic/assets/schema.json b/tests/fixtures/assets-dir-basic/assets/schema.json deleted file mode 100644 index a2c5881..0000000 --- a/tests/fixtures/assets-dir-basic/assets/schema.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "type": "object", - "properties": { - "id": { "type": "string" }, - "name": { "type": "string" } - } -} diff --git a/tests/fixtures/assets-dir-basic/commands/generate.md b/tests/fixtures/assets-dir-basic/commands/generate.md deleted file mode 100644 index 0bb1f6a..0000000 --- a/tests/fixtures/assets-dir-basic/commands/generate.md +++ /dev/null @@ -1,5 +0,0 @@ -# Generate API - -Generate API code using [this schema](../assets/schema.json). - -Check the example at ../assets/examples/response.json for reference. diff --git a/tests/fixtures/assets-dir-basic/rules/api.mdc b/tests/fixtures/assets-dir-basic/rules/api.mdc deleted file mode 100644 index 751e6ae..0000000 --- a/tests/fixtures/assets-dir-basic/rules/api.mdc +++ /dev/null @@ -1,5 +0,0 @@ -# API Guidelines - -Use the schema defined at [schema.json](../assets/schema.json) for all API responses. - -You can also reference the example: `../assets/examples/response.json` diff --git a/tests/fixtures/assets-dir-hooks/hooks.json b/tests/fixtures/assets-dir-hooks/hooks.json deleted file mode 100644 index e5aba0b..0000000 --- a/tests/fixtures/assets-dir-hooks/hooks.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "version": 1, - "hooks": { - "beforeShellExecution": [ - { - "command": "./hooks/validate.sh" - }, - { - "command": "./hooks/helper.js" - } - ] - } -} diff --git a/tests/fixtures/assets-dir-hooks/hooks/helper.js b/tests/fixtures/assets-dir-hooks/hooks/helper.js deleted file mode 100644 index 312efb3..0000000 --- a/tests/fixtures/assets-dir-hooks/hooks/helper.js +++ /dev/null @@ -1,3 +0,0 @@ -#!/usr/bin/env node -// Helper script for validation -console.log("Helper executed"); diff --git a/tests/fixtures/assets-dir-hooks/hooks/validate.sh b/tests/fixtures/assets-dir-hooks/hooks/validate.sh deleted file mode 100644 index c298889..0000000 --- a/tests/fixtures/assets-dir-hooks/hooks/validate.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/bash -# Validation script -# This script calls a helper -./helper.js -echo "Validation complete" - diff --git a/tests/fixtures/assets-dir-hooks/rules/test.mdc b/tests/fixtures/assets-dir-hooks/rules/test.mdc deleted file mode 100644 index 3dc9cf6..0000000 --- a/tests/fixtures/assets-dir-hooks/rules/test.mdc +++ /dev/null @@ -1,3 +0,0 @@ -# Test Rule - -This is a test rule. diff --git a/tests/fixtures/assets-dir-multitarget/aicm.json b/tests/fixtures/assets-dir-multitarget/aicm.json deleted file mode 100644 index e456028..0000000 --- a/tests/fixtures/assets-dir-multitarget/aicm.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "rootDir": "./", - "targets": ["cursor", "windsurf", "codex", "claude"] -} diff --git a/tests/fixtures/assets-dir-multitarget/assets/config.yaml b/tests/fixtures/assets-dir-multitarget/assets/config.yaml deleted file mode 100644 index b876ff8..0000000 --- a/tests/fixtures/assets-dir-multitarget/assets/config.yaml +++ /dev/null @@ -1,3 +0,0 @@ -version: 1.0 -settings: - enabled: true diff --git a/tests/fixtures/assets-dir-multitarget/assets/data.json b/tests/fixtures/assets-dir-multitarget/assets/data.json deleted file mode 100644 index 08c5b6e..0000000 --- a/tests/fixtures/assets-dir-multitarget/assets/data.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "items": [ - { "id": 1, "name": "Item 1" }, - { "id": 2, "name": "Item 2" } - ] -} diff --git a/tests/fixtures/assets-dir-multitarget/rules/example.mdc b/tests/fixtures/assets-dir-multitarget/rules/example.mdc deleted file mode 100644 index e61ab84..0000000 --- a/tests/fixtures/assets-dir-multitarget/rules/example.mdc +++ /dev/null @@ -1,5 +0,0 @@ -# Example Rule - -Reference the [config file](../assets/config.yaml) for configuration details. - -Also check `../assets/data.json` for data format. diff --git a/tests/fixtures/assets-linking/aicm.json b/tests/fixtures/assets-linking/aicm.json deleted file mode 100644 index 2b888e6..0000000 --- a/tests/fixtures/assets-linking/aicm.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "rootDir": "./", - "targets": ["cursor", "codex"] -} diff --git a/tests/fixtures/assets-linking/rules/example.ts b/tests/fixtures/assets-linking/rules/example.ts deleted file mode 100644 index f9ea32f..0000000 --- a/tests/fixtures/assets-linking/rules/example.ts +++ /dev/null @@ -1 +0,0 @@ -export const example = "test"; diff --git a/tests/fixtures/assets-linking/rules/my-rule.mdc b/tests/fixtures/assets-linking/rules/my-rule.mdc deleted file mode 100644 index 0563c4e..0000000 --- a/tests/fixtures/assets-linking/rules/my-rule.mdc +++ /dev/null @@ -1,7 +0,0 @@ ---- -globs: *.ts ---- - -# My Rule - -See [Example](./example.ts) diff --git a/tests/fixtures/assets-linking/rules/subdir/helper.json b/tests/fixtures/assets-linking/rules/subdir/helper.json deleted file mode 100644 index 3fce754..0000000 --- a/tests/fixtures/assets-linking/rules/subdir/helper.json +++ /dev/null @@ -1 +0,0 @@ -{ "helper": true } diff --git a/tests/fixtures/assets-linking/rules/subdir/nested-rule.mdc b/tests/fixtures/assets-linking/rules/subdir/nested-rule.mdc deleted file mode 100644 index c12312f..0000000 --- a/tests/fixtures/assets-linking/rules/subdir/nested-rule.mdc +++ /dev/null @@ -1,8 +0,0 @@ ---- -description: nested -globs: *.ts ---- -# Nested Rule - -See [Helper](./helper.json) -See [Root Example](../example.ts) diff --git a/tests/fixtures/assets-preset/aicm.json b/tests/fixtures/assets-preset/aicm.json deleted file mode 100644 index 0a9a121..0000000 --- a/tests/fixtures/assets-preset/aicm.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "presets": ["./my-preset"] -} diff --git a/tests/fixtures/assets-preset/my-preset/rules/preset-asset.txt b/tests/fixtures/assets-preset/my-preset/rules/preset-asset.txt deleted file mode 100644 index 1e350b1..0000000 --- a/tests/fixtures/assets-preset/my-preset/rules/preset-asset.txt +++ /dev/null @@ -1,2 +0,0 @@ -preset asset content - diff --git a/tests/fixtures/assets-preset/my-preset/rules/preset-rule.mdc b/tests/fixtures/assets-preset/my-preset/rules/preset-rule.mdc deleted file mode 100644 index 7b8beac..0000000 --- a/tests/fixtures/assets-preset/my-preset/rules/preset-rule.mdc +++ /dev/null @@ -1,7 +0,0 @@ ---- -description: preset rule -globs: *.ts ---- -# Preset Rule - -See [Asset](./preset-asset.txt) diff --git a/tests/fixtures/assets-preset/my-preset/rules/subdir/nested-asset.json b/tests/fixtures/assets-preset/my-preset/rules/subdir/nested-asset.json deleted file mode 100644 index 6bf6923..0000000 --- a/tests/fixtures/assets-preset/my-preset/rules/subdir/nested-asset.json +++ /dev/null @@ -1 +0,0 @@ -{ "nested": true } diff --git a/tests/fixtures/assets-preset/my-preset/rules/subdir/nested-preset-rule.mdc b/tests/fixtures/assets-preset/my-preset/rules/subdir/nested-preset-rule.mdc deleted file mode 100644 index 9607226..0000000 --- a/tests/fixtures/assets-preset/my-preset/rules/subdir/nested-preset-rule.mdc +++ /dev/null @@ -1,8 +0,0 @@ ---- -description: nested preset rule -globs: *.ts ---- -# Nested Preset Rule - -See [Root Asset](../preset-asset.txt) -See [Nested Asset](./nested-asset.json) diff --git a/tests/fixtures/claude-basic/aicm.json b/tests/fixtures/claude-basic/aicm.json deleted file mode 100644 index b91f7e1..0000000 --- a/tests/fixtures/claude-basic/aicm.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "rootDir": "./", - "targets": ["claude"] -} diff --git a/tests/fixtures/claude-basic/rules/always-rule.mdc b/tests/fixtures/claude-basic/rules/always-rule.mdc deleted file mode 100644 index e4c9af5..0000000 --- a/tests/fixtures/claude-basic/rules/always-rule.mdc +++ /dev/null @@ -1,8 +0,0 @@ ---- -description: "Always applied rule for Claude" -type: "always" ---- - -- Use yarn for package management -- Development workflow: e2e tests → implementation → verification -- Document all features in README.md \ No newline at end of file diff --git a/tests/fixtures/claude-basic/rules/file-pattern-rule.mdc b/tests/fixtures/claude-basic/rules/file-pattern-rule.mdc deleted file mode 100644 index 3888f5e..0000000 --- a/tests/fixtures/claude-basic/rules/file-pattern-rule.mdc +++ /dev/null @@ -1,9 +0,0 @@ ---- -description: "File pattern rule for TypeScript files" -type: "auto-attached" -globs: ["*.ts"] ---- - -- Use strict type checking -- Prefer interfaces over types for public APIs -- Use optional chaining and nullish coalescing when appropriate \ No newline at end of file diff --git a/tests/fixtures/claude-basic/rules/manual-rule.mdc b/tests/fixtures/claude-basic/rules/manual-rule.mdc deleted file mode 100644 index 15f5cef..0000000 --- a/tests/fixtures/claude-basic/rules/manual-rule.mdc +++ /dev/null @@ -1,6 +0,0 @@ ---- ---- - -# Manual Rule - -This is a manual rule that requires explicit reference. \ No newline at end of file diff --git a/tests/fixtures/claude-basic/rules/opt-in-rule.mdc b/tests/fixtures/claude-basic/rules/opt-in-rule.mdc deleted file mode 100644 index b81ae66..0000000 --- a/tests/fixtures/claude-basic/rules/opt-in-rule.mdc +++ /dev/null @@ -1,8 +0,0 @@ ---- -description: "Optional development practices rule" -type: "agent-requested" ---- - -- Follow semantic versioning for releases -- Use conventional commits for commit messages -- Run tests before pushing changes \ No newline at end of file diff --git a/tests/fixtures/claude-no-markers/aicm.json b/tests/fixtures/claude-no-markers/aicm.json deleted file mode 100644 index b91f7e1..0000000 --- a/tests/fixtures/claude-no-markers/aicm.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "rootDir": "./", - "targets": ["claude"] -} diff --git a/tests/fixtures/claude-no-markers/rules/no-marker-rule.mdc b/tests/fixtures/claude-no-markers/rules/no-marker-rule.mdc deleted file mode 100644 index d7bf465..0000000 --- a/tests/fixtures/claude-no-markers/rules/no-marker-rule.mdc +++ /dev/null @@ -1,6 +0,0 @@ ---- ---- - -# No Marker Rule - -This rule is used to test appending markers to an existing file without markers. \ No newline at end of file diff --git a/tests/fixtures/codex-basic/.gitkeep b/tests/fixtures/codex-basic/.gitkeep deleted file mode 100644 index 0519ecb..0000000 --- a/tests/fixtures/codex-basic/.gitkeep +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/tests/fixtures/codex-basic/aicm.json b/tests/fixtures/codex-basic/aicm.json deleted file mode 100644 index 1d41399..0000000 --- a/tests/fixtures/codex-basic/aicm.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "rootDir": "./", - "targets": ["codex"] -} diff --git a/tests/fixtures/codex-basic/rules/always-rule.mdc b/tests/fixtures/codex-basic/rules/always-rule.mdc deleted file mode 100644 index 3615207..0000000 --- a/tests/fixtures/codex-basic/rules/always-rule.mdc +++ /dev/null @@ -1,13 +0,0 @@ ---- -description: -globs: -alwaysApply: false ---- ---- -description: "Always applied rule for Codex" -type: "always" ---- - -- Use yarn for package management -- Development workflow: e2e tests → implementation → verification -- Document all features in README.md diff --git a/tests/fixtures/codex-basic/rules/file-pattern-rule.mdc b/tests/fixtures/codex-basic/rules/file-pattern-rule.mdc deleted file mode 100644 index 0239f39..0000000 --- a/tests/fixtures/codex-basic/rules/file-pattern-rule.mdc +++ /dev/null @@ -1,14 +0,0 @@ ---- -description: -globs: -alwaysApply: false ---- ---- -description: "File pattern rule for TypeScript files" -type: "file-pattern" -patterns: ["*.ts"] ---- - -- Use strict type checking -- Prefer interfaces over types for public APIs -- Use optional chaining and nullish coalescing when appropriate diff --git a/tests/fixtures/codex-basic/rules/opt-in-rule.mdc b/tests/fixtures/codex-basic/rules/opt-in-rule.mdc deleted file mode 100644 index e7c5c55..0000000 --- a/tests/fixtures/codex-basic/rules/opt-in-rule.mdc +++ /dev/null @@ -1,9 +0,0 @@ ---- -description: -globs: -alwaysApply: false ---- - -- Follow semantic versioning for releases -- Use conventional commits for commit messages -- Run tests before pushing changes diff --git a/tests/fixtures/codex-no-markers/.gitkeep b/tests/fixtures/codex-no-markers/.gitkeep deleted file mode 100644 index 0519ecb..0000000 --- a/tests/fixtures/codex-no-markers/.gitkeep +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/tests/fixtures/codex-no-markers/aicm.json b/tests/fixtures/codex-no-markers/aicm.json deleted file mode 100644 index 1d41399..0000000 --- a/tests/fixtures/codex-no-markers/aicm.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "rootDir": "./", - "targets": ["codex"] -} diff --git a/tests/fixtures/codex-no-markers/rules/no-marker-rule.mdc b/tests/fixtures/codex-no-markers/rules/no-marker-rule.mdc deleted file mode 100644 index ec905b2..0000000 --- a/tests/fixtures/codex-no-markers/rules/no-marker-rule.mdc +++ /dev/null @@ -1,9 +0,0 @@ ---- -description: -globs: -alwaysApply: false ---- - -# No Marker Rule - -This rule is used to test appending markers to an existing file without markers. diff --git a/tests/fixtures/commands-basic/commands/build/release.md b/tests/fixtures/commands-basic/commands/build/release.md deleted file mode 100644 index 3eed5fc..0000000 --- a/tests/fixtures/commands-basic/commands/build/release.md +++ /dev/null @@ -1,3 +0,0 @@ -# Release Build - -Build the project for release. diff --git a/tests/fixtures/commands-basic/commands/test.md b/tests/fixtures/commands-basic/commands/test.md deleted file mode 100644 index f92a5db..0000000 --- a/tests/fixtures/commands-basic/commands/test.md +++ /dev/null @@ -1,3 +0,0 @@ -# Test Command - -Run the unit test suite. diff --git a/tests/fixtures/commands-collision/aicm.json b/tests/fixtures/commands-collision/aicm.json deleted file mode 100644 index 50630a9..0000000 --- a/tests/fixtures/commands-collision/aicm.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "presets": ["./preset-a", "./preset-b"], - "targets": ["cursor"] -} diff --git a/tests/fixtures/commands-collision/preset-a/commands/shared/run.md b/tests/fixtures/commands-collision/preset-a/commands/shared/run.md deleted file mode 100644 index ca9401c..0000000 --- a/tests/fixtures/commands-collision/preset-a/commands/shared/run.md +++ /dev/null @@ -1,3 +0,0 @@ -# Shared Run (Preset A) - -Preset A version diff --git a/tests/fixtures/commands-collision/preset-b/aicm.json b/tests/fixtures/commands-collision/preset-b/aicm.json deleted file mode 100644 index de289e3..0000000 --- a/tests/fixtures/commands-collision/preset-b/aicm.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "rootDir": "./" -} diff --git a/tests/fixtures/commands-collision/preset-b/commands/shared/run.md b/tests/fixtures/commands-collision/preset-b/commands/shared/run.md deleted file mode 100644 index f426865..0000000 --- a/tests/fixtures/commands-collision/preset-b/commands/shared/run.md +++ /dev/null @@ -1,3 +0,0 @@ -# Shared Run (Preset B) - -Preset B version diff --git a/tests/fixtures/commands-preset/aicm.json b/tests/fixtures/commands-preset/aicm.json deleted file mode 100644 index 9526e7d..0000000 --- a/tests/fixtures/commands-preset/aicm.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "rootDir": "./", - "presets": ["./preset"] -} diff --git a/tests/fixtures/commands-preset/commands/local/custom.md b/tests/fixtures/commands-preset/commands/local/custom.md deleted file mode 100644 index 062d97b..0000000 --- a/tests/fixtures/commands-preset/commands/local/custom.md +++ /dev/null @@ -1,3 +0,0 @@ -# Custom Command - -Runs a project-specific workflow. diff --git a/tests/fixtures/commands-preset/preset/aicm.json b/tests/fixtures/commands-preset/preset/aicm.json deleted file mode 100644 index de289e3..0000000 --- a/tests/fixtures/commands-preset/preset/aicm.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "rootDir": "./" -} diff --git a/tests/fixtures/commands-preset/preset/commands/test/run-tests.md b/tests/fixtures/commands-preset/preset/commands/test/run-tests.md deleted file mode 100644 index e2ba689..0000000 --- a/tests/fixtures/commands-preset/preset/commands/test/run-tests.md +++ /dev/null @@ -1,3 +0,0 @@ -# Run Tests - -Execute the shared test suite. diff --git a/tests/fixtures/commands-subdirectory-links/commands/example.md b/tests/fixtures/commands-subdirectory-links/commands/example.md deleted file mode 100644 index 58ae22d..0000000 --- a/tests/fixtures/commands-subdirectory-links/commands/example.md +++ /dev/null @@ -1,30 +0,0 @@ -# Example Command - -This command references assets in various formats: - -## Markdown Links - -- [First asset in subdirectory](../rules/category-a/asset-one.mjs) -- [Second asset in subdirectory](../rules/category-a/asset-two.mjs) -- [Deeply nested asset](../rules/deep/nested/structure/config.json) - -## Inline Code References - -Run the script: `node ../rules/category-a/asset-one.mjs` - -Execute: `../rules/category-a/asset-two.mjs` - -## Bare Path References - -You can also run: ../rules/deep/nested/structure/config.json - -## Code Block (should NOT be rewritten) - -```bash -# This is an example and should not be rewritten -node ../rules/example/fake.js -``` - -## Non-existent Path (should NOT be rewritten) - -This path doesn't exist: ../rules/nonexistent/file.js diff --git a/tests/fixtures/commands-subdirectory-links/commands/scan.md b/tests/fixtures/commands-subdirectory-links/commands/scan.md deleted file mode 100644 index 9bff543..0000000 --- a/tests/fixtures/commands-subdirectory-links/commands/scan.md +++ /dev/null @@ -1,7 +0,0 @@ -# Example Command - -This command references: - -- [First asset in subdirectory](../rules/category-a/asset-one.mjs) -- [Second asset in subdirectory](../rules/category-a/asset-two.mjs) -- [Deeply nested asset](../rules/deep/nested/structure/config.json) diff --git a/tests/fixtures/commands-subdirectory-links/rules/category-a/asset-one.mjs b/tests/fixtures/commands-subdirectory-links/rules/category-a/asset-one.mjs deleted file mode 100644 index 1629c60..0000000 --- a/tests/fixtures/commands-subdirectory-links/rules/category-a/asset-one.mjs +++ /dev/null @@ -1,4 +0,0 @@ -// First asset -export function assetOne() { - console.log("Asset one logic"); -} diff --git a/tests/fixtures/commands-subdirectory-links/rules/category-a/asset-two.mjs b/tests/fixtures/commands-subdirectory-links/rules/category-a/asset-two.mjs deleted file mode 100644 index 0a654e8..0000000 --- a/tests/fixtures/commands-subdirectory-links/rules/category-a/asset-two.mjs +++ /dev/null @@ -1,4 +0,0 @@ -// Second asset -export function assetTwo() { - console.log("Asset two logic"); -} diff --git a/tests/fixtures/commands-subdirectory-links/rules/deep/nested/structure/config.json b/tests/fixtures/commands-subdirectory-links/rules/deep/nested/structure/config.json deleted file mode 100644 index df8bd4f..0000000 --- a/tests/fixtures/commands-subdirectory-links/rules/deep/nested/structure/config.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "setting": "value" -} diff --git a/tests/fixtures/commands-subdirectory-links/rules/example-rule.mdc b/tests/fixtures/commands-subdirectory-links/rules/example-rule.mdc deleted file mode 100644 index fb09bac..0000000 --- a/tests/fixtures/commands-subdirectory-links/rules/example-rule.mdc +++ /dev/null @@ -1,3 +0,0 @@ -# Example Rule - -This is a simple rule file. diff --git a/tests/fixtures/commands-workspace-aux-files/aicm.json b/tests/fixtures/commands-workspace-aux-files/aicm.json deleted file mode 100644 index 48c4b83..0000000 --- a/tests/fixtures/commands-workspace-aux-files/aicm.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "targets": ["cursor"] -} diff --git a/tests/fixtures/commands-workspace-aux-files/package-a/commands/test.md b/tests/fixtures/commands-workspace-aux-files/package-a/commands/test.md deleted file mode 100644 index 69dc68a..0000000 --- a/tests/fixtures/commands-workspace-aux-files/package-a/commands/test.md +++ /dev/null @@ -1,17 +0,0 @@ -# Test Command - -This command references various auxiliary files. - -## Manual Rule Reference - -See the manual rule: [Manual Rule](../rules/manual-rule.mdc) - -## Auto Rule Reference - -See the auto rule: [Auto Rule](../rules/auto-rule.mdc) - -## Helper Script - -Run the helper script: `node ../rules/helper.js` - -Or reference it directly: ../rules/helper.js diff --git a/tests/fixtures/commands-workspace-aux-files/package-a/rules/auto-rule.mdc b/tests/fixtures/commands-workspace-aux-files/package-a/rules/auto-rule.mdc deleted file mode 100644 index 61bfb51..0000000 --- a/tests/fixtures/commands-workspace-aux-files/package-a/rules/auto-rule.mdc +++ /dev/null @@ -1,8 +0,0 @@ ---- -alwaysApply: true ---- - -# Auto Rule - -This is an automatic rule that always applies. -It should trigger a warning when referenced by a command. diff --git a/tests/fixtures/commands-workspace-aux-files/package-a/rules/helper.js b/tests/fixtures/commands-workspace-aux-files/package-a/rules/helper.js deleted file mode 100644 index 1f6e896..0000000 --- a/tests/fixtures/commands-workspace-aux-files/package-a/rules/helper.js +++ /dev/null @@ -1,2 +0,0 @@ -// Helper script for testing auxiliary file copying -console.log("Helper script executed"); diff --git a/tests/fixtures/commands-workspace-aux-files/package-a/rules/manual-rule.mdc b/tests/fixtures/commands-workspace-aux-files/package-a/rules/manual-rule.mdc deleted file mode 100644 index d66dcbd..0000000 --- a/tests/fixtures/commands-workspace-aux-files/package-a/rules/manual-rule.mdc +++ /dev/null @@ -1,4 +0,0 @@ -# Manual Rule - -This is a manual rule with no frontmatter metadata. -It should not trigger a warning when referenced by a command. diff --git a/tests/fixtures/commands-workspace-aux-files/package.json b/tests/fixtures/commands-workspace-aux-files/package.json deleted file mode 100644 index 28126ac..0000000 --- a/tests/fixtures/commands-workspace-aux-files/package.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "workspace-root", - "private": true, - "workspaces": [ - "package-a" - ] -} diff --git a/tests/fixtures/commands-workspace-preset/aicm.json b/tests/fixtures/commands-workspace-preset/aicm.json deleted file mode 100644 index f46310b..0000000 --- a/tests/fixtures/commands-workspace-preset/aicm.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "workspaces": true, - "targets": ["cursor"] -} diff --git a/tests/fixtures/commands-workspace-preset/package-a/aicm.json b/tests/fixtures/commands-workspace-preset/package-a/aicm.json deleted file mode 100644 index 6d225e0..0000000 --- a/tests/fixtures/commands-workspace-preset/package-a/aicm.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "presets": ["../shared-preset"], - "targets": ["cursor"] -} diff --git a/tests/fixtures/commands-workspace-preset/package-b/aicm.json b/tests/fixtures/commands-workspace-preset/package-b/aicm.json deleted file mode 100644 index 6d225e0..0000000 --- a/tests/fixtures/commands-workspace-preset/package-b/aicm.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "presets": ["../shared-preset"], - "targets": ["cursor"] -} diff --git a/tests/fixtures/commands-workspace-preset/package.json b/tests/fixtures/commands-workspace-preset/package.json deleted file mode 100644 index d8ac17d..0000000 --- a/tests/fixtures/commands-workspace-preset/package.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "commands-workspace-preset", - "private": true, - "workspaces": [ - "package-a", - "package-b" - ] -} diff --git a/tests/fixtures/commands-workspace-preset/shared-preset/aicm.json b/tests/fixtures/commands-workspace-preset/shared-preset/aicm.json deleted file mode 100644 index 647b34e..0000000 --- a/tests/fixtures/commands-workspace-preset/shared-preset/aicm.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "rootDir": "./", - "skipInstall": true -} diff --git a/tests/fixtures/commands-workspace-preset/shared-preset/commands/test/run-tests.md b/tests/fixtures/commands-workspace-preset/shared-preset/commands/test/run-tests.md deleted file mode 100644 index 8ea68f3..0000000 --- a/tests/fixtures/commands-workspace-preset/shared-preset/commands/test/run-tests.md +++ /dev/null @@ -1,3 +0,0 @@ -# Run Workspace Tests - -Run shared workspace tests. diff --git a/tests/fixtures/commands-workspace/aicm.json b/tests/fixtures/commands-workspace/aicm.json deleted file mode 100644 index f46310b..0000000 --- a/tests/fixtures/commands-workspace/aicm.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "workspaces": true, - "targets": ["cursor"] -} diff --git a/tests/fixtures/commands-workspace/package-a/aicm.json b/tests/fixtures/commands-workspace/package-a/aicm.json deleted file mode 100644 index de289e3..0000000 --- a/tests/fixtures/commands-workspace/package-a/aicm.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "rootDir": "./" -} diff --git a/tests/fixtures/commands-workspace/package-a/commands/test-a.md b/tests/fixtures/commands-workspace/package-a/commands/test-a.md deleted file mode 100644 index be74c36..0000000 --- a/tests/fixtures/commands-workspace/package-a/commands/test-a.md +++ /dev/null @@ -1,3 +0,0 @@ -# Package A Command - -A workspace command for package A. diff --git a/tests/fixtures/commands-workspace/package-b/aicm.json b/tests/fixtures/commands-workspace/package-b/aicm.json deleted file mode 100644 index de289e3..0000000 --- a/tests/fixtures/commands-workspace/package-b/aicm.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "rootDir": "./" -} diff --git a/tests/fixtures/commands-workspace/package-b/commands/test-b.md b/tests/fixtures/commands-workspace/package-b/commands/test-b.md deleted file mode 100644 index 414e7e2..0000000 --- a/tests/fixtures/commands-workspace/package-b/commands/test-b.md +++ /dev/null @@ -1,3 +0,0 @@ -# Package B Command - -A workspace command for package B. diff --git a/tests/fixtures/commands-workspace/package.json b/tests/fixtures/commands-workspace/package.json deleted file mode 100644 index 81334e7..0000000 --- a/tests/fixtures/commands-workspace/package.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "commands-workspace", - "private": true, - "workspaces": [ - "package-a", - "package-b" - ] -} diff --git a/tests/fixtures/cursor-cleanup/.cursor/rules/aicm/stale-rule.mdc b/tests/fixtures/cursor-cleanup/.cursor/rules/aicm/stale-rule.mdc deleted file mode 100644 index 6e1c8a4..0000000 --- a/tests/fixtures/cursor-cleanup/.cursor/rules/aicm/stale-rule.mdc +++ /dev/null @@ -1,12 +0,0 @@ ---- -description: -globs: -alwaysApply: false ---- -# Stale Rule - -This is a stale rule that should be removed during installation. - -## Content - -This rule should be cleaned up when fresh rules are installed. diff --git a/tests/fixtures/cursor-cleanup/aicm.json b/tests/fixtures/cursor-cleanup/aicm.json deleted file mode 100644 index 3420015..0000000 --- a/tests/fixtures/cursor-cleanup/aicm.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "rootDir": "./", - "targets": ["cursor"], - "mcpServers": { - "cleanup-mcp": { - "command": "./scripts/cleanup-mcp.sh", - "args": ["--cleanup"], - "env": { "CLEANUP_TOKEN": "cleanup123" } - } - } -} diff --git a/tests/fixtures/cursor-cleanup/rules/fresh-rule.mdc b/tests/fixtures/cursor-cleanup/rules/fresh-rule.mdc deleted file mode 100644 index c8788fe..0000000 --- a/tests/fixtures/cursor-cleanup/rules/fresh-rule.mdc +++ /dev/null @@ -1,12 +0,0 @@ ---- -description: -globs: -alwaysApply: false ---- -# Fresh Rule - -This is a fresh rule that should be installed. - -## Content - -This rule should replace any stale rules in the .cursor directory. diff --git a/tests/fixtures/empty-presets-no-rules/.gitkeep b/tests/fixtures/empty-presets-no-rules/.gitkeep deleted file mode 100644 index 0519ecb..0000000 --- a/tests/fixtures/empty-presets-no-rules/.gitkeep +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/tests/fixtures/empty-presets-no-rules/aicm.json b/tests/fixtures/empty-presets-no-rules/aicm.json deleted file mode 100644 index 7eea0e3..0000000 --- a/tests/fixtures/empty-presets-no-rules/aicm.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "targets": ["cursor"], - "presets": [] -} diff --git a/tests/fixtures/empty-rules/instructions/.gitkeep b/tests/fixtures/empty-rules/instructions/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/tests/fixtures/hooks-collision/aicm.json b/tests/fixtures/hooks-collision/aicm.json index 3933c35..f46310b 100644 --- a/tests/fixtures/hooks-collision/aicm.json +++ b/tests/fixtures/hooks-collision/aicm.json @@ -1,3 +1,4 @@ { - "workspaces": true + "workspaces": true, + "targets": ["cursor"] } diff --git a/tests/fixtures/hooks-invalid-file/AGENTS.src.md b/tests/fixtures/hooks-invalid-file/AGENTS.src.md new file mode 100644 index 0000000..5ebc11b --- /dev/null +++ b/tests/fixtures/hooks-invalid-file/AGENTS.src.md @@ -0,0 +1,8 @@ +--- +description: Test instruction +inline: true +--- + +# Test Instruction + +Basic instruction for hooks-invalid-file test. diff --git a/tests/fixtures/hooks-workspace-content-collision/aicm.json b/tests/fixtures/hooks-workspace-content-collision/aicm.json index 3933c35..f46310b 100644 --- a/tests/fixtures/hooks-workspace-content-collision/aicm.json +++ b/tests/fixtures/hooks-workspace-content-collision/aicm.json @@ -1,3 +1,4 @@ { - "workspaces": true + "workspaces": true, + "targets": ["cursor"] } diff --git a/tests/fixtures/hooks-workspace/aicm.json b/tests/fixtures/hooks-workspace/aicm.json index 3933c35..f46310b 100644 --- a/tests/fixtures/hooks-workspace/aicm.json +++ b/tests/fixtures/hooks-workspace/aicm.json @@ -1,3 +1,4 @@ { - "workspaces": true + "workspaces": true, + "targets": ["cursor"] } diff --git a/tests/fixtures/assets-commands/aicm.json b/tests/fixtures/instructions-basic/aicm.json similarity index 100% rename from tests/fixtures/assets-commands/aicm.json rename to tests/fixtures/instructions-basic/aicm.json diff --git a/tests/fixtures/instructions-basic/instructions/general.md b/tests/fixtures/instructions-basic/instructions/general.md new file mode 100644 index 0000000..dd85e51 --- /dev/null +++ b/tests/fixtures/instructions-basic/instructions/general.md @@ -0,0 +1,8 @@ +--- +description: General instructions +inline: true +--- + +## General Instructions + +- General guidance. diff --git a/tests/fixtures/instructions-basic/instructions/testing.md b/tests/fixtures/instructions-basic/instructions/testing.md new file mode 100644 index 0000000..a72562d --- /dev/null +++ b/tests/fixtures/instructions-basic/instructions/testing.md @@ -0,0 +1,8 @@ +--- +description: Testing instructions +inline: true +--- + +## Testing Instructions + +- Testing guidance. diff --git a/tests/fixtures/instructions-both-auto-detect/AGENTS.src.md b/tests/fixtures/instructions-both-auto-detect/AGENTS.src.md new file mode 100644 index 0000000..efdb68b --- /dev/null +++ b/tests/fixtures/instructions-both-auto-detect/AGENTS.src.md @@ -0,0 +1,3 @@ +# Single File Instructions + +These instructions come from AGENTS.src.md and should be loaded together with the instructions directory. diff --git a/tests/fixtures/assets-dir-basic/aicm.json b/tests/fixtures/instructions-both-auto-detect/aicm.json similarity index 100% rename from tests/fixtures/assets-dir-basic/aicm.json rename to tests/fixtures/instructions-both-auto-detect/aicm.json diff --git a/tests/fixtures/instructions-both-auto-detect/instructions/general.md b/tests/fixtures/instructions-both-auto-detect/instructions/general.md new file mode 100644 index 0000000..11126c6 --- /dev/null +++ b/tests/fixtures/instructions-both-auto-detect/instructions/general.md @@ -0,0 +1,8 @@ +--- +description: "General team guidance" +inline: true +--- + +# General Instructions + +These instructions come from the instructions directory. diff --git a/tests/fixtures/instructions-both-explicit/RULES.md b/tests/fixtures/instructions-both-explicit/RULES.md new file mode 100644 index 0000000..29da948 --- /dev/null +++ b/tests/fixtures/instructions-both-explicit/RULES.md @@ -0,0 +1,3 @@ +# Explicit File Instructions + +These instructions are loaded from an explicit instructionsFile. diff --git a/tests/fixtures/instructions-both-explicit/aicm.json b/tests/fixtures/instructions-both-explicit/aicm.json new file mode 100644 index 0000000..d49fe6d --- /dev/null +++ b/tests/fixtures/instructions-both-explicit/aicm.json @@ -0,0 +1,6 @@ +{ + "rootDir": "./", + "instructionsFile": "RULES.md", + "instructionsDir": "my-instructions", + "targets": ["cursor"] +} diff --git a/tests/fixtures/instructions-both-explicit/my-instructions/general.md b/tests/fixtures/instructions-both-explicit/my-instructions/general.md new file mode 100644 index 0000000..f072c4a --- /dev/null +++ b/tests/fixtures/instructions-both-explicit/my-instructions/general.md @@ -0,0 +1,8 @@ +--- +description: "Explicit directory guidance" +inline: true +--- + +# Explicit Directory Instructions + +These instructions are loaded from an explicit instructions directory. diff --git a/tests/fixtures/instructions-file-explicit/RULES.md b/tests/fixtures/instructions-file-explicit/RULES.md new file mode 100644 index 0000000..beda861 --- /dev/null +++ b/tests/fixtures/instructions-file-explicit/RULES.md @@ -0,0 +1,3 @@ +## Custom Instructions File + +- These instructions come from a custom-named file. diff --git a/tests/fixtures/instructions-file-explicit/aicm.json b/tests/fixtures/instructions-file-explicit/aicm.json new file mode 100644 index 0000000..1f23845 --- /dev/null +++ b/tests/fixtures/instructions-file-explicit/aicm.json @@ -0,0 +1,5 @@ +{ + "rootDir": "./", + "instructionsFile": "RULES.md", + "targets": ["cursor"] +} diff --git a/tests/fixtures/instructions-multitarget/AGENTS.src.md b/tests/fixtures/instructions-multitarget/AGENTS.src.md new file mode 100644 index 0000000..6b5c40d --- /dev/null +++ b/tests/fixtures/instructions-multitarget/AGENTS.src.md @@ -0,0 +1,3 @@ +## Multitarget Instructions + +- Instructions for multiple targets. diff --git a/tests/fixtures/instructions-multitarget/aicm.json b/tests/fixtures/instructions-multitarget/aicm.json new file mode 100644 index 0000000..457fbca --- /dev/null +++ b/tests/fixtures/instructions-multitarget/aicm.json @@ -0,0 +1,4 @@ +{ + "rootDir": "./", + "targets": ["cursor", "claude-code"] +} diff --git a/tests/fixtures/instructions-preset/aicm.json b/tests/fixtures/instructions-preset/aicm.json new file mode 100644 index 0000000..2523a37 --- /dev/null +++ b/tests/fixtures/instructions-preset/aicm.json @@ -0,0 +1,4 @@ +{ + "targets": ["cursor"], + "presets": ["./preset-a", "./preset-b"] +} diff --git a/tests/fixtures/assets-preset/my-preset/aicm.json b/tests/fixtures/instructions-preset/preset-a/aicm.json similarity index 100% rename from tests/fixtures/assets-preset/my-preset/aicm.json rename to tests/fixtures/instructions-preset/preset-a/aicm.json diff --git a/tests/fixtures/instructions-preset/preset-a/instructions/preset-a.md b/tests/fixtures/instructions-preset/preset-a/instructions/preset-a.md new file mode 100644 index 0000000..cea78df --- /dev/null +++ b/tests/fixtures/instructions-preset/preset-a/instructions/preset-a.md @@ -0,0 +1,8 @@ +--- +description: Preset A instructions +inline: true +--- + +## Preset A Instructions + +- Instructions from preset A. diff --git a/tests/fixtures/commands-collision/preset-a/aicm.json b/tests/fixtures/instructions-preset/preset-b/aicm.json similarity index 100% rename from tests/fixtures/commands-collision/preset-a/aicm.json rename to tests/fixtures/instructions-preset/preset-b/aicm.json diff --git a/tests/fixtures/instructions-preset/preset-b/instructions/preset-b.md b/tests/fixtures/instructions-preset/preset-b/instructions/preset-b.md new file mode 100644 index 0000000..9fb7d06 --- /dev/null +++ b/tests/fixtures/instructions-preset/preset-b/instructions/preset-b.md @@ -0,0 +1,8 @@ +--- +description: Preset B instructions +inline: true +--- + +## Preset B Instructions + +- Instructions from preset B. diff --git a/tests/fixtures/assets-dir-hooks/aicm.json b/tests/fixtures/instructions-progressive/aicm.json similarity index 100% rename from tests/fixtures/assets-dir-hooks/aicm.json rename to tests/fixtures/instructions-progressive/aicm.json diff --git a/tests/fixtures/instructions-progressive/instructions/general.md b/tests/fixtures/instructions-progressive/instructions/general.md new file mode 100644 index 0000000..dd85e51 --- /dev/null +++ b/tests/fixtures/instructions-progressive/instructions/general.md @@ -0,0 +1,8 @@ +--- +description: General instructions +inline: true +--- + +## General Instructions + +- General guidance. diff --git a/tests/fixtures/instructions-progressive/instructions/testing.md b/tests/fixtures/instructions-progressive/instructions/testing.md new file mode 100644 index 0000000..26655ad --- /dev/null +++ b/tests/fixtures/instructions-progressive/instructions/testing.md @@ -0,0 +1,8 @@ +--- +description: How to run tests +inline: false +--- + +## Testing Instructions + +- Testing guidance. diff --git a/tests/fixtures/instructions-single-file/AGENTS.src.md b/tests/fixtures/instructions-single-file/AGENTS.src.md new file mode 100644 index 0000000..e18584f --- /dev/null +++ b/tests/fixtures/instructions-single-file/AGENTS.src.md @@ -0,0 +1,3 @@ +## Single File Instructions + +- Instructions from a single file. diff --git a/tests/fixtures/commands-basic/aicm.json b/tests/fixtures/instructions-single-file/aicm.json similarity index 100% rename from tests/fixtures/commands-basic/aicm.json rename to tests/fixtures/instructions-single-file/aicm.json diff --git a/tests/fixtures/list-multiple-rules/instructions/instruction1.md b/tests/fixtures/list-multiple-rules/instructions/instruction1.md new file mode 100644 index 0000000..3bb9ca1 --- /dev/null +++ b/tests/fixtures/list-multiple-rules/instructions/instruction1.md @@ -0,0 +1,8 @@ +--- +description: instruction1 +inline: true +--- + +## instruction1 + +- Instruction 1 diff --git a/tests/fixtures/list-multiple-rules/instructions/instruction2.md b/tests/fixtures/list-multiple-rules/instructions/instruction2.md new file mode 100644 index 0000000..829e99d --- /dev/null +++ b/tests/fixtures/list-multiple-rules/instructions/instruction2.md @@ -0,0 +1,8 @@ +--- +description: instruction2 +inline: true +--- + +## instruction2 + +- Instruction 2 diff --git a/tests/fixtures/list-multiple-rules/instructions/instruction3.md b/tests/fixtures/list-multiple-rules/instructions/instruction3.md new file mode 100644 index 0000000..60d13d5 --- /dev/null +++ b/tests/fixtures/list-multiple-rules/instructions/instruction3.md @@ -0,0 +1,8 @@ +--- +description: instruction3 +inline: true +--- + +## instruction3 + +- Instruction 3 diff --git a/tests/fixtures/list-no-rules/instructions/.gitkeep b/tests/fixtures/list-no-rules/instructions/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/tests/fixtures/mcp-canceled-servers/AGENTS.src.md b/tests/fixtures/mcp-canceled-servers/AGENTS.src.md new file mode 100644 index 0000000..6bd8502 --- /dev/null +++ b/tests/fixtures/mcp-canceled-servers/AGENTS.src.md @@ -0,0 +1,8 @@ +--- +description: MCP canceled server instructions +inline: true +--- + +## Test Instruction + +- Ensure canceled MCP servers are removed. diff --git a/tests/fixtures/mcp-canceled-servers/aicm.json b/tests/fixtures/mcp-canceled-servers/aicm.json index 96e055a..950b066 100644 --- a/tests/fixtures/mcp-canceled-servers/aicm.json +++ b/tests/fixtures/mcp-canceled-servers/aicm.json @@ -4,7 +4,9 @@ "mcpServers": { "active-server": { "command": "./scripts/active-server.sh", - "env": { "ACTIVE_TOKEN": "active123" } + "env": { + "ACTIVE_TOKEN": "active123" + } } } } diff --git a/tests/fixtures/mcp-preserve-existing/AGENTS.src.md b/tests/fixtures/mcp-preserve-existing/AGENTS.src.md new file mode 100644 index 0000000..6937ae3 --- /dev/null +++ b/tests/fixtures/mcp-preserve-existing/AGENTS.src.md @@ -0,0 +1,8 @@ +--- +description: MCP preservation instructions +inline: true +--- + +## Test Instruction + +- Ensure MCP servers are preserved. diff --git a/tests/fixtures/mcp-preserve-existing/aicm.json b/tests/fixtures/mcp-preserve-existing/aicm.json index 2843f1e..907135a 100644 --- a/tests/fixtures/mcp-preserve-existing/aicm.json +++ b/tests/fixtures/mcp-preserve-existing/aicm.json @@ -5,7 +5,9 @@ "aicm-managed-server": { "command": "./scripts/aicm-server.sh", "args": ["--aicm"], - "env": { "AICM_TOKEN": "aicm123" } + "env": { + "AICM_TOKEN": "aicm123" + } } } } diff --git a/tests/fixtures/mcp-stale-cleanup/AGENTS.src.md b/tests/fixtures/mcp-stale-cleanup/AGENTS.src.md new file mode 100644 index 0000000..91f3708 --- /dev/null +++ b/tests/fixtures/mcp-stale-cleanup/AGENTS.src.md @@ -0,0 +1,8 @@ +--- +description: MCP cleanup instructions +inline: true +--- + +## Test Instruction + +- Ensure stale MCP servers are removed. diff --git a/tests/fixtures/mcp-stale-cleanup/aicm.json b/tests/fixtures/mcp-stale-cleanup/aicm.json index 7db7e54..3ee26a8 100644 --- a/tests/fixtures/mcp-stale-cleanup/aicm.json +++ b/tests/fixtures/mcp-stale-cleanup/aicm.json @@ -5,12 +5,16 @@ "existing-aicm-server": { "command": "./scripts/updated-existing-server.sh", "args": ["--updated"], - "env": { "UPDATED_TOKEN": "updated456" } + "env": { + "UPDATED_TOKEN": "updated456" + } }, "new-aicm-server": { "command": "./scripts/new-aicm-server.sh", "args": ["--new"], - "env": { "NEW_TOKEN": "new123" } + "env": { + "NEW_TOKEN": "new123" + } } } } diff --git a/tests/fixtures/missing-rules/aicm.json b/tests/fixtures/missing-rules/aicm.json deleted file mode 100644 index eca1152..0000000 --- a/tests/fixtures/missing-rules/aicm.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "rootDir": "./", - "targets": ["cursor"], - "mcpServers": { - "test-mcp": { - "command": "./scripts/test-mcp.sh", - "args": ["--test"], - "env": { "TEST_TOKEN": "test123" } - } - } -} diff --git a/tests/fixtures/missing-rules/rules/.gitkeep b/tests/fixtures/missing-rules/rules/.gitkeep deleted file mode 100644 index 0519ecb..0000000 --- a/tests/fixtures/missing-rules/rules/.gitkeep +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/tests/fixtures/missing-rules/rules/broken-reference.mdc b/tests/fixtures/missing-rules/rules/broken-reference.mdc deleted file mode 100644 index 730625a..0000000 --- a/tests/fixtures/missing-rules/rules/broken-reference.mdc +++ /dev/null @@ -1,12 +0,0 @@ ---- -description: -globs: -alwaysApply: false ---- -# Broken Reference Rule - -This rule exists but references a non-existent file: does-not-exist.mdc - -## Content - -This rule should cause an error during processing. diff --git a/tests/fixtures/missing-rules/rules/existing-rule.mdc b/tests/fixtures/missing-rules/rules/existing-rule.mdc deleted file mode 100644 index 0fa3625..0000000 --- a/tests/fixtures/missing-rules/rules/existing-rule.mdc +++ /dev/null @@ -1,12 +0,0 @@ ---- -description: -globs: -alwaysApply: false ---- -# Existing Rule - -This rule exists and should be processed correctly. - -## Content - -This is a valid rule file that exists in the rules directory. diff --git a/tests/fixtures/multiple-file-types/rules/python-example.py b/tests/fixtures/multiple-file-types/rules/python-example.py deleted file mode 100644 index 820258b..0000000 --- a/tests/fixtures/multiple-file-types/rules/python-example.py +++ /dev/null @@ -1,22 +0,0 @@ -# Python Rule File -# This file demonstrates support for .py files - -""" -Python Best Practices Rule - -This rule file contains Python-specific guidelines. -""" - -def example_function(name: str) -> str: - """Example function demonstrating Python best practices.""" - return f"Hello, {name}!" - -class ExampleClass: - """Example class demonstrating Python best practices.""" - - def __init__(self, value: int): - self.value = value - - def get_value(self) -> int: - return self.value - diff --git a/tests/fixtures/multiple-file-types/rules/typescript-example.tsx b/tests/fixtures/multiple-file-types/rules/typescript-example.tsx deleted file mode 100644 index d1ba701..0000000 --- a/tests/fixtures/multiple-file-types/rules/typescript-example.tsx +++ /dev/null @@ -1,30 +0,0 @@ -// TypeScript React Rule File -// This file demonstrates support for .tsx files - -export const rule = { - name: "typescript-react-best-practices", - description: "Best practices for TypeScript React development", - rules: [ - "Always use TypeScript for type safety", - "Prefer functional components with hooks", - "Use proper typing for props and state", - ], -}; - -// Example component structure -interface ComponentProps { - title: string; - count: number; -} - -export const ExampleComponent: React.FC = ({ - title, - count, -}) => { - return ( -
-

{title}

-

Count: {count}

-
- ); -}; diff --git a/tests/fixtures/multiple-rules/.cursor/mcp.json b/tests/fixtures/multiple-rules/.cursor/mcp.json deleted file mode 100644 index d3b709e..0000000 --- a/tests/fixtures/multiple-rules/.cursor/mcp.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "mcpServers": { - "test-mcp": { - "command": "./scripts/test-mcp.sh", - "args": ["--test"], - "env": { - "TEST_TOKEN": "test123" - }, - "aicm": true - } - } -} diff --git a/tests/fixtures/multiple-rules/.cursor/rules/aicm/rule1.mdc b/tests/fixtures/multiple-rules/.cursor/rules/aicm/rule1.mdc deleted file mode 100644 index b1c7e7a..0000000 --- a/tests/fixtures/multiple-rules/.cursor/rules/aicm/rule1.mdc +++ /dev/null @@ -1,8 +0,0 @@ ---- -description: -globs: -alwaysApply: false ---- -# Rule 1 - -This is the first test rule for configuration. diff --git a/tests/fixtures/multiple-rules/.cursor/rules/aicm/rule2.mdc b/tests/fixtures/multiple-rules/.cursor/rules/aicm/rule2.mdc deleted file mode 100644 index a43fc78..0000000 --- a/tests/fixtures/multiple-rules/.cursor/rules/aicm/rule2.mdc +++ /dev/null @@ -1,8 +0,0 @@ ---- -description: -globs: -alwaysApply: false ---- -# Rule 2 - -This is the second test rule for configuration. diff --git a/tests/fixtures/multiple-rules/.cursor/rules/aicm/subdir/rule3.mdc b/tests/fixtures/multiple-rules/.cursor/rules/aicm/subdir/rule3.mdc deleted file mode 100644 index 7891003..0000000 --- a/tests/fixtures/multiple-rules/.cursor/rules/aicm/subdir/rule3.mdc +++ /dev/null @@ -1,8 +0,0 @@ ---- -description: -globs: -alwaysApply: false ---- -# Rule 3 - -This is a test rule in a subdirectory for configuration. diff --git a/tests/fixtures/multiple-rules/aicm.json b/tests/fixtures/multiple-rules/aicm.json deleted file mode 100644 index eca1152..0000000 --- a/tests/fixtures/multiple-rules/aicm.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "rootDir": "./", - "targets": ["cursor"], - "mcpServers": { - "test-mcp": { - "command": "./scripts/test-mcp.sh", - "args": ["--test"], - "env": { "TEST_TOKEN": "test123" } - } - } -} diff --git a/tests/fixtures/multiple-rules/rules/rule1.mdc b/tests/fixtures/multiple-rules/rules/rule1.mdc deleted file mode 100644 index b1c7e7a..0000000 --- a/tests/fixtures/multiple-rules/rules/rule1.mdc +++ /dev/null @@ -1,8 +0,0 @@ ---- -description: -globs: -alwaysApply: false ---- -# Rule 1 - -This is the first test rule for configuration. diff --git a/tests/fixtures/multiple-rules/rules/rule2.mdc b/tests/fixtures/multiple-rules/rules/rule2.mdc deleted file mode 100644 index a43fc78..0000000 --- a/tests/fixtures/multiple-rules/rules/rule2.mdc +++ /dev/null @@ -1,8 +0,0 @@ ---- -description: -globs: -alwaysApply: false ---- -# Rule 2 - -This is the second test rule for configuration. diff --git a/tests/fixtures/multiple-rules/rules/subdir/rule3.mdc b/tests/fixtures/multiple-rules/rules/subdir/rule3.mdc deleted file mode 100644 index 7891003..0000000 --- a/tests/fixtures/multiple-rules/rules/subdir/rule3.mdc +++ /dev/null @@ -1,8 +0,0 @@ ---- -description: -globs: -alwaysApply: false ---- -# Rule 3 - -This is a test rule in a subdirectory for configuration. diff --git a/tests/fixtures/multiple-targets/aicm.json b/tests/fixtures/multiple-targets/aicm.json deleted file mode 100644 index 326a81f..0000000 --- a/tests/fixtures/multiple-targets/aicm.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "rootDir": "./", - "targets": ["cursor", "windsurf"], - "mcpServers": { - "multi-target-mcp": { - "command": "./scripts/multi-target-mcp.sh", - "args": ["--multi"], - "env": { "MULTI_TOKEN": "multi123" } - } - } -} diff --git a/tests/fixtures/multiple-targets/rules/multi-target-rule.mdc b/tests/fixtures/multiple-targets/rules/multi-target-rule.mdc deleted file mode 100644 index 91d1145..0000000 --- a/tests/fixtures/multiple-targets/rules/multi-target-rule.mdc +++ /dev/null @@ -1,12 +0,0 @@ ---- -description: -globs: -alwaysApply: false ---- -# Multi-Target Rule - -This rule should be installed to both Cursor and Windsurf. - -## Content - -This rule demonstrates installation to multiple IDE targets. diff --git a/tests/fixtures/multiple-targets/rules/test-rule.mdc b/tests/fixtures/multiple-targets/rules/test-rule.mdc deleted file mode 100644 index f8f2c37..0000000 --- a/tests/fixtures/multiple-targets/rules/test-rule.mdc +++ /dev/null @@ -1,8 +0,0 @@ ---- -description: -globs: -alwaysApply: false ---- -# Test Rule for Multiple Targets - -This rule should be installed for both Cursor and Windsurf. diff --git a/tests/fixtures/no-mcp/AGENTS.src.md b/tests/fixtures/no-mcp/AGENTS.src.md new file mode 100644 index 0000000..d088192 --- /dev/null +++ b/tests/fixtures/no-mcp/AGENTS.src.md @@ -0,0 +1,8 @@ +--- +description: No MCP instructions +inline: true +--- + +## Test Instruction + +- No MCP servers should be configured. diff --git a/tests/fixtures/no-rules-no-presets/.gitkeep b/tests/fixtures/no-rules-no-presets/.gitkeep deleted file mode 100644 index 0519ecb..0000000 --- a/tests/fixtures/no-rules-no-presets/.gitkeep +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/tests/fixtures/no-rules-no-presets/aicm.json b/tests/fixtures/no-rules-no-presets/aicm.json deleted file mode 100644 index 48c4b83..0000000 --- a/tests/fixtures/no-rules-no-presets/aicm.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "targets": ["cursor"] -} diff --git a/tests/fixtures/preset-commands-assets/aicm.json b/tests/fixtures/preset-commands-assets/aicm.json deleted file mode 100644 index 3abd408..0000000 --- a/tests/fixtures/preset-commands-assets/aicm.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "presets": ["./my-preset"], - "targets": ["cursor"] -} diff --git a/tests/fixtures/preset-commands-assets/my-preset/aicm.json b/tests/fixtures/preset-commands-assets/my-preset/aicm.json deleted file mode 100644 index de289e3..0000000 --- a/tests/fixtures/preset-commands-assets/my-preset/aicm.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "rootDir": "./" -} diff --git a/tests/fixtures/preset-commands-assets/my-preset/assets/schema.json b/tests/fixtures/preset-commands-assets/my-preset/assets/schema.json deleted file mode 100644 index c300b9f..0000000 --- a/tests/fixtures/preset-commands-assets/my-preset/assets/schema.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "type": "object", - "properties": { - "name": { "type": "string" }, - "version": { "type": "string" } - } -} diff --git a/tests/fixtures/preset-commands-assets/my-preset/commands/setup.md b/tests/fixtures/preset-commands-assets/my-preset/commands/setup.md deleted file mode 100644 index c90249c..0000000 --- a/tests/fixtures/preset-commands-assets/my-preset/commands/setup.md +++ /dev/null @@ -1,11 +0,0 @@ -# Setup Command - -This command uses assets from the preset. - -## Schema - -The schema file is located at [schema.json](../assets/schema.json). - -You can also reference it inline: `../assets/schema.json` - -Or in a sentence: Check the file at ../assets/schema.json for more details. diff --git a/tests/fixtures/preset-commands-assets/my-preset/rules/config.json b/tests/fixtures/preset-commands-assets/my-preset/rules/config.json deleted file mode 100644 index 255472d..0000000 --- a/tests/fixtures/preset-commands-assets/my-preset/rules/config.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "name": "preset-config", - "version": "1.0.0", - "description": "A configuration file from the preset" -} diff --git a/tests/fixtures/preset-commands-assets/my-preset/rules/preset-rule.mdc b/tests/fixtures/preset-commands-assets/my-preset/rules/preset-rule.mdc deleted file mode 100644 index 6fb2c66..0000000 --- a/tests/fixtures/preset-commands-assets/my-preset/rules/preset-rule.mdc +++ /dev/null @@ -1,3 +0,0 @@ -# Preset Rule - -This is a rule from the preset that references [schema.json](../assets/schema.json). diff --git a/tests/fixtures/presets-circular/preset-a/instructions/a.md b/tests/fixtures/presets-circular/preset-a/instructions/a.md new file mode 100644 index 0000000..270f3f0 --- /dev/null +++ b/tests/fixtures/presets-circular/preset-a/instructions/a.md @@ -0,0 +1,8 @@ +--- +description: Preset A instruction +inline: true +--- + +## Preset A Instructions + +- Instructions from preset A. diff --git a/tests/fixtures/presets-circular/preset-b/instructions/b.md b/tests/fixtures/presets-circular/preset-b/instructions/b.md new file mode 100644 index 0000000..795b989 --- /dev/null +++ b/tests/fixtures/presets-circular/preset-b/instructions/b.md @@ -0,0 +1,8 @@ +--- +description: Preset B instruction +inline: true +--- + +## Preset B Instructions + +- Instructions from preset B. diff --git a/tests/fixtures/presets-from-file/aicm.json b/tests/fixtures/presets-from-file/aicm.json index 3b736b1..6342712 100644 --- a/tests/fixtures/presets-from-file/aicm.json +++ b/tests/fixtures/presets-from-file/aicm.json @@ -1,5 +1,4 @@ { - "rootDir": "./", "targets": ["cursor"], "presets": ["./company-preset-full.json"] } diff --git a/tests/fixtures/presets-from-file/instructions/react.md b/tests/fixtures/presets-from-file/instructions/react.md new file mode 100644 index 0000000..51e4b2d --- /dev/null +++ b/tests/fixtures/presets-from-file/instructions/react.md @@ -0,0 +1,8 @@ +--- +description: React instructions +inline: true +--- + +## React Best Practices + +- Follow React best practices. diff --git a/tests/fixtures/presets-from-file/instructions/typescript.md b/tests/fixtures/presets-from-file/instructions/typescript.md new file mode 100644 index 0000000..836b752 --- /dev/null +++ b/tests/fixtures/presets-from-file/instructions/typescript.md @@ -0,0 +1,8 @@ +--- +description: TypeScript instructions +inline: true +--- + +## TypeScript Best Practices + +- Follow TypeScript best practices. diff --git a/tests/fixtures/presets-inherits-only/content-preset/instructions/inherited.md b/tests/fixtures/presets-inherits-only/content-preset/instructions/inherited.md new file mode 100644 index 0000000..c2d5258 --- /dev/null +++ b/tests/fixtures/presets-inherits-only/content-preset/instructions/inherited.md @@ -0,0 +1,8 @@ +--- +description: Inherited instruction +inline: true +--- + +## Inherited Instruction + +- Instructions from content preset. diff --git a/tests/fixtures/presets-merged/AGENTS.src.md b/tests/fixtures/presets-merged/AGENTS.src.md new file mode 100644 index 0000000..969721e --- /dev/null +++ b/tests/fixtures/presets-merged/AGENTS.src.md @@ -0,0 +1,8 @@ +--- +description: Local instructions +inline: true +--- + +## Local Instruction + +- Local instructions for merged preset. diff --git a/tests/fixtures/presets-merged/company-preset.json b/tests/fixtures/presets-merged/company-preset.json index 19395a9..ccf0afa 100644 --- a/tests/fixtures/presets-merged/company-preset.json +++ b/tests/fixtures/presets-merged/company-preset.json @@ -1,5 +1,6 @@ { "rootDir": "./", + "instructionsDir": "instructions", "mcpServers": { "preset-mcp": { "command": "./scripts/preset-mcp.sh", diff --git a/tests/fixtures/presets-merged/instructions/preset.md b/tests/fixtures/presets-merged/instructions/preset.md new file mode 100644 index 0000000..a3e3854 --- /dev/null +++ b/tests/fixtures/presets-merged/instructions/preset.md @@ -0,0 +1,8 @@ +--- +description: Preset instruction +inline: true +--- + +## Preset Instruction + +- Instructions from preset. diff --git a/tests/fixtures/presets-missing-rules/aicm.json b/tests/fixtures/presets-missing-rules/aicm.json deleted file mode 100644 index 24382a5..0000000 --- a/tests/fixtures/presets-missing-rules/aicm.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "rootDir": "./", - "targets": ["cursor"], - "presets": ["./broken-preset.json"] -} diff --git a/tests/fixtures/presets-missing-rules/broken-preset.json b/tests/fixtures/presets-missing-rules/broken-preset.json deleted file mode 100644 index de289e3..0000000 --- a/tests/fixtures/presets-missing-rules/broken-preset.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "rootDir": "./" -} diff --git a/tests/fixtures/presets-missing-rules/rules/.gitkeep b/tests/fixtures/presets-missing-rules/rules/.gitkeep deleted file mode 100644 index 0519ecb..0000000 --- a/tests/fixtures/presets-missing-rules/rules/.gitkeep +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/tests/fixtures/presets-npm/node_modules/@company/ai-rules/instructions/npm.md b/tests/fixtures/presets-npm/node_modules/@company/ai-rules/instructions/npm.md new file mode 100644 index 0000000..c498714 --- /dev/null +++ b/tests/fixtures/presets-npm/node_modules/@company/ai-rules/instructions/npm.md @@ -0,0 +1,8 @@ +--- +description: NPM package instructions +inline: true +--- + +## NPM Package Instruction + +- Instructions from npm package. diff --git a/tests/fixtures/presets-only-with-app-commands/preset-rules/instructions/typescript.md b/tests/fixtures/presets-only-with-app-commands/preset-rules/instructions/typescript.md new file mode 100644 index 0000000..7a14cb0 --- /dev/null +++ b/tests/fixtures/presets-only-with-app-commands/preset-rules/instructions/typescript.md @@ -0,0 +1,8 @@ +--- +description: TypeScript instructions +inline: true +--- + +## TypeScript Best Practices (Preset) + +- Instructions from preset only. diff --git a/tests/fixtures/presets-only-with-app-commands/preset.json b/tests/fixtures/presets-only-with-app-commands/preset.json index b0b7ff3..b3cba0a 100644 --- a/tests/fixtures/presets-only-with-app-commands/preset.json +++ b/tests/fixtures/presets-only-with-app-commands/preset.json @@ -1,4 +1,3 @@ { - "rootDir": "./preset-rules", - "targets": ["cursor"] + "rootDir": "./preset-rules" } diff --git a/tests/fixtures/presets-only/.cursor/rules/aicm/preset.json/typescript.mdc b/tests/fixtures/presets-only/.cursor/rules/aicm/preset.json/typescript.mdc deleted file mode 100644 index b6e5934..0000000 --- a/tests/fixtures/presets-only/.cursor/rules/aicm/preset.json/typescript.mdc +++ /dev/null @@ -1,8 +0,0 @@ ---- -description: -globs: -alwaysApply: false ---- -# TypeScript Best Practices (Preset Only) - -Use TypeScript for type safety and better development experience. diff --git a/tests/fixtures/presets-only/instructions/typescript.md b/tests/fixtures/presets-only/instructions/typescript.md new file mode 100644 index 0000000..f9b9b6b --- /dev/null +++ b/tests/fixtures/presets-only/instructions/typescript.md @@ -0,0 +1,8 @@ +--- +description: TypeScript instructions +inline: true +--- + +## TypeScript Best Practices (Preset Only) + +- Instructions from preset only. diff --git a/tests/fixtures/presets-recursive/preset-a/instructions/a.md b/tests/fixtures/presets-recursive/preset-a/instructions/a.md new file mode 100644 index 0000000..345acad --- /dev/null +++ b/tests/fixtures/presets-recursive/preset-a/instructions/a.md @@ -0,0 +1,8 @@ +--- +description: Instruction A +inline: true +--- + +## Instruction A + +- Instructions from preset A. diff --git a/tests/fixtures/presets-recursive/preset-b/instructions/b.md b/tests/fixtures/presets-recursive/preset-b/instructions/b.md new file mode 100644 index 0000000..84452e0 --- /dev/null +++ b/tests/fixtures/presets-recursive/preset-b/instructions/b.md @@ -0,0 +1,8 @@ +--- +description: Instruction B +inline: true +--- + +## Instruction B + +- Instructions from preset B. diff --git a/tests/fixtures/presets-sibling/sibling-preset/instructions/sibling.md b/tests/fixtures/presets-sibling/sibling-preset/instructions/sibling.md new file mode 100644 index 0000000..c1f1a18 --- /dev/null +++ b/tests/fixtures/presets-sibling/sibling-preset/instructions/sibling.md @@ -0,0 +1,8 @@ +--- +description: Sibling preset instructions +inline: true +--- + +## Sibling Preset Instruction + +- Instructions from sibling preset. diff --git a/tests/fixtures/rule-subdirs/aicm.json b/tests/fixtures/rule-subdirs/aicm.json deleted file mode 100644 index 4b91fc5..0000000 --- a/tests/fixtures/rule-subdirs/aicm.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "rootDir": "./", - "targets": ["cursor"], - "mcpServers": { - "subdir-mcp": { - "command": "./scripts/subdir-mcp.sh", - "args": ["--subdir"], - "env": { "SUBDIR_TOKEN": "subdir123" } - } - } -} diff --git a/tests/fixtures/rule-subdirs/rules/subdir/nested-rule.mdc b/tests/fixtures/rule-subdirs/rules/subdir/nested-rule.mdc deleted file mode 100644 index dd9d1c3..0000000 --- a/tests/fixtures/rule-subdirs/rules/subdir/nested-rule.mdc +++ /dev/null @@ -1,12 +0,0 @@ ---- -description: -globs: -alwaysApply: false ---- -# Nested Rule - -This is a nested rule in a subdirectory. - -## Nested Rule Content - -This rule should be installed while preserving the directory structure. diff --git a/tests/fixtures/single-rule-clean/AGENTS.src.md b/tests/fixtures/single-rule-clean/AGENTS.src.md new file mode 100644 index 0000000..f3e4975 --- /dev/null +++ b/tests/fixtures/single-rule-clean/AGENTS.src.md @@ -0,0 +1,8 @@ +--- +description: Basic test instructions +inline: true +--- + +## Test Instruction + +- Follow the test instruction. diff --git a/tests/fixtures/single-rule-clean/aicm.json b/tests/fixtures/single-rule-clean/aicm.json index eca1152..13591b9 100644 --- a/tests/fixtures/single-rule-clean/aicm.json +++ b/tests/fixtures/single-rule-clean/aicm.json @@ -5,7 +5,9 @@ "test-mcp": { "command": "./scripts/test-mcp.sh", "args": ["--test"], - "env": { "TEST_TOKEN": "test123" } + "env": { + "TEST_TOKEN": "test123" + } } } } diff --git a/tests/fixtures/single-rule/.cursor/rules/aicm/test-rule.mdc b/tests/fixtures/single-rule/.cursor/rules/aicm/test-rule.mdc deleted file mode 100644 index f5f9925..0000000 --- a/tests/fixtures/single-rule/.cursor/rules/aicm/test-rule.mdc +++ /dev/null @@ -1,6 +0,0 @@ ---- -description: -globs: -alwaysApply: false ---- -# Test Rule diff --git a/tests/fixtures/single-rule/AGENTS.src.md b/tests/fixtures/single-rule/AGENTS.src.md new file mode 100644 index 0000000..f3e4975 --- /dev/null +++ b/tests/fixtures/single-rule/AGENTS.src.md @@ -0,0 +1,8 @@ +--- +description: Basic test instructions +inline: true +--- + +## Test Instruction + +- Follow the test instruction. diff --git a/tests/fixtures/single-rule/aicm.json b/tests/fixtures/single-rule/aicm.json index eca1152..13591b9 100644 --- a/tests/fixtures/single-rule/aicm.json +++ b/tests/fixtures/single-rule/aicm.json @@ -5,7 +5,9 @@ "test-mcp": { "command": "./scripts/test-mcp.sh", "args": ["--test"], - "env": { "TEST_TOKEN": "test123" } + "env": { + "TEST_TOKEN": "test123" + } } } } diff --git a/tests/fixtures/skills-legacy-namespaced/.agents/skills/aicm/legacy-skill/.aicm.json b/tests/fixtures/skills-legacy-namespaced/.agents/skills/aicm/legacy-skill/.aicm.json new file mode 100644 index 0000000..873be36 --- /dev/null +++ b/tests/fixtures/skills-legacy-namespaced/.agents/skills/aicm/legacy-skill/.aicm.json @@ -0,0 +1,4 @@ +{ + "source": "preset", + "presetName": "./legacy" +} diff --git a/tests/fixtures/skills-legacy-namespaced/.agents/skills/aicm/legacy-skill/SKILL.md b/tests/fixtures/skills-legacy-namespaced/.agents/skills/aicm/legacy-skill/SKILL.md new file mode 100644 index 0000000..7a04ee1 --- /dev/null +++ b/tests/fixtures/skills-legacy-namespaced/.agents/skills/aicm/legacy-skill/SKILL.md @@ -0,0 +1,6 @@ +--- +name: legacy-skill +description: Legacy namespaced generated skill. +--- + +# Legacy Skill diff --git a/tests/fixtures/skills-legacy-namespaced/.claude/skills/aicm/legacy-skill/.aicm.json b/tests/fixtures/skills-legacy-namespaced/.claude/skills/aicm/legacy-skill/.aicm.json new file mode 100644 index 0000000..873be36 --- /dev/null +++ b/tests/fixtures/skills-legacy-namespaced/.claude/skills/aicm/legacy-skill/.aicm.json @@ -0,0 +1,4 @@ +{ + "source": "preset", + "presetName": "./legacy" +} diff --git a/tests/fixtures/skills-legacy-namespaced/.claude/skills/aicm/legacy-skill/SKILL.md b/tests/fixtures/skills-legacy-namespaced/.claude/skills/aicm/legacy-skill/SKILL.md new file mode 100644 index 0000000..7a04ee1 --- /dev/null +++ b/tests/fixtures/skills-legacy-namespaced/.claude/skills/aicm/legacy-skill/SKILL.md @@ -0,0 +1,6 @@ +--- +name: legacy-skill +description: Legacy namespaced generated skill. +--- + +# Legacy Skill diff --git a/tests/fixtures/skills-legacy-namespaced/aicm.json b/tests/fixtures/skills-legacy-namespaced/aicm.json new file mode 100644 index 0000000..457fbca --- /dev/null +++ b/tests/fixtures/skills-legacy-namespaced/aicm.json @@ -0,0 +1,4 @@ +{ + "rootDir": "./", + "targets": ["cursor", "claude-code"] +} diff --git a/tests/fixtures/skills-legacy-namespaced/skills/current-skill/SKILL.md b/tests/fixtures/skills-legacy-namespaced/skills/current-skill/SKILL.md new file mode 100644 index 0000000..66c203c --- /dev/null +++ b/tests/fixtures/skills-legacy-namespaced/skills/current-skill/SKILL.md @@ -0,0 +1,6 @@ +--- +name: current-skill +description: Current flat skill layout. +--- + +# Current Skill diff --git a/tests/fixtures/skills-multitarget/aicm.json b/tests/fixtures/skills-multitarget/aicm.json index 772a488..457fbca 100644 --- a/tests/fixtures/skills-multitarget/aicm.json +++ b/tests/fixtures/skills-multitarget/aicm.json @@ -1,4 +1,4 @@ { "rootDir": "./", - "targets": ["cursor", "claude", "codex"] + "targets": ["cursor", "claude-code"] } diff --git a/tests/fixtures/skills-workspace/aicm.json b/tests/fixtures/skills-workspace/aicm.json index 3933c35..f46310b 100644 --- a/tests/fixtures/skills-workspace/aicm.json +++ b/tests/fixtures/skills-workspace/aicm.json @@ -1,3 +1,4 @@ { - "workspaces": true + "workspaces": true, + "targets": ["cursor"] } diff --git a/tests/fixtures/skip-install-regular/AGENTS.src.md b/tests/fixtures/skip-install-regular/AGENTS.src.md new file mode 100644 index 0000000..e918db7 --- /dev/null +++ b/tests/fixtures/skip-install-regular/AGENTS.src.md @@ -0,0 +1,8 @@ +--- +description: Test instruction for skip install +inline: true +--- + +# Test Instruction + +This should not be installed because skipInstall is true. diff --git a/tests/fixtures/target-merge-all/agents/test-agent.md b/tests/fixtures/target-merge-all/agents/test-agent.md new file mode 100644 index 0000000..723c980 --- /dev/null +++ b/tests/fixtures/target-merge-all/agents/test-agent.md @@ -0,0 +1,9 @@ +--- +name: test-agent +description: A test agent +model: inherit +--- + +# Test Agent + +This is a test agent. diff --git a/tests/fixtures/target-merge-all/aicm.json b/tests/fixtures/target-merge-all/aicm.json new file mode 100644 index 0000000..553dea1 --- /dev/null +++ b/tests/fixtures/target-merge-all/aicm.json @@ -0,0 +1,13 @@ +{ + "rootDir": "./", + "targets": ["cursor", "claude-code", "opencode", "codex"], + "mcpServers": { + "test-mcp": { + "command": "npx", + "args": ["-y", "test-mcp-server"], + "env": { + "TEST_KEY": "test-value" + } + } + } +} diff --git a/tests/fixtures/assets-dir-hooks/hooks/hooks.json b/tests/fixtures/target-merge-all/hooks.json similarity index 51% rename from tests/fixtures/assets-dir-hooks/hooks/hooks.json rename to tests/fixtures/target-merge-all/hooks.json index a30a59d..a2ce091 100644 --- a/tests/fixtures/assets-dir-hooks/hooks/hooks.json +++ b/tests/fixtures/target-merge-all/hooks.json @@ -3,10 +3,7 @@ "hooks": { "beforeShellExecution": [ { - "command": "./validate.sh" - }, - { - "command": "./helper.js" + "command": "./hooks/lint.sh" } ] } diff --git a/tests/fixtures/target-merge-all/hooks/lint.sh b/tests/fixtures/target-merge-all/hooks/lint.sh new file mode 100644 index 0000000..dca7cac --- /dev/null +++ b/tests/fixtures/target-merge-all/hooks/lint.sh @@ -0,0 +1,2 @@ +#!/bin/bash +echo lint diff --git a/tests/fixtures/target-merge-all/instructions/general.md b/tests/fixtures/target-merge-all/instructions/general.md new file mode 100644 index 0000000..d64ec76 --- /dev/null +++ b/tests/fixtures/target-merge-all/instructions/general.md @@ -0,0 +1,8 @@ +--- +description: General coding guidelines +inline: true +--- + +## General Instructions + +Follow best practices. diff --git a/tests/fixtures/target-merge-all/skills/test-skill/SKILL.md b/tests/fixtures/target-merge-all/skills/test-skill/SKILL.md new file mode 100644 index 0000000..3c6f15e --- /dev/null +++ b/tests/fixtures/target-merge-all/skills/test-skill/SKILL.md @@ -0,0 +1,8 @@ +--- +name: test-skill +description: A test skill +--- + +# Test Skill + +This is a test skill. diff --git a/tests/fixtures/target-merge-cursor-claude/agents/test-agent.md b/tests/fixtures/target-merge-cursor-claude/agents/test-agent.md new file mode 100644 index 0000000..723c980 --- /dev/null +++ b/tests/fixtures/target-merge-cursor-claude/agents/test-agent.md @@ -0,0 +1,9 @@ +--- +name: test-agent +description: A test agent +model: inherit +--- + +# Test Agent + +This is a test agent. diff --git a/tests/fixtures/target-merge-cursor-claude/aicm.json b/tests/fixtures/target-merge-cursor-claude/aicm.json new file mode 100644 index 0000000..8e2bff3 --- /dev/null +++ b/tests/fixtures/target-merge-cursor-claude/aicm.json @@ -0,0 +1,13 @@ +{ + "rootDir": "./", + "targets": ["cursor", "claude-code"], + "mcpServers": { + "test-mcp": { + "command": "npx", + "args": ["-y", "test-mcp-server"], + "env": { + "TEST_KEY": "test-value" + } + } + } +} diff --git a/tests/fixtures/target-merge-cursor-claude/hooks.json b/tests/fixtures/target-merge-cursor-claude/hooks.json new file mode 100644 index 0000000..a2ce091 --- /dev/null +++ b/tests/fixtures/target-merge-cursor-claude/hooks.json @@ -0,0 +1,10 @@ +{ + "version": 1, + "hooks": { + "beforeShellExecution": [ + { + "command": "./hooks/lint.sh" + } + ] + } +} diff --git a/tests/fixtures/target-merge-cursor-claude/hooks/lint.sh b/tests/fixtures/target-merge-cursor-claude/hooks/lint.sh new file mode 100644 index 0000000..dca7cac --- /dev/null +++ b/tests/fixtures/target-merge-cursor-claude/hooks/lint.sh @@ -0,0 +1,2 @@ +#!/bin/bash +echo lint diff --git a/tests/fixtures/target-merge-cursor-claude/instructions/general.md b/tests/fixtures/target-merge-cursor-claude/instructions/general.md new file mode 100644 index 0000000..d64ec76 --- /dev/null +++ b/tests/fixtures/target-merge-cursor-claude/instructions/general.md @@ -0,0 +1,8 @@ +--- +description: General coding guidelines +inline: true +--- + +## General Instructions + +Follow best practices. diff --git a/tests/fixtures/target-merge-cursor-claude/skills/test-skill/SKILL.md b/tests/fixtures/target-merge-cursor-claude/skills/test-skill/SKILL.md new file mode 100644 index 0000000..3c6f15e --- /dev/null +++ b/tests/fixtures/target-merge-cursor-claude/skills/test-skill/SKILL.md @@ -0,0 +1,8 @@ +--- +name: test-skill +description: A test skill +--- + +# Test Skill + +This is a test skill. diff --git a/tests/fixtures/target-merge-cursor-codex/agents/test-agent.md b/tests/fixtures/target-merge-cursor-codex/agents/test-agent.md new file mode 100644 index 0000000..a7abd21 --- /dev/null +++ b/tests/fixtures/target-merge-cursor-codex/agents/test-agent.md @@ -0,0 +1,7 @@ +--- +name: test-agent +description: A test agent +model: inherit +--- + +# Test Agent diff --git a/tests/fixtures/target-merge-cursor-codex/aicm.json b/tests/fixtures/target-merge-cursor-codex/aicm.json new file mode 100644 index 0000000..7c9b58b --- /dev/null +++ b/tests/fixtures/target-merge-cursor-codex/aicm.json @@ -0,0 +1,13 @@ +{ + "rootDir": "./", + "targets": ["cursor", "codex"], + "mcpServers": { + "test-mcp": { + "command": "npx", + "args": ["-y", "test-mcp-server"], + "env": { + "TEST_KEY": "test-value" + } + } + } +} diff --git a/tests/fixtures/target-merge-cursor-codex/hooks.json b/tests/fixtures/target-merge-cursor-codex/hooks.json new file mode 100644 index 0000000..a2ce091 --- /dev/null +++ b/tests/fixtures/target-merge-cursor-codex/hooks.json @@ -0,0 +1,10 @@ +{ + "version": 1, + "hooks": { + "beforeShellExecution": [ + { + "command": "./hooks/lint.sh" + } + ] + } +} diff --git a/tests/fixtures/target-merge-cursor-codex/hooks/lint.sh b/tests/fixtures/target-merge-cursor-codex/hooks/lint.sh new file mode 100644 index 0000000..dca7cac --- /dev/null +++ b/tests/fixtures/target-merge-cursor-codex/hooks/lint.sh @@ -0,0 +1,2 @@ +#!/bin/bash +echo lint diff --git a/tests/fixtures/target-merge-cursor-codex/instructions/general.md b/tests/fixtures/target-merge-cursor-codex/instructions/general.md new file mode 100644 index 0000000..d64ec76 --- /dev/null +++ b/tests/fixtures/target-merge-cursor-codex/instructions/general.md @@ -0,0 +1,8 @@ +--- +description: General coding guidelines +inline: true +--- + +## General Instructions + +Follow best practices. diff --git a/tests/fixtures/target-merge-cursor-codex/skills/test-skill/SKILL.md b/tests/fixtures/target-merge-cursor-codex/skills/test-skill/SKILL.md new file mode 100644 index 0000000..4aeb011 --- /dev/null +++ b/tests/fixtures/target-merge-cursor-codex/skills/test-skill/SKILL.md @@ -0,0 +1,6 @@ +--- +name: test-skill +description: A test skill +--- + +# Test Skill diff --git a/tests/fixtures/target-merge-cursor-opencode/agents/test-agent.md b/tests/fixtures/target-merge-cursor-opencode/agents/test-agent.md new file mode 100644 index 0000000..723c980 --- /dev/null +++ b/tests/fixtures/target-merge-cursor-opencode/agents/test-agent.md @@ -0,0 +1,9 @@ +--- +name: test-agent +description: A test agent +model: inherit +--- + +# Test Agent + +This is a test agent. diff --git a/tests/fixtures/target-merge-cursor-opencode/aicm.json b/tests/fixtures/target-merge-cursor-opencode/aicm.json new file mode 100644 index 0000000..9cb9a7e --- /dev/null +++ b/tests/fixtures/target-merge-cursor-opencode/aicm.json @@ -0,0 +1,13 @@ +{ + "rootDir": "./", + "targets": ["cursor", "opencode"], + "mcpServers": { + "test-mcp": { + "command": "npx", + "args": ["-y", "test-mcp-server"], + "env": { + "TEST_KEY": "test-value" + } + } + } +} diff --git a/tests/fixtures/target-merge-cursor-opencode/hooks.json b/tests/fixtures/target-merge-cursor-opencode/hooks.json new file mode 100644 index 0000000..a2ce091 --- /dev/null +++ b/tests/fixtures/target-merge-cursor-opencode/hooks.json @@ -0,0 +1,10 @@ +{ + "version": 1, + "hooks": { + "beforeShellExecution": [ + { + "command": "./hooks/lint.sh" + } + ] + } +} diff --git a/tests/fixtures/target-merge-cursor-opencode/hooks/lint.sh b/tests/fixtures/target-merge-cursor-opencode/hooks/lint.sh new file mode 100644 index 0000000..dca7cac --- /dev/null +++ b/tests/fixtures/target-merge-cursor-opencode/hooks/lint.sh @@ -0,0 +1,2 @@ +#!/bin/bash +echo lint diff --git a/tests/fixtures/target-merge-cursor-opencode/instructions/general.md b/tests/fixtures/target-merge-cursor-opencode/instructions/general.md new file mode 100644 index 0000000..d64ec76 --- /dev/null +++ b/tests/fixtures/target-merge-cursor-opencode/instructions/general.md @@ -0,0 +1,8 @@ +--- +description: General coding guidelines +inline: true +--- + +## General Instructions + +Follow best practices. diff --git a/tests/fixtures/target-merge-cursor-opencode/skills/test-skill/SKILL.md b/tests/fixtures/target-merge-cursor-opencode/skills/test-skill/SKILL.md new file mode 100644 index 0000000..3c6f15e --- /dev/null +++ b/tests/fixtures/target-merge-cursor-opencode/skills/test-skill/SKILL.md @@ -0,0 +1,8 @@ +--- +name: test-skill +description: A test skill +--- + +# Test Skill + +This is a test skill. diff --git a/tests/fixtures/target-merge-existing-claude/CLAUDE.md b/tests/fixtures/target-merge-existing-claude/CLAUDE.md new file mode 100644 index 0000000..c8aafe5 --- /dev/null +++ b/tests/fixtures/target-merge-existing-claude/CLAUDE.md @@ -0,0 +1,3 @@ +# My Custom Claude Config + +This is user-managed content. diff --git a/tests/fixtures/target-merge-existing-claude/agents/test-agent.md b/tests/fixtures/target-merge-existing-claude/agents/test-agent.md new file mode 100644 index 0000000..723c980 --- /dev/null +++ b/tests/fixtures/target-merge-existing-claude/agents/test-agent.md @@ -0,0 +1,9 @@ +--- +name: test-agent +description: A test agent +model: inherit +--- + +# Test Agent + +This is a test agent. diff --git a/tests/fixtures/target-merge-existing-claude/aicm.json b/tests/fixtures/target-merge-existing-claude/aicm.json new file mode 100644 index 0000000..8e2bff3 --- /dev/null +++ b/tests/fixtures/target-merge-existing-claude/aicm.json @@ -0,0 +1,13 @@ +{ + "rootDir": "./", + "targets": ["cursor", "claude-code"], + "mcpServers": { + "test-mcp": { + "command": "npx", + "args": ["-y", "test-mcp-server"], + "env": { + "TEST_KEY": "test-value" + } + } + } +} diff --git a/tests/fixtures/target-merge-existing-claude/hooks.json b/tests/fixtures/target-merge-existing-claude/hooks.json new file mode 100644 index 0000000..a2ce091 --- /dev/null +++ b/tests/fixtures/target-merge-existing-claude/hooks.json @@ -0,0 +1,10 @@ +{ + "version": 1, + "hooks": { + "beforeShellExecution": [ + { + "command": "./hooks/lint.sh" + } + ] + } +} diff --git a/tests/fixtures/target-merge-existing-claude/hooks/lint.sh b/tests/fixtures/target-merge-existing-claude/hooks/lint.sh new file mode 100644 index 0000000..dca7cac --- /dev/null +++ b/tests/fixtures/target-merge-existing-claude/hooks/lint.sh @@ -0,0 +1,2 @@ +#!/bin/bash +echo lint diff --git a/tests/fixtures/target-merge-existing-claude/instructions/general.md b/tests/fixtures/target-merge-existing-claude/instructions/general.md new file mode 100644 index 0000000..d64ec76 --- /dev/null +++ b/tests/fixtures/target-merge-existing-claude/instructions/general.md @@ -0,0 +1,8 @@ +--- +description: General coding guidelines +inline: true +--- + +## General Instructions + +Follow best practices. diff --git a/tests/fixtures/target-merge-existing-claude/skills/test-skill/SKILL.md b/tests/fixtures/target-merge-existing-claude/skills/test-skill/SKILL.md new file mode 100644 index 0000000..3c6f15e --- /dev/null +++ b/tests/fixtures/target-merge-existing-claude/skills/test-skill/SKILL.md @@ -0,0 +1,8 @@ +--- +name: test-skill +description: A test skill +--- + +# Test Skill + +This is a test skill. diff --git a/tests/fixtures/commands-subdirectory-links/aicm.json b/tests/fixtures/target-presets-backward-compat/aicm.json similarity index 100% rename from tests/fixtures/commands-subdirectory-links/aicm.json rename to tests/fixtures/target-presets-backward-compat/aicm.json diff --git a/tests/fixtures/target-presets-backward-compat/instructions/general.md b/tests/fixtures/target-presets-backward-compat/instructions/general.md new file mode 100644 index 0000000..8f6f221 --- /dev/null +++ b/tests/fixtures/target-presets-backward-compat/instructions/general.md @@ -0,0 +1,8 @@ +--- +description: General instructions for backward compat test +inline: true +--- + +## General Instructions + +- This is a test instruction for backward compatibility. diff --git a/tests/fixtures/target-presets-both/aicm.json b/tests/fixtures/target-presets-both/aicm.json new file mode 100644 index 0000000..457fbca --- /dev/null +++ b/tests/fixtures/target-presets-both/aicm.json @@ -0,0 +1,4 @@ +{ + "rootDir": "./", + "targets": ["cursor", "claude-code"] +} diff --git a/tests/fixtures/target-presets-both/instructions/general.md b/tests/fixtures/target-presets-both/instructions/general.md new file mode 100644 index 0000000..1267276 --- /dev/null +++ b/tests/fixtures/target-presets-both/instructions/general.md @@ -0,0 +1,8 @@ +--- +description: General instructions for both presets test +inline: true +--- + +## General Instructions + +- This is a test instruction for both presets. diff --git a/tests/fixtures/commands-workspace-aux-files/package-a/aicm.json b/tests/fixtures/target-presets-cursor/aicm.json similarity index 100% rename from tests/fixtures/commands-workspace-aux-files/package-a/aicm.json rename to tests/fixtures/target-presets-cursor/aicm.json diff --git a/tests/fixtures/target-presets-cursor/instructions/general.md b/tests/fixtures/target-presets-cursor/instructions/general.md new file mode 100644 index 0000000..3986b26 --- /dev/null +++ b/tests/fixtures/target-presets-cursor/instructions/general.md @@ -0,0 +1,8 @@ +--- +description: General instructions for cursor preset test +inline: true +--- + +## General Instructions + +- This is a test instruction for cursor preset. diff --git a/tests/fixtures/target-presets-hooks-both/aicm.json b/tests/fixtures/target-presets-hooks-both/aicm.json new file mode 100644 index 0000000..457fbca --- /dev/null +++ b/tests/fixtures/target-presets-hooks-both/aicm.json @@ -0,0 +1,4 @@ +{ + "rootDir": "./", + "targets": ["cursor", "claude-code"] +} diff --git a/tests/fixtures/target-presets-hooks-both/hooks.json b/tests/fixtures/target-presets-hooks-both/hooks.json new file mode 100644 index 0000000..a0664ce --- /dev/null +++ b/tests/fixtures/target-presets-hooks-both/hooks.json @@ -0,0 +1,15 @@ +{ + "version": 1, + "hooks": { + "beforeShellExecution": [ + { + "command": "./hooks/audit.sh" + } + ], + "afterFileEdit": [ + { + "command": "./hooks/format.js" + } + ] + } +} diff --git a/tests/fixtures/target-presets-hooks-both/hooks/audit.sh b/tests/fixtures/target-presets-hooks-both/hooks/audit.sh new file mode 100644 index 0000000..14afed9 --- /dev/null +++ b/tests/fixtures/target-presets-hooks-both/hooks/audit.sh @@ -0,0 +1,2 @@ +#!/bin/bash +echo "Running audit..." diff --git a/tests/fixtures/target-presets-hooks-both/hooks/format.js b/tests/fixtures/target-presets-hooks-both/hooks/format.js new file mode 100644 index 0000000..3bb31e6 --- /dev/null +++ b/tests/fixtures/target-presets-hooks-both/hooks/format.js @@ -0,0 +1,2 @@ +#!/usr/bin/env node +console.log("Formatting file..."); diff --git a/tests/fixtures/target-presets-hooks-claude/aicm.json b/tests/fixtures/target-presets-hooks-claude/aicm.json new file mode 100644 index 0000000..9671cf6 --- /dev/null +++ b/tests/fixtures/target-presets-hooks-claude/aicm.json @@ -0,0 +1,4 @@ +{ + "rootDir": "./", + "targets": ["claude-code"] +} diff --git a/tests/fixtures/target-presets-hooks-claude/hooks.json b/tests/fixtures/target-presets-hooks-claude/hooks.json new file mode 100644 index 0000000..c0986f8 --- /dev/null +++ b/tests/fixtures/target-presets-hooks-claude/hooks.json @@ -0,0 +1,20 @@ +{ + "version": 1, + "hooks": { + "beforeShellExecution": [ + { + "command": "./hooks/audit.sh" + } + ], + "afterFileEdit": [ + { + "command": "./hooks/format.js" + } + ], + "stop": [ + { + "command": "./hooks/cleanup.sh" + } + ] + } +} diff --git a/tests/fixtures/target-presets-hooks-claude/hooks/audit.sh b/tests/fixtures/target-presets-hooks-claude/hooks/audit.sh new file mode 100644 index 0000000..14afed9 --- /dev/null +++ b/tests/fixtures/target-presets-hooks-claude/hooks/audit.sh @@ -0,0 +1,2 @@ +#!/bin/bash +echo "Running audit..." diff --git a/tests/fixtures/target-presets-hooks-claude/hooks/cleanup.sh b/tests/fixtures/target-presets-hooks-claude/hooks/cleanup.sh new file mode 100644 index 0000000..2478074 --- /dev/null +++ b/tests/fixtures/target-presets-hooks-claude/hooks/cleanup.sh @@ -0,0 +1,2 @@ +#!/bin/bash +echo "Running cleanup..." diff --git a/tests/fixtures/target-presets-hooks-claude/hooks/format.js b/tests/fixtures/target-presets-hooks-claude/hooks/format.js new file mode 100644 index 0000000..3bb31e6 --- /dev/null +++ b/tests/fixtures/target-presets-hooks-claude/hooks/format.js @@ -0,0 +1,2 @@ +#!/usr/bin/env node +console.log("Formatting file..."); diff --git a/tests/fixtures/target-presets-invalid/aicm.json b/tests/fixtures/target-presets-invalid/aicm.json new file mode 100644 index 0000000..83a1eac --- /dev/null +++ b/tests/fixtures/target-presets-invalid/aicm.json @@ -0,0 +1,4 @@ +{ + "rootDir": "./", + "targets": ["cursor", "nonexistent-preset"] +} diff --git a/tests/fixtures/target-presets-invalid/instructions/general.md b/tests/fixtures/target-presets-invalid/instructions/general.md new file mode 100644 index 0000000..6c03d64 --- /dev/null +++ b/tests/fixtures/target-presets-invalid/instructions/general.md @@ -0,0 +1,6 @@ +--- +description: test +inline: true +--- + +Test diff --git a/tests/fixtures/target-presets-override/aicm.json b/tests/fixtures/target-presets-override/aicm.json new file mode 100644 index 0000000..6d96fb2 --- /dev/null +++ b/tests/fixtures/target-presets-override/aicm.json @@ -0,0 +1,10 @@ +{ + "rootDir": "./", + "targets": ["cursor"], + "mcpServers": { + "test-server": { + "command": "echo", + "args": ["hello"] + } + } +} diff --git a/tests/fixtures/target-presets-override/instructions/general.md b/tests/fixtures/target-presets-override/instructions/general.md new file mode 100644 index 0000000..dbf1103 --- /dev/null +++ b/tests/fixtures/target-presets-override/instructions/general.md @@ -0,0 +1,8 @@ +--- +description: General instructions for override test +inline: true +--- + +## General Instructions + +- This is a test instruction for preset override. diff --git a/tests/fixtures/windsurf-basic/aicm.json b/tests/fixtures/windsurf-basic/aicm.json deleted file mode 100644 index 0eb90b9..0000000 --- a/tests/fixtures/windsurf-basic/aicm.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "rootDir": "./", - "targets": ["cursor", "windsurf"] -} diff --git a/tests/fixtures/windsurf-basic/rules/always-rule.mdc b/tests/fixtures/windsurf-basic/rules/always-rule.mdc deleted file mode 100644 index d4b0165..0000000 --- a/tests/fixtures/windsurf-basic/rules/always-rule.mdc +++ /dev/null @@ -1,15 +0,0 @@ ---- -description: -globs: -alwaysApply: false ---- ---- -description: "Development Standards" -alwaysApply: true ---- - -# Development Standards - -- Use yarn for package management -- Development workflow: e2e tests → implementation → verification -- Document all features in README.md diff --git a/tests/fixtures/windsurf-basic/rules/file-pattern-rule.mdc b/tests/fixtures/windsurf-basic/rules/file-pattern-rule.mdc deleted file mode 100644 index d9b2912..0000000 --- a/tests/fixtures/windsurf-basic/rules/file-pattern-rule.mdc +++ /dev/null @@ -1,15 +0,0 @@ ---- -description: -globs: -alwaysApply: false ---- ---- -description: "TypeScript Best Practices" -alwaysApply: false ---- - -# TypeScript Best Practices - -- Use strict type checking -- Prefer interfaces over types for public APIs -- Use optional chaining and nullish coalescing when appropriate diff --git a/tests/fixtures/windsurf-basic/rules/opt-in-rule.mdc b/tests/fixtures/windsurf-basic/rules/opt-in-rule.mdc deleted file mode 100644 index 750fff0..0000000 --- a/tests/fixtures/windsurf-basic/rules/opt-in-rule.mdc +++ /dev/null @@ -1,15 +0,0 @@ ---- -description: -globs: -alwaysApply: false ---- ---- -description: "E2E Testing Best Practices" -alwaysApply: false ---- - -# E2E Testing Best Practices - -- Store initial test state in fixtures -- Reference documentation: tests/E2E_TESTS.md -- Include .gitkeep file in empty fixture directories diff --git a/tests/fixtures/windsurf-cleanup/.aicm/fresh-windsurf-rule.md b/tests/fixtures/windsurf-cleanup/.aicm/fresh-windsurf-rule.md deleted file mode 100644 index 1e8bea2..0000000 --- a/tests/fixtures/windsurf-cleanup/.aicm/fresh-windsurf-rule.md +++ /dev/null @@ -1,7 +0,0 @@ -# Fresh Windsurf Rule - -This is OLD fresh Windsurf content. - -## Content - -This content should be replaced with the new content from the rules directory. diff --git a/tests/fixtures/windsurf-cleanup/.aicm/stale-windsurf-rule.md b/tests/fixtures/windsurf-cleanup/.aicm/stale-windsurf-rule.md deleted file mode 100644 index d4c4a8f..0000000 --- a/tests/fixtures/windsurf-cleanup/.aicm/stale-windsurf-rule.md +++ /dev/null @@ -1,7 +0,0 @@ -# Stale Windsurf Rule - -This is a stale Windsurf rule that should be removed. - -## Content - -This rule should be cleaned up during installation. diff --git a/tests/fixtures/windsurf-cleanup/.windsurfrules b/tests/fixtures/windsurf-cleanup/.windsurfrules deleted file mode 100644 index cfc7c92..0000000 --- a/tests/fixtures/windsurf-cleanup/.windsurfrules +++ /dev/null @@ -1,6 +0,0 @@ - -- .aicm/stale-windsurf-rule.md -- .aicm/fresh-windsurf-rule.md - - -Some other manual rules here. \ No newline at end of file diff --git a/tests/fixtures/windsurf-cleanup/aicm.json b/tests/fixtures/windsurf-cleanup/aicm.json deleted file mode 100644 index 670411d..0000000 --- a/tests/fixtures/windsurf-cleanup/aicm.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "rootDir": "./", - "targets": ["windsurf"], - "mcpServers": { - "windsurf-cleanup-mcp": { - "command": "./scripts/windsurf-cleanup-mcp.sh", - "args": ["--windsurf-cleanup"], - "env": { "WINDSURF_TOKEN": "windsurf123" } - } - } -} diff --git a/tests/fixtures/windsurf-cleanup/rules/fresh-windsurf-rule.mdc b/tests/fixtures/windsurf-cleanup/rules/fresh-windsurf-rule.mdc deleted file mode 100644 index 9b7af97..0000000 --- a/tests/fixtures/windsurf-cleanup/rules/fresh-windsurf-rule.mdc +++ /dev/null @@ -1,12 +0,0 @@ ---- -description: -globs: -alwaysApply: false ---- -# Fresh Windsurf Rule - -This is fresh Windsurf content. - -## Content - -This rule should replace the old content in the .aicm directory. diff --git a/tests/fixtures/windsurf-existing/.windsurfrules b/tests/fixtures/windsurf-existing/.windsurfrules deleted file mode 100644 index 363a145..0000000 --- a/tests/fixtures/windsurf-existing/.windsurfrules +++ /dev/null @@ -1,13 +0,0 @@ -Manually written rules - - -Some custom rules content here - - - - - -The following rules apply to all files in the project: -- .aicm/existing-rule.md - - \ No newline at end of file diff --git a/tests/fixtures/windsurf-existing/aicm.json b/tests/fixtures/windsurf-existing/aicm.json deleted file mode 100644 index 0eb90b9..0000000 --- a/tests/fixtures/windsurf-existing/aicm.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "rootDir": "./", - "targets": ["cursor", "windsurf"] -} diff --git a/tests/fixtures/windsurf-existing/rules/new-rule.mdc b/tests/fixtures/windsurf-existing/rules/new-rule.mdc deleted file mode 100644 index 2845dfe..0000000 --- a/tests/fixtures/windsurf-existing/rules/new-rule.mdc +++ /dev/null @@ -1,13 +0,0 @@ ---- -description: -globs: -alwaysApply: false ---- ---- -description: "New Rule" -alwaysApply: false ---- - -# New Rule - -- This is a new rule that should be added to the .windsurfrules file diff --git a/tests/fixtures/workspaces-auto-detect/packages/backend/AGENTS.src.md b/tests/fixtures/workspaces-auto-detect/packages/backend/AGENTS.src.md new file mode 100644 index 0000000..1cecc58 --- /dev/null +++ b/tests/fixtures/workspaces-auto-detect/packages/backend/AGENTS.src.md @@ -0,0 +1,8 @@ +--- +description: Backend instructions +inline: true +--- + +## Backend Development Instructions (Auto-detected) + +- Backend instructions for auto-detected workspaces. diff --git a/tests/fixtures/workspaces-auto-detect/packages/frontend/AGENTS.src.md b/tests/fixtures/workspaces-auto-detect/packages/frontend/AGENTS.src.md new file mode 100644 index 0000000..7390f57 --- /dev/null +++ b/tests/fixtures/workspaces-auto-detect/packages/frontend/AGENTS.src.md @@ -0,0 +1,8 @@ +--- +description: Frontend instructions +inline: true +--- + +## Frontend Development Instructions (Auto-detected) + +- Frontend instructions for auto-detected workspaces. diff --git a/tests/fixtures/workspaces-bazel-basic/services/api/AGENTS.src.md b/tests/fixtures/workspaces-bazel-basic/services/api/AGENTS.src.md new file mode 100644 index 0000000..d4ccd46 --- /dev/null +++ b/tests/fixtures/workspaces-bazel-basic/services/api/AGENTS.src.md @@ -0,0 +1,8 @@ +--- +description: API service instructions +inline: true +--- + +## API Service Instructions + +- Instructions for API service. diff --git a/tests/fixtures/workspaces-bazel-basic/services/worker/AGENTS.src.md b/tests/fixtures/workspaces-bazel-basic/services/worker/AGENTS.src.md new file mode 100644 index 0000000..1406305 --- /dev/null +++ b/tests/fixtures/workspaces-bazel-basic/services/worker/AGENTS.src.md @@ -0,0 +1,8 @@ +--- +description: Worker service instructions +inline: true +--- + +## Worker Service Instructions + +- Instructions for worker service. diff --git a/tests/fixtures/workspaces-empty-root-config/packages/backend/AGENTS.src.md b/tests/fixtures/workspaces-empty-root-config/packages/backend/AGENTS.src.md new file mode 100644 index 0000000..2142850 --- /dev/null +++ b/tests/fixtures/workspaces-empty-root-config/packages/backend/AGENTS.src.md @@ -0,0 +1,8 @@ +--- +description: Backend instructions +inline: true +--- + +## Backend Development Instructions (Empty Root Config) + +- Backend instructions for empty root config. diff --git a/tests/fixtures/workspaces-empty-root-config/packages/frontend/AGENTS.src.md b/tests/fixtures/workspaces-empty-root-config/packages/frontend/AGENTS.src.md new file mode 100644 index 0000000..1424d61 --- /dev/null +++ b/tests/fixtures/workspaces-empty-root-config/packages/frontend/AGENTS.src.md @@ -0,0 +1,8 @@ +--- +description: Frontend instructions +inline: true +--- + +## Frontend Development Instructions (Empty Root Config) + +- Frontend instructions for empty root config. diff --git a/tests/fixtures/workspaces-error-scenarios/missing-rule/instructions/.gitkeep b/tests/fixtures/workspaces-error-scenarios/missing-rule/instructions/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/tests/fixtures/workspaces-error-scenarios/valid-package/AGENTS.src.md b/tests/fixtures/workspaces-error-scenarios/valid-package/AGENTS.src.md new file mode 100644 index 0000000..a5ca776 --- /dev/null +++ b/tests/fixtures/workspaces-error-scenarios/valid-package/AGENTS.src.md @@ -0,0 +1,8 @@ +--- +description: Valid package instructions +inline: true +--- + +## Valid Package Instructions + +- Instructions for valid package. diff --git a/tests/fixtures/workspaces-explicit-false/AGENTS.src.md b/tests/fixtures/workspaces-explicit-false/AGENTS.src.md new file mode 100644 index 0000000..ee53783 --- /dev/null +++ b/tests/fixtures/workspaces-explicit-false/AGENTS.src.md @@ -0,0 +1,8 @@ +--- +description: Main instruction +inline: true +--- + +## Main Instruction (Explicit False) + +- Instructions for explicit false workspaces. diff --git a/tests/fixtures/workspaces-mcp-conflict/packages/backend/AGENTS.src.md b/tests/fixtures/workspaces-mcp-conflict/packages/backend/AGENTS.src.md new file mode 100644 index 0000000..ee19443 --- /dev/null +++ b/tests/fixtures/workspaces-mcp-conflict/packages/backend/AGENTS.src.md @@ -0,0 +1,8 @@ +--- +description: Backend instructions +inline: true +--- + +## Backend Instructions + +- Backend instructions for MCP conflict. diff --git a/tests/fixtures/workspaces-mcp-conflict/packages/frontend/AGENTS.src.md b/tests/fixtures/workspaces-mcp-conflict/packages/frontend/AGENTS.src.md new file mode 100644 index 0000000..1dca416 --- /dev/null +++ b/tests/fixtures/workspaces-mcp-conflict/packages/frontend/AGENTS.src.md @@ -0,0 +1,8 @@ +--- +description: Frontend instructions +inline: true +--- + +## Frontend Instructions + +- Frontend instructions for MCP conflict. diff --git a/tests/fixtures/workspaces-mcp-merge/packages/backend/AGENTS.src.md b/tests/fixtures/workspaces-mcp-merge/packages/backend/AGENTS.src.md new file mode 100644 index 0000000..8541e55 --- /dev/null +++ b/tests/fixtures/workspaces-mcp-merge/packages/backend/AGENTS.src.md @@ -0,0 +1,8 @@ +--- +description: Backend instructions +inline: true +--- + +## Backend Instructions + +- Backend instructions for MCP merge. diff --git a/tests/fixtures/workspaces-mcp-merge/packages/frontend/AGENTS.src.md b/tests/fixtures/workspaces-mcp-merge/packages/frontend/AGENTS.src.md new file mode 100644 index 0000000..bd88c6e --- /dev/null +++ b/tests/fixtures/workspaces-mcp-merge/packages/frontend/AGENTS.src.md @@ -0,0 +1,8 @@ +--- +description: Frontend instructions +inline: true +--- + +## Frontend Instructions + +- Frontend instructions for MCP merge. diff --git a/tests/fixtures/workspaces-mcp-no-cursor/aicm.json b/tests/fixtures/workspaces-mcp-no-cursor/aicm.json index e0c8244..fd1468b 100644 --- a/tests/fixtures/workspaces-mcp-no-cursor/aicm.json +++ b/tests/fixtures/workspaces-mcp-no-cursor/aicm.json @@ -1,5 +1,5 @@ { "rootDir": "./", "workspaces": true, - "targets": ["codex"] + "targets": ["claude-code"] } diff --git a/tests/fixtures/workspaces-mcp-no-cursor/packages/backend/AGENTS.src.md b/tests/fixtures/workspaces-mcp-no-cursor/packages/backend/AGENTS.src.md new file mode 100644 index 0000000..061cf6a --- /dev/null +++ b/tests/fixtures/workspaces-mcp-no-cursor/packages/backend/AGENTS.src.md @@ -0,0 +1,8 @@ +--- +description: Backend instructions +inline: true +--- + +## Backend Instructions + +- Backend instructions without cursor target. diff --git a/tests/fixtures/workspaces-mcp-no-cursor/packages/backend/aicm.json b/tests/fixtures/workspaces-mcp-no-cursor/packages/backend/aicm.json index adaef34..0e72775 100644 --- a/tests/fixtures/workspaces-mcp-no-cursor/packages/backend/aicm.json +++ b/tests/fixtures/workspaces-mcp-no-cursor/packages/backend/aicm.json @@ -1,6 +1,6 @@ { "rootDir": "./", - "targets": ["codex"], + "targets": ["claude-code"], "mcpServers": { "backend-mcp": { "command": "./scripts/backend.sh" diff --git a/tests/fixtures/workspaces-mcp-no-cursor/packages/frontend/AGENTS.src.md b/tests/fixtures/workspaces-mcp-no-cursor/packages/frontend/AGENTS.src.md new file mode 100644 index 0000000..8b81c7e --- /dev/null +++ b/tests/fixtures/workspaces-mcp-no-cursor/packages/frontend/AGENTS.src.md @@ -0,0 +1,8 @@ +--- +description: Frontend instructions +inline: true +--- + +## Frontend Instructions + +- Frontend instructions without cursor target. diff --git a/tests/fixtures/workspaces-mcp-no-cursor/packages/frontend/aicm.json b/tests/fixtures/workspaces-mcp-no-cursor/packages/frontend/aicm.json index 42e6b04..5644e65 100644 --- a/tests/fixtures/workspaces-mcp-no-cursor/packages/frontend/aicm.json +++ b/tests/fixtures/workspaces-mcp-no-cursor/packages/frontend/aicm.json @@ -1,6 +1,6 @@ { "rootDir": "./", - "targets": ["codex"], + "targets": ["claude-code"], "mcpServers": { "frontend-mcp": { "command": "./scripts/frontend.sh" diff --git a/tests/fixtures/workspaces-mixed/backend-service/AGENTS.src.md b/tests/fixtures/workspaces-mixed/backend-service/AGENTS.src.md new file mode 100644 index 0000000..cdc879e --- /dev/null +++ b/tests/fixtures/workspaces-mixed/backend-service/AGENTS.src.md @@ -0,0 +1,8 @@ +--- +description: Backend service instructions +inline: true +--- + +## Backend Service Instructions + +- Instructions for backend service. diff --git a/tests/fixtures/workspaces-mixed/frontend/AGENTS.src.md b/tests/fixtures/workspaces-mixed/frontend/AGENTS.src.md new file mode 100644 index 0000000..03d5605 --- /dev/null +++ b/tests/fixtures/workspaces-mixed/frontend/AGENTS.src.md @@ -0,0 +1,8 @@ +--- +description: Frontend instructions +inline: true +--- + +## Frontend Instructions + +- Instructions for frontend. diff --git a/tests/fixtures/workspaces-no-config/packages/backend/AGENTS.src.md b/tests/fixtures/workspaces-no-config/packages/backend/AGENTS.src.md new file mode 100644 index 0000000..01f2cee --- /dev/null +++ b/tests/fixtures/workspaces-no-config/packages/backend/AGENTS.src.md @@ -0,0 +1,8 @@ +--- +description: Backend instructions +inline: true +--- + +## Backend Development Instructions (No Config) + +- Backend instructions for no-config workspace. diff --git a/tests/fixtures/workspaces-no-config/packages/frontend/AGENTS.src.md b/tests/fixtures/workspaces-no-config/packages/frontend/AGENTS.src.md new file mode 100644 index 0000000..eadb1fb --- /dev/null +++ b/tests/fixtures/workspaces-no-config/packages/frontend/AGENTS.src.md @@ -0,0 +1,8 @@ +--- +description: Frontend instructions +inline: true +--- + +## Frontend Development Instructions (No Config) + +- Frontend instructions for no-config workspace. diff --git a/tests/fixtures/workspaces-npm-basic/packages/backend/AGENTS.src.md b/tests/fixtures/workspaces-npm-basic/packages/backend/AGENTS.src.md new file mode 100644 index 0000000..dc78fa8 --- /dev/null +++ b/tests/fixtures/workspaces-npm-basic/packages/backend/AGENTS.src.md @@ -0,0 +1,8 @@ +--- +description: Backend development instructions +inline: true +--- + +## Backend Development Instructions + +- Backend specific instructions. diff --git a/tests/fixtures/workspaces-npm-basic/packages/frontend/AGENTS.src.md b/tests/fixtures/workspaces-npm-basic/packages/frontend/AGENTS.src.md new file mode 100644 index 0000000..6db7002 --- /dev/null +++ b/tests/fixtures/workspaces-npm-basic/packages/frontend/AGENTS.src.md @@ -0,0 +1,8 @@ +--- +description: Frontend development instructions +inline: true +--- + +## Frontend Development Instructions + +- Frontend specific instructions. diff --git a/tests/fixtures/workspaces-npm-nested/apps/web/AGENTS.src.md b/tests/fixtures/workspaces-npm-nested/apps/web/AGENTS.src.md new file mode 100644 index 0000000..98da0e7 --- /dev/null +++ b/tests/fixtures/workspaces-npm-nested/apps/web/AGENTS.src.md @@ -0,0 +1,8 @@ +--- +description: Web app instructions +inline: true +--- + +## Web App Instructions + +- Instructions for web app. diff --git a/tests/fixtures/workspaces-npm-nested/packages/ui/AGENTS.src.md b/tests/fixtures/workspaces-npm-nested/packages/ui/AGENTS.src.md new file mode 100644 index 0000000..e0f3794 --- /dev/null +++ b/tests/fixtures/workspaces-npm-nested/packages/ui/AGENTS.src.md @@ -0,0 +1,8 @@ +--- +description: UI instructions +inline: true +--- + +## UI Components Instructions + +- Instructions for UI components. diff --git a/tests/fixtures/workspaces-npm-nested/tools/build/AGENTS.src.md b/tests/fixtures/workspaces-npm-nested/tools/build/AGENTS.src.md new file mode 100644 index 0000000..a9df03f --- /dev/null +++ b/tests/fixtures/workspaces-npm-nested/tools/build/AGENTS.src.md @@ -0,0 +1,8 @@ +--- +description: Build tools instructions +inline: true +--- + +## Build Tools Instructions + +- Instructions for build tools. diff --git a/tests/fixtures/workspaces-partial-configs/packages/also-with-config/AGENTS.src.md b/tests/fixtures/workspaces-partial-configs/packages/also-with-config/AGENTS.src.md new file mode 100644 index 0000000..d849e55 --- /dev/null +++ b/tests/fixtures/workspaces-partial-configs/packages/also-with-config/AGENTS.src.md @@ -0,0 +1,8 @@ +--- +description: Package three instructions +inline: true +--- + +## Package Three Instructions + +- Instructions for package three. diff --git a/tests/fixtures/workspaces-partial-configs/packages/with-config/AGENTS.src.md b/tests/fixtures/workspaces-partial-configs/packages/with-config/AGENTS.src.md new file mode 100644 index 0000000..898442c --- /dev/null +++ b/tests/fixtures/workspaces-partial-configs/packages/with-config/AGENTS.src.md @@ -0,0 +1,8 @@ +--- +description: Package one instructions +inline: true +--- + +## Package One Instructions + +- Instructions for package one. diff --git a/tests/fixtures/workspaces-single-package/AGENTS.src.md b/tests/fixtures/workspaces-single-package/AGENTS.src.md new file mode 100644 index 0000000..f12f37f --- /dev/null +++ b/tests/fixtures/workspaces-single-package/AGENTS.src.md @@ -0,0 +1,8 @@ +--- +description: Local instructions +inline: true +--- + +## Local Instruction + +- Local workspace instructions. diff --git a/tests/fixtures/workspaces-skip-install/packages/preset-package/AGENTS.src.md b/tests/fixtures/workspaces-skip-install/packages/preset-package/AGENTS.src.md new file mode 100644 index 0000000..2189e7d --- /dev/null +++ b/tests/fixtures/workspaces-skip-install/packages/preset-package/AGENTS.src.md @@ -0,0 +1,8 @@ +--- +description: Preset instruction +inline: true +--- + +# Preset Instruction + +This should be skipped because skipInstall is true. diff --git a/tests/fixtures/workspaces-skip-install/packages/regular-package/AGENTS.src.md b/tests/fixtures/workspaces-skip-install/packages/regular-package/AGENTS.src.md new file mode 100644 index 0000000..6468d96 --- /dev/null +++ b/tests/fixtures/workspaces-skip-install/packages/regular-package/AGENTS.src.md @@ -0,0 +1,8 @@ +--- +description: Regular package instructions +inline: true +--- + +## Regular Package Instruction + +- Instructions for regular package. diff --git a/tests/setup.ts b/tests/setup.ts index 188f29c..cb96a9f 100644 --- a/tests/setup.ts +++ b/tests/setup.ts @@ -1,9 +1 @@ -import { execSync } from "child_process"; - jest.setTimeout(20000); - -beforeAll(() => { - execSync("npm run build"); -}); - -afterAll(() => {}); diff --git a/tests/unit/git.test.ts b/tests/unit/git.test.ts new file mode 100644 index 0000000..164df45 --- /dev/null +++ b/tests/unit/git.test.ts @@ -0,0 +1,238 @@ +import fs from "fs-extra"; +import path from "path"; +import os from "os"; +import { execSync } from "child_process"; +import { shallowClone, sparseClone } from "../../src/utils/git"; + +/** + * Helper to create a local bare git repo with aicm preset content. + * Returns the file:// URL to the bare repo. + */ +async function createBareGitRepo( + content: Record, +): Promise<{ bareUrl: string; cleanup: () => Promise }> { + const tempBase = await fs.mkdtemp(path.join(os.tmpdir(), "aicm-git-test-")); + const workDir = path.join(tempBase, "work"); + const bareDir = path.join(tempBase, "bare.git"); + + // Create a work directory with content + await fs.ensureDir(workDir); + + for (const [filePath, fileContent] of Object.entries(content)) { + const fullPath = path.join(workDir, filePath); + await fs.ensureDir(path.dirname(fullPath)); + await fs.writeFile(fullPath, fileContent); + } + + // Initialize git repo, add content, and create a bare clone + execSync("git init", { cwd: workDir, stdio: "pipe" }); + execSync("git add -A", { cwd: workDir, stdio: "pipe" }); + execSync( + 'git -c user.email="test@test.com" -c user.name="Test" commit -m "init"', + { + cwd: workDir, + stdio: "pipe", + }, + ); + execSync(`git clone --bare "${workDir}" "${bareDir}"`, { + stdio: "pipe", + }); + + const bareUrl = `file://${bareDir}`; + + return { + bareUrl, + cleanup: async () => { + await fs.remove(tempBase); + }, + }; +} + +describe("git clone operations", () => { + let tempDest: string; + + beforeEach(async () => { + tempDest = await fs.mkdtemp(path.join(os.tmpdir(), "aicm-clone-dest-")); + }); + + afterEach(async () => { + await fs.remove(tempDest); + }); + + describe("shallowClone", () => { + test("clones a repository with aicm.json at root", async () => { + const { bareUrl, cleanup } = await createBareGitRepo({ + "aicm.json": JSON.stringify({ + rootDir: "./", + instructions: "instructions", + }), + "instructions/general.md": + "---\ndescription: General rules\ninline: true\n---\nBe helpful.", + }); + + try { + const cloneDest = path.join(tempDest, "repo"); + await shallowClone(bareUrl, cloneDest); + + expect(fs.existsSync(path.join(cloneDest, "aicm.json"))).toBe(true); + expect( + fs.existsSync(path.join(cloneDest, "instructions", "general.md")), + ).toBe(true); + + const config = JSON.parse( + fs.readFileSync(path.join(cloneDest, "aicm.json"), "utf8"), + ); + expect(config.rootDir).toBe("./"); + } finally { + await cleanup(); + } + }); + + test("clones a specific branch", async () => { + const tempBase = await fs.mkdtemp( + path.join(os.tmpdir(), "aicm-branch-test-"), + ); + const workDir = path.join(tempBase, "work"); + const bareDir = path.join(tempBase, "bare.git"); + + try { + await fs.ensureDir(workDir); + + // Create initial commit on main + execSync("git init -b main", { cwd: workDir, stdio: "pipe" }); + fs.writeFileSync( + path.join(workDir, "aicm.json"), + JSON.stringify({ rootDir: "./" }), + ); + fs.ensureDirSync(path.join(workDir, "instructions")); + fs.writeFileSync( + path.join(workDir, "instructions", "main.md"), + "---\ndescription: Main branch rule\ninline: true\n---\nMain branch content", + ); + execSync("git add -A", { cwd: workDir, stdio: "pipe" }); + execSync( + 'git -c user.email="test@test.com" -c user.name="Test" commit -m "main"', + { cwd: workDir, stdio: "pipe" }, + ); + + // Create a feature branch with different content + execSync("git checkout -b feature", { cwd: workDir, stdio: "pipe" }); + fs.writeFileSync( + path.join(workDir, "instructions", "feature.md"), + "---\ndescription: Feature branch rule\ninline: true\n---\nFeature branch content", + ); + execSync("git add -A", { cwd: workDir, stdio: "pipe" }); + execSync( + 'git -c user.email="test@test.com" -c user.name="Test" commit -m "feature"', + { cwd: workDir, stdio: "pipe" }, + ); + + // Create bare repo + execSync(`git clone --bare "${workDir}" "${bareDir}"`, { + stdio: "pipe", + }); + + // Clone the feature branch + const cloneDest = path.join(tempDest, "feature-repo"); + await shallowClone(`file://${bareDir}`, cloneDest, "feature"); + + expect( + fs.existsSync(path.join(cloneDest, "instructions", "feature.md")), + ).toBe(true); + expect( + fs.existsSync(path.join(cloneDest, "instructions", "main.md")), + ).toBe(true); + } finally { + await fs.remove(tempBase); + } + }); + + test("throws on invalid URL", async () => { + const cloneDest = path.join(tempDest, "bad-repo"); + await expect( + shallowClone("file:///nonexistent/repo.git", cloneDest), + ).rejects.toThrow("Git operation failed"); + }); + }); + + describe("sparseClone", () => { + test("clones only specified paths", async () => { + const { bareUrl, cleanup } = await createBareGitRepo({ + "packages/preset-a/aicm.json": JSON.stringify({ + rootDir: "./", + instructions: "instructions", + }), + "packages/preset-a/instructions/rule-a.md": + "---\ndescription: Rule A\ninline: true\n---\nRule A content", + "packages/preset-b/aicm.json": JSON.stringify({ + rootDir: "./", + instructions: "instructions", + }), + "packages/preset-b/instructions/rule-b.md": + "---\ndescription: Rule B\ninline: true\n---\nRule B content", + "other/large-file.txt": "This should not be downloaded", + }); + + try { + const cloneDest = path.join(tempDest, "sparse-repo"); + await sparseClone(bareUrl, cloneDest, ["packages/preset-a"]); + + // The sparse-checkout should include preset-a + expect( + fs.existsSync( + path.join(cloneDest, "packages", "preset-a", "aicm.json"), + ), + ).toBe(true); + expect( + fs.existsSync( + path.join( + cloneDest, + "packages", + "preset-a", + "instructions", + "rule-a.md", + ), + ), + ).toBe(true); + + // preset-b and other/ should NOT be materialized + expect( + fs.existsSync( + path.join(cloneDest, "packages", "preset-b", "aicm.json"), + ), + ).toBe(false); + expect( + fs.existsSync(path.join(cloneDest, "other", "large-file.txt")), + ).toBe(false); + } finally { + await cleanup(); + } + }); + + test("clones multiple sparse paths", async () => { + const { bareUrl, cleanup } = await createBareGitRepo({ + "config/aicm.json": JSON.stringify({ rootDir: "../src" }), + "src/instructions/rule.md": + "---\ndescription: Src rule\ninline: true\n---\nSrc content", + "unrelated/stuff.txt": "Should not appear", + }); + + try { + const cloneDest = path.join(tempDest, "multi-sparse"); + await sparseClone(bareUrl, cloneDest, ["config", "src"]); + + expect(fs.existsSync(path.join(cloneDest, "config", "aicm.json"))).toBe( + true, + ); + expect( + fs.existsSync(path.join(cloneDest, "src", "instructions", "rule.md")), + ).toBe(true); + expect( + fs.existsSync(path.join(cloneDest, "unrelated", "stuff.txt")), + ).toBe(false); + } finally { + await cleanup(); + } + }); + }); +}); diff --git a/tests/unit/github-integration.test.ts b/tests/unit/github-integration.test.ts new file mode 100644 index 0000000..cedb958 --- /dev/null +++ b/tests/unit/github-integration.test.ts @@ -0,0 +1,47 @@ +/** + * Integration tests that hit real GitHub infrastructure. + * + * These two tests verify the actual git clone and sparse checkout + * operations work against a real public repository (ranyitz/aicm). + */ + +import fs from "fs-extra"; +import path from "path"; +import os from "os"; +import { shallowClone, sparseClone } from "../../src/utils/git"; + +describe("GitHub clone integration", () => { + let tempDest: string; + + beforeEach(async () => { + tempDest = await fs.mkdtemp( + path.join(os.tmpdir(), "aicm-integration-dest-"), + ); + }); + + afterEach(async () => { + await fs.remove(tempDest); + }); + + test("shallow clone of a real GitHub repository", async () => { + const cloneDest = path.join(tempDest, "aicm"); + await shallowClone("https://github.com/ranyitz/aicm.git", cloneDest); + + expect(fs.existsSync(path.join(cloneDest, "aicm.json"))).toBe(true); + expect(fs.existsSync(path.join(cloneDest, "package.json"))).toBe(true); + expect(fs.existsSync(path.join(cloneDest, "src"))).toBe(true); + }); + + test("sparse clone of a real GitHub repository", async () => { + const cloneDest = path.join(tempDest, "aicm-sparse"); + await sparseClone("https://github.com/ranyitz/aicm.git", cloneDest, [ + "src/utils", + ]); + + // Sparse path is materialized + expect(fs.existsSync(path.join(cloneDest, "src", "utils"))).toBe(true); + + // Directories outside the sparse set are NOT materialized + expect(fs.existsSync(path.join(cloneDest, "tests"))).toBe(false); + }); +}); diff --git a/tests/unit/install-cache.test.ts b/tests/unit/install-cache.test.ts new file mode 100644 index 0000000..42b8d0d --- /dev/null +++ b/tests/unit/install-cache.test.ts @@ -0,0 +1,189 @@ +import fs from "fs-extra"; +import path from "path"; +import os from "os"; +import { + readInstallCache, + writeInstallCache, + getCacheEntry, + setCacheEntry, + isCacheValid, + getRepoCachePath, + buildCacheKey, + getInstallCachePath, + InstallCache, + InstallCacheEntry, +} from "../../src/utils/install-cache"; + +// We override the home directory to isolate tests from the real ~/.aicm/ +let originalHomedir: () => string; +let tempHome: string; + +beforeEach(async () => { + tempHome = await fs.mkdtemp(path.join(os.tmpdir(), "aicm-cache-test-")); + originalHomedir = os.homedir; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (os as any).homedir = () => tempHome; +}); + +afterEach(async () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (os as any).homedir = originalHomedir; + await fs.remove(tempHome); +}); + +describe("install-cache paths", () => { + test("getInstallCachePath returns correct path", () => { + const cachePath = getInstallCachePath(); + expect(cachePath).toBe(path.join(tempHome, ".aicm", "install-cache.json")); + }); + + test("getRepoCachePath returns correct path", () => { + const repoPath = getRepoCachePath("owner", "repo"); + expect(repoPath).toBe( + path.join(tempHome, ".aicm", "repos", "owner", "repo"), + ); + }); + + test("buildCacheKey creates canonical URL", () => { + expect(buildCacheKey("ranyitz", "aicm")).toBe( + "https://github.com/ranyitz/aicm", + ); + }); +}); + +describe("readInstallCache", () => { + test("returns empty cache when file does not exist", async () => { + const cache = await readInstallCache(); + expect(cache).toEqual({ version: 1, entries: {} }); + }); + + test("returns empty cache when file is invalid JSON", async () => { + const cachePath = getInstallCachePath(); + await fs.ensureDir(path.dirname(cachePath)); + await fs.writeFile(cachePath, "not json"); + + const cache = await readInstallCache(); + expect(cache).toEqual({ version: 1, entries: {} }); + }); + + test("returns empty cache when version mismatches", async () => { + const cachePath = getInstallCachePath(); + await fs.ensureDir(path.dirname(cachePath)); + await fs.writeFile( + cachePath, + JSON.stringify({ version: 999, entries: { old: {} } }), + ); + + const cache = await readInstallCache(); + expect(cache).toEqual({ version: 1, entries: {} }); + }); + + test("reads valid cache", async () => { + const expected: InstallCache = { + version: 1, + entries: { + "https://github.com/owner/repo": { + url: "https://github.com/owner/repo", + cachedAt: "2026-01-01T00:00:00.000Z", + cachePath: "/some/path", + }, + }, + }; + + const cachePath = getInstallCachePath(); + await fs.ensureDir(path.dirname(cachePath)); + await fs.writeFile(cachePath, JSON.stringify(expected)); + + const cache = await readInstallCache(); + expect(cache).toEqual(expected); + }); +}); + +describe("writeInstallCache", () => { + test("creates directory and writes cache", async () => { + const cache: InstallCache = { + version: 1, + entries: { + "https://github.com/owner/repo": { + url: "https://github.com/owner/repo", + cachedAt: "2026-01-01T00:00:00.000Z", + cachePath: "/some/path", + }, + }, + }; + + await writeInstallCache(cache); + + const cachePath = getInstallCachePath(); + expect(fs.existsSync(cachePath)).toBe(true); + + const written = JSON.parse(await fs.readFile(cachePath, "utf8")); + expect(written).toEqual(cache); + }); +}); + +describe("getCacheEntry / setCacheEntry", () => { + test("returns null for missing entry", async () => { + const entry = await getCacheEntry("https://github.com/owner/repo"); + expect(entry).toBeNull(); + }); + + test("sets and gets an entry", async () => { + const entry: InstallCacheEntry = { + url: "https://github.com/owner/repo", + ref: "main", + subpath: "packages/preset", + cachedAt: "2026-01-01T00:00:00.000Z", + cachePath: "/some/path", + }; + + await setCacheEntry("https://github.com/owner/repo", entry); + const result = await getCacheEntry("https://github.com/owner/repo"); + expect(result).toEqual(entry); + }); + + test("overwrites existing entry", async () => { + const entry1: InstallCacheEntry = { + url: "https://github.com/owner/repo", + cachedAt: "2026-01-01T00:00:00.000Z", + cachePath: "/old/path", + }; + + const entry2: InstallCacheEntry = { + url: "https://github.com/owner/repo", + cachedAt: "2026-02-01T00:00:00.000Z", + cachePath: "/new/path", + }; + + await setCacheEntry("https://github.com/owner/repo", entry1); + await setCacheEntry("https://github.com/owner/repo", entry2); + + const result = await getCacheEntry("https://github.com/owner/repo"); + expect(result).toEqual(entry2); + }); +}); + +describe("isCacheValid", () => { + test("returns true when cachePath exists", async () => { + const dir = path.join(tempHome, "test-repo"); + await fs.ensureDir(dir); + + const entry: InstallCacheEntry = { + url: "https://github.com/owner/repo", + cachedAt: "2026-01-01T00:00:00.000Z", + cachePath: dir, + }; + + expect(isCacheValid(entry)).toBe(true); + }); + + test("returns false when cachePath does not exist", () => { + const entry: InstallCacheEntry = { + url: "https://github.com/owner/repo", + cachedAt: "2026-01-01T00:00:00.000Z", + cachePath: "/nonexistent/path", + }; + + expect(isCacheValid(entry)).toBe(false); + }); +}); diff --git a/tests/unit/preset-source.test.ts b/tests/unit/preset-source.test.ts new file mode 100644 index 0000000..70d36fd --- /dev/null +++ b/tests/unit/preset-source.test.ts @@ -0,0 +1,188 @@ +import { + parsePresetSource, + parseGitHubUrl, + isGitHubPreset, +} from "../../src/utils/preset-source"; + +describe("parsePresetSource", () => { + describe("github URLs", () => { + test("detects simple GitHub repo URL", () => { + const result = parsePresetSource("https://github.com/owner/repo"); + expect(result.type).toBe("github"); + expect(result.raw).toBe("https://github.com/owner/repo"); + if (result.type === "github") { + expect(result.owner).toBe("owner"); + expect(result.repo).toBe("repo"); + expect(result.ref).toBeUndefined(); + expect(result.subpath).toBeUndefined(); + expect(result.cloneUrl).toBe("https://github.com/owner/repo.git"); + } + }); + + test("detects GitHub tree URL with ref", () => { + const result = parsePresetSource( + "https://github.com/owner/repo/tree/main", + ); + expect(result.type).toBe("github"); + if (result.type === "github") { + expect(result.owner).toBe("owner"); + expect(result.repo).toBe("repo"); + expect(result.ref).toBe("main"); + expect(result.subpath).toBeUndefined(); + } + }); + + test("detects GitHub tree URL with ref and subpath", () => { + const result = parsePresetSource( + "https://github.com/owner/repo/tree/main/packages/preset", + ); + expect(result.type).toBe("github"); + if (result.type === "github") { + expect(result.owner).toBe("owner"); + expect(result.repo).toBe("repo"); + expect(result.ref).toBe("main"); + expect(result.subpath).toBe("packages/preset"); + } + }); + + test("detects GitHub tree URL with tag ref", () => { + const result = parsePresetSource( + "https://github.com/owner/repo/tree/v1.0.0/config", + ); + expect(result.type).toBe("github"); + if (result.type === "github") { + expect(result.ref).toBe("v1.0.0"); + expect(result.subpath).toBe("config"); + } + }); + + test("handles trailing slash in GitHub URL", () => { + const result = parsePresetSource("https://github.com/owner/repo/"); + expect(result.type).toBe("github"); + if (result.type === "github") { + expect(result.owner).toBe("owner"); + expect(result.repo).toBe("repo"); + expect(result.ref).toBeUndefined(); + } + }); + }); + + describe("local paths", () => { + test("detects relative path starting with ./", () => { + const result = parsePresetSource("./presets/my-preset"); + expect(result.type).toBe("local"); + expect(result.raw).toBe("./presets/my-preset"); + }); + + test("detects relative path starting with ../", () => { + const result = parsePresetSource("../shared/preset"); + expect(result.type).toBe("local"); + }); + + test("detects absolute Unix path", () => { + const result = parsePresetSource("/home/user/presets/my-preset"); + expect(result.type).toBe("local"); + }); + + test("detects Windows drive path", () => { + const result = parsePresetSource("C:\\Users\\user\\presets\\my-preset"); + expect(result.type).toBe("local"); + }); + + test("detects Windows drive path with forward slash", () => { + const result = parsePresetSource("D:/presets/my-preset"); + expect(result.type).toBe("local"); + }); + + test("detects path starting with .", () => { + const result = parsePresetSource(".hidden-preset"); + expect(result.type).toBe("local"); + }); + }); + + describe("npm packages", () => { + test("detects scoped npm package", () => { + const result = parsePresetSource("@company/ai-preset"); + expect(result.type).toBe("npm"); + expect(result.raw).toBe("@company/ai-preset"); + }); + + test("detects unscoped npm package", () => { + const result = parsePresetSource("my-aicm-preset"); + expect(result.type).toBe("npm"); + }); + + test("detects npm package with subpath", () => { + const result = parsePresetSource("@company/preset/aicm.json"); + expect(result.type).toBe("npm"); + }); + }); +}); + +describe("parseGitHubUrl", () => { + test("parses simple repo URL", () => { + const result = parseGitHubUrl("https://github.com/ranyitz/aicm"); + expect(result.owner).toBe("ranyitz"); + expect(result.repo).toBe("aicm"); + expect(result.ref).toBeUndefined(); + expect(result.subpath).toBeUndefined(); + expect(result.cloneUrl).toBe("https://github.com/ranyitz/aicm.git"); + }); + + test("parses tree URL with branch", () => { + const result = parseGitHubUrl( + "https://github.com/ranyitz/aicm/tree/develop", + ); + expect(result.owner).toBe("ranyitz"); + expect(result.repo).toBe("aicm"); + expect(result.ref).toBe("develop"); + expect(result.subpath).toBeUndefined(); + }); + + test("parses tree URL with ref and deep subpath", () => { + const result = parseGitHubUrl( + "https://github.com/org/monorepo/tree/main/packages/ai/preset", + ); + expect(result.owner).toBe("org"); + expect(result.repo).toBe("monorepo"); + expect(result.ref).toBe("main"); + expect(result.subpath).toBe("packages/ai/preset"); + }); + + test("throws on non-GitHub URL", () => { + expect(() => parseGitHubUrl("https://gitlab.com/owner/repo")).toThrow( + "Not a GitHub URL", + ); + }); + + test("throws on URL with only owner", () => { + expect(() => parseGitHubUrl("https://github.com/owner")).toThrow( + "Expected format", + ); + }); + + test("throws on non-tree path segment", () => { + expect(() => + parseGitHubUrl("https://github.com/owner/repo/blob/main/file.ts"), + ).toThrow("Only /tree/ URLs are supported"); + }); + + test("throws on tree URL without ref", () => { + expect(() => parseGitHubUrl("https://github.com/owner/repo/tree")).toThrow( + "Missing branch/tag after /tree/", + ); + }); +}); + +describe("isGitHubPreset", () => { + test("returns true for GitHub URLs", () => { + expect(isGitHubPreset("https://github.com/owner/repo")).toBe(true); + expect(isGitHubPreset("https://github.com/o/r/tree/main/path")).toBe(true); + }); + + test("returns false for non-GitHub inputs", () => { + expect(isGitHubPreset("@company/preset")).toBe(false); + expect(isGitHubPreset("./local/path")).toBe(false); + expect(isGitHubPreset("https://gitlab.com/owner/repo")).toBe(false); + }); +}); diff --git a/tsconfig.json b/tsconfig.json index b5205ad..9b2ebab 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,5 +12,5 @@ "resolveJsonModule": true }, "include": ["src/**/*"], - "exclude": ["node_modules", "dist"] + "exclude": ["node_modules", "dist", "src-legacy"] }