diff --git a/SPEC.md b/SPEC.md index e97b447..58c8c2f 100644 --- a/SPEC.md +++ b/SPEC.md @@ -1,10 +1,30 @@ # aisona.yml Format Specification v1 -> The portable AI persona format. Define your AI's personality once, deploy everywhere. +> Pack your AI habits and take them anywhere. A portable format for your AI usage preferences, rules, and learned context — backed by Git, exported to any tool. ## Overview -`aisona.yml` is a structured YAML file that defines an AI assistant's persona — personality, communication style, rules, preferences, and memories — in a tool-agnostic format that can be exported to any AI coding tool. +`aisona.yml` is a structured YAML file that captures how YOU use AI tools — your language preference, your rules, your workflow habits, things your AI has learned about you. It's portable: write it once, export to Claude Code, Cursor, Gemini, Copilot, or any tool that reads config files. + +Think of it like your browser bookmarks or your shell dotfiles — except for your AI. + +## What It Captures + +``` +✅ Things worth packing: + - "Reply in Cantonese" → language preference + - "Always ask before committing" → hard rule + - "Use Playwright, not Chrome DevTools" → tool preference + - "I'm a junior dev, explain things" → context for AI calibration + - "bare npx corrupts nvm, use full path" → learned lesson + - "End tasks with a learning section" → behavior preference + +❌ Things NOT worth packing (waste of tokens): + - Your life philosophy / worldview + - Your personality contradictions + - Words you'd never use + - Your rhetorical style analysis +``` ## Schema @@ -12,157 +32,187 @@ # aisona.yml v1 version: 1 -# === IDENTITY === -# Who the user is (so the AI knows who it's talking to) +# === WHO YOU ARE === +# Enough context for AI to calibrate responses. Keep it short. identity: - name: "" # User's name or alias - role: "" # Professional role/context - experience: "" # Level of experience (helps AI calibrate explanations) - context: "" # Current work context - language: "" # Preferred response language (e.g., "Cantonese", "English") - -# === PERSONALITY === -# How the AI should behave and communicate -personality: - tone: "" # Overall communication tone + name: "" # Your name or alias + role: "" # What you do (e.g., "Backend engineer") + experience: "" # Level (e.g., "11 months", "senior", "student") + language: "" # Preferred AI response language + +# === HOW YOU LIKE IT === +# Your AI usage habits — how you want the AI to behave. +preferences: + tone: "" # e.g., "Direct and concise" or "Detailed with examples" verbosity: "" # "concise" | "balanced" | "detailed" - style: [] # List of style directives - teaching: "" # How to teach/explain (if applicable) - autonomy: "" # How autonomous the AI should be + autonomy: "" # e.g., "Work autonomously, only ask before destructive actions" + teaching: "" # e.g., "Explain what changed and why after each task" + habits: [] # List of specific behavior preferences -# === RULES === -# Hard rules the AI must follow -rules: [] # List of rule strings +# === HARD RULES === +# Things the AI must ALWAYS or NEVER do. Violations = bugs. +rules: [] -# === PREFERENCES === -# Soft preferences (nice-to-have, not hard rules) -preferences: [] # List of preference strings +# === THINGS AI LEARNED ABOUT YOU === +# Facts, lessons, context from past usage. Portable memory. +memories: [] -# === MEMORIES === -# Learned facts and preferences from past interactions -memories: [] # List of memory strings - -# === TOOLS === -# Per-tool overrides and extra configuration +# === PER-TOOL SETTINGS === +# Tool-specific overrides. Only enable tools you actually use. tools: claude: enabled: true - extra_rules: [] # Rules only for Claude - extra_context: "" # Additional context for Claude + extra: [] # Claude-specific rules/preferences cursor: enabled: true - extra_rules: [] + extra: [] gemini: - enabled: true - extra_rules: [] - copilot: - enabled: true - extra_rules: [] - windsurf: enabled: false - extra_rules: [] - aider: + extra: [] + copilot: enabled: false - extra_rules: [] - openrouter: + extra: [] + windsurf: enabled: false - system_prompt_append: "" # Appended to system prompt + extra: [] +``` + +## Real Example + +```yaml +version: 1 + +identity: + name: Nicole + role: Backend engineer + experience: 11 months + language: Cantonese (廣東話) + +preferences: + tone: Direct and concise, like a helpful senior engineer + verbosity: concise + autonomy: Work autonomously. Only ask before destructive actions or pushing to remote. + teaching: End every task with a learning section — what changed, why, and one reusable concept. + habits: + - Show execution flow step by step, like a debugger + - Don't skip hidden middle steps + - Use Playwright for testing, not Chrome DevTools + - Use full nvm path for node/npx + +rules: + - Always ask before committing or pushing + - Never include company names in public repos + - Use feature branches for big changes + - Never ask before reading files, searching, or editing + +memories: + - Side projects use GitHub ithiria894, never company account + - bare npx corrupts nvm default alias — always use full path + - Reddit posts need narrative hook, no limitations talk + - After every workflow run, update playbook + OVERVIEW.md + +tools: + claude: + enabled: true + extra: + - Explain like stepping through a debugger + - End tasks with 7-point learning section + cursor: + enabled: true + extra: [] ``` ## Field Definitions -### identity (required) +### identity -Tells the AI who it's working with. Helps calibrate tone, explanations, and assumptions. +Minimum context so AI knows who it's talking to. Keep it short — every word costs tokens. | Field | Type | Required | Description | |-------|------|----------|-------------| -| `name` | string | yes | User's name or preferred alias | -| `role` | string | yes | What the user does (e.g., "Backend engineer") | -| `experience` | string | no | Experience level (e.g., "11 months", "senior", "student") | -| `context` | string | no | Current work context | -| `language` | string | no | Preferred language for responses | +| `name` | string | yes | Your name or alias | +| `role` | string | no | What you do | +| `experience` | string | no | Helps AI calibrate explanation depth | +| `language` | string | no | Preferred response language | -### personality (required) +### preferences -Defines how the AI communicates. This is what makes your AI feel like "yours." +How you like your AI to behave. These are soft — AI should follow but they're not hard rules. | Field | Type | Required | Description | |-------|------|----------|-------------| -| `tone` | string | yes | Overall tone (e.g., "Direct and concise, like a senior engineer") | -| `verbosity` | string | no | How verbose responses should be | -| `style` | string[] | no | Specific style directives | +| `tone` | string | no | Communication style | +| `verbosity` | string | no | concise / balanced / detailed | +| `autonomy` | string | no | When to ask vs just do | | `teaching` | string | no | How to explain things | -| `autonomy` | string | no | How much to ask vs. just do | +| `habits` | string[] | no | Specific behavior preferences | -### rules (required) +### rules -Hard constraints. The AI must follow these. Violations are bugs. +Hard constraints. AI must follow these. Breaking them = bug. Type: `string[]` -Example: -```yaml -rules: - - "Never commit without asking" - - "Never include company names in public repos" - - "Use feature branches for big changes" -``` +Keep rules **actionable and specific**. "Never commit without asking" is good. "Be ethical" is useless. -### preferences (optional) +### memories -Soft preferences. The AI should try to follow these but they're not hard rules. +Things your AI learned about you from past interactions. Portable context. Type: `string[]` -### memories (optional) +These are facts and lessons, not feelings. "User's npm org is ithiria" is good. "User seems frustrated today" is not. -Facts learned from past interactions. Manually curated. +### tools -Type: `string[]` - -### tools (optional) - -Per-tool configuration. Each tool key can have: +Per-tool overrides. Each key maps to a supported export target. | Field | Type | Description | |-------|------|-------------| | `enabled` | boolean | Whether to export to this tool | -| `extra_rules` | string[] | Rules that only apply to this tool | -| `extra_context` | string | Additional context for this tool | +| `extra` | string[] | Additional rules/preferences for this tool only | ## Export Targets | Tool | Output File | Location | |------|------------|----------| | Claude Code | `CLAUDE.md` | Project root or `~/.claude/` | -| Cursor | `.cursorrules` or `.cursor/rules/*.mdc` | Project root | +| Cursor | `.cursorrules` | Project root | | Gemini CLI | `GEMINI.md` | Project root or `~/.gemini/` | | GitHub Copilot | `.github/copilot-instructions.md` | Project root | | Windsurf | `.windsurfrules` | Project root | -| Aider | `.aider.conf.yml` | Project root | -| OpenRouter | System prompt text | API config | | AGENTS.md | `AGENTS.md` | Project root | ## Design Principles -1. **Human-readable** — YAML, not JSON. Easy to edit by hand. -2. **Tool-agnostic** — No tool-specific concepts in the core schema. -3. **Personality-first** — Identity and personality are required. Rules are secondary. -4. **User-owned** — Stored in user's own Git. No platform dependency. -5. **Backwards-compatible** — New fields are always optional. v1 files work with v2+ tools. -6. **Exportable** — Every field maps to at least one export target. +1. **Practical, not philosophical** — Capture habits and preferences, not worldview and soul +2. **Token-efficient** — Every field should be worth the tokens it costs in the AI's context window +3. **Human-readable** — YAML, not JSON. Easy to edit by hand +4. **User-owned** — Stored in your own Git repo. No platform dependency +5. **Tool-agnostic** — Core schema has no tool-specific concepts +6. **Backwards-compatible** — New fields always optional. v1 files work with v2+ tools +7. **Git as protocol** — aisona.yml lives in Git. Version history, branching, sharing all come free -## Versioning +## How It Works -The `version` field indicates the schema version. The CLI will warn if the file uses a newer version than supported. +``` +1. aisona init → Scans your existing CLAUDE.md → generates aisona.yml +2. Edit aisona.yml → Tweak your preferences, add rules, curate memories +3. aisona export --all → Generates CLAUDE.md + .cursorrules + GEMINI.md + ... +4. aisona watch --git → Auto re-export on change + git commit/push +5. New machine: + git clone your-repo + aisona export --all → All your AI tools instantly know you again +``` ## Prior Art -- SOUL.md — Personality-focused but free-form markdown, no schema, no export -- AGENTS.md — Coding rules standard, no personality/memory -- rulesync — Structured rules with export to 27+ tools, no personality -- Character Card V3 — JSON schema for roleplay, not coding -- CrewAI — role/goal/backstory YAML, too minimal +| Tool | What it does | What aisona adds | +|------|-------------|-----------------| +| SOUL.md | AI personality templates (fill-in-the-blank) | Structured schema + CLI + auto-export + git sync | +| personas.sh | Download pre-made AI personas (marketplace) | Pack YOUR OWN habits, not someone else's | +| PersonaSpec | JSON format for portable identity (API-first) | YAML (human-editable) + CLI + local-first + developer-focused | +| rulesync | Sync coding rules across 27+ tools | Also captures preferences, memories, teaching style — not just rules | +| memoir | Push/restore AI tool config files | Format-first (aisona.yml as source of truth, not raw file copying) | -aisona.yml combines the personality focus of SOUL.md with the structured exportability of rulesync. +aisona = **your AI habits, packed in a YAML, exported everywhere, synced via Git.** diff --git a/bin/aisona.js b/bin/aisona.js index 8f0f8a4..bd05a8b 100755 --- a/bin/aisona.js +++ b/bin/aisona.js @@ -1,3 +1,42 @@ #!/usr/bin/env node -console.log('aisona — Own your AI\'s persona. Coming soon.'); -console.log('https://github.com/mcpware/aisona'); + +import { program } from 'commander'; +import { initCommand } from '../src/commands/init.js'; +import { exportCommand } from '../src/commands/export.js'; +import { statusCommand } from '../src/commands/status.js'; +import { watchCommand } from '../src/commands/watch.js'; + +program + .name('aisona') + .description('Own your AI\'s persona. Define it once, use it everywhere.') + .version('0.1.0'); + +program + .command('init') + .description('Create aisona.yml from your existing AI tool configs') + .option('--from ', 'Import from a specific tool (claude, cursor, gemini)') + .option('--dir ', 'Working directory', process.cwd()) + .action(initCommand); + +program + .command('export') + .description('Export aisona.yml to AI tool configs') + .option('--to ', 'Export to a specific tool (claude, cursor, gemini, copilot)') + .option('--all', 'Export to all enabled tools') + .option('--dir ', 'Working directory', process.cwd()) + .action(exportCommand); + +program + .command('status') + .description('Show what tools are detected and sync status') + .option('--dir ', 'Working directory', process.cwd()) + .action(statusCommand); + +program + .command('watch') + .description('Watch aisona.yml and auto re-export on change') + .option('--dir ', 'Working directory', process.cwd()) + .option('--git', 'Auto git commit + push on change') + .action(watchCommand); + +program.parse(); diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..eaa8510 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2110 @@ +{ + "name": "@agents-io/aisona", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@agents-io/aisona", + "version": "0.0.1", + "license": "MIT", + "dependencies": { + "chalk": "^5.6.2", + "chokidar": "^5.0.0", + "commander": "^14.0.3", + "handlebars": "^4.7.9", + "inquirer": "^13.3.2", + "js-yaml": "^4.1.1", + "ora": "^9.3.0", + "simple-git": "^3.33.0" + }, + "bin": { + "aisona": "bin/aisona.js" + }, + "devDependencies": { + "vitest": "^4.1.2" + } + }, + "node_modules/@emnapi/core": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.1.tgz", + "integrity": "sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.1.tgz", + "integrity": "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz", + "integrity": "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@inquirer/ansi": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-2.0.4.tgz", + "integrity": "sha512-DpcZrQObd7S0R/U3bFdkcT5ebRwbTTC4D3tCc1vsJizmgPLxNJBo+AAFmrZwe8zk30P2QzgzGWZ3Q9uJwWuhIg==", + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + } + }, + "node_modules/@inquirer/checkbox": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-5.1.2.tgz", + "integrity": "sha512-PubpMPO2nJgMufkoB3P2wwxNXEMUXnBIKi/ACzDUYfaoPuM7gSTmuxJeMscoLVEsR4qqrCMf5p0SiYGWnVJ8kw==", + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.4", + "@inquirer/core": "^11.1.7", + "@inquirer/figures": "^2.0.4", + "@inquirer/type": "^4.0.4" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/confirm": { + "version": "6.0.10", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-6.0.10.tgz", + "integrity": "sha512-tiNyA73pgpQ0FQ7axqtoLUe4GDYjNCDcVsbgcA5anvwg2z6i+suEngLKKJrWKJolT//GFPZHwN30binDIHgSgQ==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.1.7", + "@inquirer/type": "^4.0.4" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core": { + "version": "11.1.7", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-11.1.7.tgz", + "integrity": "sha512-1BiBNDk9btIwYIzNZpkikIHXWeNzNncJePPqwDyVMhXhD1ebqbpn1mKGctpoqAbzywZfdG0O4tvmsGIcOevAPQ==", + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.4", + "@inquirer/figures": "^2.0.4", + "@inquirer/type": "^4.0.4", + "cli-width": "^4.1.0", + "fast-wrap-ansi": "^0.2.0", + "mute-stream": "^3.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/editor": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-5.0.10.tgz", + "integrity": "sha512-VJx4XyaKea7t8hEApTw5dxeIyMtWXre2OiyJcICCRZI4hkoHsMoCnl/KbUnJJExLbH9csLLHMVR144ZhFE1CwA==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.1.7", + "@inquirer/external-editor": "^2.0.4", + "@inquirer/type": "^4.0.4" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/expand": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-5.0.10.tgz", + "integrity": "sha512-fC0UHJPXsTRvY2fObiwuQYaAnHrp3aDqfwKUJSdfpgv18QUG054ezGbaRNStk/BKD5IPijeMKWej8VV8O5Q/eQ==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.1.7", + "@inquirer/type": "^4.0.4" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/external-editor": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-2.0.4.tgz", + "integrity": "sha512-Prenuv9C1PHj2Itx0BcAOVBTonz02Hc2Nd2DbU67PdGUaqn0nPCnV34oDyyoaZHnmfRxkpuhh/u51ThkrO+RdA==", + "license": "MIT", + "dependencies": { + "chardet": "^2.1.1", + "iconv-lite": "^0.7.2" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/figures": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-2.0.4.tgz", + "integrity": "sha512-eLBsjlS7rPS3WEhmOmh1znQ5IsQrxWzxWDxO51e4urv+iVrSnIHbq4zqJIOiyNdYLa+BVjwOtdetcQx1lWPpiQ==", + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + } + }, + "node_modules/@inquirer/input": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-5.0.10.tgz", + "integrity": "sha512-nvZ6qEVeX/zVtZ1dY2hTGDQpVGD3R7MYPLODPgKO8Y+RAqxkrP3i/3NwF3fZpLdaMiNuK0z2NaYIx9tPwiSegQ==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.1.7", + "@inquirer/type": "^4.0.4" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/number": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-4.0.10.tgz", + "integrity": "sha512-Ht8OQstxiS3APMGjHV0aYAjRAysidWdwurWEo2i8yI5xbhOBWqizT0+MU1S2GCcuhIBg+3SgWVjEoXgfhY+XaA==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.1.7", + "@inquirer/type": "^4.0.4" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/password": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-5.0.10.tgz", + "integrity": "sha512-QbNyvIE8q2GTqKLYSsA8ATG+eETo+m31DSR0+AU7x3d2FhaTWzqQek80dj3JGTo743kQc6mhBR0erMjYw5jQ0A==", + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.4", + "@inquirer/core": "^11.1.7", + "@inquirer/type": "^4.0.4" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/prompts": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-8.3.2.tgz", + "integrity": "sha512-yFroiSj2iiBFlm59amdTvAcQFvWS6ph5oKESls/uqPBect7rTU2GbjyZO2DqxMGuIwVA8z0P4K6ViPcd/cp+0w==", + "license": "MIT", + "dependencies": { + "@inquirer/checkbox": "^5.1.2", + "@inquirer/confirm": "^6.0.10", + "@inquirer/editor": "^5.0.10", + "@inquirer/expand": "^5.0.10", + "@inquirer/input": "^5.0.10", + "@inquirer/number": "^4.0.10", + "@inquirer/password": "^5.0.10", + "@inquirer/rawlist": "^5.2.6", + "@inquirer/search": "^4.1.6", + "@inquirer/select": "^5.1.2" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/rawlist": { + "version": "5.2.6", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-5.2.6.tgz", + "integrity": "sha512-jfw0MLJ5TilNsa9zlJ6nmRM0ZFVZhhTICt4/6CU2Dv1ndY7l3sqqo1gIYZyMMDw0LvE1u1nzJNisfHEhJIxq5w==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.1.7", + "@inquirer/type": "^4.0.4" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/search": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-4.1.6.tgz", + "integrity": "sha512-3/6kTRae98hhDevENScy7cdFEuURnSpM3JbBNg8yfXLw88HgTOl+neUuy/l9W0No5NzGsLVydhBzTIxZP7yChQ==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.1.7", + "@inquirer/figures": "^2.0.4", + "@inquirer/type": "^4.0.4" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/select": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-5.1.2.tgz", + "integrity": "sha512-kTK8YIkHV+f02y7bWCh7E0u2/11lul5WepVTclr3UMBtBr05PgcZNWfMa7FY57ihpQFQH/spLMHTcr0rXy50tA==", + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.4", + "@inquirer/core": "^11.1.7", + "@inquirer/figures": "^2.0.4", + "@inquirer/type": "^4.0.4" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/type": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-4.0.4.tgz", + "integrity": "sha512-PamArxO3cFJZoOzspzo6cxVlLeIftyBsZw/S9bKY5DzxqJVZgjoj1oP8d0rskKtp7sZxBycsoer1g6UeJV1BBA==", + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@kwsites/file-exists": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz", + "integrity": "sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==", + "license": "MIT", + "dependencies": { + "debug": "^4.1.1" + } + }, + "node_modules/@kwsites/promise-deferred": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@kwsites/promise-deferred/-/promise-deferred-1.1.1.tgz", + "integrity": "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==", + "license": "MIT" + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz", + "integrity": "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1", + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.122.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.122.0.tgz", + "integrity": "sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.12.tgz", + "integrity": "sha512-pv1y2Fv0JybcykuiiD3qBOBdz6RteYojRFY1d+b95WVuzx211CRh+ytI/+9iVyWQ6koTh5dawe4S/yRfOFjgaA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.12.tgz", + "integrity": "sha512-cFYr6zTG/3PXXF3pUO+umXxt1wkRK/0AYT8lDwuqvRC+LuKYWSAQAQZjCWDQpAH172ZV6ieYrNnFzVVcnSflAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.12.tgz", + "integrity": "sha512-ZCsYknnHzeXYps0lGBz8JrF37GpE9bFVefrlmDrAQhOEi4IOIlcoU1+FwHEtyXGx2VkYAvhu7dyBf75EJQffBw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.12.tgz", + "integrity": "sha512-dMLeprcVsyJsKolRXyoTH3NL6qtsT0Y2xeuEA8WQJquWFXkEC4bcu1rLZZSnZRMtAqwtrF/Ib9Ddtpa/Gkge9Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.12.tgz", + "integrity": "sha512-YqWjAgGC/9M1lz3GR1r1rP79nMgo3mQiiA+Hfo+pvKFK1fAJ1bCi0ZQVh8noOqNacuY1qIcfyVfP6HoyBRZ85Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.12.tgz", + "integrity": "sha512-/I5AS4cIroLpslsmzXfwbe5OmWvSsrFuEw3mwvbQ1kDxJ822hFHIx+vsN/TAzNVyepI/j/GSzrtCIwQPeKCLIg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.12.tgz", + "integrity": "sha512-V6/wZztnBqlx5hJQqNWwFdxIKN0m38p8Jas+VoSfgH54HSj9tKTt1dZvG6JRHcjh6D7TvrJPWFGaY9UBVOaWPw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.12.tgz", + "integrity": "sha512-AP3E9BpcUYliZCxa3w5Kwj9OtEVDYK6sVoUzy4vTOJsjPOgdaJZKFmN4oOlX0Wp0RPV2ETfmIra9x1xuayFB7g==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.12.tgz", + "integrity": "sha512-nWwpvUSPkoFmZo0kQazZYOrT7J5DGOJ/+QHHzjvNlooDZED8oH82Yg67HvehPPLAg5fUff7TfWFHQS8IV1n3og==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.12.tgz", + "integrity": "sha512-RNrafz5bcwRy+O9e6P8Z/OCAJW/A+qtBczIqVYwTs14pf4iV1/+eKEjdOUta93q2TsT/FI0XYDP3TCky38LMAg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.12.tgz", + "integrity": "sha512-Jpw/0iwoKWx3LJ2rc1yjFrj+T7iHZn2JDg1Yny1ma0luviFS4mhAIcd1LFNxK3EYu3DHWCps0ydXQ5i/rrJ2ig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.12.tgz", + "integrity": "sha512-vRugONE4yMfVn0+7lUKdKvN4D5YusEiPilaoO2sgUWpCvrncvWgPMzK00ZFFJuiPgLwgFNP5eSiUlv2tfc+lpA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.12.tgz", + "integrity": "sha512-ykGiLr/6kkiHc0XnBfmFJuCjr5ZYKKofkx+chJWDjitX+KsJuAmrzWhwyOMSHzPhzOHOy7u9HlFoa5MoAOJ/Zg==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^1.1.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.12.tgz", + "integrity": "sha512-5eOND4duWkwx1AzCxadcOrNeighiLwMInEADT0YM7xeEOOFcovWZCq8dadXgcRHSf3Ulh1kFo/qvzoFiCLOL1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.12.tgz", + "integrity": "sha512-PyqoipaswDLAZtot351MLhrlrh6lcZPo2LSYE+VDxbVk24LVKAGOuE4hb8xZQmrPAuEtTZW8E6D2zc5EUZX4Lw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.12.tgz", + "integrity": "sha512-HHMwmarRKvoFsJorqYlFeFRzXZqCt2ETQlEDOb9aqssrnVBB1/+xgTGtuTrIk5vzLNX1MjMtTf7W9z3tsSbrxw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitest/expect": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.2.tgz", + "integrity": "sha512-gbu+7B0YgUJ2nkdsRJrFFW6X7NTP44WlhiclHniUhxADQJH5Szt9mZ9hWnJPJ8YwOK5zUOSSlSvyzRf0u1DSBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.2", + "@vitest/utils": "4.1.2", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.2.tgz", + "integrity": "sha512-Ize4iQtEALHDttPRCmN+FKqOl2vxTiNUhzobQFFt/BM1lRUTG7zRCLOykG/6Vo4E4hnUdfVLo5/eqKPukcWW7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.2", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.2.tgz", + "integrity": "sha512-dwQga8aejqeuB+TvXCMzSQemvV9hNEtDDpgUKDzOmNQayl2OG241PSWeJwKRH3CiC+sESrmoFd49rfnq7T4RnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.2.tgz", + "integrity": "sha512-Gr+FQan34CdiYAwpGJmQG8PgkyFVmARK8/xSijia3eTFgVfpcpztWLuP6FttGNfPLJhaZVP/euvujeNYar36OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.2", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.2.tgz", + "integrity": "sha512-g7yfUmxYS4mNxk31qbOYsSt2F4m1E02LFqO53Xpzg3zKMhLAPZAjjfyl9e6z7HrW6LvUdTwAQR3HHfLjpko16A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.2", + "@vitest/utils": "4.1.2", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.2.tgz", + "integrity": "sha512-DU4fBnbVCJGNBwVA6xSToNXrkZNSiw59H8tcuUspVMsBDBST4nfvsPsEHDHGtWRRnqBERBQu7TrTKskmjqTXKA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.2.tgz", + "integrity": "sha512-xw2/TiX82lQHA06cgbqRKFb5lCAy3axQ4H4SoUFhUsg+wztiet+co86IAMDtF6Vm1hc7J6j09oh/rgDn+JdKIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.2", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chardet": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.1.tgz", + "integrity": "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==", + "license": "MIT" + }, + "node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "license": "MIT", + "dependencies": { + "readdirp": "^5.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-spinners": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-3.4.0.tgz", + "integrity": "sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw==", + "license": "MIT", + "engines": { + "node": ">=18.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, + "node_modules/commander": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/es-module-lexer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", + "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", + "dev": true, + "license": "MIT" + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fast-string-truncated-width": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fast-string-truncated-width/-/fast-string-truncated-width-3.0.3.tgz", + "integrity": "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==", + "license": "MIT" + }, + "node_modules/fast-string-width": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/fast-string-width/-/fast-string-width-3.0.2.tgz", + "integrity": "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==", + "license": "MIT", + "dependencies": { + "fast-string-truncated-width": "^3.0.2" + } + }, + "node_modules/fast-wrap-ansi": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/fast-wrap-ansi/-/fast-wrap-ansi-0.2.0.tgz", + "integrity": "sha512-rLV8JHxTyhVmFYhBJuMujcrHqOT2cnO5Zxj37qROj23CP39GXubJRBUFF0z8KFK77Uc0SukZUf7JZhsVEQ6n8w==", + "license": "MIT", + "dependencies": { + "fast-string-width": "^3.0.2" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.5.0.tgz", + "integrity": "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/handlebars": { + "version": "4.7.9", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz", + "integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/inquirer": { + "version": "13.3.2", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-13.3.2.tgz", + "integrity": "sha512-bh/OjBGxNR9qvfQj1n5bxtIF58mbOTp2InN5dKuwUK03dXcDGFsjlDinQRuXMZ4EGiJaFieUWHCAaxH2p7iUBw==", + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.4", + "@inquirer/core": "^11.1.7", + "@inquirer/prompts": "^8.3.2", + "@inquirer/type": "^4.0.4", + "mute-stream": "^3.0.0", + "run-async": "^4.0.6", + "rxjs": "^7.8.2" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/is-interactive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", + "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/log-symbols": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-7.0.1.tgz", + "integrity": "sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==", + "license": "MIT", + "dependencies": { + "is-unicode-supported": "^2.0.0", + "yoctocolors": "^2.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/mute-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-3.0.0.tgz", + "integrity": "sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==", + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "license": "MIT" + }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, + "node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/ora/-/ora-9.3.0.tgz", + "integrity": "sha512-lBX72MWFduWEf7v7uWf5DHp9Jn5BI8bNPGuFgtXMmr2uDz2Gz2749y3am3agSDdkhHPHYmmxEGSKH85ZLGzgXw==", + "license": "MIT", + "dependencies": { + "chalk": "^5.6.2", + "cli-cursor": "^5.0.0", + "cli-spinners": "^3.2.0", + "is-interactive": "^2.0.0", + "is-unicode-supported": "^2.1.0", + "log-symbols": "^7.0.1", + "stdin-discarder": "^0.3.1", + "string-width": "^8.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/readdirp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", + "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/rolldown": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.12.tgz", + "integrity": "sha512-yP4USLIMYrwpPHEFB5JGH1uxhcslv6/hL0OyvTuY+3qlOSJvZ7ntYnoWpehBxufkgN0cvXxppuTu5hHa/zPh+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.122.0", + "@rolldown/pluginutils": "1.0.0-rc.12" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.0-rc.12", + "@rolldown/binding-darwin-arm64": "1.0.0-rc.12", + "@rolldown/binding-darwin-x64": "1.0.0-rc.12", + "@rolldown/binding-freebsd-x64": "1.0.0-rc.12", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.12", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.12", + "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.12", + "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.12", + "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.12", + "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.12", + "@rolldown/binding-linux-x64-musl": "1.0.0-rc.12", + "@rolldown/binding-openharmony-arm64": "1.0.0-rc.12", + "@rolldown/binding-wasm32-wasi": "1.0.0-rc.12", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.12", + "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.12" + } + }, + "node_modules/run-async": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-4.0.6.tgz", + "integrity": "sha512-IoDlSLTs3Yq593mb3ZoKWKXMNu3UpObxhgA/Xuid5p4bbfi2jdY1Hj0m1K+0/tEuQTxIGMhQDqGjKb7RuxGpAQ==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/simple-git": { + "version": "3.33.0", + "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.33.0.tgz", + "integrity": "sha512-D4V/tGC2sjsoNhoMybKyGoE+v8A60hRawKQ1iFRA1zwuDgGZCBJ4ByOzZ5J8joBbi4Oam0qiPH+GhzmSBwbJng==", + "license": "MIT", + "dependencies": { + "@kwsites/file-exists": "^1.1.1", + "@kwsites/promise-deferred": "^1.1.1", + "debug": "^4.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/steveukx/git-js?sponsor=1" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.0.0.tgz", + "integrity": "sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/stdin-discarder": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.3.1.tgz", + "integrity": "sha512-reExS1kSGoElkextOcPkel4NE99S0BWxjUHQeDFnR8S993JxpPX7KU4MNmO19NXhlJp+8dmdCbKQVNgLJh2teA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.0.tgz", + "integrity": "sha512-6hJPQ8N0V0P3SNmP6h2J99RLuzrWz2gvT7VnK5tKvrNqJoyS9W4/Fb8mo31UiPvy00z7DQXkP2hnKBVav76thw==", + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.4.tgz", + "integrity": "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "license": "BSD-2-Clause", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/vite": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.3.tgz", + "integrity": "sha512-B9ifbFudT1TFhfltfaIPgjo9Z3mDynBTJSUYxTjOQruf/zHH+ezCQKcoqO+h7a9Pw9Nm/OtlXAiGT1axBgwqrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.8", + "rolldown": "1.0.0-rc.12", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.0", + "esbuild": "^0.27.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.2.tgz", + "integrity": "sha512-xjR1dMTVHlFLh98JE3i/f/WePqJsah4A0FK9cc8Ehp9Udk0AZk6ccpIZhh1qJ/yxVWRZ+Q54ocnD8TXmkhspGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.2", + "@vitest/mocker": "4.1.2", + "@vitest/pretty-format": "4.1.2", + "@vitest/runner": "4.1.2", + "@vitest/snapshot": "4.1.2", + "@vitest/spy": "4.1.2", + "@vitest/utils": "4.1.2", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.2", + "@vitest/browser-preview": "4.1.2", + "@vitest/browser-webdriverio": "4.1.2", + "@vitest/ui": "4.1.2", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "license": "MIT" + }, + "node_modules/yoctocolors": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.2.tgz", + "integrity": "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/package.json b/package.json index 170da65..9aea0c7 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ }, "scripts": { "start": "node bin/aisona.js", - "test": "echo \"No tests yet\" && exit 0" + "test": "vitest run" }, "keywords": [ "ai", @@ -33,5 +33,18 @@ "homepage": "https://github.com/agents-io/aisona", "bugs": { "url": "https://github.com/agents-io/aisona/issues" + }, + "dependencies": { + "chalk": "^5.6.2", + "chokidar": "^5.0.0", + "commander": "^14.0.3", + "handlebars": "^4.7.9", + "inquirer": "^13.3.2", + "js-yaml": "^4.1.1", + "ora": "^9.3.0", + "simple-git": "^3.33.0" + }, + "devDependencies": { + "vitest": "^4.1.2" } -} \ No newline at end of file +} diff --git a/src/commands/export.js b/src/commands/export.js new file mode 100644 index 0000000..1c3f06c --- /dev/null +++ b/src/commands/export.js @@ -0,0 +1,65 @@ +import path from 'path'; +import chalk from 'chalk'; +import { findAisonaFile, loadAisona } from '../lib/config.js'; +import { exportToTool, exportToAll, getSupportedTools } from '../lib/exporter.js'; + +export async function exportCommand(options) { + const dir = path.resolve(options.dir || process.cwd()); + + console.log(chalk.bold('\n aisona export\n')); + + // Find aisona.yml + const aisonaPath = findAisonaFile(dir); + if (!aisonaPath) { + console.log(chalk.red(' No aisona.yml found. Run: aisona init\n')); + process.exit(1); + } + + const aisona = loadAisona(aisonaPath); + console.log(chalk.dim(` Using: ${aisonaPath}\n`)); + + if (options.to) { + // Export to specific tool + const toolId = options.to.toLowerCase(); + const supported = getSupportedTools(); + + if (!supported.includes(toolId)) { + console.log(chalk.red(` Unknown tool: ${toolId}`)); + console.log(chalk.dim(` Supported: ${supported.join(', ')}\n`)); + process.exit(1); + } + + const result = exportToTool(aisona, toolId, dir); + if (result.success) { + console.log(chalk.green(` Exported to ${result.tool} → ${result.path}\n`)); + } else { + console.log(chalk.red(` Failed: ${result.error}\n`)); + } + } else if (options.all) { + // Export to all enabled tools + const results = exportToAll(aisona, dir); + + if (results.length === 0) { + console.log(chalk.yellow(' No tools enabled in aisona.yml.\n')); + return; + } + + const succeeded = results.filter(r => r.success); + const failed = results.filter(r => !r.success); + + for (const r of succeeded) { + console.log(chalk.green(` ✓ ${r.tool} → ${r.path}`)); + } + for (const r of failed) { + console.log(chalk.red(` ✗ ${r.tool}: ${r.error}`)); + } + + console.log(chalk.dim(`\n Exported to ${succeeded.length} tool(s)\n`)); + } else { + console.log(chalk.yellow(' Specify --to or --all\n')); + console.log(chalk.dim(' Examples:')); + console.log(chalk.dim(' aisona export --to claude')); + console.log(chalk.dim(' aisona export --to cursor')); + console.log(chalk.dim(' aisona export --all\n')); + } +} diff --git a/src/commands/init.js b/src/commands/init.js new file mode 100644 index 0000000..948fce8 --- /dev/null +++ b/src/commands/init.js @@ -0,0 +1,116 @@ +import fs from 'fs'; +import path from 'path'; +import chalk from 'chalk'; +import inquirer from 'inquirer'; +import { detectTools, parseClaudeMd, parseCursorRules } from '../lib/parser.js'; +import { findAisonaFile, getDefaultAisona, saveAisona } from '../lib/config.js'; + +export async function initCommand(options) { + const dir = path.resolve(options.dir || process.cwd()); + const home = process.env.HOME; + + console.log(chalk.bold('\n aisona init\n')); + console.log(chalk.dim(' Own your AI\'s persona. Define it once, use it everywhere.\n')); + + // Check if aisona.yml already exists + const existing = findAisonaFile(dir); + if (existing) { + const { overwrite } = await inquirer.prompt([{ + type: 'confirm', + name: 'overwrite', + message: `aisona.yml already exists at ${existing}. Overwrite?`, + default: false, + }]); + if (!overwrite) { + console.log(chalk.yellow(' Aborted.')); + return; + } + } + + // Detect existing AI tool configs + console.log(chalk.blue(' Scanning for existing AI tool configs...\n')); + const detected = detectTools(dir); + + if (detected.length === 0) { + console.log(chalk.yellow(' No existing AI tool configs found.')); + console.log(chalk.dim(' Creating a blank aisona.yml for you to fill in.\n')); + } else { + console.log(chalk.green(` Found ${detected.length} config(s):\n`)); + for (const d of detected) { + console.log(` ${chalk.cyan(d.tool)} (${d.scope}) → ${chalk.dim(d.path)}`); + } + console.log(); + } + + // Start with default structure + const aisona = getDefaultAisona(); + + // If --from specified, import from that tool + const importFrom = options.from; + const claudeConfig = detected.find(d => d.tool === 'claude'); + + if (importFrom === 'claude' || (!importFrom && claudeConfig)) { + const claudePath = importFrom === 'claude' + ? claudeConfig?.path || path.join(home, '.claude', 'CLAUDE.md') + : claudeConfig.path; + + if (fs.existsSync(claudePath)) { + console.log(chalk.blue(` Importing from ${claudePath}...\n`)); + const parsed = parseClaudeMd(claudePath); + + // Merge parsed data into aisona + if (parsed.preferences.tone) aisona.preferences.tone = parsed.preferences.tone; + if (parsed.preferences.habits.length) aisona.preferences.habits = parsed.preferences.habits; + if (parsed.preferences.teaching) aisona.preferences.teaching = parsed.preferences.teaching; + if (parsed.preferences.autonomy) aisona.preferences.autonomy = parsed.preferences.autonomy; + if (parsed.rules.length) aisona.rules = parsed.rules; + if (parsed.memories.length) aisona.memories = parsed.memories; + + console.log(chalk.green(` Imported: ${parsed.rules.length} rules, ${parsed.preferences.habits.length} habits, ${parsed.memories.length} memories\n`)); + } + } + + // Interactive: ask for basic identity info if not imported + if (!aisona.identity.name) { + const answers = await inquirer.prompt([ + { type: 'input', name: 'name', message: 'Your name (or alias):', default: process.env.USER || '' }, + { type: 'input', name: 'role', message: 'Your role:', default: 'Software developer' }, + { type: 'input', name: 'language', message: 'Preferred AI response language:', default: 'English' }, + ]); + aisona.identity.name = answers.name; + aisona.identity.role = answers.role; + aisona.identity.language = answers.language; + } + + // Ask which tools to enable + const toolChoices = ['claude', 'cursor', 'gemini', 'copilot', 'windsurf']; + const detectedToolIds = detected.map(d => d.tool); + const defaultEnabled = toolChoices.filter(t => detectedToolIds.includes(t)); + + const { enabledTools } = await inquirer.prompt([{ + type: 'checkbox', + name: 'enabledTools', + message: 'Which tools do you want to export to?', + choices: toolChoices.map(t => ({ + name: t + (detectedToolIds.includes(t) ? chalk.dim(' (detected)') : ''), + value: t, + checked: defaultEnabled.includes(t) || t === 'claude' || t === 'cursor', + })), + }]); + + for (const tool of toolChoices) { + if (aisona.tools[tool]) { + aisona.tools[tool].enabled = enabledTools.includes(tool); + } + } + + // Save + const outPath = path.join(dir, 'aisona.yml'); + saveAisona(outPath, aisona); + + console.log(chalk.green(`\n Created ${outPath}\n`)); + console.log(chalk.dim(' Next steps:')); + console.log(chalk.dim(' 1. Edit aisona.yml to refine your persona')); + console.log(chalk.dim(' 2. Run: aisona export --all')); + console.log(chalk.dim(' 3. Run: aisona watch --git (auto-sync)\n')); +} diff --git a/src/commands/status.js b/src/commands/status.js new file mode 100644 index 0000000..9a36ec5 --- /dev/null +++ b/src/commands/status.js @@ -0,0 +1,63 @@ +import path from 'path'; +import chalk from 'chalk'; +import { findAisonaFile, loadAisona } from '../lib/config.js'; +import { detectTools } from '../lib/parser.js'; +import { getSupportedTools } from '../lib/exporter.js'; + +export async function statusCommand(options) { + const dir = path.resolve(options.dir || process.cwd()); + + console.log(chalk.bold('\n aisona status\n')); + + // Check for aisona.yml + const aisonaPath = findAisonaFile(dir); + if (!aisonaPath) { + console.log(chalk.yellow(' No aisona.yml found. Run: aisona init\n')); + + // Still show detected tools + const detected = detectTools(dir); + if (detected.length > 0) { + console.log(chalk.blue(' Detected AI tool configs:\n')); + for (const d of detected) { + console.log(` ${chalk.cyan(d.tool)} (${d.scope}) → ${chalk.dim(d.path)}`); + } + console.log(); + } + return; + } + + const aisona = loadAisona(aisonaPath); + + // Persona summary + console.log(chalk.blue(' Persona:')); + if (aisona.identity?.name) console.log(` Name: ${aisona.identity.name}`); + if (aisona.identity?.role) console.log(` Role: ${aisona.identity.role}`); + if (aisona.identity?.language) console.log(` Language: ${aisona.identity.language}`); + console.log(` Rules: ${aisona.rules?.length || 0}`); + console.log(` Preferences: ${aisona.preferences?.length || 0}`); + console.log(` Memories: ${aisona.memories?.length || 0}`); + console.log(); + + // Tool status + console.log(chalk.blue(' Tools:')); + const supported = getSupportedTools(); + for (const toolId of supported) { + const config = aisona.tools?.[toolId]; + const enabled = config?.enabled !== false; + const icon = enabled ? chalk.green('✓') : chalk.dim('○'); + console.log(` ${icon} ${toolId}${!enabled ? chalk.dim(' (disabled)') : ''}`); + } + console.log(); + + // Detected configs + const detected = detectTools(dir); + if (detected.length > 0) { + console.log(chalk.blue(' Detected existing configs:')); + for (const d of detected) { + console.log(` ${chalk.cyan(d.tool)} → ${chalk.dim(d.path)}`); + } + console.log(); + } + + console.log(chalk.dim(` aisona.yml: ${aisonaPath}\n`)); +} diff --git a/src/commands/watch.js b/src/commands/watch.js new file mode 100644 index 0000000..658eb27 --- /dev/null +++ b/src/commands/watch.js @@ -0,0 +1,111 @@ +import path from 'path'; +import chalk from 'chalk'; +import chokidar from 'chokidar'; +import { simpleGit } from 'simple-git'; +import { findAisonaFile, loadAisona } from '../lib/config.js'; +import { exportToAll } from '../lib/exporter.js'; + +export async function watchCommand(options) { + const dir = path.resolve(options.dir || process.cwd()); + const useGit = options.git || false; + + console.log(chalk.bold('\n aisona watch\n')); + + const aisonaPath = findAisonaFile(dir); + if (!aisonaPath) { + console.log(chalk.red(' No aisona.yml found. Run: aisona init\n')); + process.exit(1); + } + + console.log(chalk.dim(` Watching: ${aisonaPath}`)); + if (useGit) console.log(chalk.dim(' Git auto-commit: enabled')); + console.log(chalk.dim(' Press Ctrl+C to stop.\n')); + + // Do initial export + await doExport(aisonaPath, dir, useGit); + + // Watch for changes + let debounceTimer = null; + + const watcher = chokidar.watch(aisonaPath, { + persistent: true, + ignoreInitial: true, + }); + + watcher.on('change', () => { + // Debounce: wait 2s after last change before exporting + if (debounceTimer) clearTimeout(debounceTimer); + debounceTimer = setTimeout(() => { + doExport(aisonaPath, dir, useGit); + }, 2000); + }); + + // Keep process alive + process.on('SIGINT', () => { + console.log(chalk.dim('\n Stopped watching.\n')); + watcher.close(); + process.exit(0); + }); +} + +async function doExport(aisonaPath, dir, useGit) { + try { + const aisona = loadAisona(aisonaPath); + const results = exportToAll(aisona, dir); + + const succeeded = results.filter(r => r.success); + const timestamp = new Date().toLocaleTimeString(); + + if (succeeded.length > 0) { + const tools = succeeded.map(r => r.tool).join(', '); + console.log(chalk.green(` [${timestamp}] Exported to: ${tools}`)); + + if (useGit) { + await gitCommitAndPush(dir, succeeded); + } + } + } catch (err) { + console.log(chalk.red(` Error: ${err.message}`)); + } +} + +async function gitCommitAndPush(dir, exportResults) { + try { + const git = simpleGit(dir); + + // Check if we're in a git repo + const isRepo = await git.checkIsRepo(); + if (!isRepo) { + console.log(chalk.dim(' (not a git repo — skipping git sync)')); + return; + } + + // Stage exported files + aisona.yml + const files = ['aisona.yml', ...exportResults.map(r => path.relative(dir, r.path))]; + await git.add(files); + + // Check if there are staged changes + const status = await git.status(); + if (status.staged.length === 0) { + return; // Nothing to commit + } + + // Commit + const tools = exportResults.map(r => r.tool).join(', '); + await git.commit(`aisona: auto-sync persona to ${tools}`); + console.log(chalk.dim(' git: committed')); + + // Push if remote exists + try { + const remotes = await git.getRemotes(true); + if (remotes.length > 0) { + await git.push(); + console.log(chalk.dim(' git: pushed')); + } + } catch { + // Push failed — probably no remote or auth issue. Silently skip. + } + } catch (err) { + console.log(chalk.dim(` git: ${err.message}`)); + } +} diff --git a/src/index.js b/src/index.js index 266e19f..6167f86 100644 --- a/src/index.js +++ b/src/index.js @@ -1 +1,3 @@ -export const version = '0.0.1'; +export { loadAisona, saveAisona, findAisonaFile, getDefaultAisona } from './lib/config.js'; +export { exportToTool, exportToAll, getSupportedTools } from './lib/exporter.js'; +export { detectTools, parseClaudeMd, parseCursorRules } from './lib/parser.js'; diff --git a/src/lib/config.js b/src/lib/config.js new file mode 100644 index 0000000..eed85ec --- /dev/null +++ b/src/lib/config.js @@ -0,0 +1,60 @@ +import fs from 'fs'; +import path from 'path'; +import yaml from 'js-yaml'; + +const AISONA_FILE = 'aisona.yml'; + +export function findAisonaFile(dir) { + const filePath = path.join(dir, AISONA_FILE); + if (fs.existsSync(filePath)) return filePath; + + // Also check ~/.aisona/ + const globalPath = path.join(process.env.HOME, '.aisona', AISONA_FILE); + if (fs.existsSync(globalPath)) return globalPath; + + return null; +} + +export function loadAisona(filePath) { + const raw = fs.readFileSync(filePath, 'utf8'); + return yaml.load(raw); +} + +export function saveAisona(filePath, data) { + const out = yaml.dump(data, { + lineWidth: 120, + noRefs: true, + quotingType: '"', + forceQuotes: false, + }); + fs.mkdirSync(path.dirname(filePath), { recursive: true }); + fs.writeFileSync(filePath, out, 'utf8'); +} + +export function getDefaultAisona() { + return { + version: 1, + identity: { + name: '', + role: '', + experience: '', + language: 'English', + }, + preferences: { + tone: '', + verbosity: 'balanced', + autonomy: '', + teaching: '', + habits: [], + }, + rules: [], + memories: [], + tools: { + claude: { enabled: true, extra: [] }, + cursor: { enabled: true, extra: [] }, + gemini: { enabled: false, extra: [] }, + copilot: { enabled: false, extra: [] }, + windsurf: { enabled: false, extra: [] }, + }, + }; +} diff --git a/src/lib/exporter.js b/src/lib/exporter.js new file mode 100644 index 0000000..1c40b1b --- /dev/null +++ b/src/lib/exporter.js @@ -0,0 +1,87 @@ +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; +import Handlebars from 'handlebars'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const TEMPLATES_DIR = path.join(__dirname, '..', 'templates'); + +// Map tool id → output file path (relative to project root or home) +const TOOL_OUTPUTS = { + claude: { file: 'CLAUDE.md', globalFile: '.claude/CLAUDE.md' }, + cursor: { file: '.cursorrules' }, + gemini: { file: 'GEMINI.md', globalFile: '.gemini/GEMINI.md' }, + copilot: { file: '.github/copilot-instructions.md' }, + windsurf: { file: '.windsurfrules' }, +}; + +function loadTemplate(toolId) { + const templatePath = path.join(TEMPLATES_DIR, `${toolId}.hbs`); + if (!fs.existsSync(templatePath)) { + return null; + } + const raw = fs.readFileSync(templatePath, 'utf8'); + return Handlebars.compile(raw, { noEscape: true }); +} + +/** + * Clean up template output — remove excessive blank lines + */ +function cleanOutput(text) { + return text + .replace(/\n{3,}/g, '\n\n') // max 2 consecutive newlines + .trim() + '\n'; +} + +/** + * Export aisona data to a specific tool's config file + */ +export function exportToTool(aisona, toolId, dir) { + // Check enabled BEFORE loading template + const toolConfig = aisona.tools?.[toolId]; + if (toolConfig && toolConfig.enabled === false) { + return { success: false, error: `Tool ${toolId} is disabled in aisona.yml` }; + } + + const template = loadTemplate(toolId); + if (!template) { + return { success: false, error: `No template found for tool: ${toolId}` }; + } + + const output = cleanOutput(template(aisona)); + + const toolOutput = TOOL_OUTPUTS[toolId]; + if (!toolOutput) { + return { success: false, error: `Unknown output path for tool: ${toolId}` }; + } + + const outPath = path.join(dir, toolOutput.file); + fs.mkdirSync(path.dirname(outPath), { recursive: true }); + fs.writeFileSync(outPath, output, 'utf8'); + + return { success: true, path: outPath, tool: toolId }; +} + +/** + * Export to all enabled tools + */ +export function exportToAll(aisona, dir) { + const results = []; + + for (const [toolId, config] of Object.entries(aisona.tools || {})) { + if (config.enabled === false) continue; + if (!TOOL_OUTPUTS[toolId]) continue; + + const result = exportToTool(aisona, toolId, dir); + results.push(result); + } + + return results; +} + +/** + * Get list of supported tools + */ +export function getSupportedTools() { + return Object.keys(TOOL_OUTPUTS); +} diff --git a/src/lib/parser.js b/src/lib/parser.js new file mode 100644 index 0000000..7140562 --- /dev/null +++ b/src/lib/parser.js @@ -0,0 +1,255 @@ +/** + * Parse existing AI tool configs into aisona.yml structure. + * This is the "import" logic — reads CLAUDE.md, .cursorrules, etc. + * and extracts personality, rules, and preferences. + */ + +import fs from 'fs'; +import path from 'path'; + +/** + * Parse a CLAUDE.md file into structured sections. + * CLAUDE.md is free-form markdown, so we use heuristics: + * - H2/H3 headers = section boundaries + * - Bullet points (- or *) = extractable items + * - Numbered lists (1. 2. 3.) = extractable items + * - Prose paragraphs = context/description + */ +export function parseClaudeMd(filePath) { + const content = fs.readFileSync(filePath, 'utf8'); + const lines = content.split('\n'); + + const result = { + preferences: { tone: '', habits: [], teaching: '', autonomy: '' }, + rules: [], + memories: [], + raw_sections: {}, + }; + + let currentSection = '_preamble'; + + for (const line of lines) { + // Detect H2 or H3 headers as section boundaries + const headerMatch = line.match(/^#{2,3}\s+(.+)/); + if (headerMatch) { + currentSection = headerMatch[1].trim().toLowerCase(); + if (!result.raw_sections[currentSection]) { + result.raw_sections[currentSection] = []; + } + continue; + } + + if (!result.raw_sections[currentSection]) { + result.raw_sections[currentSection] = []; + } + result.raw_sections[currentSection].push(line); + } + + // Section classification — ordered by specificity (most specific first) + const sectionMap = [ + { keywords: ['teaching style', 'teaching angle'], category: 'teaching' }, + { keywords: ['technical explanations', 'explanations'], category: 'teaching' }, + { keywords: ['language'], category: 'language' }, + { keywords: ['tone and style', 'tone', 'communication', 'personality'], category: 'personality' }, + { keywords: ['autonomy'], category: 'autonomy' }, + { keywords: ['rules', 'constraints', 'boundaries', 'hard rules'], category: 'rules' }, + { keywords: ['style', 'code style', 'coding style', 'conventions', 'preferences'], category: 'preferences' }, + { keywords: ['memory', 'memories', 'learned', 'context', 'project knowledge'], category: 'memories' }, + ]; + + function classifySection(sectionName) { + for (const entry of sectionMap) { + if (entry.keywords.some(k => sectionName.includes(k))) { + return entry.category; + } + } + return 'unknown'; + } + + for (const [section, sectionLines] of Object.entries(result.raw_sections)) { + if (section === '_preamble') continue; + + const category = classifySection(section); + + // Extract bullet points (- item, * item) and numbered lists (1. item) + const bullets = sectionLines + .filter(l => l.match(/^\s*[-*]\s+/) || l.match(/^\s*\d+\.\s+/)) + .map(l => l.replace(/^\s*[-*]\s+/, '').replace(/^\s*\d+\.\s+/, '').trim()) + .filter(l => l.length > 0 && !l.match(/^\*\*[^*]+\*\*$/)); // skip standalone bold headers + + // Extract prose — non-bullet, non-header, non-empty lines + const proseLines = sectionLines + .filter(l => + !l.match(/^\s*[-*]\s+/) && + !l.match(/^\s*\d+\.\s+/) && + !l.match(/^#{1,4}\s+/) && + !l.match(/^---\s*$/) && + l.trim().length > 0 + ) + .map(l => l.trim()); + + // First meaningful prose line (for short descriptions) + const firstProse = proseLines[0] || ''; + // Full prose (for longer sections) + const fullProse = proseLines.join(' ').trim(); + + switch (category) { + case 'language': + result.preferences.tone = firstProse || fullProse; + result.preferences.habits.push(...bullets); + break; + + case 'personality': + if (fullProse) result.preferences.tone = fullProse; + result.preferences.habits.push(...bullets); + break; + + case 'teaching': + // For teaching, extract first paragraph as summary, bullets as preferences + if (firstProse) { + // If we already have teaching, append. Otherwise set. + result.preferences.teaching = result.preferences.teaching + ? result.preferences.teaching + ' ' + firstProse + : firstProse; + } + // Bullets from teaching sections go to preferences (they're style guides) + result.preferences.habits.push(...bullets); + break; + + case 'autonomy': { + // Autonomy sections mix prose and rules. Extract both. + // Prose lines that start with "Always" or "Never" or "Only" are rules + const autonomyRules = proseLines.filter(l => + l.match(/^(Always|Never|Only|Do not|Don't)\b/i) + ); + const autonomyProse = proseLines.filter(l => + !l.match(/^(Always|Never|Only|Do not|Don't)\b/i) + ).join(' ').trim(); + + if (autonomyProse) result.preferences.autonomy = autonomyProse; + result.rules.push(...autonomyRules); + result.rules.push(...bullets); + break; + } + + case 'rules': + result.rules.push(...bullets); + if (fullProse && !bullets.length) { + // If rules section has prose but no bullets, treat prose as a rule + result.rules.push(fullProse); + } + break; + + case 'preferences': + result.preferences.habits.push(...bullets); + break; + + case 'memories': + result.memories.push(...bullets); + if (fullProse && !bullets.length) { + result.memories.push(fullProse); + } + break; + + case 'unknown': + default: + // Unknown sections — bullets go to preferences, prose to memories + if (bullets.length > 0) { + result.preferences.habits.push(...bullets); + } + break; + } + } + + return result; +} + +/** + * Parse .cursorrules (simple markdown, usually just rules) + */ +export function parseCursorRules(filePath) { + const content = fs.readFileSync(filePath, 'utf8'); + const lines = content.split('\n'); + + const rules = lines + .filter(l => l.match(/^[-*]\s+/)) + .map(l => l.replace(/^[-*]\s+/, '').trim()) + .filter(l => l.length > 0); + + // Non-bullet lines = prose context + const prose = lines + .filter(l => !l.match(/^[-*]\s+/) && !l.match(/^#/) && l.trim().length > 0) + .map(l => l.trim()) + .join(' ') + .trim(); + + return { rules, context: prose }; +} + +/** + * Parse GEMINI.md (same format as CLAUDE.md essentially) + */ +export function parseGeminiMd(filePath) { + return parseClaudeMd(filePath); // Same markdown format +} + +/** + * Detect which AI tool configs exist in a directory + */ +export function detectTools(dir) { + const home = process.env.HOME; + const detected = []; + + // Claude Code + const claudePaths = [ + path.join(home, '.claude', 'CLAUDE.md'), + path.join(dir, 'CLAUDE.md'), + ]; + for (const p of claudePaths) { + if (fs.existsSync(p)) { + detected.push({ tool: 'claude', path: p, scope: p.includes(home + '/.claude') ? 'global' : 'project' }); + } + } + + // Cursor + const cursorPaths = [ + path.join(dir, '.cursorrules'), + path.join(dir, '.cursor', 'rules'), + ]; + for (const p of cursorPaths) { + if (fs.existsSync(p)) { + detected.push({ tool: 'cursor', path: p, scope: 'project' }); + } + } + + // Gemini + const geminiPaths = [ + path.join(home, '.gemini', 'GEMINI.md'), + path.join(dir, 'GEMINI.md'), + ]; + for (const p of geminiPaths) { + if (fs.existsSync(p)) { + detected.push({ tool: 'gemini', path: p, scope: p.includes(home + '/.gemini') ? 'global' : 'project' }); + } + } + + // Copilot + const copilotPath = path.join(dir, '.github', 'copilot-instructions.md'); + if (fs.existsSync(copilotPath)) { + detected.push({ tool: 'copilot', path: copilotPath, scope: 'project' }); + } + + // Windsurf + const windsurfPath = path.join(dir, '.windsurfrules'); + if (fs.existsSync(windsurfPath)) { + detected.push({ tool: 'windsurf', path: windsurfPath, scope: 'project' }); + } + + // AGENTS.md + const agentsPath = path.join(dir, 'AGENTS.md'); + if (fs.existsSync(agentsPath)) { + detected.push({ tool: 'agentsmd', path: agentsPath, scope: 'project' }); + } + + return detected; +} diff --git a/src/templates/claude.hbs b/src/templates/claude.hbs new file mode 100644 index 0000000..950765b --- /dev/null +++ b/src/templates/claude.hbs @@ -0,0 +1,57 @@ +{{! Generated by aisona — https://github.com/agents-io/aisona }} +{{! Source: aisona.yml | Do not edit directly. }} + +{{#if identity.language}} +## Language + +{{identity.language}} +{{/if}} + +{{#if preferences.tone}} +## Tone and Style + +{{preferences.tone}} +{{#if preferences.verbosity}} +Verbosity: {{preferences.verbosity}} +{{/if}} + +{{#each preferences.habits}} +- {{this}} +{{/each}} +{{/if}} + +{{#if preferences.teaching}} +## Teaching Style + +{{preferences.teaching}} +{{/if}} + +{{#if preferences.autonomy}} +## Autonomy + +{{preferences.autonomy}} +{{/if}} + +{{#if rules.length}} +## Rules + +{{#each rules}} +- {{this}} +{{/each}} +{{/if}} + +{{#if memories.length}} +## Context + +{{#each memories}} +- {{this}} +{{/each}} +{{/if}} + +{{#if tools.claude.extra.length}} +## Claude-Specific + +{{#each tools.claude.extra}} +- {{this}} +{{/each}} +{{/if}} diff --git a/src/templates/copilot.hbs b/src/templates/copilot.hbs new file mode 100644 index 0000000..d2d98bd --- /dev/null +++ b/src/templates/copilot.hbs @@ -0,0 +1,28 @@ +{{! Generated by aisona — https://github.com/agents-io/aisona }} +{{! Source: aisona.yml | Do not edit directly. }} + +{{#if identity.language}} +Respond in {{identity.language}}. +{{/if}} + +{{#if preferences.tone}} +## Style +{{preferences.tone}} +{{#each preferences.habits}} +- {{this}} +{{/each}} +{{/if}} + +{{#if rules.length}} +## Rules +{{#each rules}} +- {{this}} +{{/each}} +{{/if}} + +{{#if tools.copilot.extra.length}} +## Copilot-Specific +{{#each tools.copilot.extra}} +- {{this}} +{{/each}} +{{/if}} diff --git a/src/templates/cursor.hbs b/src/templates/cursor.hbs new file mode 100644 index 0000000..824c92f --- /dev/null +++ b/src/templates/cursor.hbs @@ -0,0 +1,39 @@ +{{! Generated by aisona — https://github.com/agents-io/aisona }} +{{! Source: aisona.yml | Do not edit directly. }} + +{{#if identity.role}} +# {{identity.role}} +{{/if}} + +{{#if identity.language}} +Always respond in {{identity.language}}. +{{/if}} + +{{#if preferences.tone}} +## Style +{{preferences.tone}} +{{#each preferences.habits}} +- {{this}} +{{/each}} +{{/if}} + +{{#if rules.length}} +## Rules +{{#each rules}} +- {{this}} +{{/each}} +{{/if}} + +{{#if memories.length}} +## Context +{{#each memories}} +- {{this}} +{{/each}} +{{/if}} + +{{#if tools.cursor.extra.length}} +## Cursor-Specific +{{#each tools.cursor.extra}} +- {{this}} +{{/each}} +{{/if}} diff --git a/src/templates/gemini.hbs b/src/templates/gemini.hbs new file mode 100644 index 0000000..de53dc9 --- /dev/null +++ b/src/templates/gemini.hbs @@ -0,0 +1,40 @@ +{{! Generated by aisona — https://github.com/agents-io/aisona }} +{{! Source: aisona.yml | Do not edit directly. }} + +{{#if identity.language}} +Respond in {{identity.language}}. +{{/if}} + +{{#if identity.role}} +## About the User +{{identity.role}}{{#if identity.experience}} ({{identity.experience}}){{/if}} +{{/if}} + +{{#if preferences.tone}} +## Communication +{{preferences.tone}} +{{#each preferences.habits}} +- {{this}} +{{/each}} +{{/if}} + +{{#if rules.length}} +## Rules +{{#each rules}} +- {{this}} +{{/each}} +{{/if}} + +{{#if memories.length}} +## Context +{{#each memories}} +- {{this}} +{{/each}} +{{/if}} + +{{#if tools.gemini.extra.length}} +## Gemini-Specific +{{#each tools.gemini.extra}} +- {{this}} +{{/each}} +{{/if}} diff --git a/test-output/.cursorrules b/test-output/.cursorrules new file mode 100644 index 0000000..eed1b49 --- /dev/null +++ b/test-output/.cursorrules @@ -0,0 +1,28 @@ +# Role: Backend engineer + +Always respond in Cantonese (廣東話). + +## Communication +Always reply in Cantonese (廣東話) unless the user writes in another language or explicitly asks for English. + +## Rules +- Destructive actions: deleting files, dropping DB tables, `reset --hard`, force push to shared branches +- Actions visible to others: pushing to remote, opening/closing PRs, sending messages + +## Preferences +- Show where data comes from and where it goes +- Name the actual tables, functions, fields involved +- If there's a queue/worker, explain who creates the job and who picks it up +- If there's branching, explain each branch +- Do not skip the hidden middle steps +- Do not jump from input to output too quickly +- Do not say "this basically handles it" without explaining how +- **API change** → request/response flow, schema validation, where logic lives, how layers connect +- **DB / migration / schema** → why the column/table/enum matters, what breaks without it, how it propagates to the API layer +- **Bug fix** → what category of bug it was, why it happened, what general debugging lesson applies +- **Refactor** → what design problem existed, what principle the new structure improves +- **Cross-repo / multi-service work** → why a change in one repo requires a matching change in another, how data flows between services +- Short and clear — 3–8 sentences or a tight bullet list is usually enough +- Never skip teaching just because the task felt simple +- Do not re-explain things the user has clearly already understood +- One reusable concept or pattern per task is the minimum diff --git a/test-output/.github/copilot-instructions.md b/test-output/.github/copilot-instructions.md new file mode 100644 index 0000000..4f01798 --- /dev/null +++ b/test-output/.github/copilot-instructions.md @@ -0,0 +1,26 @@ +Respond in Cantonese (廣東話). + +## Style +Always reply in Cantonese (廣東話) unless the user writes in another language or explicitly asks for English. + +## Rules +- Destructive actions: deleting files, dropping DB tables, `reset --hard`, force push to shared branches +- Actions visible to others: pushing to remote, opening/closing PRs, sending messages + +## Preferences +- Show where data comes from and where it goes +- Name the actual tables, functions, fields involved +- If there's a queue/worker, explain who creates the job and who picks it up +- If there's branching, explain each branch +- Do not skip the hidden middle steps +- Do not jump from input to output too quickly +- Do not say "this basically handles it" without explaining how +- **API change** → request/response flow, schema validation, where logic lives, how layers connect +- **DB / migration / schema** → why the column/table/enum matters, what breaks without it, how it propagates to the API layer +- **Bug fix** → what category of bug it was, why it happened, what general debugging lesson applies +- **Refactor** → what design problem existed, what principle the new structure improves +- **Cross-repo / multi-service work** → why a change in one repo requires a matching change in another, how data flows between services +- Short and clear — 3–8 sentences or a tight bullet list is usually enough +- Never skip teaching just because the task felt simple +- Do not re-explain things the user has clearly already understood +- One reusable concept or pattern per task is the minimum diff --git a/test-output/CLAUDE.md b/test-output/CLAUDE.md new file mode 100644 index 0000000..b8ff8f7 --- /dev/null +++ b/test-output/CLAUDE.md @@ -0,0 +1,44 @@ +## Language + +Cantonese (廣東話) + +## Tone and Style + +Always reply in Cantonese (廣東話) unless the user writes in another language or explicitly asks for English. + +## Teaching Style + +The user is a junior developer (~11 months experience) working in the AI era. Because AI makes implementation fast, there is a real risk of "done but nothing learned". The goal is to make sure every task also builds real engineering knowledge. Do not assume "done" means "learned". Completing the task is not enough — help the user understand what this task is an example of, so they could reason about similar problems themselves next time without AI. **During the task** — if a step involves a concept worth noting, mention it inline. For example, if touching a Pydantic schema, briefly note what role schemas play. If writing a migration, briefly note why it exists. Keep these inline comments short (1–2 sentences), not a full lesson. **After the task** — end every completed task with a learning section. Even simple tasks have teachable patterns. Never skip this. After finishing, cover these points (not all need to be long — tailor depth to complexity): 1. **What changed** — which files, which layers, what was added or modified 2. **What problem it solves** — why this change was needed 3. **How the flow works step by step** — trace data from input to output through each layer 4. **Why this implementation** — why this approach instead of alternatives 5. **The concept or pattern** — what engineering idea this task is an example of 6. **Tradeoffs / alternatives** — what the downside of this approach is, or what another option would have been 7. **What to remember next time** — one reusable mental model or rule of thumb For different task types, emphasise: Talk like a helpful senior engineer — direct, concrete, practical. Not a textbook. Not overly formal. Use analogies when the concept is abstract. The goal is reusable mental models, not just a task report. Frame the lesson as: "If you had to do this yourself without AI, here is what you would need to understand." --- + +## Autonomy + +Work autonomously. Only ask for confirmation before: Always ask before: committing, pushing to remote, opening/closing PRs, and any destructive CLI commands (rm, rmdir, kill, pkill, truncate, dd, mkfs, chmod -R, chown -R, or anything that permanently deletes/overwrites data). Never ask before: reading files, searching, grep, find, ls, cat, editing files, running tests, installing packages. Just do it and report what you did. + +## Rules + +- Destructive actions: deleting files, dropping DB tables, `reset --hard`, force push to shared branches +- Actions visible to others: pushing to remote, opening/closing PRs, sending messages + +## Preferences + +- Show where data comes from and where it goes +- Name the actual tables, functions, fields involved +- If there's a queue/worker, explain who creates the job and who picks it up +- If there's branching, explain each branch +- Do not skip the hidden middle steps +- Do not jump from input to output too quickly +- Do not say "this basically handles it" without explaining how +- **API change** → request/response flow, schema validation, where logic lives, how layers connect +- **DB / migration / schema** → why the column/table/enum matters, what breaks without it, how it propagates to the API layer +- **Bug fix** → what category of bug it was, why it happened, what general debugging lesson applies +- **Refactor** → what design problem existed, what principle the new structure improves +- **Cross-repo / multi-service work** → why a change in one repo requires a matching change in another, how data flows between services +- Short and clear — 3–8 sentences or a tight bullet list is usually enough +- Never skip teaching just because the task felt simple +- Do not re-explain things the user has clearly already understood +- One reusable concept or pattern per task is the minimum + +## Claude-Specific + +- End every task with a learning section +- Explain like stepping through a debugger diff --git a/test-output/GEMINI.md b/test-output/GEMINI.md new file mode 100644 index 0000000..51565c5 --- /dev/null +++ b/test-output/GEMINI.md @@ -0,0 +1,37 @@ +## Language + +Respond in Cantonese (廣東話). + +## About the User + +Role: Backend engineer +Experience: 11 months +Context: AI security control plane, side projects @agents-io + +## Communication Style + +Always reply in Cantonese (廣東話) unless the user writes in another language or explicitly asks for English. + +## Rules + +- Destructive actions: deleting files, dropping DB tables, `reset --hard`, force push to shared branches +- Actions visible to others: pushing to remote, opening/closing PRs, sending messages + +## Preferences + +- Show where data comes from and where it goes +- Name the actual tables, functions, fields involved +- If there's a queue/worker, explain who creates the job and who picks it up +- If there's branching, explain each branch +- Do not skip the hidden middle steps +- Do not jump from input to output too quickly +- Do not say "this basically handles it" without explaining how +- **API change** → request/response flow, schema validation, where logic lives, how layers connect +- **DB / migration / schema** → why the column/table/enum matters, what breaks without it, how it propagates to the API layer +- **Bug fix** → what category of bug it was, why it happened, what general debugging lesson applies +- **Refactor** → what design problem existed, what principle the new structure improves +- **Cross-repo / multi-service work** → why a change in one repo requires a matching change in another, how data flows between services +- Short and clear — 3–8 sentences or a tight bullet list is usually enough +- Never skip teaching just because the task felt simple +- Do not re-explain things the user has clearly already understood +- One reusable concept or pattern per task is the minimum diff --git a/test-output/aisona.yml b/test-output/aisona.yml new file mode 100644 index 0000000..f514861 --- /dev/null +++ b/test-output/aisona.yml @@ -0,0 +1,80 @@ +version: 1 +identity: + name: Nicole + role: Backend engineer + experience: 11 months + language: Cantonese (廣東話) + context: AI security control plane, side projects @agents-io +personality: + tone: Always reply in Cantonese (廣東話) unless the user writes in another language or explicitly asks for English. + style: [] + teaching: >- + The user is a junior developer (~11 months experience) working in the AI era. Because AI makes implementation fast, + there is a real risk of "done but nothing learned". The goal is to make sure every task also builds real engineering + knowledge. Do not assume "done" means "learned". Completing the task is not enough — help the user understand what + this task is an example of, so they could reason about similar problems themselves next time without AI. **During + the task** — if a step involves a concept worth noting, mention it inline. For example, if touching a Pydantic + schema, briefly note what role schemas play. If writing a migration, briefly note why it exists. Keep these inline + comments short (1–2 sentences), not a full lesson. **After the task** — end every completed task with a learning + section. Even simple tasks have teachable patterns. Never skip this. After finishing, cover these points (not all + need to be long — tailor depth to complexity): 1. **What changed** — which files, which layers, what was added or + modified 2. **What problem it solves** — why this change was needed 3. **How the flow works step by step** — trace + data from input to output through each layer 4. **Why this implementation** — why this approach instead of + alternatives 5. **The concept or pattern** — what engineering idea this task is an example of 6. **Tradeoffs / + alternatives** — what the downside of this approach is, or what another option would have been 7. **What to remember + next time** — one reusable mental model or rule of thumb For different task types, emphasise: Talk like a helpful + senior engineer — direct, concrete, practical. Not a textbook. Not overly formal. Use analogies when the concept is + abstract. The goal is reusable mental models, not just a task report. Frame the lesson as: "If you had to do this + yourself without AI, here is what you would need to understand." --- + autonomy: >- + Work autonomously. Only ask for confirmation before: Always ask before: committing, pushing to remote, + opening/closing PRs, and any destructive CLI commands (rm, rmdir, kill, pkill, truncate, dd, mkfs, chmod -R, chown + -R, or anything that permanently deletes/overwrites data). Never ask before: reading files, searching, grep, find, + ls, cat, editing files, running tests, installing packages. Just do it and report what you did. + verbosity: concise +rules: + - "Destructive actions: deleting files, dropping DB tables, `reset --hard`, force push to shared branches" + - "Actions visible to others: pushing to remote, opening/closing PRs, sending messages" +preferences: + - Show where data comes from and where it goes + - Name the actual tables, functions, fields involved + - If there's a queue/worker, explain who creates the job and who picks it up + - If there's branching, explain each branch + - Do not skip the hidden middle steps + - Do not jump from input to output too quickly + - Do not say "this basically handles it" without explaining how + - "**API change** → request/response flow, schema validation, where logic lives, how layers connect" + - >- + **DB / migration / schema** → why the column/table/enum matters, what breaks without it, how it propagates to the + API layer + - "**Bug fix** → what category of bug it was, why it happened, what general debugging lesson applies" + - "**Refactor** → what design problem existed, what principle the new structure improves" + - >- + **Cross-repo / multi-service work** → why a change in one repo requires a matching change in another, how data flows + between services + - Short and clear — 3–8 sentences or a tight bullet list is usually enough + - Never skip teaching just because the task felt simple + - Do not re-explain things the user has clearly already understood + - One reusable concept or pattern per task is the minimum +memories: [] +tools: + claude: + enabled: true + extra_rules: + - End every task with a learning section + - Explain like stepping through a debugger + cursor: + enabled: true + extra_rules: [] + gemini: + enabled: true + extra_rules: [] + copilot: + enabled: true + extra_rules: [] + windsurf: + enabled: false + extra_rules: [] + aider: + enabled: false + extra_rules: [] diff --git a/tests/config.test.js b/tests/config.test.js new file mode 100644 index 0000000..eaef6a9 --- /dev/null +++ b/tests/config.test.js @@ -0,0 +1,78 @@ +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import fs from 'fs'; +import path from 'path'; +import os from 'os'; +import { loadAisona, saveAisona, getDefaultAisona, findAisonaFile } from '../src/lib/config.js'; + +const TMP = path.join(os.tmpdir(), 'aisona-test-config-' + Date.now()); + +beforeEach(() => fs.mkdirSync(TMP, { recursive: true })); +afterEach(() => fs.rmSync(TMP, { recursive: true, force: true })); + +describe('getDefaultAisona', () => { + it('returns valid structure with all required fields', () => { + const def = getDefaultAisona(); + expect(def.version).toBe(1); + expect(def.identity).toBeDefined(); + expect(def.identity.name).toBe(''); + expect(def.preferences).toBeDefined(); + expect(def.preferences.tone).toBe(''); + expect(def.preferences.habits).toEqual([]); + expect(def.rules).toEqual([]); + expect(def.memories).toEqual([]); + expect(def.tools.claude.enabled).toBe(true); + expect(def.tools.cursor.enabled).toBe(true); + expect(def.tools.gemini.enabled).toBe(false); + }); +}); + +describe('saveAisona + loadAisona roundtrip', () => { + it('saves and loads YAML correctly', () => { + const data = getDefaultAisona(); + data.identity.name = 'Test User'; + data.identity.language = 'Cantonese'; + data.rules = ['Never commit without asking', 'Use feature branches']; + data.preferences.tone = 'Direct and concise'; + data.memories = ['User prefers short responses']; + + const filePath = path.join(TMP, 'aisona.yml'); + saveAisona(filePath, data); + + expect(fs.existsSync(filePath)).toBe(true); + + const loaded = loadAisona(filePath); + expect(loaded.identity.name).toBe('Test User'); + expect(loaded.identity.language).toBe('Cantonese'); + expect(loaded.rules).toEqual(['Never commit without asking', 'Use feature branches']); + expect(loaded.preferences.tone).toBe('Direct and concise'); + expect(loaded.memories).toEqual(['User prefers short responses']); + }); + + it('preserves tool config through roundtrip', () => { + const data = getDefaultAisona(); + data.tools.claude.extra = ['Explain like a debugger']; + data.tools.cursor.enabled = false; + + const filePath = path.join(TMP, 'aisona.yml'); + saveAisona(filePath, data); + const loaded = loadAisona(filePath); + + expect(loaded.tools.claude.extra).toEqual(['Explain like a debugger']); + expect(loaded.tools.cursor.enabled).toBe(false); + }); +}); + +describe('findAisonaFile', () => { + it('finds aisona.yml in the given directory', () => { + const filePath = path.join(TMP, 'aisona.yml'); + fs.writeFileSync(filePath, 'version: 1\n'); + + const found = findAisonaFile(TMP); + expect(found).toBe(filePath); + }); + + it('returns null when no aisona.yml exists', () => { + const found = findAisonaFile(TMP); + expect(found).toBeNull(); + }); +}); diff --git a/tests/exporter.test.js b/tests/exporter.test.js new file mode 100644 index 0000000..103d463 --- /dev/null +++ b/tests/exporter.test.js @@ -0,0 +1,163 @@ +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import fs from 'fs'; +import path from 'path'; +import os from 'os'; +import { exportToTool, exportToAll, getSupportedTools } from '../src/lib/exporter.js'; + +const TMP = path.join(os.tmpdir(), 'aisona-test-export-' + Date.now()); + +beforeEach(() => fs.mkdirSync(TMP, { recursive: true })); +afterEach(() => fs.rmSync(TMP, { recursive: true, force: true })); + +const SAMPLE = { + version: 1, + identity: { + name: 'TestUser', + role: 'Backend engineer', + experience: '11 months', + language: 'Cantonese (廣東話)', + }, + preferences: { + tone: 'Direct and concise', + verbosity: 'concise', + autonomy: 'Work autonomously', + teaching: 'Explain like a senior engineer', + habits: ['No emojis', 'Short sentences'], + }, + rules: ['Never commit without asking', 'Use feature branches'], + memories: ['User prefers Cantonese'], + tools: { + claude: { enabled: true, extra: ['End tasks with learning section'] }, + cursor: { enabled: true, extra: ['Use .mdc format'] }, + gemini: { enabled: true, extra: [] }, + copilot: { enabled: true, extra: [] }, + windsurf: { enabled: false, extra: [] }, + }, +}; + +describe('getSupportedTools', () => { + it('returns supported tool ids', () => { + const tools = getSupportedTools(); + expect(tools).toContain('claude'); + expect(tools).toContain('cursor'); + expect(tools).toContain('gemini'); + expect(tools).toContain('copilot'); + expect(tools).toContain('windsurf'); + }); +}); + +describe('exportToTool — Claude', () => { + it('generates valid CLAUDE.md with all sections', () => { + const result = exportToTool(SAMPLE, 'claude', TMP); + expect(result.success).toBe(true); + + const content = fs.readFileSync(result.path, 'utf8'); + expect(content).toContain('Cantonese (廣東話)'); + expect(content).toContain('Direct and concise'); + expect(content).toContain('No emojis'); + expect(content).toContain('Never commit without asking'); + expect(content).toContain('User prefers Cantonese'); + expect(content).toContain('End tasks with learning section'); + expect(content).toContain('## Language'); + expect(content).toContain('## Rules'); + expect(content).toContain('## Claude-Specific'); + }); + + it('writes to CLAUDE.md', () => { + exportToTool(SAMPLE, 'claude', TMP); + expect(fs.existsSync(path.join(TMP, 'CLAUDE.md'))).toBe(true); + }); +}); + +describe('exportToTool — Cursor', () => { + it('generates valid .cursorrules', () => { + const result = exportToTool(SAMPLE, 'cursor', TMP); + expect(result.success).toBe(true); + + const content = fs.readFileSync(result.path, 'utf8'); + expect(content).toContain('Backend engineer'); + expect(content).toContain('Cantonese'); + expect(content).toContain('Never commit without asking'); + expect(content).toContain('Use .mdc format'); + }); +}); + +describe('exportToTool — Gemini', () => { + it('generates valid GEMINI.md', () => { + const result = exportToTool(SAMPLE, 'gemini', TMP); + expect(result.success).toBe(true); + + const content = fs.readFileSync(result.path, 'utf8'); + expect(content).toContain('Cantonese'); + expect(content).toContain('Backend engineer'); + expect(content).toContain('11 months'); + }); +}); + +describe('exportToTool — Copilot', () => { + it('generates copilot-instructions.md', () => { + const result = exportToTool(SAMPLE, 'copilot', TMP); + expect(result.success).toBe(true); + expect(fs.existsSync(path.join(TMP, '.github', 'copilot-instructions.md'))).toBe(true); + }); +}); + +describe('exportToTool — disabled', () => { + it('refuses to export disabled tool', () => { + const result = exportToTool(SAMPLE, 'windsurf', TMP); + expect(result.success).toBe(false); + expect(result.error).toContain('disabled'); + }); +}); + +describe('exportToAll', () => { + it('exports to all enabled tools', () => { + const results = exportToAll(SAMPLE, TMP); + const succeeded = results.filter(r => r.success); + expect(succeeded.length).toBe(4); + + expect(fs.existsSync(path.join(TMP, 'CLAUDE.md'))).toBe(true); + expect(fs.existsSync(path.join(TMP, '.cursorrules'))).toBe(true); + expect(fs.existsSync(path.join(TMP, 'GEMINI.md'))).toBe(true); + expect(fs.existsSync(path.join(TMP, '.github', 'copilot-instructions.md'))).toBe(true); + expect(fs.existsSync(path.join(TMP, '.windsurfrules'))).toBe(false); + }); +}); + +describe('output quality', () => { + it('no triple blank lines', () => { + const result = exportToTool(SAMPLE, 'claude', TMP); + const content = fs.readFileSync(result.path, 'utf8'); + expect(content).not.toMatch(/\n{4,}/); + }); + + it('ends with single newline', () => { + const result = exportToTool(SAMPLE, 'claude', TMP); + const content = fs.readFileSync(result.path, 'utf8'); + expect(content.endsWith('\n')).toBe(true); + expect(content.endsWith('\n\n')).toBe(false); + }); + + it('no raw handlebars in output', () => { + const result = exportToTool(SAMPLE, 'claude', TMP); + const content = fs.readFileSync(result.path, 'utf8'); + expect(content).not.toContain('{{'); + }); + + it('handles empty arrays gracefully', () => { + const minimal = { + version: 1, + identity: { name: 'Test', language: 'English' }, + preferences: { tone: 'Friendly', habits: [] }, + rules: [], + memories: [], + tools: { claude: { enabled: true, extra: [] } }, + }; + + const result = exportToTool(minimal, 'claude', TMP); + expect(result.success).toBe(true); + const content = fs.readFileSync(result.path, 'utf8'); + expect(content).toContain('English'); + expect(content).toContain('Friendly'); + }); +}); diff --git a/tests/parser.test.js b/tests/parser.test.js new file mode 100644 index 0000000..dade82a --- /dev/null +++ b/tests/parser.test.js @@ -0,0 +1,110 @@ +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import fs from 'fs'; +import path from 'path'; +import os from 'os'; +import { parseClaudeMd, parseCursorRules, detectTools } from '../src/lib/parser.js'; + +const TMP = path.join(os.tmpdir(), 'aisona-test-parser-' + Date.now()); + +beforeEach(() => fs.mkdirSync(TMP, { recursive: true })); +afterEach(() => fs.rmSync(TMP, { recursive: true, force: true })); + +describe('parseClaudeMd', () => { + it('parses rules and habits from simple CLAUDE.md', () => { + const content = `## Language\n\nAlways reply in Cantonese.\n\n## Rules\n\n- Never commit without asking\n- Never push to remote\n\n## Preferences\n\n- Use Playwright for testing\n- Short responses\n`; + fs.writeFileSync(path.join(TMP, 'CLAUDE.md'), content); + + const result = parseClaudeMd(path.join(TMP, 'CLAUDE.md')); + expect(result.rules).toContain('Never commit without asking'); + expect(result.rules).toContain('Never push to remote'); + expect(result.preferences.habits).toContain('Use Playwright for testing'); + }); + + it('extracts tone from personality sections', () => { + const content = `## Tone and Style\n\nBe concise and direct.\n\n- No emojis unless asked\n- Short sentences\n`; + fs.writeFileSync(path.join(TMP, 'CLAUDE.md'), content); + + const result = parseClaudeMd(path.join(TMP, 'CLAUDE.md')); + expect(result.preferences.tone).toContain('concise and direct'); + expect(result.preferences.habits).toContain('No emojis unless asked'); + }); + + it('extracts autonomy rules', () => { + const content = `## Autonomy\n\nWork autonomously.\n\nAlways ask before git push.\nNever ask before reading files.\n\n- Destructive actions need confirmation\n`; + fs.writeFileSync(path.join(TMP, 'CLAUDE.md'), content); + + const result = parseClaudeMd(path.join(TMP, 'CLAUDE.md')); + expect(result.preferences.autonomy).toContain('Work autonomously'); + expect(result.rules).toContain('Always ask before git push.'); + expect(result.rules).toContain('Never ask before reading files.'); + expect(result.rules).toContain('Destructive actions need confirmation'); + }); + + it('extracts teaching preferences', () => { + const content = `## Teaching Style\n\nThe user is a junior developer.\n\n- Frame lessons as reusable mental models\n- Never skip teaching\n`; + fs.writeFileSync(path.join(TMP, 'CLAUDE.md'), content); + + const result = parseClaudeMd(path.join(TMP, 'CLAUDE.md')); + expect(result.preferences.teaching).toContain('junior developer'); + expect(result.preferences.habits).toContain('Frame lessons as reusable mental models'); + }); + + it('handles empty CLAUDE.md', () => { + fs.writeFileSync(path.join(TMP, 'CLAUDE.md'), ''); + + const result = parseClaudeMd(path.join(TMP, 'CLAUDE.md')); + expect(result.rules).toEqual([]); + expect(result.preferences.habits).toEqual([]); + }); + + it('handles Nicole real-world structure', () => { + const content = `## Language\n\nAlways reply in Cantonese.\n\n## Technical Explanations\n\nExplain step by step.\n\n- Show where data comes from\n- Name actual tables\n\n## Teaching Style\n\nThe user is a junior developer.\n\n- End tasks with learning section\n\n## Autonomy\n\nWork autonomously.\n\nAlways ask before committing.\nNever ask before reading files.\n\n- Destructive actions: deleting files\n- Actions visible to others: pushing\n\n## Project Knowledge Capture\n\n- Flag reusable discoveries\n`; + fs.writeFileSync(path.join(TMP, 'CLAUDE.md'), content); + + const result = parseClaudeMd(path.join(TMP, 'CLAUDE.md')); + expect(result.rules.length).toBeGreaterThanOrEqual(4); + expect(result.preferences.teaching).toContain('junior developer'); + expect(result.preferences.habits.some(h => h.includes('data comes from'))).toBe(true); + expect(result.memories.length).toBeGreaterThanOrEqual(1); + }); +}); + +describe('parseCursorRules', () => { + it('extracts rules from .cursorrules', () => { + const content = `# Rules\n\n- Always use TypeScript\n- Prefer functional style\n\nUse concise names.\n`; + fs.writeFileSync(path.join(TMP, '.cursorrules'), content); + + const result = parseCursorRules(path.join(TMP, '.cursorrules')); + expect(result.rules).toContain('Always use TypeScript'); + expect(result.context).toContain('concise names'); + }); +}); + +describe('detectTools', () => { + it('detects CLAUDE.md in project directory', () => { + fs.writeFileSync(path.join(TMP, 'CLAUDE.md'), '# Test'); + const detected = detectTools(TMP); + expect(detected.find(d => d.tool === 'claude' && d.scope === 'project')).toBeDefined(); + }); + + it('detects .cursorrules', () => { + fs.writeFileSync(path.join(TMP, '.cursorrules'), '# Test'); + expect(detectTools(TMP).find(d => d.tool === 'cursor')).toBeDefined(); + }); + + it('detects copilot instructions', () => { + fs.mkdirSync(path.join(TMP, '.github'), { recursive: true }); + fs.writeFileSync(path.join(TMP, '.github', 'copilot-instructions.md'), '# Test'); + expect(detectTools(TMP).find(d => d.tool === 'copilot')).toBeDefined(); + }); + + it('detects AGENTS.md', () => { + fs.writeFileSync(path.join(TMP, 'AGENTS.md'), '# Test'); + expect(detectTools(TMP).find(d => d.tool === 'agentsmd')).toBeDefined(); + }); + + it('returns empty for empty directory (project-level)', () => { + const projectLevel = detectTools(TMP).filter(d => d.scope === 'project'); + expect(projectLevel).toEqual([]); + }); +});