diff --git a/.aidd/.gitkeep b/.aidd/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..69ebee3 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,5 @@ +# SQLite database files - treat as binary but enable SQL diff +*.db binary diff=sqlite3 + +# Keep .aidd database in git +.aidd/index.db binary diff=sqlite3 diff --git a/.gitignore b/.gitignore index fa4fc0e..5c42ee1 100644 --- a/.gitignore +++ b/.gitignore @@ -141,6 +141,15 @@ vite.config.ts.timestamp-* # AI agent tools .agent-tools/ +# AIDD local database +.aidd/ + +# RLM task files (local only) +tasks/rlm/ + # Core dump files core core.* + +# Claude Code local settings (contains user-specific permissions) +.claude/settings.local.json diff --git a/README.md b/README.md index 38d094d..78a444b 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ Includes: - [๐Ÿงช User Testing](#-user-testing) - [Why SudoLang?](#why-sudolang) - [What's Included](#whats-included) + - [Indexing & Search Tools](#indexing--search-tools) - [๐Ÿš€ AIDD Server Framework](#-aidd-server-framework) - [Authentication Middleware](#authentication-middleware) - [๐Ÿ› ๏ธ AIDD CLI Reference](#-aidd-cli-reference) @@ -196,6 +197,7 @@ Modules include: - ๐Ÿงช Test generators - ๐Ÿ› ๏ธ Development process automation scripts - ๐Ÿš€ Optional composable server framework (lightweight Express alternative) +- ๐Ÿ” SQLite-based indexing and search tools for codebase exploration Coming soon: @@ -203,6 +205,18 @@ Coming soon: - ๐Ÿ“„ Documentation generators - ๐Ÿ”Œ API design +### Indexing & Search Tools + +Fast codebase exploration using SQLite FTS5: + +```bash +npm run aidd:index # Index your project +npm run aidd:query "auth" # Search for content +npm run aidd:find-related file # Find dependencies +``` + +๐Ÿ“– **[See Indexing & Search documentation โ†’](ai/tools/README.md)** + ## ๐Ÿš€ AIDD Server Framework A lightweight alternative to Express, built for function composition and type-safe development. diff --git a/ai/index.md b/ai/index.md index 0419529..12dc7e8 100644 --- a/ai/index.md +++ b/ai/index.md @@ -12,3 +12,7 @@ See [`commands/index.md`](./commands/index.md) for contents. See [`rules/index.md`](./rules/index.md) for contents. +### ๐Ÿ“ tools/ + +See [`tools/index.md`](./tools/index.md) for contents. + diff --git a/ai/rules/index.md b/ai/rules/index.md index da91dcb..6001f82 100644 --- a/ai/rules/index.md +++ b/ai/rules/index.md @@ -16,6 +16,10 @@ See [`javascript/index.md`](./javascript/index.md) for contents. See [`security/index.md`](./security/index.md) for contents. +### ๐Ÿ“ sudolang/ + +See [`sudolang/index.md`](./sudolang/index.md) for contents. + ## Files ### Aiden Agent Orchestrator diff --git a/ai/rules/sudolang/index.md b/ai/rules/sudolang/index.md new file mode 100644 index 0000000..b2f47e7 --- /dev/null +++ b/ai/rules/sudolang/index.md @@ -0,0 +1,12 @@ +# sudolang + +This index provides an overview of the contents in this directory. + +## Files + +### SudoLang Syntax + +**File:** `sudolang-syntax.mdc` + +A quick cheat sheet for SudoLang syntax. + diff --git a/ai/tools/README.md b/ai/tools/README.md new file mode 100644 index 0000000..04e2b20 --- /dev/null +++ b/ai/tools/README.md @@ -0,0 +1,283 @@ +# AIDD Indexing & Search Tools + +SQLite-based indexing and search tools for fast codebase exploration. + +## Overview + +These tools create a queryable index of your project's markdown files (`.md`, `.mdc`), enabling: + +- **Full-text search** using SQLite FTS5 +- **Metadata filtering** by frontmatter fields +- **Dependency graph traversal** for understanding file relationships +- **Fan-out search** combining multiple strategies for comprehensive results + +The index is stored in `.aidd/index.db` and can be committed to git for instant availability after clone. + +## Quick Start + +```bash +# Index your project +npm run aidd:index + +# Search for content +npm run aidd:query "authentication" + +# Find related files +npm run aidd:find-related ai/rules/tdd.mdc +``` + +## CLI Commands + +### Indexing + +```bash +# Full reindex with dependency scanning +npm run aidd:index + +# Incremental update (only changed files) +npm run aidd:index:incremental + +# With options +node ai/tools/cli/index-cli.js --help +node ai/tools/cli/index-cli.js --full --deps --stats +``` + +**Options:** +| Option | Description | +|--------|-------------| +| `--full` | Rebuild entire index (default is incremental) | +| `--deps` | Also index file dependencies | +| `--stats` | Show detailed statistics | +| `--db ` | Database path (default: `.aidd/index.db`) | +| `--root ` | Root directory to index | +| `--quiet` | Suppress output | + +### Querying + +```bash +# Basic search +npm run aidd:query "search term" + +# With options +node ai/tools/cli/query-cli.js "TDD" --type rule --limit 10 +node ai/tools/cli/query-cli.js "security" --json +node ai/tools/cli/query-cli.js "error handling" --snippets +``` + +**Options:** +| Option | Description | +|--------|-------------| +| `--type ` | Filter by document type (rule, command, task, etc.) | +| `--limit ` | Maximum results (default: 20) | +| `--json` | Output as JSON | +| `--snippets` | Include content snippets | +| `--fts-only` | Use only full-text search | + +### Finding Related Files + +```bash +# Find dependencies and dependents +npm run aidd:find-related path/to/file.js + +# With options +node ai/tools/cli/find-related-cli.js ai/tools/index.js --direction forward +node ai/tools/cli/find-related-cli.js ai/tools/db/connection.js --direction reverse +node ai/tools/cli/find-related-cli.js ai/tools/index.js --depth 5 --json +``` + +**Options:** +| Option | Description | +|--------|-------------| +| `--direction` | `forward` (imports), `reverse` (imported by), or `both` | +| `--depth ` | Maximum traversal depth (default: 3) | +| `--json` | Output as JSON | + +## Document Types + +Files are automatically categorized based on their path: + +| Type | Path Pattern | +|------|--------------| +| `rule` | `ai/rules/**` | +| `command` | `ai/commands/**` | +| `skill` | `ai/skills/**` | +| `task` | `tasks/**` | +| `story-map` | `plan/story-map/**` | +| `other` | Everything else | + +## Database Schema + +### Documents Table + +Stores file metadata and content: + +```sql +CREATE TABLE documents ( + path TEXT PRIMARY KEY, -- Relative file path + type TEXT, -- Document type (rule, command, etc.) + frontmatter TEXT, -- YAML frontmatter as JSON + content TEXT, -- File content without frontmatter + hash TEXT, -- SHA3-256 hash for change detection + file_size INTEGER, + modified_at INTEGER, + indexed_at INTEGER +); +``` + +### FTS5 Virtual Table + +Full-text search index: + +```sql +CREATE VIRTUAL TABLE fts_documents USING fts5( + path, frontmatter, content +); +``` + +### Dependencies Table + +File import relationships: + +```sql +CREATE TABLE dependencies ( + from_file TEXT, -- File containing the import + to_file TEXT, -- File being imported + import_type TEXT, -- 'import', 'require', 'dynamic-import', 'reference' + line_number INTEGER, + import_text TEXT -- Original import statement +); +``` + +## Programmatic API + +Import functions directly for custom tooling: + +```javascript +import { + // Database + createDatabase, + closeDatabase, + initializeSchema, + + // Indexing + indexDirectory, + indexIncremental, + indexAllDependencies, + + // Search + fanOutSearch, + searchFts5, + searchMetadata, + + // Graph traversal + findRelated, + getForwardDeps, + getReverseDeps, +} from 'aidd/tools'; + +// Example: Custom search +const db = createDatabase('.aidd/index.db'); +const results = await fanOutSearch(db, 'authentication', { + type: 'rule', + limit: 10, +}); +closeDatabase(db); +``` + +## Fan-out Search + +The fan-out search combines multiple strategies: + +1. **FTS5 Search** - Full-text keyword matching +2. **Metadata Search** - Filter by frontmatter fields +3. **Semantic Search** - (Stub for future RAG integration) + +Results are deduplicated by path and ranked by: +- Strategy weight (FTS5 > metadata > semantic) +- Position within each strategy's results +- Boost for documents found by multiple strategies + +## Git Integration + +The database is configured for git: + +```gitattributes +# .gitattributes +*.db binary diff=sqlite3 +.aidd/index.db binary diff=sqlite3 +``` + +To enable SQL diffs (optional): + +```bash +git config diff.sqlite3.textconv 'sqlite3 $1 .dump' +``` + +## Pre-commit Hook + +Add automatic indexing before commits: + +```bash +# .husky/pre-commit +#!/bin/sh +node ai/tools/cli/index-cli.js --deps --stats +git add .aidd/index.db +``` + +## Architecture + +``` +ai/tools/ +โ”œโ”€โ”€ db/ +โ”‚ โ”œโ”€โ”€ connection.js # Database factory (WAL mode, foreign keys) +โ”‚ โ””โ”€โ”€ schema.js # Table definitions, FTS5 triggers +โ”œโ”€โ”€ indexers/ +โ”‚ โ”œโ”€โ”€ frontmatter.js # YAML extraction, incremental updates +โ”‚ โ””โ”€โ”€ dependencies.js # Import/require parsing +โ”œโ”€โ”€ search/ +โ”‚ โ”œโ”€โ”€ fts5.js # Full-text search +โ”‚ โ”œโ”€โ”€ metadata.js # JSON field filtering +โ”‚ โ””โ”€โ”€ fan-out.js # Multi-strategy orchestration +โ”œโ”€โ”€ graph/ +โ”‚ โ””โ”€โ”€ traverse.js # Recursive CTE traversal +โ”œโ”€โ”€ cli/ +โ”‚ โ”œโ”€โ”€ index-cli.js # Indexing command +โ”‚ โ”œโ”€โ”€ query-cli.js # Search command +โ”‚ โ””โ”€โ”€ find-related-cli.js # Graph traversal command +โ””โ”€โ”€ index.js # Public API exports +``` + +## Performance + +- **Indexing**: ~68 files in ~50ms +- **Search**: <50ms for typical queries +- **Graph traversal**: <100ms for depth 5 + +## Testing + +```bash +# Run all tools tests +npx vitest run ai/tools/ + +# Run specific module tests +npx vitest run ai/tools/search/ +npx vitest run ai/tools/indexers/ +``` + +## Direct Database Access + +Query the database directly with SQLite: + +```bash +# Document counts by type +sqlite3 .aidd/index.db "SELECT type, COUNT(*) FROM documents GROUP BY type;" + +# FTS search +sqlite3 .aidd/index.db "SELECT path FROM fts_documents WHERE fts_documents MATCH 'security';" + +# View dependencies +sqlite3 .aidd/index.db "SELECT from_file, to_file FROM dependencies LIMIT 10;" + +# Find files with specific frontmatter +sqlite3 .aidd/index.db "SELECT path FROM documents WHERE json_extract(frontmatter, '$.alwaysApply') = 1;" +``` diff --git a/ai/tools/cli/find-related-cli.js b/ai/tools/cli/find-related-cli.js new file mode 100644 index 0000000..b6978ba --- /dev/null +++ b/ai/tools/cli/find-related-cli.js @@ -0,0 +1,127 @@ +#!/usr/bin/env node + +/** + * CLI for finding related files via dependency graph traversal. + * Usage: node ai/tools/cli/find-related-cli.js [options] + */ + +import { program } from "commander"; +import path from "path"; +import fs from "fs-extra"; +import chalk from "chalk"; + +import { createDatabase, closeDatabase } from "../db/connection.js"; +import { + findRelated, + getForwardDeps, + getReverseDeps, +} from "../graph/traverse.js"; + +const DEFAULT_DB_PATH = ".aidd/index.db"; + +/** + * Format related files for console output. + */ +export const formatRelated = (results, options) => { + if (options.json) { + return JSON.stringify(results, null, 2); + } + + if (results.length === 0) { + return chalk.yellow("No related files found."); + } + + const lines = []; + lines.push(chalk.blue(`Found ${results.length} related file(s):\n`)); + + // Group by direction if showing both + const forward = results.filter((r) => r.direction === "forward"); + const reverse = results.filter((r) => r.direction === "reverse"); + + if ( + forward.length > 0 && + (options.direction === "both" || options.direction === "forward") + ) { + lines.push( + chalk.white(" Dependencies (imports):"), + ...forward.map((file) => + chalk.gray( + ` ${" ".repeat(file.depth)}โ†’ ${file.file} (depth: ${file.depth})`, + ), + ), + "", + ); + } + + if ( + reverse.length > 0 && + (options.direction === "both" || options.direction === "reverse") + ) { + lines.push( + chalk.white(" Dependents (imported by):"), + ...reverse.map((file) => + chalk.gray( + ` ${" ".repeat(file.depth)}โ† ${file.file} (depth: ${file.depth})`, + ), + ), + "", + ); + } + + return lines.join("\n"); +}; + +program + .name("aidd-find-related") + .description("Find files related through the dependency graph") + .argument("", "File path to analyze") + .option("-d, --db ", "Database path", DEFAULT_DB_PATH) + .option("-r, --root ", "Root directory", process.cwd()) + .option( + "--direction ", + "Traversal direction: forward, reverse, or both", + "both", + ) + .option("--depth ", "Maximum traversal depth", "3") + .option("--json", "Output as JSON") + .action(async (file, options) => { + try { + const dbPath = path.resolve(options.root, options.db); + + // Validate database exists + if (!(await fs.pathExists(dbPath))) { + console.error(chalk.red(`Error: Database not found at ${dbPath}`)); + console.error( + chalk.yellow("Run 'npm run aidd:index' first to create the index."), + ); + process.exit(1); + } + + const db = createDatabase(dbPath); + + const maxDepth = parseInt(options.depth, 10); + const direction = options.direction; + + let results; + if (direction === "forward") { + results = getForwardDeps(db, file, { maxDepth }); + } else if (direction === "reverse") { + results = getReverseDeps(db, file, { maxDepth }); + } else { + results = findRelated(db, file, { direction: "both", maxDepth }); + } + + console.log(formatRelated(results, { ...options, direction })); + + closeDatabase(db); + process.exit(0); + } catch (error) { + console.error(chalk.red(`Error: ${error.message}`)); + process.exit(1); + } + }); + +// Only parse when run directly, not when imported for testing +if (process.argv[1]?.endsWith("find-related-cli.js")) { + program.parse(); +} diff --git a/ai/tools/cli/find-related-cli.test.js b/ai/tools/cli/find-related-cli.test.js new file mode 100644 index 0000000..aba7a57 --- /dev/null +++ b/ai/tools/cli/find-related-cli.test.js @@ -0,0 +1,301 @@ +import { describe, test } from "vitest"; +import { assert } from "riteway/vitest"; + +import { formatRelated } from "./find-related-cli.js"; + +describe("cli/find-related-cli", () => { + describe("formatRelated", () => { + test("returns JSON when json option is true", () => { + const results = [{ file: "test.js", direction: "forward", depth: 1 }]; + + const output = formatRelated(results, { json: true, direction: "both" }); + const parsed = JSON.parse(output); + + assert({ + given: "results with json option", + should: "return valid JSON string", + actual: parsed.length, + expected: 1, + }); + }); + + test("returns formatted JSON with indentation", () => { + const results = [{ file: "test.js", direction: "forward", depth: 1 }]; + + const output = formatRelated(results, { json: true, direction: "both" }); + + assert({ + given: "json option", + should: "return pretty-printed JSON", + actual: output.includes("\n"), + expected: true, + }); + }); + + test("returns no results message for empty array", () => { + const output = formatRelated([], { direction: "both" }); + + assert({ + given: "empty results", + should: "return no results message", + actual: output.includes("No related files found"), + expected: true, + }); + }); + + test("includes result count in output", () => { + const results = [ + { file: "a.js", direction: "forward", depth: 1 }, + { file: "b.js", direction: "reverse", depth: 1 }, + ]; + + const output = formatRelated(results, { direction: "both" }); + + assert({ + given: "multiple results", + should: "include result count", + actual: output.includes("2 related file(s)"), + expected: true, + }); + }); + + test("shows forward dependencies with imports label", () => { + const results = [ + { file: "lib/utils.js", direction: "forward", depth: 1 }, + ]; + + const output = formatRelated(results, { direction: "forward" }); + + assert({ + given: "forward dependencies", + should: "show Dependencies (imports) header", + actual: output.includes("Dependencies (imports)"), + expected: true, + }); + }); + + test("shows reverse dependencies with imported by label", () => { + const results = [{ file: "src/app.js", direction: "reverse", depth: 1 }]; + + const output = formatRelated(results, { direction: "reverse" }); + + assert({ + given: "reverse dependencies", + should: "show Dependents (imported by) header", + actual: output.includes("Dependents (imported by)"), + expected: true, + }); + }); + + test("includes file path in forward results", () => { + const results = [ + { file: "lib/helpers.js", direction: "forward", depth: 1 }, + ]; + + const output = formatRelated(results, { direction: "forward" }); + + assert({ + given: "forward dependency", + should: "include the file path", + actual: output.includes("lib/helpers.js"), + expected: true, + }); + }); + + test("includes file path in reverse results", () => { + const results = [{ file: "src/main.js", direction: "reverse", depth: 1 }]; + + const output = formatRelated(results, { direction: "reverse" }); + + assert({ + given: "reverse dependency", + should: "include the file path", + actual: output.includes("src/main.js"), + expected: true, + }); + }); + + test("includes depth information in output", () => { + const results = [{ file: "deep.js", direction: "forward", depth: 3 }]; + + const output = formatRelated(results, { direction: "forward" }); + + assert({ + given: "result with depth", + should: "include depth in output", + actual: output.includes("depth: 3"), + expected: true, + }); + }); + + test("uses arrow indicator for forward deps", () => { + const results = [{ file: "dep.js", direction: "forward", depth: 1 }]; + + const output = formatRelated(results, { direction: "forward" }); + + assert({ + given: "forward dependency", + should: "use right arrow indicator", + actual: output.includes("โ†’"), + expected: true, + }); + }); + + test("uses arrow indicator for reverse deps", () => { + const results = [{ file: "consumer.js", direction: "reverse", depth: 1 }]; + + const output = formatRelated(results, { direction: "reverse" }); + + assert({ + given: "reverse dependency", + should: "use left arrow indicator", + actual: output.includes("โ†"), + expected: true, + }); + }); + + test("shows both sections when direction is both", () => { + const results = [ + { file: "forward.js", direction: "forward", depth: 1 }, + { file: "reverse.js", direction: "reverse", depth: 1 }, + ]; + + const output = formatRelated(results, { direction: "both" }); + + assert({ + given: "both forward and reverse results", + should: "show both sections", + actual: + output.includes("Dependencies (imports)") && + output.includes("Dependents (imported by)"), + expected: true, + }); + }); + + test("only shows forward section when direction is forward", () => { + const results = [ + { file: "forward.js", direction: "forward", depth: 1 }, + { file: "reverse.js", direction: "reverse", depth: 1 }, + ]; + + const output = formatRelated(results, { direction: "forward" }); + + assert({ + given: "direction forward with mixed results", + should: "only show forward section", + actual: + output.includes("Dependencies (imports)") && + !output.includes("Dependents (imported by)"), + expected: true, + }); + }); + + test("only shows reverse section when direction is reverse", () => { + const results = [ + { file: "forward.js", direction: "forward", depth: 1 }, + { file: "reverse.js", direction: "reverse", depth: 1 }, + ]; + + const output = formatRelated(results, { direction: "reverse" }); + + assert({ + given: "direction reverse with mixed results", + should: "only show reverse section", + actual: + !output.includes("Dependencies (imports)") && + output.includes("Dependents (imported by)"), + expected: true, + }); + }); + + test("indents based on depth level", () => { + const results = [ + { file: "depth1.js", direction: "forward", depth: 1 }, + { file: "depth2.js", direction: "forward", depth: 2 }, + ]; + + const output = formatRelated(results, { direction: "forward" }); + const lines = output.split("\n"); + const depth1Line = lines.find((l) => l.includes("depth1.js")); + const depth2Line = lines.find((l) => l.includes("depth2.js")); + + assert({ + given: "results with different depths", + should: "have more indentation for deeper results", + actual: depth2Line.indexOf("โ†’") > depth1Line.indexOf("โ†’"), + expected: true, + }); + }); + + test("handles empty forward results with both direction", () => { + const results = [{ file: "reverse.js", direction: "reverse", depth: 1 }]; + + const output = formatRelated(results, { direction: "both" }); + + assert({ + given: "only reverse results with both direction", + should: "not show forward section", + actual: output.includes("Dependencies (imports)"), + expected: false, + }); + }); + + test("handles empty reverse results with both direction", () => { + const results = [{ file: "forward.js", direction: "forward", depth: 1 }]; + + const output = formatRelated(results, { direction: "both" }); + + assert({ + given: "only forward results with both direction", + should: "not show reverse section", + actual: output.includes("Dependents (imported by)"), + expected: false, + }); + }); + + test("returns JSON for empty results with json option", () => { + const output = formatRelated([], { json: true, direction: "both" }); + const parsed = JSON.parse(output); + + assert({ + given: "empty results with json option", + should: "return empty JSON array", + actual: parsed, + expected: [], + }); + }); + + test("handles multiple forward dependencies at same depth", () => { + const results = [ + { file: "a.js", direction: "forward", depth: 1 }, + { file: "b.js", direction: "forward", depth: 1 }, + { file: "c.js", direction: "forward", depth: 1 }, + ]; + + const output = formatRelated(results, { direction: "forward" }); + + assert({ + given: "multiple forward deps at same depth", + should: "include all files", + actual: + output.includes("a.js") && + output.includes("b.js") && + output.includes("c.js"), + expected: true, + }); + }); + + test("handles deep nesting with correct indentation", () => { + const results = [{ file: "level5.js", direction: "forward", depth: 5 }]; + + const output = formatRelated(results, { direction: "forward" }); + + assert({ + given: "deeply nested dependency", + should: "show depth 5 in output", + actual: output.includes("depth: 5"), + expected: true, + }); + }); + }); +}); diff --git a/ai/tools/cli/index-cli.js b/ai/tools/cli/index-cli.js new file mode 100644 index 0000000..bed1d91 --- /dev/null +++ b/ai/tools/cli/index-cli.js @@ -0,0 +1,147 @@ +#!/usr/bin/env node + +/** + * CLI for indexing aidd files into SQLite. + * Usage: node ai/tools/cli/index-cli.js [options] + */ + +import { program } from "commander"; +import path from "path"; +import fs from "fs-extra"; +import chalk from "chalk"; + +import { createDatabase, closeDatabase } from "../db/connection.js"; +import { initializeSchema } from "../db/schema.js"; +import { indexDirectory, indexIncremental } from "../indexers/frontmatter.js"; +import { indexAllDependencies } from "../indexers/dependencies.js"; + +const DEFAULT_DB_PATH = ".aidd/index.db"; + +/** + * Ensure database directory exists and initialize schema. + */ +export const ensureDatabase = async (dbPath) => { + const dir = path.dirname(dbPath); + await fs.ensureDir(dir); + + const db = createDatabase(dbPath); + initializeSchema(db); + return db; +}; + +/** + * Format duration for display. + */ +export const formatDuration = (ms) => { + if (ms < 1000) return `${ms}ms`; + return `${(ms / 1000).toFixed(2)}s`; +}; + +program + .name("aidd-index") + .description("Index aidd files into SQLite for fast querying") + .option("-d, --db ", "Database path", DEFAULT_DB_PATH) + .option("-r, --root ", "Root directory to index", process.cwd()) + .option("--full", "Full reindex (default is incremental)") + .option("--deps", "Also index dependencies") + .option("-s, --stats", "Show detailed statistics") + .option("-q, --quiet", "Suppress output") + .action(async (options) => { + const startTime = Date.now(); + const log = options.quiet ? () => {} : console.log; + + try { + const dbPath = path.resolve(options.root, options.db); + const rootDir = path.resolve(options.root); + + log(chalk.blue("Indexing aidd files...")); + log(chalk.gray(` Database: ${dbPath}`)); + log(chalk.gray(` Root: ${rootDir}`)); + + const db = await ensureDatabase(dbPath); + + // Index frontmatter + let frontmatterStats; + if (options.full) { + log(chalk.yellow("\nPerforming full reindex...")); + frontmatterStats = await indexDirectory(db, rootDir); + } else { + log(chalk.yellow("\nPerforming incremental index...")); + frontmatterStats = await indexIncremental(db, rootDir); + } + + // Index dependencies if requested + let depStats = { indexed: 0, files: 0 }; + if (options.deps) { + log(chalk.yellow("\nIndexing dependencies...")); + depStats = await indexAllDependencies(db, rootDir); + } + + const duration = Date.now() - startTime; + + // Summary + log(chalk.green("\nโœ“ Indexing complete")); + + if (options.stats) { + log(chalk.white("\nStatistics:")); + + if (options.full) { + log(chalk.gray(` Documents indexed: ${frontmatterStats.indexed}`)); + } else { + log(chalk.gray(` Documents updated: ${frontmatterStats.updated}`)); + log(chalk.gray(` Documents deleted: ${frontmatterStats.deleted}`)); + log( + chalk.gray(` Documents unchanged: ${frontmatterStats.unchanged}`), + ); + } + + if (options.deps) { + log(chalk.gray(` Dependencies indexed: ${depStats.indexed}`)); + log(chalk.gray(` Files scanned for deps: ${depStats.files}`)); + } + + log(chalk.gray(` Duration: ${formatDuration(duration)}`)); + + // Show any errors + const errors = [ + ...(frontmatterStats.errors || []), + ...(depStats.errors || []), + ]; + if (errors.length > 0) { + log(chalk.red(`\n Errors (${errors.length}):`)); + errors.slice(0, 5).forEach((e) => log(chalk.red(` ${e}`))); + if (errors.length > 5) { + log(chalk.red(` ... and ${errors.length - 5} more`)); + } + } + } else { + if (options.full) { + log( + chalk.gray( + ` ${frontmatterStats.indexed} documents indexed in ${formatDuration(duration)}`, + ), + ); + } else { + log( + chalk.gray( + ` ${frontmatterStats.updated} updated, ${frontmatterStats.deleted} deleted in ${formatDuration(duration)}`, + ), + ); + } + } + + closeDatabase(db); + process.exit(0); + } catch (error) { + console.error(chalk.red(`\nโœ— Error: ${error.message}`)); + if (options.stats) { + console.error(chalk.gray(error.stack)); + } + process.exit(1); + } + }); + +// Only parse when run directly, not when imported for testing +if (process.argv[1]?.endsWith("index-cli.js")) { + program.parse(); +} diff --git a/ai/tools/cli/index-cli.test.js b/ai/tools/cli/index-cli.test.js new file mode 100644 index 0000000..a7eeed3 --- /dev/null +++ b/ai/tools/cli/index-cli.test.js @@ -0,0 +1,225 @@ +import { describe, test, onTestFinished } from "vitest"; +import { assert } from "riteway/vitest"; +import path from "path"; +import fs from "fs-extra"; +import os from "os"; + +import { ensureDatabase, formatDuration } from "./index-cli.js"; +import { closeDatabase } from "../db/connection.js"; + +const createTempDir = async () => { + const uniqueId = `${Date.now()}-${Math.random().toString(36).slice(2, 11)}`; + const tempDir = path.join(os.tmpdir(), `aidd-test-${uniqueId}`); + await fs.ensureDir(tempDir); + return tempDir; +}; + +describe("cli/index-cli", () => { + describe("formatDuration", () => { + test("formats milliseconds under 1 second", () => { + assert({ + given: "500 milliseconds", + should: "return value in ms format", + actual: formatDuration(500), + expected: "500ms", + }); + }); + + test("formats zero milliseconds", () => { + assert({ + given: "0 milliseconds", + should: "return 0ms", + actual: formatDuration(0), + expected: "0ms", + }); + }); + + test("formats exactly 1 second", () => { + assert({ + given: "1000 milliseconds", + should: "return value in seconds format", + actual: formatDuration(1000), + expected: "1.00s", + }); + }); + + test("formats seconds with decimal precision", () => { + assert({ + given: "1500 milliseconds", + should: "return 1.50s", + actual: formatDuration(1500), + expected: "1.50s", + }); + }); + + test("formats large durations in seconds", () => { + assert({ + given: "65432 milliseconds", + should: "return value in seconds", + actual: formatDuration(65432), + expected: "65.43s", + }); + }); + + test("handles value just under 1 second", () => { + assert({ + given: "999 milliseconds", + should: "return value in ms format", + actual: formatDuration(999), + expected: "999ms", + }); + }); + }); + + describe("ensureDatabase", () => { + test("creates database directory if it does not exist", async () => { + const tempDir = await createTempDir(); + const dbPath = path.join(tempDir, "nested", "dir", "test.db"); + + const db = await ensureDatabase(dbPath); + onTestFinished(async () => { + closeDatabase(db); + await fs.remove(tempDir); + }); + + const dirExists = await fs.pathExists(path.dirname(dbPath)); + + assert({ + given: "database path with non-existent directories", + should: "create the directory structure", + actual: dirExists, + expected: true, + }); + }); + + test("creates database file", async () => { + const tempDir = await createTempDir(); + const dbPath = path.join(tempDir, "test.db"); + + const db = await ensureDatabase(dbPath); + onTestFinished(async () => { + closeDatabase(db); + await fs.remove(tempDir); + }); + + const dbExists = await fs.pathExists(dbPath); + + assert({ + given: "valid database path", + should: "create the database file", + actual: dbExists, + expected: true, + }); + }); + + test("initializes schema with documents table", async () => { + const tempDir = await createTempDir(); + const dbPath = path.join(tempDir, "test.db"); + + const db = await ensureDatabase(dbPath); + onTestFinished(async () => { + closeDatabase(db); + await fs.remove(tempDir); + }); + + const tables = db + .prepare( + `SELECT name FROM sqlite_master WHERE type='table' AND name='documents'`, + ) + .all(); + + assert({ + given: "newly created database", + should: "have documents table", + actual: tables.length, + expected: 1, + }); + }); + + test("initializes schema with fts_documents virtual table", async () => { + const tempDir = await createTempDir(); + const dbPath = path.join(tempDir, "test.db"); + + const db = await ensureDatabase(dbPath); + onTestFinished(async () => { + closeDatabase(db); + await fs.remove(tempDir); + }); + + const tables = db + .prepare( + `SELECT name FROM sqlite_master WHERE type='table' AND name='fts_documents'`, + ) + .all(); + + assert({ + given: "newly created database", + should: "have fts_documents virtual table", + actual: tables.length, + expected: 1, + }); + }); + + test("initializes schema with dependencies table", async () => { + const tempDir = await createTempDir(); + const dbPath = path.join(tempDir, "test.db"); + + const db = await ensureDatabase(dbPath); + onTestFinished(async () => { + closeDatabase(db); + await fs.remove(tempDir); + }); + + const tables = db + .prepare( + `SELECT name FROM sqlite_master WHERE type='table' AND name='dependencies'`, + ) + .all(); + + assert({ + given: "newly created database", + should: "have dependencies table", + actual: tables.length, + expected: 1, + }); + }); + + test("returns valid database connection", async () => { + const tempDir = await createTempDir(); + const dbPath = path.join(tempDir, "test.db"); + + const db = await ensureDatabase(dbPath); + onTestFinished(async () => { + closeDatabase(db); + await fs.remove(tempDir); + }); + + const result = db.prepare("SELECT 1 as value").get(); + + assert({ + given: "database connection", + should: "be able to execute queries", + actual: result.value, + expected: 1, + }); + }); + + test("works with existing directory", async () => { + const tempDir = await createTempDir(); + const dbPath = path.join(tempDir, "test.db"); + + const db = await ensureDatabase(dbPath); + onTestFinished(async () => { + closeDatabase(db); + await fs.remove(tempDir); + }); + + assert({ + given: "existing directory", + should: "create database without error", + actual: db !== null, + expected: true, + }); + }); + }); +}); diff --git a/ai/tools/cli/query-cli.js b/ai/tools/cli/query-cli.js new file mode 100644 index 0000000..52763b8 --- /dev/null +++ b/ai/tools/cli/query-cli.js @@ -0,0 +1,108 @@ +#!/usr/bin/env node + +/** + * CLI for querying the aidd index. + * Usage: node ai/tools/cli/query-cli.js [options] + */ + +import { program } from "commander"; +import path from "path"; +import fs from "fs-extra"; +import chalk from "chalk"; + +import { createDatabase, closeDatabase } from "../db/connection.js"; +import { fanOutSearch } from "../search/fan-out.js"; +import { searchFts5 } from "../search/fts5.js"; +import { searchMetadata } from "../search/metadata.js"; + +const DEFAULT_DB_PATH = ".aidd/index.db"; + +/** + * Format search results for console output. + */ +export const formatResults = (results, options) => { + if (options.json) { + return JSON.stringify(results, null, 2); + } + + if (results.length === 0) { + return chalk.yellow("No results found."); + } + + const lines = [ + chalk.blue(`Found ${results.length} result(s):\n`), + ...results.flatMap((result) => [ + chalk.white(` ${result.path}`), + chalk.gray(` Type: ${result.type}`), + ...(result.relevanceScore !== undefined + ? [chalk.gray(` Score: ${result.relevanceScore.toFixed(3)}`)] + : []), + ...(result.frontmatter?.description + ? [chalk.gray(` ${result.frontmatter.description}`)] + : []), + ...(result.snippet && options.snippets + ? [chalk.gray(` ...${result.snippet.trim().slice(0, 100)}...`)] + : []), + "", + ]), + ]; + + return lines.join("\n"); +}; + +program + .name("aidd-query") + .description("Query the aidd index") + .argument("", "Search query") + .option("-d, --db ", "Database path", DEFAULT_DB_PATH) + .option("-r, --root ", "Root directory", process.cwd()) + .option("-t, --type ", "Filter by document type") + .option("-l, --limit ", "Maximum results", "20") + .option("--json", "Output as JSON") + .option("--snippets", "Include content snippets") + .option("--fts-only", "Use only FTS5 search") + .option("--metadata-only", "Use only metadata search") + .action(async (query, options) => { + try { + const dbPath = path.resolve(options.root, options.db); + + // Validate database exists + if (!(await fs.pathExists(dbPath))) { + console.error(chalk.red(`Error: Database not found at ${dbPath}`)); + console.error( + chalk.yellow("Run 'npm run aidd:index' first to create the index."), + ); + process.exit(1); + } + + const db = createDatabase(dbPath); + + const limit = parseInt(options.limit, 10); + let results; + + if (options.ftsOnly) { + results = searchFts5(db, query, { type: options.type, limit }); + } else if (options.metadataOnly) { + const filters = options.type ? { type: options.type } : {}; + results = searchMetadata(db, filters, { limit }); + } else { + results = await fanOutSearch(db, query, { + type: options.type, + limit, + }); + } + + console.log(formatResults(results, options)); + + closeDatabase(db); + process.exit(0); + } catch (error) { + console.error(chalk.red(`Error: ${error.message}`)); + process.exit(1); + } + }); + +// Only parse when run directly, not when imported for testing +if (process.argv[1]?.endsWith("query-cli.js")) { + program.parse(); +} diff --git a/ai/tools/cli/query-cli.test.js b/ai/tools/cli/query-cli.test.js new file mode 100644 index 0000000..8472d44 --- /dev/null +++ b/ai/tools/cli/query-cli.test.js @@ -0,0 +1,258 @@ +import { describe, test } from "vitest"; +import { assert } from "riteway/vitest"; + +import { formatResults } from "./query-cli.js"; + +describe("cli/query-cli", () => { + describe("formatResults", () => { + test("returns JSON when json option is true", () => { + const results = [{ path: "test.md", type: "rule", frontmatter: {} }]; + + const output = formatResults(results, { json: true }); + const parsed = JSON.parse(output); + + assert({ + given: "results with json option", + should: "return valid JSON string", + actual: parsed.length, + expected: 1, + }); + }); + + test("returns formatted JSON with indentation", () => { + const results = [{ path: "test.md", type: "rule", frontmatter: {} }]; + + const output = formatResults(results, { json: true }); + + assert({ + given: "json option", + should: "return pretty-printed JSON", + actual: output.includes("\n"), + expected: true, + }); + }); + + test("returns no results message for empty array", () => { + const output = formatResults([], {}); + + assert({ + given: "empty results", + should: "return no results message", + actual: output.includes("No results found"), + expected: true, + }); + }); + + test("includes result count in output", () => { + const results = [ + { path: "test1.md", type: "rule", frontmatter: {} }, + { path: "test2.md", type: "rule", frontmatter: {} }, + ]; + + const output = formatResults(results, {}); + + assert({ + given: "multiple results", + should: "include result count", + actual: output.includes("2 result(s)"), + expected: true, + }); + }); + + test("includes file path in output", () => { + const results = [ + { path: "ai/rules/auth.mdc", type: "rule", frontmatter: {} }, + ]; + + const output = formatResults(results, {}); + + assert({ + given: "result with path", + should: "include the file path", + actual: output.includes("ai/rules/auth.mdc"), + expected: true, + }); + }); + + test("includes document type in output", () => { + const results = [{ path: "test.md", type: "command", frontmatter: {} }]; + + const output = formatResults(results, {}); + + assert({ + given: "result with type", + should: "include the document type", + actual: output.includes("Type: command"), + expected: true, + }); + }); + + test("includes relevance score when present", () => { + const results = [ + { + path: "test.md", + type: "rule", + frontmatter: {}, + relevanceScore: 0.8567, + }, + ]; + + const output = formatResults(results, {}); + + assert({ + given: "result with relevance score", + should: "include formatted score", + actual: output.includes("Score: 0.857"), + expected: true, + }); + }); + + test("includes description from frontmatter when present", () => { + const results = [ + { + path: "test.md", + type: "rule", + frontmatter: { description: "Authentication rules" }, + }, + ]; + + const output = formatResults(results, {}); + + assert({ + given: "result with frontmatter description", + should: "include the description", + actual: output.includes("Authentication rules"), + expected: true, + }); + }); + + test("includes snippet when snippets option is true", () => { + const results = [ + { + path: "test.md", + type: "rule", + frontmatter: {}, + snippet: "This is the content snippet from the file", + }, + ]; + + const output = formatResults(results, { snippets: true }); + + assert({ + given: "result with snippet and snippets option", + should: "include the snippet", + actual: output.includes("content snippet"), + expected: true, + }); + }); + + test("excludes snippet when snippets option is false", () => { + const results = [ + { + path: "test.md", + type: "rule", + frontmatter: {}, + snippet: "This is the content snippet", + }, + ]; + + const output = formatResults(results, { snippets: false }); + + assert({ + given: "result with snippet but snippets option false", + should: "not include the snippet", + actual: output.includes("content snippet"), + expected: false, + }); + }); + + test("truncates long snippets to 100 characters", () => { + const longSnippet = "a".repeat(200); + const results = [ + { + path: "test.md", + type: "rule", + frontmatter: {}, + snippet: longSnippet, + }, + ]; + + const output = formatResults(results, { snippets: true }); + + assert({ + given: "result with long snippet", + should: "truncate snippet content", + actual: + output.includes("a".repeat(100)) && !output.includes("a".repeat(101)), + expected: true, + }); + }); + + test("handles results without relevance score", () => { + const results = [{ path: "test.md", type: "rule", frontmatter: {} }]; + + const output = formatResults(results, {}); + + assert({ + given: "result without relevance score", + should: "not include Score line", + actual: output.includes("Score:"), + expected: false, + }); + }); + + test("handles results without frontmatter description", () => { + const results = [{ path: "test.md", type: "rule", frontmatter: {} }]; + + const output = formatResults(results, {}); + + assert({ + given: "result without description", + should: "still format successfully", + actual: output.includes("test.md"), + expected: true, + }); + }); + + test("handles null frontmatter gracefully", () => { + const results = [{ path: "test.md", type: "rule", frontmatter: null }]; + + const output = formatResults(results, {}); + + assert({ + given: "result with null frontmatter", + should: "format without error", + actual: output.includes("test.md"), + expected: true, + }); + }); + + test("formats multiple results with correct spacing", () => { + const results = [ + { path: "file1.md", type: "rule", frontmatter: {} }, + { path: "file2.md", type: "command", frontmatter: {} }, + ]; + + const output = formatResults(results, {}); + + assert({ + given: "multiple results", + should: "include both files", + actual: output.includes("file1.md") && output.includes("file2.md"), + expected: true, + }); + }); + + test("returns JSON for empty results with json option", () => { + const output = formatResults([], { json: true }); + const parsed = JSON.parse(output); + + assert({ + given: "empty results with json option", + should: "return empty JSON array", + actual: parsed, + expected: [], + }); + }); + }); +}); diff --git a/ai/tools/db/connection.js b/ai/tools/db/connection.js new file mode 100644 index 0000000..cfaa3b8 --- /dev/null +++ b/ai/tools/db/connection.js @@ -0,0 +1,40 @@ +import Database from "better-sqlite3"; + +/** + * Create a new SQLite database connection with optimized settings. + * Uses WAL mode for better concurrency and enables foreign keys. + * + * @param {string} dbPath - Path to database file, or ':memory:' for in-memory + * @returns {Database.Database} Configured database instance + */ +const createDatabase = (dbPath) => { + const db = new Database(dbPath); + + // Enable WAL mode for better concurrent read/write performance + db.pragma("journal_mode = WAL"); + + // Enable foreign key constraints + db.pragma("foreign_keys = ON"); + + return db; +}; + +/** + * Safely close a database connection. + * + * @param {Database.Database} db - Database instance to close + * @returns {boolean} True if closed successfully, false if already closed + */ +const closeDatabase = (db) => { + try { + if (db.open) { + db.close(); + return true; + } + return false; + } catch { + return false; + } +}; + +export { createDatabase, closeDatabase }; diff --git a/ai/tools/db/connection.test.js b/ai/tools/db/connection.test.js new file mode 100644 index 0000000..d7f69b2 --- /dev/null +++ b/ai/tools/db/connection.test.js @@ -0,0 +1,125 @@ +import { describe, test, onTestFinished } from "vitest"; +import { assert } from "riteway/vitest"; +import path from "path"; +import fs from "fs-extra"; +import os from "os"; +import { createId } from "@paralleldrive/cuid2"; + +import { createDatabase, closeDatabase } from "./connection.js"; + +const setupTestDirectory = async () => { + const tempDir = path.join(os.tmpdir(), `aidd-test-${createId()}`); + await fs.ensureDir(tempDir); + const dbPath = path.join(tempDir, "test.db"); + + onTestFinished(async () => { + await fs.remove(tempDir); + }); + + return { tempDir, dbPath }; +}; + +describe("db/connection", () => { + describe("createDatabase", () => { + test("creates a database connection", async () => { + const { dbPath } = await setupTestDirectory(); + const db = createDatabase(dbPath); + + assert({ + given: "a valid database path", + should: "return a database object", + actual: typeof db.prepare, + expected: "function", + }); + + closeDatabase(db); + }); + + test("enables WAL mode for better concurrency", async () => { + const { dbPath } = await setupTestDirectory(); + const db = createDatabase(dbPath); + const result = db.pragma("journal_mode", { simple: true }); + + assert({ + given: "a new database", + should: "use WAL journal mode", + actual: result, + expected: "wal", + }); + + closeDatabase(db); + }); + + test("enables foreign keys", async () => { + const { dbPath } = await setupTestDirectory(); + const db = createDatabase(dbPath); + const result = db.pragma("foreign_keys", { simple: true }); + + assert({ + given: "a new database", + should: "have foreign keys enabled", + actual: result, + expected: 1, + }); + + closeDatabase(db); + }); + + test("creates the database file on disk", async () => { + const { dbPath } = await setupTestDirectory(); + const db = createDatabase(dbPath); + closeDatabase(db); + + const exists = await fs.pathExists(dbPath); + + assert({ + given: "createDatabase called", + should: "create the database file", + actual: exists, + expected: true, + }); + }); + + test("uses in-memory database when path is :memory:", () => { + const db = createDatabase(":memory:"); + + assert({ + given: ":memory: as path", + should: "create an in-memory database", + actual: db.memory, + expected: true, + }); + + closeDatabase(db); + }); + }); + + describe("closeDatabase", () => { + test("closes the database connection", async () => { + const { dbPath } = await setupTestDirectory(); + const db = createDatabase(dbPath); + const result = closeDatabase(db); + + assert({ + given: "an open database", + should: "close successfully and return true", + actual: result, + expected: true, + }); + }); + + test("returns false for already closed database", async () => { + const { dbPath } = await setupTestDirectory(); + const db = createDatabase(dbPath); + closeDatabase(db); + const result = closeDatabase(db); + + assert({ + given: "an already closed database", + should: "return false", + actual: result, + expected: false, + }); + }); + }); +}); diff --git a/ai/tools/db/schema.js b/ai/tools/db/schema.js new file mode 100644 index 0000000..da5c1c1 --- /dev/null +++ b/ai/tools/db/schema.js @@ -0,0 +1,196 @@ +/** + * Database schema for aidd index. + * Creates tables for documents, full-text search, and dependencies. + */ + +const CURRENT_SCHEMA_VERSION = 1; + +/** + * Check if a table exists in the database. + * + * @param {import('better-sqlite3').Database} db + * @param {string} tableName + * @returns {boolean} + */ +const tableExists = (db, tableName) => { + const result = db + .prepare( + ` + SELECT name FROM sqlite_master + WHERE type='table' AND name = ? + `, + ) + .get(tableName); + return result !== undefined; +}; + +/** + * Get the current schema version from the database. + * Returns 0 if schema_version table doesn't exist. + * + * @param {import('better-sqlite3').Database} db + * @returns {number} + */ +const getSchemaVersion = (db) => { + if (!tableExists(db, "schema_version")) { + return 0; + } + const result = db + .prepare("SELECT version FROM schema_version ORDER BY version DESC LIMIT 1") + .get(); + return result?.version ?? 0; +}; + +/** + * Create the schema_version table for tracking migrations. + * + * @param {import('better-sqlite3').Database} db + */ +const createSchemaVersionTable = (db) => { + db.exec(` + CREATE TABLE IF NOT EXISTS schema_version ( + version INTEGER PRIMARY KEY, + applied_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now') * 1000) + ) + `); +}; + +/** + * Create the documents table for storing file metadata and content. + * + * @param {import('better-sqlite3').Database} db + */ +const createDocumentsTable = (db) => { + db.exec(` + CREATE TABLE IF NOT EXISTS documents ( + path TEXT PRIMARY KEY, + type TEXT NOT NULL DEFAULT 'other', + frontmatter TEXT NOT NULL DEFAULT '{}', + content TEXT NOT NULL DEFAULT '', + hash TEXT NOT NULL, + file_size INTEGER, + modified_at INTEGER, + indexed_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now') * 1000) + ) + `); + + // Index for querying by type + db.exec(` + CREATE INDEX IF NOT EXISTS idx_documents_type ON documents(type) + `); + + // Index for finding stale entries + db.exec(` + CREATE INDEX IF NOT EXISTS idx_documents_hash ON documents(hash) + `); +}; + +/** + * Create FTS5 virtual table for full-text search. + * Includes triggers to keep it in sync with documents table. + * + * @param {import('better-sqlite3').Database} db + */ +const createFtsTable = (db) => { + // FTS5 virtual table for full-text search on content and frontmatter + db.exec(` + CREATE VIRTUAL TABLE IF NOT EXISTS fts_documents USING fts5( + path, + frontmatter, + content, + content='documents', + content_rowid='rowid' + ) + `); + + // Trigger to sync inserts + db.exec(` + CREATE TRIGGER IF NOT EXISTS fts_documents_insert AFTER INSERT ON documents BEGIN + INSERT INTO fts_documents(rowid, path, frontmatter, content) + VALUES (NEW.rowid, NEW.path, NEW.frontmatter, NEW.content); + END + `); + + // Trigger to sync updates + db.exec(` + CREATE TRIGGER IF NOT EXISTS fts_documents_update AFTER UPDATE ON documents BEGIN + INSERT INTO fts_documents(fts_documents, rowid, path, frontmatter, content) + VALUES ('delete', OLD.rowid, OLD.path, OLD.frontmatter, OLD.content); + INSERT INTO fts_documents(rowid, path, frontmatter, content) + VALUES (NEW.rowid, NEW.path, NEW.frontmatter, NEW.content); + END + `); + + // Trigger to sync deletes + db.exec(` + CREATE TRIGGER IF NOT EXISTS fts_documents_delete AFTER DELETE ON documents BEGIN + INSERT INTO fts_documents(fts_documents, rowid, path, frontmatter, content) + VALUES ('delete', OLD.rowid, OLD.path, OLD.frontmatter, OLD.content); + END + `); +}; + +/** + * Create the dependencies table for tracking file imports. + * + * @param {import('better-sqlite3').Database} db + */ +const createDependenciesTable = (db) => { + db.exec(` + CREATE TABLE IF NOT EXISTS dependencies ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + from_file TEXT NOT NULL, + to_file TEXT NOT NULL, + import_type TEXT NOT NULL DEFAULT 'import', + line_number INTEGER, + import_text TEXT, + FOREIGN KEY (from_file) REFERENCES documents(path) ON DELETE CASCADE, + UNIQUE(from_file, to_file, import_type) + ) + `); + + // Index for forward dependency lookups + db.exec(` + CREATE INDEX IF NOT EXISTS idx_dependencies_from ON dependencies(from_file) + `); + + // Index for reverse dependency lookups + db.exec(` + CREATE INDEX IF NOT EXISTS idx_dependencies_to ON dependencies(to_file) + `); +}; + +/** + * Initialize the database schema. + * Safe to call multiple times (idempotent). + * + * @param {import('better-sqlite3').Database} db + */ +const initializeSchema = (db) => { + const currentVersion = getSchemaVersion(db); + + if (currentVersion >= CURRENT_SCHEMA_VERSION) { + return; // Already up to date + } + + db.transaction(() => { + createSchemaVersionTable(db); + createDocumentsTable(db); + createFtsTable(db); + createDependenciesTable(db); + + // Record schema version + db.prepare( + ` + INSERT OR REPLACE INTO schema_version (version) VALUES (?) + `, + ).run(CURRENT_SCHEMA_VERSION); + })(); +}; + +export { + initializeSchema, + getSchemaVersion, + tableExists, + CURRENT_SCHEMA_VERSION, +}; diff --git a/ai/tools/db/schema.test.js b/ai/tools/db/schema.test.js new file mode 100644 index 0000000..3834b38 --- /dev/null +++ b/ai/tools/db/schema.test.js @@ -0,0 +1,397 @@ +import { describe, test, onTestFinished } from "vitest"; +import { assert } from "riteway/vitest"; + +import { createDatabase, closeDatabase } from "./connection.js"; +import { + initializeSchema, + getSchemaVersion, + tableExists, + CURRENT_SCHEMA_VERSION, +} from "./schema.js"; + +const setupTestDatabase = () => { + const db = createDatabase(":memory:"); + onTestFinished(() => closeDatabase(db)); + return db; +}; + +describe("db/schema", () => { + describe("initializeSchema", () => { + test("creates schema_version table", () => { + const db = setupTestDatabase(); + initializeSchema(db); + + assert({ + given: "initializeSchema called", + should: "create schema_version table", + actual: tableExists(db, "schema_version"), + expected: true, + }); + }); + + test("creates documents table", () => { + const db = setupTestDatabase(); + initializeSchema(db); + + assert({ + given: "initializeSchema called", + should: "create documents table", + actual: tableExists(db, "documents"), + expected: true, + }); + }); + + test("creates fts_documents virtual table", () => { + const db = setupTestDatabase(); + initializeSchema(db); + + assert({ + given: "initializeSchema called", + should: "create fts_documents FTS5 virtual table", + actual: tableExists(db, "fts_documents"), + expected: true, + }); + }); + + test("creates dependencies table", () => { + const db = setupTestDatabase(); + initializeSchema(db); + + assert({ + given: "initializeSchema called", + should: "create dependencies table", + actual: tableExists(db, "dependencies"), + expected: true, + }); + }); + + test("sets schema version", () => { + const db = setupTestDatabase(); + initializeSchema(db); + + assert({ + given: "initializeSchema called", + should: "set current schema version", + actual: getSchemaVersion(db), + expected: CURRENT_SCHEMA_VERSION, + }); + }); + + test("is idempotent - safe to call multiple times", () => { + const db = setupTestDatabase(); + initializeSchema(db); + initializeSchema(db); + initializeSchema(db); + + assert({ + given: "initializeSchema called multiple times", + should: "still have correct schema version", + actual: getSchemaVersion(db), + expected: CURRENT_SCHEMA_VERSION, + }); + }); + }); + + describe("documents table structure", () => { + test("allows inserting document with all fields", () => { + const db = setupTestDatabase(); + initializeSchema(db); + + const insert = db.prepare(` + INSERT INTO documents (path, type, frontmatter, content, hash, file_size, modified_at) + VALUES (?, ?, ?, ?, ?, ?, ?) + `); + + const result = insert.run( + "ai/rules/test.mdc", + "rule", + JSON.stringify({ description: "Test rule" }), + "# Test\n\nContent here", + "abc123", + 1024, + Date.now(), + ); + + assert({ + given: "inserting a document", + should: "succeed with changes = 1", + actual: result.changes, + expected: 1, + }); + }); + + test("enforces unique path constraint", () => { + const db = setupTestDatabase(); + initializeSchema(db); + + const insert = db.prepare(` + INSERT INTO documents (path, type, frontmatter, content, hash) + VALUES (?, ?, ?, ?, ?) + `); + + insert.run("ai/rules/test.mdc", "rule", "{}", "content", "hash1"); + + let threw = false; + try { + insert.run("ai/rules/test.mdc", "rule", "{}", "content", "hash2"); + } catch { + threw = true; + } + + assert({ + given: "inserting duplicate path", + should: "throw constraint error", + actual: threw, + expected: true, + }); + }); + + test("supports UPSERT via INSERT OR REPLACE", () => { + const db = setupTestDatabase(); + initializeSchema(db); + + const upsert = db.prepare(` + INSERT OR REPLACE INTO documents (path, type, frontmatter, content, hash) + VALUES (?, ?, ?, ?, ?) + `); + + upsert.run("ai/rules/test.mdc", "rule", "{}", "content v1", "hash1"); + upsert.run("ai/rules/test.mdc", "rule", "{}", "content v2", "hash2"); + + const doc = db + .prepare("SELECT content, hash FROM documents WHERE path = ?") + .get("ai/rules/test.mdc"); + + assert({ + given: "upserting a document", + should: "update existing record", + actual: { content: doc.content, hash: doc.hash }, + expected: { content: "content v2", hash: "hash2" }, + }); + }); + }); + + describe("FTS5 full-text search", () => { + test("syncs content to FTS index via trigger", () => { + const db = setupTestDatabase(); + initializeSchema(db); + + db.prepare( + ` + INSERT INTO documents (path, type, frontmatter, content, hash) + VALUES (?, ?, ?, ?, ?) + `, + ).run( + "ai/rules/auth.mdc", + "rule", + JSON.stringify({ description: "Authentication rules" }), + "# Authentication\n\nHandle user login and sessions", + "hash123", + ); + + const results = db + .prepare( + ` + SELECT path FROM fts_documents WHERE fts_documents MATCH ? + `, + ) + .all("authentication"); + + assert({ + given: "document with 'authentication' in frontmatter", + should: "be found via FTS search", + actual: results.length, + expected: 1, + }); + }); + + test("finds content matches", () => { + const db = setupTestDatabase(); + initializeSchema(db); + + db.prepare( + ` + INSERT INTO documents (path, type, frontmatter, content, hash) + VALUES (?, ?, ?, ?, ?) + `, + ).run( + "ai/rules/session.mdc", + "rule", + "{}", + "# Session Management\n\nHandle user sessions and cookies", + "hash456", + ); + + const results = db + .prepare( + ` + SELECT path FROM fts_documents WHERE fts_documents MATCH ? + `, + ) + .all("cookies"); + + assert({ + given: "document with 'cookies' in content", + should: "be found via FTS search", + actual: results.map((r) => r.path), + expected: ["ai/rules/session.mdc"], + }); + }); + + test("updates FTS index when document is updated", () => { + const db = setupTestDatabase(); + initializeSchema(db); + + db.prepare( + ` + INSERT INTO documents (path, type, frontmatter, content, hash) + VALUES (?, ?, ?, ?, ?) + `, + ).run("ai/rules/test.mdc", "rule", "{}", "original content", "hash1"); + + db.prepare( + ` + UPDATE documents SET content = ?, hash = ? WHERE path = ? + `, + ).run("updated content with newterm", "hash2", "ai/rules/test.mdc"); + + const results = db + .prepare( + ` + SELECT path FROM fts_documents WHERE fts_documents MATCH ? + `, + ) + .all("newterm"); + + assert({ + given: "document updated with new content", + should: "find new content via FTS", + actual: results.length, + expected: 1, + }); + }); + + test("removes from FTS index when document is deleted", () => { + const db = setupTestDatabase(); + initializeSchema(db); + + db.prepare( + ` + INSERT INTO documents (path, type, frontmatter, content, hash) + VALUES (?, ?, ?, ?, ?) + `, + ).run("ai/rules/temp.mdc", "rule", "{}", "temporary content", "hash1"); + + db.prepare("DELETE FROM documents WHERE path = ?").run( + "ai/rules/temp.mdc", + ); + + const results = db + .prepare( + ` + SELECT path FROM fts_documents WHERE fts_documents MATCH ? + `, + ) + .all("temporary"); + + assert({ + given: "document deleted", + should: "no longer appear in FTS results", + actual: results.length, + expected: 0, + }); + }); + }); + + describe("dependencies table", () => { + test("allows inserting dependency relationships", () => { + const db = setupTestDatabase(); + initializeSchema(db); + + // Insert parent documents first + const insertDoc = db.prepare(` + INSERT INTO documents (path, type, frontmatter, content, hash) + VALUES (?, ?, ?, ?, ?) + `); + insertDoc.run("src/index.js", "other", "{}", "content", "hash1"); + insertDoc.run("src/utils.js", "other", "{}", "content", "hash2"); + + const insertDep = db.prepare(` + INSERT INTO dependencies (from_file, to_file, import_type, line_number, import_text) + VALUES (?, ?, ?, ?, ?) + `); + + const result = insertDep.run( + "src/index.js", + "src/utils.js", + "import", + 5, + "import { helper } from './utils.js'", + ); + + assert({ + given: "inserting a dependency", + should: "succeed with changes = 1", + actual: result.changes, + expected: 1, + }); + }); + + test("cascades delete when from_file document is deleted", () => { + const db = setupTestDatabase(); + initializeSchema(db); + + const insertDoc = db.prepare(` + INSERT INTO documents (path, type, frontmatter, content, hash) + VALUES (?, ?, ?, ?, ?) + `); + insertDoc.run("src/a.js", "other", "{}", "content", "hash1"); + insertDoc.run("src/b.js", "other", "{}", "content", "hash2"); + + db.prepare( + ` + INSERT INTO dependencies (from_file, to_file, import_type) + VALUES (?, ?, ?) + `, + ).run("src/a.js", "src/b.js", "import"); + + db.prepare("DELETE FROM documents WHERE path = ?").run("src/a.js"); + + const deps = db + .prepare("SELECT * FROM dependencies WHERE from_file = ?") + .all("src/a.js"); + + assert({ + given: "from_file document deleted", + should: "cascade delete the dependency", + actual: deps.length, + expected: 0, + }); + }); + }); + + describe("tableExists", () => { + test("returns true for existing table", () => { + const db = setupTestDatabase(); + initializeSchema(db); + + assert({ + given: "documents table created", + should: "return true", + actual: tableExists(db, "documents"), + expected: true, + }); + }); + + test("returns false for non-existing table", () => { + const db = setupTestDatabase(); + + assert({ + given: "table does not exist", + should: "return false", + actual: tableExists(db, "nonexistent"), + expected: false, + }); + }); + }); +}); diff --git a/ai/tools/errors.js b/ai/tools/errors.js new file mode 100644 index 0000000..a69dfa6 --- /dev/null +++ b/ai/tools/errors.js @@ -0,0 +1,17 @@ +/** + * Error definitions for the AIDD tools module. + * Uses error-causes for structured error handling. + */ + +import { errorCauses } from "error-causes"; + +const [toolsErrors, handleToolsErrors] = errorCauses({ + ValidationError: { + code: "VALIDATION_ERROR", + message: "Input validation failed", + }, +}); + +const { ValidationError } = toolsErrors; + +export { toolsErrors, handleToolsErrors, ValidationError }; diff --git a/ai/tools/graph/traverse.js b/ai/tools/graph/traverse.js new file mode 100644 index 0000000..4fda7bd --- /dev/null +++ b/ai/tools/graph/traverse.js @@ -0,0 +1,216 @@ +/** + * Dependency graph traversal using recursive CTEs. + */ + +// Delimiter for tracking visited paths in recursive CTEs. +// Using char(0) (null byte) which is invalid in file paths on all operating systems. +const VISITED_DELIMITER = "char(0)"; + +/** + * Get forward dependencies (files that this file imports). + * Uses recursive CTE for efficient traversal. + * + * @param {import('better-sqlite3').Database} db + * @param {string} filePath - Starting file path + * @param {Object} options + * @param {number} [options.maxDepth=3] - Maximum traversal depth + * @returns {Array<{file: string, depth: number, importType: string}>} + */ +const getForwardDeps = (db, filePath, { maxDepth = 3 } = {}) => { + const query = ` + WITH RECURSIVE forward_deps(file, depth, visited) AS ( + -- Base case: direct dependencies + SELECT + to_file, + 1, + from_file || ${VISITED_DELIMITER} || to_file + FROM dependencies + WHERE from_file = ? + + UNION + + -- Recursive case + SELECT + d.to_file, + fd.depth + 1, + fd.visited || ${VISITED_DELIMITER} || d.to_file + FROM dependencies d + JOIN forward_deps fd ON d.from_file = fd.file + WHERE fd.depth < ? + AND instr(${VISITED_DELIMITER} || fd.visited || ${VISITED_DELIMITER}, ${VISITED_DELIMITER} || d.to_file || ${VISITED_DELIMITER}) = 0 + ) + SELECT DISTINCT file, MIN(depth) as depth + FROM forward_deps + GROUP BY file + ORDER BY depth, file + `; + + return db + .prepare(query) + .all(filePath, maxDepth) + .map((row) => ({ + file: row.file, + depth: row.depth, + direction: "forward", + })); +}; + +/** + * Get reverse dependencies (files that import this file). + * Uses recursive CTE for efficient traversal. + * + * @param {import('better-sqlite3').Database} db + * @param {string} filePath - Target file path + * @param {Object} options + * @param {number} [options.maxDepth=3] - Maximum traversal depth + * @returns {Array<{file: string, depth: number, importType: string}>} + */ +const getReverseDeps = (db, filePath, { maxDepth = 3 } = {}) => { + const query = ` + WITH RECURSIVE reverse_deps(file, depth, visited) AS ( + -- Base case: files that directly import target + SELECT + from_file, + 1, + to_file || ${VISITED_DELIMITER} || from_file + FROM dependencies + WHERE to_file = ? + + UNION + + -- Recursive case + SELECT + d.from_file, + rd.depth + 1, + rd.visited || ${VISITED_DELIMITER} || d.from_file + FROM dependencies d + JOIN reverse_deps rd ON d.to_file = rd.file + WHERE rd.depth < ? + AND instr(${VISITED_DELIMITER} || rd.visited || ${VISITED_DELIMITER}, ${VISITED_DELIMITER} || d.from_file || ${VISITED_DELIMITER}) = 0 + ) + SELECT DISTINCT file, MIN(depth) as depth + FROM reverse_deps + GROUP BY file + ORDER BY depth, file + `; + + return db + .prepare(query) + .all(filePath, maxDepth) + .map((row) => ({ + file: row.file, + depth: row.depth, + direction: "reverse", + })); +}; + +/** + * Find all related files (forward, reverse, or both). + * + * @param {import('better-sqlite3').Database} db + * @param {string} filePath - Starting file path + * @param {Object} options + * @param {'forward' | 'reverse' | 'both'} [options.direction='both'] - Direction to traverse + * @param {number} [options.maxDepth=3] - Maximum traversal depth + * @returns {Array<{file: string, depth: number, direction: string}>} + */ +const findRelated = ( + db, + filePath, + { direction = "both", maxDepth = 3 } = {}, +) => { + const results = []; + + if (direction === "forward" || direction === "both") { + results.push(...getForwardDeps(db, filePath, { maxDepth })); + } + + if (direction === "reverse" || direction === "both") { + results.push(...getReverseDeps(db, filePath, { maxDepth })); + } + + // Deduplicate by file path, keeping the shortest depth + const byFile = results.reduce((acc, result) => { + const existing = acc.get(result.file); + if (!existing || result.depth < existing.depth) { + acc.set(result.file, result); + } + return acc; + }, new Map()); + + return [...byFile.values()].sort((a, b) => { + if (a.depth !== b.depth) return a.depth - b.depth; + return a.file.localeCompare(b.file); + }); +}; + +/** + * Get the full dependency graph as an adjacency list. + * + * @param {import('better-sqlite3').Database} db + * @returns {Map} + */ +const getDependencyGraph = (db) => { + const deps = db.prepare("SELECT from_file, to_file FROM dependencies").all(); + + const graph = new Map(); + for (const { from_file, to_file } of deps) { + if (!graph.has(from_file)) { + graph.set(from_file, []); + } + graph.get(from_file).push(to_file); + } + + return graph; +}; + +/** + * Find all entry points (files with no dependents). + * + * @param {import('better-sqlite3').Database} db + * @returns {Array} + */ +const findEntryPoints = (db) => { + const query = ` + SELECT DISTINCT d.path + FROM documents d + WHERE d.path NOT IN (SELECT to_file FROM dependencies) + AND d.path IN (SELECT from_file FROM dependencies) + ORDER BY d.path + `; + + return db + .prepare(query) + .all() + .map((row) => row.path); +}; + +/** + * Find all leaf nodes (files with no dependencies). + * + * @param {import('better-sqlite3').Database} db + * @returns {Array} + */ +const findLeafNodes = (db) => { + const query = ` + SELECT DISTINCT d.path + FROM documents d + WHERE d.path NOT IN (SELECT from_file FROM dependencies) + AND d.path IN (SELECT to_file FROM dependencies) + ORDER BY d.path + `; + + return db + .prepare(query) + .all() + .map((row) => row.path); +}; + +export { + findRelated, + getForwardDeps, + getReverseDeps, + getDependencyGraph, + findEntryPoints, + findLeafNodes, +}; diff --git a/ai/tools/graph/traverse.test.js b/ai/tools/graph/traverse.test.js new file mode 100644 index 0000000..dda16bb --- /dev/null +++ b/ai/tools/graph/traverse.test.js @@ -0,0 +1,237 @@ +import { describe, test, onTestFinished } from "vitest"; +import { assert } from "riteway/vitest"; + +import { createDatabase, closeDatabase } from "../db/connection.js"; +import { initializeSchema } from "../db/schema.js"; +import { findRelated, getForwardDeps, getReverseDeps } from "./traverse.js"; + +const setupTestDatabase = () => { + const db = createDatabase(":memory:"); + initializeSchema(db); + + // Create a dependency graph: + // a.js -> b.js -> c.js -> d.js + // -> e.js + // f.js -> b.js (another path to b) + + const insertDoc = db.prepare(` + INSERT INTO documents (path, type, frontmatter, content, hash) + VALUES (?, ?, ?, ?, ?) + `); + + insertDoc.run("a.js", "other", "{}", "content", "hash1"); + insertDoc.run("b.js", "other", "{}", "content", "hash2"); + insertDoc.run("c.js", "other", "{}", "content", "hash3"); + insertDoc.run("d.js", "other", "{}", "content", "hash4"); + insertDoc.run("e.js", "other", "{}", "content", "hash5"); + insertDoc.run("f.js", "other", "{}", "content", "hash6"); + + const insertDep = db.prepare(` + INSERT INTO dependencies (from_file, to_file, import_type) + VALUES (?, ?, ?) + `); + + insertDep.run("a.js", "b.js", "import"); + insertDep.run("a.js", "e.js", "import"); + insertDep.run("b.js", "c.js", "import"); + insertDep.run("c.js", "d.js", "import"); + insertDep.run("f.js", "b.js", "import"); + + onTestFinished(() => closeDatabase(db)); + + return db; +}; + +describe("graph/traverse", () => { + describe("getForwardDeps", () => { + test("finds direct dependencies", () => { + const db = setupTestDatabase(); + const deps = getForwardDeps(db, "a.js", { maxDepth: 1 }); + + assert({ + given: "file with two imports", + should: "return both direct dependencies", + actual: deps.map((d) => d.file).sort(), + expected: ["b.js", "e.js"], + }); + }); + + test("finds transitive dependencies", () => { + const db = setupTestDatabase(); + const deps = getForwardDeps(db, "a.js", { maxDepth: 3 }); + const files = deps.map((d) => d.file); + + assert({ + given: "maxDepth of 3", + should: "include transitive deps (b, c, d, e)", + actual: files.includes("d.js"), + expected: true, + }); + }); + + test("respects maxDepth limit", () => { + const db = setupTestDatabase(); + const deps = getForwardDeps(db, "a.js", { maxDepth: 2 }); + const files = deps.map((d) => d.file); + + assert({ + given: "maxDepth of 2", + should: "not include depth 3 deps", + actual: files.includes("d.js"), + expected: false, + }); + }); + + test("includes depth information", () => { + const db = setupTestDatabase(); + const deps = getForwardDeps(db, "a.js", { maxDepth: 3 }); + const bDep = deps.find((d) => d.file === "b.js"); + const dDep = deps.find((d) => d.file === "d.js"); + + assert({ + given: "traversed dependencies", + should: "include correct depth", + actual: { bDepth: bDep.depth, dDepth: dDep.depth }, + expected: { bDepth: 1, dDepth: 3 }, + }); + }); + + test("returns empty array for file with no deps", () => { + const db = setupTestDatabase(); + const deps = getForwardDeps(db, "d.js", { maxDepth: 3 }); + + assert({ + given: "file with no dependencies", + should: "return empty array", + actual: deps, + expected: [], + }); + }); + }); + + describe("getReverseDeps", () => { + test("finds files that depend on target", () => { + const db = setupTestDatabase(); + const deps = getReverseDeps(db, "b.js", { maxDepth: 1 }); + + assert({ + given: "file that is imported by two others", + should: "return both dependents", + actual: deps.map((d) => d.file).sort(), + expected: ["a.js", "f.js"], + }); + }); + + test("finds transitive dependents", () => { + const db = setupTestDatabase(); + const deps = getReverseDeps(db, "d.js", { maxDepth: 3 }); + const files = deps.map((d) => d.file); + + assert({ + given: "file at end of chain", + should: "find all files that transitively depend on it", + actual: files.includes("a.js"), + expected: true, + }); + }); + + test("returns empty for file with no dependents", () => { + const db = setupTestDatabase(); + const deps = getReverseDeps(db, "a.js", { maxDepth: 3 }); + + assert({ + given: "root file with no dependents", + should: "return empty array", + actual: deps, + expected: [], + }); + }); + }); + + describe("findRelated", () => { + test("finds both forward and reverse with direction=both", () => { + const db = setupTestDatabase(); + const related = findRelated(db, "b.js", { + direction: "both", + maxDepth: 2, + }); + const files = related.map((r) => r.file); + + // Should find: a.js and f.js (reverse), c.js and d.js (forward) + assert({ + given: "direction: both", + should: "find files in both directions", + actual: files.includes("a.js") && files.includes("c.js"), + expected: true, + }); + }); + + test("marks direction in results", () => { + const db = setupTestDatabase(); + const related = findRelated(db, "b.js", { + direction: "both", + maxDepth: 1, + }); + const aResult = related.find((r) => r.file === "a.js"); + const cResult = related.find((r) => r.file === "c.js"); + + assert({ + given: "bidirectional search", + should: "mark direction for each result", + actual: { aDir: aResult.direction, cDir: cResult.direction }, + expected: { aDir: "reverse", cDir: "forward" }, + }); + }); + + test("defaults to both direction", () => { + const db = setupTestDatabase(); + const related = findRelated(db, "b.js", { maxDepth: 1 }); + + assert({ + given: "no direction specified", + should: "search both directions", + actual: related.length >= 2, + expected: true, + }); + }); + + test("handles circular dependencies gracefully", () => { + const db = setupTestDatabase(); + + // Add circular dependency: d.js -> a.js + db.prepare( + ` + INSERT INTO dependencies (from_file, to_file, import_type) + VALUES (?, ?, ?) + `, + ).run("d.js", "a.js", "import"); + + // Should not throw or hang + const related = findRelated(db, "a.js", { maxDepth: 10 }); + + assert({ + given: "circular dependency", + should: "complete without infinite loop", + actual: Array.isArray(related), + expected: true, + }); + }); + + test("deduplicates files found via multiple paths", () => { + const db = setupTestDatabase(); + const related = findRelated(db, "b.js", { + direction: "forward", + maxDepth: 3, + }); + const paths = related.map((r) => r.file); + const uniquePaths = [...new Set(paths)]; + + assert({ + given: "graph with multiple paths", + should: "deduplicate results", + actual: paths.length, + expected: uniquePaths.length, + }); + }); + }); +}); diff --git a/ai/tools/index.d.ts b/ai/tools/index.d.ts new file mode 100644 index 0000000..8f11f8a --- /dev/null +++ b/ai/tools/index.d.ts @@ -0,0 +1,412 @@ +/** + * TypeScript definitions for aidd indexing and search tools. + */ + +import type { Database } from "better-sqlite3"; + +// Error types + +export interface ErrorType { + name: string; + code: string; + message: string; +} + +export interface ToolsErrors { + ValidationError: ErrorType; +} + +/** + * Error type definitions for tools module. + */ +export const toolsErrors: ToolsErrors; + +/** + * ValidationError type for input validation failures. + */ +export const ValidationError: ErrorType; + +/** + * Error handler for tools errors. + * @param handlers - Object mapping error names to handler functions + * @returns Error handler function + */ +export function handleToolsErrors( + handlers: Record T>, +): (error: Error) => T; + +// Database types + +/** + * Create a new SQLite database connection. + * @param dbPath - Path to the SQLite database file (use ":memory:" for in-memory) + * @returns Database instance + */ +export function createDatabase(dbPath: string): Database; + +/** + * Close a database connection safely. + * @param db - Database instance to close + */ +export function closeDatabase(db: Database): void; + +/** + * Initialize the database schema (creates tables if not exist). + * @param db - Database instance + */ +export function initializeSchema(db: Database): void; + +/** + * Get the current schema version from the database. + * @param db - Database instance + * @returns Schema version number or null if not set + */ +export function getSchemaVersion(db: Database): number | null; + +/** + * Check if a table exists in the database. + * @param db - Database instance + * @param tableName - Name of the table to check + * @returns True if table exists + */ +export function tableExists(db: Database, tableName: string): boolean; + +/** + * Current schema version constant. + */ +export const CURRENT_SCHEMA_VERSION: number; + +// Indexer types + +export type DocumentType = + | "rule" + | "command" + | "skill" + | "task" + | "story-map" + | "other"; + +export interface ParsedFrontmatter { + frontmatter: Record; + content: string; +} + +export interface IndexResult { + indexed: number; + errors: string[]; +} + +export interface IncrementalIndexResult { + updated: number; + deleted: number; + unchanged: number; +} + +/** + * Index a single file into the database. + * @param db - Database instance + * @param filePath - Absolute path to file + * @param rootDir - Root directory for relative paths + */ +export function indexFile( + db: Database, + filePath: string, + rootDir: string, +): Promise; + +/** + * Index all markdown files in a directory. + * @param db - Database instance + * @param rootDir - Root directory to index + * @returns Index result with count and errors + */ +export function indexDirectory( + db: Database, + rootDir: string, +): Promise; + +/** + * Incrementally index only changed files. + * @param db - Database instance + * @param rootDir - Root directory to index + * @returns Incremental index result + */ +export function indexIncremental( + db: Database, + rootDir: string, +): Promise; + +/** + * Detect document type based on file path. + * @param filePath - Relative path to the file + * @returns Document type + */ +export function detectDocumentType(filePath: string): DocumentType; + +/** + * Compute SHA3-256 hash of file contents. + * @param filePath - Absolute path to file + * @returns Hex-encoded hash + */ +export function computeFileHash(filePath: string): Promise; + +/** + * Parse frontmatter from content, with prototype pollution protection. + * @param content - File content + * @returns Parsed frontmatter and content + */ +export function parseFrontmatter(content: string): ParsedFrontmatter; + +// Dependency indexer types + +export type ImportType = "esm" | "commonjs" | "dynamic"; + +export interface Dependency { + source: string; + importType: ImportType; + line: number; +} + +/** + * Extract import/require dependencies from file content. + * @param content - File content + * @returns Array of dependencies + */ +export function extractDependencies(content: string): Dependency[]; + +/** + * Index dependencies for a single file. + * @param db - Database instance + * @param filePath - Path of the file being indexed + * @param content - File content + * @param rootDir - Root directory for resolution + */ +export function indexFileDependencies( + db: Database, + filePath: string, + content: string, + rootDir: string, +): void; + +/** + * Index dependencies for all files in database. + * @param db - Database instance + * @param rootDir - Root directory for resolution + */ +export function indexAllDependencies(db: Database, rootDir: string): void; + +/** + * Resolve import path to actual file. + * @param importPath - Import path from source + * @param fromFile - File containing the import + * @param rootDir - Root directory + * @returns Resolved file path or null + */ +export function resolveImportPath( + importPath: string, + fromFile: string, + rootDir: string, +): string | null; + +// Search types + +export interface SearchResult { + path: string; + type: string; + frontmatter: Record; + snippet?: string; + rank?: number; + content?: string; +} + +export interface FanOutSearchResult extends SearchResult { + relevanceScore: number; + matchedStrategies: string[]; +} + +export interface SearchOptions { + limit?: number; + offset?: number; + type?: string; + silent?: boolean; +} + +export interface MetadataFilter { + type?: string; + [key: `frontmatter.${string}`]: + | string + | number + | boolean + | { contains: string }; +} + +export interface FanOutSearchOptions { + strategies?: Array<"fts5" | "metadata" | "semantic">; + filters?: MetadataFilter; + type?: string; + limit?: number; + weights?: Record; + silent?: boolean; +} + +/** + * Search documents using FTS5 full-text search. + * @param db - Database instance + * @param query - Search query (supports FTS5 operators) + * @param options - Search options + * @returns Array of search results + */ +export function searchFts5( + db: Database, + query: string, + options?: SearchOptions, +): SearchResult[]; + +/** + * Extract a relevant snippet from content around the search terms. + * @param content - Document content + * @param query - Search query + * @param contextChars - Characters of context around match + * @returns Snippet string + */ +export function extractSnippet( + content: string, + query: string, + contextChars?: number, +): string; + +/** + * Highlight matching terms in text. + * @param text - Text to highlight + * @param query - Search query + * @returns Text with markdown bold highlights + */ +export function highlightMatches(text: string, query: string): string; + +/** + * Search documents by metadata/frontmatter fields. + * @param db - Database instance + * @param filters - Filter conditions + * @param options - Search options + * @returns Array of search results + */ +export function searchMetadata( + db: Database, + filters?: MetadataFilter, + options?: { limit?: number; offset?: number }, +): SearchResult[]; + +/** + * Get all unique values for a frontmatter field. + * @param db - Database instance + * @param field - Frontmatter field name + * @returns Array of unique values + */ +export function getFieldValues(db: Database, field: string): string[]; + +/** + * Get all unique document types. + * @param db - Database instance + * @returns Array of document types + */ +export function getDocumentTypes(db: Database): string[]; + +/** + * Execute fan-out search across multiple strategies. + * @param db - Database instance + * @param query - Search query + * @param options - Fan-out search options + * @returns Promise of search results with relevance scores + */ +export function fanOutSearch( + db: Database, + query: string, + options?: FanOutSearchOptions, +): Promise; + +/** + * Aggregate results from multiple search strategies. + * @param resultsByStrategy - Results grouped by strategy name + * @param options - Aggregation options + * @returns Aggregated and ranked results + */ +export function aggregateResults( + resultsByStrategy: Record, + options?: { weights?: Record; limit?: number }, +): FanOutSearchResult[]; + +// Graph traversal types + +export interface RelatedFile { + file: string; + depth: number; + direction: "forward" | "reverse"; +} + +export interface TraversalOptions { + maxDepth?: number; +} + +export interface FindRelatedOptions extends TraversalOptions { + direction?: "forward" | "reverse" | "both"; +} + +/** + * Find all related files (forward, reverse, or both). + * @param db - Database instance + * @param filePath - Starting file path + * @param options - Traversal options + * @returns Array of related files + */ +export function findRelated( + db: Database, + filePath: string, + options?: FindRelatedOptions, +): RelatedFile[]; + +/** + * Get forward dependencies (files that this file imports). + * @param db - Database instance + * @param filePath - Starting file path + * @param options - Traversal options + * @returns Array of dependencies + */ +export function getForwardDeps( + db: Database, + filePath: string, + options?: TraversalOptions, +): RelatedFile[]; + +/** + * Get reverse dependencies (files that import this file). + * @param db - Database instance + * @param filePath - Target file path + * @param options - Traversal options + * @returns Array of dependents + */ +export function getReverseDeps( + db: Database, + filePath: string, + options?: TraversalOptions, +): RelatedFile[]; + +/** + * Get the full dependency graph as an adjacency list. + * @param db - Database instance + * @returns Map of file to its dependencies + */ +export function getDependencyGraph(db: Database): Map; + +/** + * Find all entry points (files with no dependents). + * @param db - Database instance + * @returns Array of entry point file paths + */ +export function findEntryPoints(db: Database): string[]; + +/** + * Find all leaf nodes (files with no dependencies). + * @param db - Database instance + * @returns Array of leaf node file paths + */ +export function findLeafNodes(db: Database): string[]; diff --git a/ai/tools/index.js b/ai/tools/index.js new file mode 100644 index 0000000..fc86318 --- /dev/null +++ b/ai/tools/index.js @@ -0,0 +1,53 @@ +/** + * aidd indexing and search tools. + * + * Re-exports all public APIs for convenient access. + */ + +// Errors +export { toolsErrors, handleToolsErrors, ValidationError } from "./errors.js"; + +// Database +export { createDatabase, closeDatabase } from "./db/connection.js"; +export { + initializeSchema, + getSchemaVersion, + tableExists, + CURRENT_SCHEMA_VERSION, +} from "./db/schema.js"; + +// Indexers +export { + indexFile, + indexDirectory, + indexIncremental, + detectDocumentType, + computeFileHash, + parseFrontmatter, +} from "./indexers/frontmatter.js"; + +export { + extractDependencies, + indexFileDependencies, + indexAllDependencies, + resolveImportPath, +} from "./indexers/dependencies.js"; + +// Search +export { searchFts5, extractSnippet, highlightMatches } from "./search/fts5.js"; +export { + searchMetadata, + getFieldValues, + getDocumentTypes, +} from "./search/metadata.js"; +export { fanOutSearch, aggregateResults } from "./search/fan-out.js"; + +// Graph traversal +export { + findRelated, + getForwardDeps, + getReverseDeps, + getDependencyGraph, + findEntryPoints, + findLeafNodes, +} from "./graph/traverse.js"; diff --git a/ai/tools/index.md b/ai/tools/index.md new file mode 100644 index 0000000..a4a8d54 --- /dev/null +++ b/ai/tools/index.md @@ -0,0 +1,12 @@ +# tools + +This index provides an overview of the contents in this directory. + +## Files + +### AIDD Indexing & Search Tools + +**File:** `README.md` + +*No description available* + diff --git a/ai/tools/indexers/dependencies.js b/ai/tools/indexers/dependencies.js new file mode 100644 index 0000000..aaf3a10 --- /dev/null +++ b/ai/tools/indexers/dependencies.js @@ -0,0 +1,396 @@ +import path from "path"; +import fs from "fs-extra"; +import { cruise } from "dependency-cruiser"; + +/** + * Check if an import path is a local/relative import (not npm package). + * + * @param {string} importPath + * @returns {boolean} + */ +const isLocalImport = (importPath) => + importPath.startsWith("./") || importPath.startsWith("../"); + +/** + * Resolve a relative import path to a normalized path. + * + * @param {string} importPath - The raw import path (e.g., './utils.js') + * @param {string} fromFile - The file containing the import (e.g., 'src/index.js') + * @returns {string} Resolved path (e.g., 'src/utils.js') + */ +const resolveImportPath = (importPath, fromFile) => { + const fromDir = path.dirname(fromFile); + const resolved = path.join(fromDir, importPath); + // Normalize to forward slashes + return resolved.replace(/\\/g, "/"); +}; + +/** + * Get line number for a match index in content. + * + * @param {string} content + * @param {number} index + * @returns {number} 1-based line number + */ +const getLineNumber = (content, index) => { + const lines = content.slice(0, index).split("\n"); + return lines.length; +}; + +/** + * Extract markdown link references from content. + * Uses regex because dependency-cruiser doesn't support markdown. + * + * @param {string} content - File content + * @param {string} filePath - Path to file (for resolving relative imports) + * @returns {Array<{rawPath: string, resolvedPath: string, importType: string, lineNumber: number, importText: string}>} + */ +const extractMarkdownLinks = (content, filePath) => { + const markdownLinkRegex = /\[([^\]]*)\]\(([^)]+\.(?:md|mdc))\)/g; + + return [...content.matchAll(markdownLinkRegex)] + .filter((match) => isLocalImport(match[2])) + .map((match) => ({ + rawPath: match[2], + resolvedPath: resolveImportPath(match[2], filePath), + importType: "reference", + lineNumber: getLineNumber(content, match.index), + importText: match[0], + })); +}; + +/** + * Map dependency-cruiser dependency type to our import type. + * + * @param {string} dependencyType + * @returns {string} + */ +const mapDependencyType = (dependencyType) => { + if (dependencyType === "require") return "require"; + if (dependencyType === "dynamic-import") return "dynamic-import"; + return "import"; // import, import-equals, export, re-export, etc. +}; + +/** + * Extract dependencies from file content using regex patterns. + * Kept for backward compatibility and testing. + * + * @param {string} content - File content + * @param {string} filePath - Path to file (for resolving relative imports) + * @returns {Array<{rawPath: string, resolvedPath: string, importType: string, lineNumber: number, importText: string}>} + */ +const extractDependencies = (content, filePath) => { + // Use regex for all extraction (for single-file/content-based extraction) + const extractors = [ + // ES module static imports + { + regex: /import\s+(?:[\w*{}\s,]+\s+from\s+)?['"]([^'"]+)['"]/g, + type: "import", + group: 1, + }, + // CommonJS requires + { + regex: /require\s*\(\s*['"]([^'"]+)['"]\s*\)/g, + type: "require", + group: 1, + }, + // Dynamic imports + { + regex: /import\s*\(\s*['"]([^'"]+)['"]\s*\)/g, + type: "dynamic-import", + group: 1, + }, + // Markdown links + { + regex: /\[([^\]]*)\]\(([^)]+\.(?:md|mdc))\)/g, + type: "reference", + group: 2, + }, + ]; + + return extractors.flatMap(({ regex, type, group }) => + [...content.matchAll(regex)] + .filter((match) => isLocalImport(match[group])) + .map((match) => ({ + rawPath: match[group], + resolvedPath: resolveImportPath(match[group], filePath), + importType: type, + lineNumber: getLineNumber(content, match.index), + importText: match[0], + })), + ); +}; + +/** + * Index dependencies for a single file. + * + * @param {import('better-sqlite3').Database} db + * @param {string} filePath - Absolute path to file + * @param {string} rootDir - Root directory for relative paths + * @returns {Promise} Number of dependencies indexed + */ +const indexFileDependencies = async (db, filePath, rootDir) => { + const relativePath = path.relative(rootDir, filePath).replace(/\\/g, "/"); + const content = await fs.readFile(filePath, "utf-8"); + + const deps = extractDependencies(content, relativePath); + + const insertStmt = db.prepare(` + INSERT OR IGNORE INTO dependencies (from_file, to_file, import_type, line_number, import_text) + VALUES (?, ?, ?, ?, ?) + `); + + const checkExists = db.prepare("SELECT 1 FROM documents WHERE path = ?"); + + // Filter to deps with existing targets, then insert + const validDeps = deps.filter((dep) => checkExists.get(dep.resolvedPath)); + + validDeps.forEach((dep) => + insertStmt.run( + relativePath, + dep.resolvedPath, + dep.importType, + dep.lineNumber, + dep.importText, + ), + ); + + return validDeps.length; +}; + +/** + * Recursively find all markdown files for link extraction. + * + * @param {string} dirPath + * @returns {Promise} + */ +const findMarkdownFiles = async (dirPath) => { + const files = []; + const items = await fs.readdir(dirPath, { withFileTypes: true }); + + for (const item of items) { + const fullPath = path.join(dirPath, item.name); + + if (item.isDirectory()) { + // Skip node_modules and hidden directories + if (item.name === "node_modules" || item.name.startsWith(".")) { + continue; + } + const stats = await fs.lstat(fullPath); + if (!stats.isSymbolicLink()) { + const nested = await findMarkdownFiles(fullPath); + files.push(...nested); + } + } else if (item.isFile()) { + const ext = path.extname(item.name).toLowerCase(); + if ([".md", ".mdc"].includes(ext)) { + files.push(fullPath); + } + } + } + + return files; +}; + +/** + * Recursively find all indexable files (JS, TS, MD, MDC). + * + * @param {string} dirPath + * @param {string} rootDir + * @returns {Promise} + */ +const findIndexableFiles = async (dirPath, rootDir = dirPath) => { + const files = []; + const items = await fs.readdir(dirPath, { withFileTypes: true }); + + const indexableExtensions = [".js", ".ts", ".jsx", ".tsx", ".md", ".mdc"]; + + for (const item of items) { + const fullPath = path.join(dirPath, item.name); + + if (item.isDirectory()) { + // Skip node_modules and hidden directories + if (item.name === "node_modules" || item.name.startsWith(".")) { + continue; + } + const stats = await fs.lstat(fullPath); + if (!stats.isSymbolicLink()) { + const nested = await findIndexableFiles(fullPath, rootDir); + files.push(...nested); + } + } else if (item.isFile()) { + const ext = path.extname(item.name).toLowerCase(); + if (indexableExtensions.includes(ext)) { + files.push(fullPath); + } + } + } + + return files; +}; + +/** + * Normalize a path from dependency-cruiser to be relative to rootDir. + * Handles symlinks and relative paths from different working directories. + * + * @param {string} cruiserPath - Path from dependency-cruiser + * @param {string} rootDir - Root directory + * @returns {string} Normalized relative path + */ +const normalizeCruiserPath = (cruiserPath, rootDir) => { + // Resolve to absolute path (handles ../ prefixes) + const absolutePath = path.resolve(process.cwd(), cruiserPath); + // Resolve any symlinks (e.g., /var -> /private/var on macOS) + const realPath = fs.realpathSync(absolutePath); + const realRootDir = fs.realpathSync(rootDir); + // Make relative to rootDir + return path.relative(realRootDir, realPath).replace(/\\/g, "/"); +}; + +/** + * Index all dependencies in a directory. + * Uses dependency-cruiser for JS/TS files and regex for markdown links. + * + * @param {import('better-sqlite3').Database} db + * @param {string} rootDir + * @returns {Promise<{ indexed: number, files: number, errors: string[] }>} + */ +const indexAllDependencies = async (db, rootDir) => { + const errors = []; + const allDeps = []; + + // 1. Use dependency-cruiser for JS/TS dependencies + // Handles re-exports, barrel exports, and complex import patterns that regex cannot + try { + const cruiseResult = await cruise([rootDir], { + doNotFollow: { + path: "node_modules", + }, + exclude: { + path: "node_modules", + }, + }); + + // Process dependency-cruiser results + const modules = cruiseResult.output.modules || []; + const jsExtensions = [".js", ".ts", ".jsx", ".tsx"]; + + // Use a Set to deduplicate dependencies (symlink paths can cause duplicates) + const seenDeps = new Set(); + + modules.forEach((module) => { + // Only process JS/TS files + const sourceExt = path.extname(module.source).toLowerCase(); + if (!jsExtensions.includes(sourceExt)) return; + + // Normalize source path + const fromFile = normalizeCruiserPath(module.source, rootDir); + + (module.dependencies || []).forEach((dep) => { + // Only include local dependencies (not node_modules, not unresolved) + if ( + !dep.resolved || + dep.resolved.includes("node_modules") || + dep.couldNotResolve + ) { + return; + } + + const toFile = normalizeCruiserPath(dep.resolved, rootDir); + + // Skip if same file or external (starts with ..) + if (!toFile || toFile.startsWith("..") || fromFile === toFile) { + return; + } + + // Deduplicate using a composite key + const depKey = `${fromFile}:${toFile}`; + if (seenDeps.has(depKey)) return; + seenDeps.add(depKey); + + allDeps.push({ + fromFile, + toFile, + importType: mapDependencyType(dep.dependencyTypes?.[0] || "import"), + lineNumber: dep.line || 0, + importText: dep.module || "", + }); + }); + }); + } catch (err) { + errors.push(`dependency-cruiser: ${err.message}`); + } + + // 2. Extract markdown links using regex + try { + const mdFiles = await findMarkdownFiles(rootDir); + + for (const filePath of mdFiles) { + try { + const relativePath = path + .relative(rootDir, filePath) + .replace(/\\/g, "/"); + const content = await fs.readFile(filePath, "utf-8"); + const mdLinks = extractMarkdownLinks(content, relativePath); + + mdLinks.forEach((link) => { + allDeps.push({ + fromFile: relativePath, + toFile: link.resolvedPath, + importType: link.importType, + lineNumber: link.lineNumber, + importText: link.importText, + }); + }); + } catch (err) { + errors.push(`${filePath}: ${err.message}`); + } + } + } catch (err) { + errors.push(`markdown scan: ${err.message}`); + } + + // 3. Insert into database + db.prepare("DELETE FROM dependencies").run(); + + const insertStmt = db.prepare(` + INSERT OR IGNORE INTO dependencies (from_file, to_file, import_type, line_number, import_text) + VALUES (?, ?, ?, ?, ?) + `); + const checkExists = db.prepare("SELECT 1 FROM documents WHERE path = ?"); + + let totalIndexed = 0; + + db.transaction(() => { + allDeps + .filter((dep) => checkExists.get(dep.toFile)) + .forEach((dep) => { + insertStmt.run( + dep.fromFile, + dep.toFile, + dep.importType, + dep.lineNumber, + dep.importText, + ); + totalIndexed++; + }); + })(); + + // Count unique files that had dependencies + const uniqueFiles = new Set(allDeps.map((d) => d.fromFile)).size; + + return { + indexed: totalIndexed, + files: uniqueFiles, + errors, + }; +}; + +export { + extractDependencies, + indexFileDependencies, + indexAllDependencies, + resolveImportPath, + isLocalImport, + findIndexableFiles, +}; diff --git a/ai/tools/indexers/dependencies.test.js b/ai/tools/indexers/dependencies.test.js new file mode 100644 index 0000000..28806b7 --- /dev/null +++ b/ai/tools/indexers/dependencies.test.js @@ -0,0 +1,476 @@ +import { describe, test, onTestFinished } from "vitest"; +import { assert } from "riteway/vitest"; +import path from "path"; +import fs from "fs-extra"; +import os from "os"; +import { createId } from "@paralleldrive/cuid2"; + +import { createDatabase, closeDatabase } from "../db/connection.js"; +import { initializeSchema } from "../db/schema.js"; +import { + extractDependencies, + indexFileDependencies, + indexAllDependencies, + resolveImportPath, +} from "./dependencies.js"; + +const setupTestDatabaseWithDirectory = async () => { + const db = createDatabase(":memory:"); + initializeSchema(db); + + const tempDir = path.join(os.tmpdir(), `aidd-test-${createId()}`); + await fs.ensureDir(tempDir); + + onTestFinished(async () => { + closeDatabase(db); + await fs.remove(tempDir); + }); + + return { db, tempDir }; +}; + +describe("indexers/dependencies", () => { + describe("extractDependencies", () => { + test("extracts ES module imports", () => { + const content = ` +import foo from './foo.js'; +import { bar } from '../utils/bar.js'; +import * as baz from './baz.js'; +`; + const deps = extractDependencies(content, "src/index.js"); + + assert({ + given: "content with ES module imports", + should: "extract all import paths", + actual: deps.map((d) => d.rawPath), + expected: ["./foo.js", "../utils/bar.js", "./baz.js"], + }); + }); + + test("extracts CommonJS requires", () => { + const content = ` +const foo = require('./foo.js'); +const { bar } = require('../bar.js'); +`; + const deps = extractDependencies(content, "src/index.js"); + + assert({ + given: "content with require statements", + should: "extract all require paths", + actual: deps.map((d) => d.rawPath), + expected: ["./foo.js", "../bar.js"], + }); + }); + + test("extracts dynamic imports", () => { + const content = ` +const mod = await import('./dynamic.js'); +`; + const deps = extractDependencies(content, "src/index.js"); + + assert({ + given: "content with dynamic import", + should: "extract dynamic import path", + actual: deps.map((d) => d.rawPath), + expected: ["./dynamic.js"], + }); + }); + + test("identifies import types correctly", () => { + const content = ` +import foo from './foo.js'; +const bar = require('./bar.js'); +const baz = await import('./baz.js'); +`; + const deps = extractDependencies(content, "src/index.js"); + + assert({ + given: "mixed import types", + should: "identify each type correctly", + actual: deps.map((d) => d.importType), + expected: ["import", "require", "dynamic-import"], + }); + }); + + test("extracts line numbers", () => { + const content = `line 1 +import foo from './foo.js'; +line 3 +const bar = require('./bar.js'); +`; + const deps = extractDependencies(content, "src/index.js"); + + assert({ + given: "imports on specific lines", + should: "extract correct line numbers", + actual: deps.map((d) => d.lineNumber), + expected: [2, 4], + }); + }); + + test("ignores node_modules imports", () => { + const content = ` +import lodash from 'lodash'; +import local from './local.js'; +import express from 'express'; +`; + const deps = extractDependencies(content, "src/index.js"); + + assert({ + given: "mix of local and npm imports", + should: "only extract local imports", + actual: deps.map((d) => d.rawPath), + expected: ["./local.js"], + }); + }); + + test("extracts markdown link references", () => { + const content = ` +# Documentation + +See [related doc](./related.md) for more info. +Also check [other](../docs/other.mdc). +`; + const deps = extractDependencies(content, "docs/readme.md"); + + assert({ + given: "markdown with local links", + should: "extract link paths", + actual: deps.map((d) => d.rawPath), + expected: ["./related.md", "../docs/other.mdc"], + }); + }); + + test("stores original import text", () => { + const content = `import { helper } from './utils.js';`; + const deps = extractDependencies(content, "src/index.js"); + + assert({ + given: "an import statement", + should: "store the original text", + actual: deps[0].importText.includes("import { helper }"), + expected: true, + }); + }); + }); + + describe("resolveImportPath", () => { + test("resolves relative paths", () => { + const result = resolveImportPath("./utils.js", "src/index.js"); + + assert({ + given: "relative import from src/index.js", + should: "resolve to src/utils.js", + actual: result, + expected: "src/utils.js", + }); + }); + + test("resolves parent directory paths", () => { + const result = resolveImportPath("../lib/helper.js", "src/deep/file.js"); + + assert({ + given: "parent directory import", + should: "resolve correctly", + actual: result, + expected: "src/lib/helper.js", + }); + }); + + test("normalizes path separators", () => { + const result = resolveImportPath("./sub\\file.js", "src\\index.js"); + + assert({ + given: "paths with backslashes", + should: "normalize to forward slashes", + actual: result.includes("\\"), + expected: false, + }); + }); + }); + + describe("indexFileDependencies", () => { + test("indexes dependencies for a JavaScript file", async () => { + const { db, tempDir } = await setupTestDatabaseWithDirectory(); + + // Create source files + const indexPath = path.join(tempDir, "index.js"); + const utilsPath = path.join(tempDir, "utils.js"); + + await fs.writeFile( + indexPath, + `import { helper } from './utils.js';\nconsole.log(helper);`, + ); + await fs.writeFile(utilsPath, `export const helper = () => {};`); + + // Index the source documents first + db.prepare( + `INSERT INTO documents (path, type, frontmatter, content, hash) VALUES (?, ?, ?, ?, ?)`, + ).run("index.js", "other", "{}", "content", "hash1"); + db.prepare( + `INSERT INTO documents (path, type, frontmatter, content, hash) VALUES (?, ?, ?, ?, ?)`, + ).run("utils.js", "other", "{}", "content", "hash2"); + + await indexFileDependencies(db, indexPath, tempDir); + + const deps = db.prepare("SELECT * FROM dependencies").all(); + + assert({ + given: "JS file with import", + should: "create dependency record", + actual: deps.length, + expected: 1, + }); + }); + + test("stores correct dependency details", async () => { + const { db, tempDir } = await setupTestDatabaseWithDirectory(); + + const indexPath = path.join(tempDir, "index.js"); + const utilsPath = path.join(tempDir, "utils.js"); + + await fs.writeFile(indexPath, `import { helper } from './utils.js';\n`); + await fs.writeFile(utilsPath, `export const helper = () => {};`); + + db.prepare( + `INSERT INTO documents (path, type, frontmatter, content, hash) VALUES (?, ?, ?, ?, ?)`, + ).run("index.js", "other", "{}", "content", "hash1"); + db.prepare( + `INSERT INTO documents (path, type, frontmatter, content, hash) VALUES (?, ?, ?, ?, ?)`, + ).run("utils.js", "other", "{}", "content", "hash2"); + + await indexFileDependencies(db, indexPath, tempDir); + + const dep = db.prepare("SELECT * FROM dependencies").get(); + + assert({ + given: "indexed dependency", + should: "have correct from_file and to_file", + actual: { from: dep.from_file, to: dep.to_file }, + expected: { from: "index.js", to: "utils.js" }, + }); + }); + }); + + describe("indexAllDependencies", () => { + test("indexes dependencies for all JS files", async () => { + const { db, tempDir } = await setupTestDatabaseWithDirectory(); + + // Create a simple dependency chain: a.js -> b.js -> c.js + await fs.writeFile(path.join(tempDir, "a.js"), `import b from './b.js';`); + await fs.writeFile( + path.join(tempDir, "b.js"), + `import c from './c.js'; export default c;`, + ); + await fs.writeFile(path.join(tempDir, "c.js"), `export default 'c';`); + + // Index documents first + db.prepare( + `INSERT INTO documents (path, type, frontmatter, content, hash) VALUES (?, ?, ?, ?, ?)`, + ).run("a.js", "other", "{}", "content", "hash1"); + db.prepare( + `INSERT INTO documents (path, type, frontmatter, content, hash) VALUES (?, ?, ?, ?, ?)`, + ).run("b.js", "other", "{}", "content", "hash2"); + db.prepare( + `INSERT INTO documents (path, type, frontmatter, content, hash) VALUES (?, ?, ?, ?, ?)`, + ).run("c.js", "other", "{}", "content", "hash3"); + + const stats = await indexAllDependencies(db, tempDir); + + assert({ + given: "directory with dependency chain", + should: "index all dependencies", + actual: stats.indexed, + expected: 2, // a->b and b->c + }); + }); + + test("clears existing dependencies before reindexing", async () => { + const { db, tempDir } = await setupTestDatabaseWithDirectory(); + + await fs.writeFile(path.join(tempDir, "a.js"), `import b from './b.js';`); + await fs.writeFile(path.join(tempDir, "b.js"), `export default 'b';`); + + db.prepare( + `INSERT INTO documents (path, type, frontmatter, content, hash) VALUES (?, ?, ?, ?, ?)`, + ).run("a.js", "other", "{}", "content", "hash1"); + db.prepare( + `INSERT INTO documents (path, type, frontmatter, content, hash) VALUES (?, ?, ?, ?, ?)`, + ).run("b.js", "other", "{}", "content", "hash2"); + + // Index twice + await indexAllDependencies(db, tempDir); + await indexAllDependencies(db, tempDir); + + const count = db + .prepare("SELECT COUNT(*) as count FROM dependencies") + .get().count; + + assert({ + given: "indexing run twice", + should: "not duplicate dependencies", + actual: count, + expected: 1, + }); + }); + + test("handles re-exports (export from)", async () => { + const { db, tempDir } = await setupTestDatabaseWithDirectory(); + + // Re-export pattern: index.js re-exports from utils.js + await fs.writeFile( + path.join(tempDir, "index.js"), + `export { helper } from './utils.js';`, + ); + await fs.writeFile( + path.join(tempDir, "utils.js"), + `export const helper = () => {};`, + ); + + db.prepare( + `INSERT INTO documents (path, type, frontmatter, content, hash) VALUES (?, ?, ?, ?, ?)`, + ).run("index.js", "other", "{}", "content", "hash1"); + db.prepare( + `INSERT INTO documents (path, type, frontmatter, content, hash) VALUES (?, ?, ?, ?, ?)`, + ).run("utils.js", "other", "{}", "content", "hash2"); + + const stats = await indexAllDependencies(db, tempDir); + + assert({ + given: "file with re-export", + should: "detect the dependency", + actual: stats.indexed, + expected: 1, + }); + }); + + test("handles export * from (barrel exports)", async () => { + const { db, tempDir } = await setupTestDatabaseWithDirectory(); + + // Barrel export pattern + await fs.writeFile( + path.join(tempDir, "index.js"), + `export * from './module.js';`, + ); + await fs.writeFile( + path.join(tempDir, "module.js"), + `export const foo = 1; export const bar = 2;`, + ); + + db.prepare( + `INSERT INTO documents (path, type, frontmatter, content, hash) VALUES (?, ?, ?, ?, ?)`, + ).run("index.js", "other", "{}", "content", "hash1"); + db.prepare( + `INSERT INTO documents (path, type, frontmatter, content, hash) VALUES (?, ?, ?, ?, ?)`, + ).run("module.js", "other", "{}", "content", "hash2"); + + const stats = await indexAllDependencies(db, tempDir); + + assert({ + given: "file with barrel export", + should: "detect the dependency", + actual: stats.indexed, + expected: 1, + }); + }); + + test("handles TypeScript files with imports", async () => { + const { db, tempDir } = await setupTestDatabaseWithDirectory(); + + // TypeScript files with standard imports + await fs.writeFile( + path.join(tempDir, "consumer.ts"), + `import { helper } from './helper.ts';\nconsole.log(helper);`, + ); + await fs.writeFile( + path.join(tempDir, "helper.ts"), + `export const helper = 'hi';`, + ); + + db.prepare( + `INSERT INTO documents (path, type, frontmatter, content, hash) VALUES (?, ?, ?, ?, ?)`, + ).run("consumer.ts", "other", "{}", "content", "hash1"); + db.prepare( + `INSERT INTO documents (path, type, frontmatter, content, hash) VALUES (?, ?, ?, ?, ?)`, + ).run("helper.ts", "other", "{}", "content", "hash2"); + + const stats = await indexAllDependencies(db, tempDir); + + assert({ + given: "TypeScript files with imports", + should: "detect the dependency", + actual: stats.indexed, + expected: 1, + }); + }); + + test("handles side-effect imports", async () => { + const { db, tempDir } = await setupTestDatabaseWithDirectory(); + + // Side-effect import (no bindings) + await fs.writeFile( + path.join(tempDir, "main.js"), + `import './setup.js';\nconsole.log('ready');`, + ); + await fs.writeFile( + path.join(tempDir, "setup.js"), + `globalThis.configured = true;`, + ); + + db.prepare( + `INSERT INTO documents (path, type, frontmatter, content, hash) VALUES (?, ?, ?, ?, ?)`, + ).run("main.js", "other", "{}", "content", "hash1"); + db.prepare( + `INSERT INTO documents (path, type, frontmatter, content, hash) VALUES (?, ?, ?, ?, ?)`, + ).run("setup.js", "other", "{}", "content", "hash2"); + + const stats = await indexAllDependencies(db, tempDir); + + assert({ + given: "file with side-effect import", + should: "detect the dependency", + actual: stats.indexed, + expected: 1, + }); + }); + + test("handles mixed imports and re-exports in same file", async () => { + const { db, tempDir } = await setupTestDatabaseWithDirectory(); + + // Complex file with both imports and re-exports + await fs.writeFile( + path.join(tempDir, "index.js"), + `import { internal } from './internal.js'; +export { external } from './external.js'; +export default internal;`, + ); + await fs.writeFile( + path.join(tempDir, "internal.js"), + `export const internal = 'internal';`, + ); + await fs.writeFile( + path.join(tempDir, "external.js"), + `export const external = 'external';`, + ); + + db.prepare( + `INSERT INTO documents (path, type, frontmatter, content, hash) VALUES (?, ?, ?, ?, ?)`, + ).run("index.js", "other", "{}", "content", "hash1"); + db.prepare( + `INSERT INTO documents (path, type, frontmatter, content, hash) VALUES (?, ?, ?, ?, ?)`, + ).run("internal.js", "other", "{}", "content", "hash2"); + db.prepare( + `INSERT INTO documents (path, type, frontmatter, content, hash) VALUES (?, ?, ?, ?, ?)`, + ).run("external.js", "other", "{}", "content", "hash3"); + + const stats = await indexAllDependencies(db, tempDir); + + assert({ + given: "file with both imports and re-exports", + should: "detect both dependencies", + actual: stats.indexed, + expected: 2, + }); + }); + }); +}); diff --git a/ai/tools/indexers/frontmatter.js b/ai/tools/indexers/frontmatter.js new file mode 100644 index 0000000..ac7d310 --- /dev/null +++ b/ai/tools/indexers/frontmatter.js @@ -0,0 +1,300 @@ +import path from "path"; +import fs from "fs-extra"; +import matter from "gray-matter"; +import sha3 from "js-sha3"; + +const { sha3_256 } = sha3; + +// Keys that could cause prototype pollution +const FORBIDDEN_KEYS = ["__proto__", "prototype", "constructor"]; + +/** + * Detect document type based on file path. + * + * @param {string} filePath - Relative path to the file + * @returns {'rule' | 'command' | 'skill' | 'task' | 'story-map' | 'other'} + */ +const detectDocumentType = (filePath) => { + const normalized = filePath.replace(/\\/g, "/"); + + if (normalized.includes("ai/rules/") || normalized.startsWith("rules/")) { + return "rule"; + } + if ( + normalized.includes("ai/commands/") || + normalized.startsWith("commands/") + ) { + return "command"; + } + if (normalized.includes("ai/skills/") || normalized.startsWith("skills/")) { + return "skill"; + } + if (normalized.includes("tasks/") || normalized.startsWith("tasks/")) { + return "task"; + } + if ( + normalized.includes("plan/story-map/") || + normalized.includes("story-map/") + ) { + return "story-map"; + } + + return "other"; +}; + +/** + * Compute SHA3-256 hash of file contents. + * + * @param {string} filePath - Absolute path to file + * @returns {Promise} Hex-encoded hash + */ +const computeFileHash = async (filePath) => { + const content = await fs.readFile(filePath, "utf-8"); + return sha3_256(content); +}; + +/** + * Parse frontmatter from content, with prototype pollution protection. + * + * @param {string} content - File content + * @returns {{ frontmatter: object, content: string }} + */ +const parseFrontmatter = (content) => { + try { + const { data, content: body } = matter(content); + + // Filter out prototype pollution keys + const safeFrontmatter = Object.create(null); + for (const [key, value] of Object.entries(data)) { + if (!FORBIDDEN_KEYS.includes(key)) { + safeFrontmatter[key] = value; + } + } + + return { + frontmatter: safeFrontmatter, + content: body.trim(), + }; + } catch { + return { + frontmatter: {}, + content: content.trim(), + }; + } +}; + +/** + * Index a single file into the database. + * + * @param {import('better-sqlite3').Database} db + * @param {string} filePath - Absolute path to file + * @param {string} rootDir - Root directory for relative paths + */ +const indexFile = async (db, filePath, rootDir) => { + const relativePath = path.relative(rootDir, filePath).replace(/\\/g, "/"); + const content = await fs.readFile(filePath, "utf-8"); + const hash = sha3_256(content); + const stats = await fs.stat(filePath); + + const { frontmatter, content: body } = parseFrontmatter(content); + const type = detectDocumentType(relativePath); + + const stmt = db.prepare(` + INSERT OR REPLACE INTO documents (path, type, frontmatter, content, hash, file_size, modified_at) + VALUES (?, ?, ?, ?, ?, ?, ?) + `); + + stmt.run( + relativePath, + type, + JSON.stringify(frontmatter), + body, + hash, + stats.size, + stats.mtimeMs, + ); +}; + +/** + * Recursively find all markdown files in a directory. + * + * @param {string} dirPath - Directory to search + * @param {string} rootDir - Root directory for relative paths + * @returns {Promise} Array of absolute file paths + */ +// Directories to skip during indexing +const SKIP_DIRECTORIES = ["node_modules", ".git", ".aidd", "dist", "build"]; + +const findMarkdownFiles = async (dirPath, rootDir = dirPath) => { + const files = []; + const items = await fs.readdir(dirPath, { withFileTypes: true }); + + for (const item of items) { + const fullPath = path.join(dirPath, item.name); + + if (item.isDirectory()) { + // Skip node_modules, .git, and other build directories + if (SKIP_DIRECTORIES.includes(item.name) || item.name.startsWith(".")) { + continue; + } + // Skip symlinks to prevent infinite recursion + const stats = await fs.lstat(fullPath); + if (!stats.isSymbolicLink()) { + const nested = await findMarkdownFiles(fullPath, rootDir); + files.push(...nested); + } + } else if (item.isFile()) { + const ext = path.extname(item.name).toLowerCase(); + // Include .md and .mdc, exclude index.md + if ((ext === ".md" || ext === ".mdc") && item.name !== "index.md") { + files.push(fullPath); + } + } + } + + return files; +}; + +/** + * Index all markdown files in a directory. + * + * @param {import('better-sqlite3').Database} db + * @param {string} rootDir - Root directory to index + * @returns {Promise<{ indexed: number, errors: string[] }>} + */ +const indexDirectory = async (db, rootDir) => { + const files = await findMarkdownFiles(rootDir); + const errors = []; + + const indexAll = db.transaction(() => { + for (const filePath of files) { + try { + // Sync version for transaction + const relativePath = path + .relative(rootDir, filePath) + .replace(/\\/g, "/"); + const content = fs.readFileSync(filePath, "utf-8"); + const hash = sha3_256(content); + const stats = fs.statSync(filePath); + + const { frontmatter, content: body } = parseFrontmatter(content); + const type = detectDocumentType(relativePath); + + db.prepare( + ` + INSERT OR REPLACE INTO documents (path, type, frontmatter, content, hash, file_size, modified_at) + VALUES (?, ?, ?, ?, ?, ?, ?) + `, + ).run( + relativePath, + type, + JSON.stringify(frontmatter), + body, + hash, + stats.size, + stats.mtimeMs, + ); + } catch (err) { + errors.push(`${filePath}: ${err.message}`); + } + } + }); + + indexAll(); + + return { + indexed: files.length - errors.length, + errors, + }; +}; + +/** + * Incrementally index only changed files. + * + * @param {import('better-sqlite3').Database} db + * @param {string} rootDir - Root directory to index + * @returns {Promise<{ updated: number, deleted: number, unchanged: number }>} + */ +const indexIncremental = async (db, rootDir) => { + // Get current state from database + const existingDocs = db.prepare("SELECT path, hash FROM documents").all(); + const existingMap = new Map(existingDocs.map((d) => [d.path, d.hash])); + + // Get current files on disk + const files = await findMarkdownFiles(rootDir); + const currentPaths = new Set(); + + const toUpdate = []; + const toDelete = []; + + // Check each file on disk + for (const filePath of files) { + const relativePath = path.relative(rootDir, filePath).replace(/\\/g, "/"); + currentPaths.add(relativePath); + + const content = await fs.readFile(filePath, "utf-8"); + const newHash = sha3_256(content); + + if (existingMap.get(relativePath) !== newHash) { + toUpdate.push(filePath); + } + } + + // Find deleted files + for (const [existingPath] of existingMap) { + if (!currentPaths.has(existingPath)) { + toDelete.push(existingPath); + } + } + + // Apply changes in transaction + db.transaction(() => { + // Delete removed files + const deleteStmt = db.prepare("DELETE FROM documents WHERE path = ?"); + for (const deletePath of toDelete) { + deleteStmt.run(deletePath); + } + + // Update/insert changed files + for (const filePath of toUpdate) { + const relativePath = path.relative(rootDir, filePath).replace(/\\/g, "/"); + const content = fs.readFileSync(filePath, "utf-8"); + const hash = sha3_256(content); + const stats = fs.statSync(filePath); + + const { frontmatter, content: body } = parseFrontmatter(content); + const type = detectDocumentType(relativePath); + + db.prepare( + ` + INSERT OR REPLACE INTO documents (path, type, frontmatter, content, hash, file_size, modified_at) + VALUES (?, ?, ?, ?, ?, ?, ?) + `, + ).run( + relativePath, + type, + JSON.stringify(frontmatter), + body, + hash, + stats.size, + stats.mtimeMs, + ); + } + })(); + + return { + updated: toUpdate.length, + deleted: toDelete.length, + unchanged: files.length - toUpdate.length, + }; +}; + +export { + indexFile, + indexDirectory, + indexIncremental, + detectDocumentType, + computeFileHash, + parseFrontmatter, + findMarkdownFiles, +}; diff --git a/ai/tools/indexers/frontmatter.test.js b/ai/tools/indexers/frontmatter.test.js new file mode 100644 index 0000000..118f09a --- /dev/null +++ b/ai/tools/indexers/frontmatter.test.js @@ -0,0 +1,389 @@ +import { describe, test, onTestFinished } from "vitest"; +import { assert } from "riteway/vitest"; +import path from "path"; +import fs from "fs-extra"; +import os from "os"; +import { createId } from "@paralleldrive/cuid2"; + +import { createDatabase, closeDatabase } from "../db/connection.js"; +import { initializeSchema } from "../db/schema.js"; +import { + indexFile, + indexDirectory, + indexIncremental, + detectDocumentType, + computeFileHash, +} from "./frontmatter.js"; + +const setupTestDatabaseWithDirectory = async () => { + const db = createDatabase(":memory:"); + initializeSchema(db); + + const tempDir = path.join(os.tmpdir(), `aidd-test-${createId()}`); + await fs.ensureDir(tempDir); + + onTestFinished(async () => { + closeDatabase(db); + await fs.remove(tempDir); + }); + + return { db, tempDir }; +}; + +describe("indexers/frontmatter", () => { + describe("detectDocumentType", () => { + test("detects rule type from ai/rules path", () => { + assert({ + given: "path in ai/rules/", + should: "return 'rule'", + actual: detectDocumentType("ai/rules/test.mdc"), + expected: "rule", + }); + }); + + test("detects command type from ai/commands path", () => { + assert({ + given: "path in ai/commands/", + should: "return 'command'", + actual: detectDocumentType("ai/commands/help.md"), + expected: "command", + }); + }); + + test("detects skill type from ai/skills path", () => { + assert({ + given: "path in ai/skills/", + should: "return 'skill'", + actual: detectDocumentType("ai/skills/rlm/SKILL.md"), + expected: "skill", + }); + }); + + test("detects task type from tasks path", () => { + assert({ + given: "path in tasks/", + should: "return 'task'", + actual: detectDocumentType("tasks/rlm/phase-1.md"), + expected: "task", + }); + }); + + test("detects story-map type from plan/story-map path", () => { + assert({ + given: "path in plan/story-map/", + should: "return 'story-map'", + actual: detectDocumentType("plan/story-map/personas.yaml"), + expected: "story-map", + }); + }); + + test("returns other for unknown paths", () => { + assert({ + given: "path not matching known patterns", + should: "return 'other'", + actual: detectDocumentType("docs/readme.md"), + expected: "other", + }); + }); + }); + + describe("computeFileHash", () => { + test("computes consistent hash for same content", async () => { + const { tempDir } = await setupTestDatabaseWithDirectory(); + const filePath = path.join(tempDir, "test.md"); + await fs.writeFile(filePath, "# Test\n\nSome content"); + + const hash1 = await computeFileHash(filePath); + const hash2 = await computeFileHash(filePath); + + assert({ + given: "same file content", + should: "return same hash", + actual: hash1 === hash2, + expected: true, + }); + }); + + test("computes different hash for different content", async () => { + const { tempDir } = await setupTestDatabaseWithDirectory(); + const file1 = path.join(tempDir, "test1.md"); + const file2 = path.join(tempDir, "test2.md"); + await fs.writeFile(file1, "content A"); + await fs.writeFile(file2, "content B"); + + const hash1 = await computeFileHash(file1); + const hash2 = await computeFileHash(file2); + + assert({ + given: "different file content", + should: "return different hashes", + actual: hash1 !== hash2, + expected: true, + }); + }); + }); + + describe("indexFile", () => { + test("indexes file with frontmatter", async () => { + const { db, tempDir } = await setupTestDatabaseWithDirectory(); + const filePath = path.join(tempDir, "test.mdc"); + await fs.writeFile( + filePath, + `--- +description: Test description +alwaysApply: true +--- + +# Test Rule + +Some content here. +`, + ); + + await indexFile(db, filePath, tempDir); + + const doc = db + .prepare("SELECT * FROM documents WHERE path = ?") + .get("test.mdc"); + + assert({ + given: "file with frontmatter", + should: "store frontmatter as JSON", + actual: JSON.parse(doc.frontmatter), + expected: { description: "Test description", alwaysApply: true }, + }); + }); + + test("indexes file without frontmatter", async () => { + const { db, tempDir } = await setupTestDatabaseWithDirectory(); + const filePath = path.join(tempDir, "simple.md"); + await fs.writeFile(filePath, "# Simple\n\nJust content."); + + await indexFile(db, filePath, tempDir); + + const doc = db + .prepare("SELECT * FROM documents WHERE path = ?") + .get("simple.md"); + + assert({ + given: "file without frontmatter", + should: "store empty frontmatter object", + actual: JSON.parse(doc.frontmatter), + expected: {}, + }); + }); + + test("stores content without frontmatter delimiter", async () => { + const { db, tempDir } = await setupTestDatabaseWithDirectory(); + const filePath = path.join(tempDir, "test.md"); + await fs.writeFile( + filePath, + `--- +title: Test +--- + +# Heading + +Body content. +`, + ); + + await indexFile(db, filePath, tempDir); + + const doc = db + .prepare("SELECT content FROM documents WHERE path = ?") + .get("test.md"); + + assert({ + given: "file with frontmatter", + should: "store content without frontmatter", + actual: doc.content.trim(), + expected: "# Heading\n\nBody content.", + }); + }); + + test("computes and stores file hash", async () => { + const { db, tempDir } = await setupTestDatabaseWithDirectory(); + const filePath = path.join(tempDir, "hashed.md"); + await fs.writeFile(filePath, "test content"); + + await indexFile(db, filePath, tempDir); + + const doc = db + .prepare("SELECT hash FROM documents WHERE path = ?") + .get("hashed.md"); + + assert({ + given: "indexed file", + should: "have non-empty hash", + actual: doc.hash.length > 0, + expected: true, + }); + }); + }); + + describe("indexDirectory", () => { + test("indexes all .md and .mdc files recursively", async () => { + const { db, tempDir } = await setupTestDatabaseWithDirectory(); + await fs.ensureDir(path.join(tempDir, "subdir")); + await fs.writeFile(path.join(tempDir, "root.md"), "# Root"); + await fs.writeFile(path.join(tempDir, "subdir/nested.mdc"), "# Nested"); + await fs.writeFile(path.join(tempDir, "ignore.txt"), "Not markdown"); + + await indexDirectory(db, tempDir); + + const count = db + .prepare("SELECT COUNT(*) as count FROM documents") + .get().count; + + assert({ + given: "directory with .md and .mdc files", + should: "index only markdown files", + actual: count, + expected: 2, + }); + }); + + test("returns indexing statistics", async () => { + const { db, tempDir } = await setupTestDatabaseWithDirectory(); + await fs.writeFile(path.join(tempDir, "file1.md"), "# File 1"); + await fs.writeFile(path.join(tempDir, "file2.md"), "# File 2"); + await fs.writeFile(path.join(tempDir, "file3.mdc"), "# File 3"); + + const stats = await indexDirectory(db, tempDir); + + assert({ + given: "indexing multiple files", + should: "return count of indexed files", + actual: stats.indexed, + expected: 3, + }); + }); + + test("skips index.md files", async () => { + const { db, tempDir } = await setupTestDatabaseWithDirectory(); + await fs.writeFile(path.join(tempDir, "index.md"), "# Index"); + await fs.writeFile(path.join(tempDir, "regular.md"), "# Regular"); + + await indexDirectory(db, tempDir); + + const paths = db + .prepare("SELECT path FROM documents") + .all() + .map((d) => d.path); + + assert({ + given: "directory with index.md", + should: "skip index.md file", + actual: paths, + expected: ["regular.md"], + }); + }); + + test("uses transaction for atomicity", async () => { + const { db, tempDir } = await setupTestDatabaseWithDirectory(); + await fs.writeFile(path.join(tempDir, "file1.md"), "# File 1"); + await fs.writeFile(path.join(tempDir, "file2.md"), "# File 2"); + + await indexDirectory(db, tempDir); + + // If transaction worked, both should be indexed + const count = db + .prepare("SELECT COUNT(*) as count FROM documents") + .get().count; + + assert({ + given: "indexing with transaction", + should: "index all files atomically", + actual: count, + expected: 2, + }); + }); + }); + + describe("indexIncremental", () => { + test("only indexes changed files", async () => { + const { db, tempDir } = await setupTestDatabaseWithDirectory(); + const file1 = path.join(tempDir, "unchanged.md"); + const file2 = path.join(tempDir, "changed.md"); + + await fs.writeFile(file1, "# Unchanged"); + await fs.writeFile(file2, "# Original"); + + // Initial index + await indexDirectory(db, tempDir); + + // Modify one file + await fs.writeFile(file2, "# Modified content"); + + const stats = await indexIncremental(db, tempDir); + + assert({ + given: "one file changed", + should: "only update changed file", + actual: stats.updated, + expected: 1, + }); + }); + + test("removes deleted files from index", async () => { + const { db, tempDir } = await setupTestDatabaseWithDirectory(); + const file1 = path.join(tempDir, "keep.md"); + const file2 = path.join(tempDir, "delete.md"); + + await fs.writeFile(file1, "# Keep"); + await fs.writeFile(file2, "# Delete"); + + await indexDirectory(db, tempDir); + + // Delete one file + await fs.remove(file2); + + const stats = await indexIncremental(db, tempDir); + + assert({ + given: "file deleted from disk", + should: "report deleted count", + actual: stats.deleted, + expected: 1, + }); + }); + + test("deleted files are removed from database", async () => { + const { db, tempDir } = await setupTestDatabaseWithDirectory(); + const filePath = path.join(tempDir, "temp.md"); + await fs.writeFile(filePath, "# Temp"); + + await indexDirectory(db, tempDir); + await fs.remove(filePath); + await indexIncremental(db, tempDir); + + const doc = db + .prepare("SELECT * FROM documents WHERE path = ?") + .get("temp.md"); + + assert({ + given: "file deleted and incremental run", + should: "remove from database", + actual: doc, + expected: undefined, + }); + }); + + test("adds new files", async () => { + const { db, tempDir } = await setupTestDatabaseWithDirectory(); + await fs.writeFile(path.join(tempDir, "existing.md"), "# Existing"); + await indexDirectory(db, tempDir); + + await fs.writeFile(path.join(tempDir, "new.md"), "# New"); + const stats = await indexIncremental(db, tempDir); + + assert({ + given: "new file added", + should: "report as updated", + actual: stats.updated >= 1, + expected: true, + }); + }); + }); +}); diff --git a/ai/tools/search/fan-out.js b/ai/tools/search/fan-out.js new file mode 100644 index 0000000..a56d28c --- /dev/null +++ b/ai/tools/search/fan-out.js @@ -0,0 +1,158 @@ +/** + * Fan-out search orchestrator. + * Combines multiple search strategies and aggregates results. + */ + +import { searchFts5 } from "./fts5.js"; +import { searchMetadata } from "./metadata.js"; + +// Default strategy weights for ranking +const DEFAULT_WEIGHTS = { + fts5: 1.0, // Full-text matches are strong + metadata: 0.8, // Metadata matches are good + semantic: 0.6, // Semantic search can be noisy (stubbed for now) +}; + +/** + * Aggregate results from multiple search strategies. + * Deduplicates by path and boosts documents found by multiple strategies. + * + * @param {Object} resultsByStrategy - { strategyName: results[] } + * @param {Object} options + * @param {Object} [options.weights] - Strategy weights for scoring + * @param {number} [options.limit=20] - Maximum results to return + * @returns {Array<{path: string, type: string, frontmatter: object, relevanceScore: number}>} + */ +const aggregateResults = ( + resultsByStrategy, + { weights = DEFAULT_WEIGHTS, limit = 20 } = {}, +) => { + const scored = new Map(); // path -> { doc, score } + + for (const [strategy, results] of Object.entries(resultsByStrategy)) { + const weight = weights[strategy] ?? 0.5; + + results.reduce((acc, doc, idx) => { + // Position-based score (first result = highest) + const positionScore = 1 / (idx + 1); + const score = weight * positionScore; + + if (acc.has(doc.path)) { + // Boost if found by multiple strategies + const existing = acc.get(doc.path); + existing.score += score; + existing.matchedStrategies.push(strategy); + } else { + acc.set(doc.path, { + doc: { + path: doc.path, + type: doc.type, + frontmatter: doc.frontmatter, + snippet: doc.snippet, + }, + score, + matchedStrategies: [strategy], + }); + } + return acc; + }, scored); + } + + return [...scored.values()] + .sort((a, b) => b.score - a.score) + .slice(0, limit) + .map(({ doc, score, matchedStrategies }) => ({ + ...doc, + relevanceScore: score, + matchedStrategies, + })); +}; + +/** + * Stub for semantic search (RAG integration). + * Will be implemented when RAG capabilities are available. + * + * @param {import('better-sqlite3').Database} db + * @param {string} query + * @returns {Promise} + */ +const searchSemantic = async (db, query) => { + // Stub - returns empty array + // See: https://github.com/paralleldrive/aidd/issues/89 + return []; +}; + +/** + * Execute fan-out search across multiple strategies. + * + * @param {import('better-sqlite3').Database} db + * @param {string} query - Search query + * @param {Object} options + * @param {Array} [options.strategies=['fts5', 'metadata']] - Strategies to use + * @param {Object} [options.filters] - Metadata filters + * @param {string} [options.type] - Document type filter + * @param {number} [options.limit=20] - Maximum results + * @param {Object} [options.weights] - Strategy weights + * @param {boolean} [options.silent=false] - Suppress error logging + * @returns {Promise>} + */ +const fanOutSearch = async ( + db, + query, + { + strategies = ["fts5", "metadata"], + filters = {}, + type, + limit = 20, + weights = DEFAULT_WEIGHTS, + silent = false, + } = {}, +) => { + if (!query || query.trim() === "") { + return []; + } + + // Build search functions for each strategy + const searchFns = { + fts5: () => searchFts5(db, query, { type, limit: limit * 2, silent }), + metadata: () => { + // Only run metadata search if filters are provided + const hasFilters = Object.keys(filters).length > 0 || type; + if (!hasFilters) { + return []; + } + // Combine type filter with other filters + const allFilters = { ...filters }; + if (type) { + allFilters.type = type; + } + return searchMetadata(db, allFilters, { limit: limit * 2 }); + }, + semantic: () => searchSemantic(db, query), + }; + + // Execute active strategies in parallel + const activeStrategies = strategies.filter((s) => searchFns[s]); + + const results = await Promise.all( + activeStrategies.map(async (strategy) => { + try { + const strategyResults = await searchFns[strategy](); + return [strategy, strategyResults]; + } catch (err) { + if (!silent) { + console.error(`Search strategy '${strategy}' failed: ${err.message}`); + } + return [strategy, []]; + } + }), + ); + + // Convert to object + const resultsByStrategy = Object.fromEntries(results); + + // Aggregate and rank + return aggregateResults(resultsByStrategy, { weights, limit }); +}; + +export { fanOutSearch, aggregateResults, DEFAULT_WEIGHTS }; diff --git a/ai/tools/search/fan-out.test.js b/ai/tools/search/fan-out.test.js new file mode 100644 index 0000000..c1fd3e0 --- /dev/null +++ b/ai/tools/search/fan-out.test.js @@ -0,0 +1,250 @@ +import { describe, test, onTestFinished } from "vitest"; +import { assert } from "riteway/vitest"; + +import { createDatabase, closeDatabase } from "../db/connection.js"; +import { initializeSchema } from "../db/schema.js"; +import { fanOutSearch, aggregateResults } from "./fan-out.js"; + +const setupTestDatabase = () => { + const db = createDatabase(":memory:"); + initializeSchema(db); + + // Seed test documents + const insert = db.prepare(` + INSERT INTO documents (path, type, frontmatter, content, hash) + VALUES (?, ?, ?, ?, ?) + `); + + insert.run( + "ai/rules/auth.mdc", + "rule", + JSON.stringify({ + description: "Authentication rules", + alwaysApply: true, + tags: ["security"], + }), + "# Authentication\n\nHandle user login and JWT tokens.", + "hash1", + ); + + insert.run( + "ai/rules/security.mdc", + "rule", + JSON.stringify({ + description: "Security best practices", + tags: ["security", "csrf"], + }), + "# Security\n\nProtect against XSS and CSRF attacks.", + "hash2", + ); + + insert.run( + "ai/commands/help.md", + "command", + JSON.stringify({ description: "Display help" }), + "# Help\n\nShows available commands.", + "hash3", + ); + + insert.run( + "docs/getting-started.md", + "other", + JSON.stringify({ title: "Getting Started" }), + "# Getting Started\n\nWelcome to the documentation.", + "hash4", + ); + + onTestFinished(() => closeDatabase(db)); + + return db; +}; + +describe("search/fan-out", () => { + describe("fanOutSearch", () => { + test("combines FTS5 and metadata results", async () => { + const db = setupTestDatabase(); + const results = await fanOutSearch(db, "security", { + filters: { "frontmatter.tags": { contains: "security" } }, + }); + + assert({ + given: "query with both keyword and metadata match", + should: "return results", + actual: results.length >= 1, + expected: true, + }); + }); + + test("deduplicates results by path", async () => { + const db = setupTestDatabase(); + const results = await fanOutSearch(db, "authentication security", { + filters: { type: "rule" }, + }); + + const paths = results.map((r) => r.path); + const uniquePaths = [...new Set(paths)]; + + assert({ + given: "search that might find same doc multiple ways", + should: "deduplicate by path", + actual: paths.length, + expected: uniquePaths.length, + }); + }); + + test("ranks results by relevance", async () => { + const db = setupTestDatabase(); + const results = await fanOutSearch(db, "security"); + + // Results should have relevanceScore + assert({ + given: "search results", + should: "include relevanceScore", + actual: results.every((r) => "relevanceScore" in r), + expected: true, + }); + }); + + test("respects limit option", async () => { + const db = setupTestDatabase(); + const results = await fanOutSearch(db, "security", { limit: 1 }); + + assert({ + given: "limit of 1", + should: "return at most 1 result", + actual: results.length, + expected: 1, + }); + }); + + test("uses only specified strategies", async () => { + const db = setupTestDatabase(); + const ftsResults = await fanOutSearch(db, "authentication", { + strategies: ["fts5"], + }); + + assert({ + given: "only fts5 strategy", + should: "return FTS results", + actual: ftsResults.length >= 1, + expected: true, + }); + }); + + test("returns empty array for no matches", async () => { + const db = setupTestDatabase(); + const results = await fanOutSearch(db, "nonexistentterm12345"); + + assert({ + given: "no matching documents", + should: "return empty array", + actual: results, + expected: [], + }); + }); + + test("handles empty query gracefully", async () => { + const db = setupTestDatabase(); + const results = await fanOutSearch(db, ""); + + assert({ + given: "empty query", + should: "return empty array", + actual: results, + expected: [], + }); + }); + }); + + describe("aggregateResults", () => { + test("boosts documents found by multiple strategies", () => { + const ftsResults = [ + { path: "doc1.md", type: "rule", frontmatter: {} }, + { path: "doc2.md", type: "rule", frontmatter: {} }, + ]; + const metadataResults = [ + { path: "doc1.md", type: "rule", frontmatter: {} }, + { path: "doc3.md", type: "rule", frontmatter: {} }, + ]; + + const aggregated = aggregateResults({ + fts5: ftsResults, + metadata: metadataResults, + }); + + // doc1 should be ranked higher since it appears in both + const doc1 = aggregated.find((r) => r.path === "doc1.md"); + const doc2 = aggregated.find((r) => r.path === "doc2.md"); + + assert({ + given: "document in multiple result sets", + should: "have higher relevance score", + actual: doc1.relevanceScore > doc2.relevanceScore, + expected: true, + }); + }); + + test("applies strategy weights", () => { + const ftsResults = [ + { path: "fts-doc.md", type: "rule", frontmatter: {} }, + ]; + const metadataResults = [ + { path: "meta-doc.md", type: "rule", frontmatter: {} }, + ]; + + const aggregated = aggregateResults( + { fts5: ftsResults, metadata: metadataResults }, + { weights: { fts5: 1.0, metadata: 0.5 } }, + ); + + const ftsDoc = aggregated.find((r) => r.path === "fts-doc.md"); + const metaDoc = aggregated.find((r) => r.path === "meta-doc.md"); + + assert({ + given: "different strategy weights", + should: "weight FTS higher", + actual: ftsDoc.relevanceScore > metaDoc.relevanceScore, + expected: true, + }); + }); + + test("respects limit", () => { + const ftsResults = Array.from({ length: 10 }, (_, i) => ({ + path: `doc${i}.md`, + type: "rule", + frontmatter: {}, + })); + + const aggregated = aggregateResults({ fts5: ftsResults }, { limit: 5 }); + + assert({ + given: "limit of 5", + should: "return at most 5 results", + actual: aggregated.length, + expected: 5, + }); + }); + + test("sorts by relevance score descending", () => { + const ftsResults = [ + { path: "doc1.md", type: "rule", frontmatter: {} }, + { path: "doc2.md", type: "rule", frontmatter: {} }, + ]; + const metadataResults = [ + { path: "doc2.md", type: "rule", frontmatter: {} }, + ]; + + const aggregated = aggregateResults({ + fts5: ftsResults, + metadata: metadataResults, + }); + + assert({ + given: "multiple results", + should: "sort by relevance descending", + actual: aggregated[0].relevanceScore >= aggregated[1].relevanceScore, + expected: true, + }); + }); + }); +}); diff --git a/ai/tools/search/fts5.js b/ai/tools/search/fts5.js new file mode 100644 index 0000000..b46263c --- /dev/null +++ b/ai/tools/search/fts5.js @@ -0,0 +1,138 @@ +/** + * Full-text search using SQLite FTS5. + */ + +/** + * Search documents using FTS5 full-text search. + * + * @param {import('better-sqlite3').Database} db + * @param {string} query - Search query (supports FTS5 operators) + * @param {Object} options + * @param {number} [options.limit=20] - Maximum results + * @param {number} [options.offset=0] - Result offset + * @param {string} [options.type] - Filter by document type + * @param {boolean} [options.silent=false] - Suppress error logging + * @returns {Array<{path: string, type: string, frontmatter: object, snippet: string, rank: number}>} + */ +const searchFts5 = ( + db, + query, + { limit = 20, offset = 0, type, silent = false } = {}, +) => { + if (!query || query.trim() === "") { + return []; + } + + // Build query with optional type filter + const sqlParts = [ + `SELECT + d.path, + d.type, + d.frontmatter, + d.content, + bm25(fts_documents) as rank + FROM fts_documents f + JOIN documents d ON d.path = f.path + WHERE fts_documents MATCH ?`, + ]; + + const params = [query]; + + if (type) { + sqlParts.push(`AND d.type = ?`); + params.push(type); + } + + sqlParts.push(`ORDER BY rank LIMIT ? OFFSET ?`); + params.push(limit, offset); + + const sql = sqlParts.join(" "); + + try { + const rows = db.prepare(sql).all(...params); + + return rows.map((row) => ({ + path: row.path, + type: row.type, + frontmatter: JSON.parse(row.frontmatter ?? "{}"), + snippet: extractSnippet(row.content, query), + rank: row.rank, + })); + } catch (err) { + // FTS5 query syntax error - log and return empty results + if (!silent) { + console.error(`FTS5 query error: ${err.message}`); + } + return []; + } +}; + +/** + * Extract a relevant snippet from content around the search terms. + * + * @param {string} content + * @param {string} query + * @param {number} contextChars - Characters of context around match + * @returns {string} + */ +const extractSnippet = (content, query, contextChars = 100) => { + // Get first significant word from query (ignore operators) + const words = query + .replace(/['"()]/g, "") + .split(/\s+/) + .filter((w) => !["AND", "OR", "NOT", "NEAR"].includes(w.toUpperCase())); + + if (words.length === 0) { + return content.slice(0, contextChars * 2) + "..."; + } + + const searchWord = words[0].toLowerCase(); + const contentLower = content.toLowerCase(); + const idx = contentLower.indexOf(searchWord); + + if (idx === -1) { + return content.slice(0, contextChars * 2) + "..."; + } + + const start = Math.max(0, idx - contextChars); + const end = Math.min(content.length, idx + searchWord.length + contextChars); + + const snippet = + (start > 0 ? "..." : "") + + content.slice(start, end) + + (end < content.length ? "..." : ""); + + return snippet; +}; + +/** + * Highlight matching terms in text. + * + * @param {string} text + * @param {string} query + * @returns {string} + */ +const highlightMatches = (text, query) => { + // Get words from query (ignore operators) + const words = query + .replace(/['"()]/g, "") + .split(/\s+/) + .filter((w) => !["AND", "OR", "NOT", "NEAR"].includes(w.toUpperCase())) + .filter((w) => w.length > 0); + + return words.reduce( + (result, word) => + result.replace(new RegExp(`(${escapeRegex(word)})`, "gi"), "**$1**"), + text, + ); +}; + +/** + * Escape special regex characters. + * + * @param {string} str + * @returns {string} + */ +const escapeRegex = (str) => str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + +export { searchFts5, extractSnippet, highlightMatches }; diff --git a/ai/tools/search/fts5.test.js b/ai/tools/search/fts5.test.js new file mode 100644 index 0000000..0227f90 --- /dev/null +++ b/ai/tools/search/fts5.test.js @@ -0,0 +1,244 @@ +import { describe, test, onTestFinished } from "vitest"; +import { assert } from "riteway/vitest"; + +import { createDatabase, closeDatabase } from "../db/connection.js"; +import { initializeSchema } from "../db/schema.js"; +import { searchFts5, highlightMatches } from "./fts5.js"; + +const setupTestDatabase = () => { + const db = createDatabase(":memory:"); + initializeSchema(db); + + // Seed test documents + const insert = db.prepare(` + INSERT INTO documents (path, type, frontmatter, content, hash) + VALUES (?, ?, ?, ?, ?) + `); + + insert.run( + "ai/rules/auth.mdc", + "rule", + JSON.stringify({ description: "Authentication and authorization rules" }), + "# Authentication\n\nHandle user login, sessions, and JWT tokens.", + "hash1", + ); + + insert.run( + "ai/rules/security.mdc", + "rule", + JSON.stringify({ + description: "Security best practices", + tags: ["security", "csrf"], + }), + "# Security\n\nProtect against XSS, CSRF, and injection attacks.", + "hash2", + ); + + insert.run( + "ai/commands/help.md", + "command", + JSON.stringify({ description: "Display help information" }), + "# Help Command\n\nShows available commands and their usage.", + "hash3", + ); + + insert.run( + "docs/readme.md", + "other", + JSON.stringify({ title: "Project README" }), + "# Getting Started\n\nWelcome to the project documentation.", + "hash4", + ); + + onTestFinished(() => closeDatabase(db)); + + return db; +}; + +describe("search/fts5", () => { + describe("searchFts5", () => { + test("finds documents by content keyword", () => { + const db = setupTestDatabase(); + const results = searchFts5(db, "authentication"); + + assert({ + given: "search for 'authentication'", + should: "find the auth rule", + actual: results.map((r) => r.path), + expected: ["ai/rules/auth.mdc"], + }); + }); + + test("finds documents by frontmatter content", () => { + const db = setupTestDatabase(); + const results = searchFts5(db, "authorization"); + + assert({ + given: "search for 'authorization' (in frontmatter)", + should: "find the auth rule", + actual: results.length, + expected: 1, + }); + }); + + test("returns multiple matches", () => { + const db = setupTestDatabase(); + const results = searchFts5(db, "security"); + + assert({ + given: "search for 'security'", + should: "find multiple documents", + actual: results.length >= 1, + expected: true, + }); + }); + + test("returns empty array for no matches", () => { + const db = setupTestDatabase(); + const results = searchFts5(db, "nonexistentterm12345"); + + assert({ + given: "search for non-existent term", + should: "return empty array", + actual: results, + expected: [], + }); + }); + + test("respects limit option", () => { + const db = setupTestDatabase(); + + // Add more documents + for (let i = 0; i < 10; i++) { + db.prepare( + ` + INSERT INTO documents (path, type, frontmatter, content, hash) + VALUES (?, ?, ?, ?, ?) + `, + ).run( + `docs/doc${i}.md`, + "other", + "{}", + `Document about security ${i}`, + `hash${i + 10}`, + ); + } + + const results = searchFts5(db, "security", { limit: 3 }); + + assert({ + given: "limit of 3", + should: "return at most 3 results", + actual: results.length <= 3, + expected: true, + }); + }); + + test("filters by document type", () => { + const db = setupTestDatabase(); + const results = searchFts5(db, "help OR security", { type: "command" }); + + assert({ + given: "type filter for command", + should: "only return commands", + actual: results.every((r) => r.type === "command"), + expected: true, + }); + }); + + test("includes document metadata in results", () => { + const db = setupTestDatabase(); + const results = searchFts5(db, "authentication"); + + assert({ + given: "search result", + should: "include path, type, and frontmatter", + actual: + results[0] && + "path" in results[0] && + "type" in results[0] && + "frontmatter" in results[0], + expected: true, + }); + }); + + test("handles FTS5 operators", () => { + const db = setupTestDatabase(); + const results = searchFts5(db, "security AND csrf"); + + assert({ + given: "AND operator in query", + should: "find documents with both terms", + actual: results.length, + expected: 1, + }); + }); + + test("handles phrases in quotes", () => { + const db = setupTestDatabase(); + const results = searchFts5(db, '"JWT tokens"'); + + assert({ + given: "phrase search", + should: "find exact phrase", + actual: results.map((r) => r.path), + expected: ["ai/rules/auth.mdc"], + }); + }); + + test("returns empty array for invalid FTS5 syntax", () => { + const db = setupTestDatabase(); + // Invalid FTS5 syntax - unmatched quote + const results = searchFts5(db, '"unclosed quote', { silent: true }); + + assert({ + given: "invalid FTS5 syntax", + should: "return empty array", + actual: results, + expected: [], + }); + }); + }); + + describe("highlightMatches", () => { + test("wraps matched terms with markers", () => { + const result = highlightMatches( + "authentication is important", + "authentication", + ); + + assert({ + given: "text with matching term", + should: "wrap match with markers", + actual: result.includes("**authentication**"), + expected: true, + }); + }); + + test("handles multiple matches", () => { + const result = highlightMatches( + "security is about security practices", + "security", + ); + const matchCount = (result.match(/\*\*security\*\*/g) || []).length; + + assert({ + given: "text with multiple matches", + should: "highlight all matches", + actual: matchCount, + expected: 2, + }); + }); + + test("is case insensitive", () => { + const result = highlightMatches("Authentication works", "authentication"); + + assert({ + given: "case mismatch", + should: "still highlight", + actual: result.includes("**"), + expected: true, + }); + }); + }); +}); diff --git a/ai/tools/search/metadata.js b/ai/tools/search/metadata.js new file mode 100644 index 0000000..26fafe9 --- /dev/null +++ b/ai/tools/search/metadata.js @@ -0,0 +1,141 @@ +/** + * Metadata search for filtering documents by frontmatter fields. + */ + +import { createError } from "error-causes"; +import { ValidationError } from "../errors.js"; + +/** + * Validate JSON path to prevent SQL injection. + * Only allows alphanumeric characters, underscores, and dots. + * + * @param {string} jsonPath + * @returns {boolean} + */ +const isValidJsonPath = (jsonPath) => /^[a-zA-Z0-9_.]+$/.test(jsonPath); + +/** + * Search documents by metadata/frontmatter fields. + * + * @param {import('better-sqlite3').Database} db + * @param {Object} filters - Filter conditions + * @param {Object} options + * @param {number} [options.limit=20] - Maximum results + * @param {number} [options.offset=0] - Result offset + * @returns {Array<{path: string, type: string, frontmatter: object}>} + */ +const searchMetadata = (db, filters = {}, { limit = 20, offset = 0 } = {}) => { + const conditions = []; + const params = []; + + for (const [key, value] of Object.entries(filters)) { + if (key === "type") { + // Direct column filter + conditions.push("type = ?"); + params.push(value); + } else if (key.startsWith("frontmatter.")) { + // JSON field filter + const jsonPath = key.replace("frontmatter.", ""); + + // Validate jsonPath to prevent SQL injection + if (!isValidJsonPath(jsonPath)) { + throw createError({ + ...ValidationError, + message: `Invalid JSON path: ${jsonPath}. Only alphanumeric characters, underscores, and dots are allowed.`, + jsonPath, + }); + } + + if (typeof value === "object" && value !== null && "contains" in value) { + // Array contains check using JSON functions + conditions.push( + `EXISTS ( + SELECT 1 FROM json_each(json_extract(frontmatter, '$.${jsonPath}')) + WHERE value = ? + )`, + ); + params.push(value.contains); + } else if (typeof value === "boolean") { + // Boolean comparison + conditions.push(`json_extract(frontmatter, '$.${jsonPath}') = ?`); + params.push(value ? 1 : 0); + } else if (typeof value === "number") { + // Numeric comparison + conditions.push(`json_extract(frontmatter, '$.${jsonPath}') = ?`); + params.push(value); + } else { + // String comparison + conditions.push(`json_extract(frontmatter, '$.${jsonPath}') = ?`); + params.push(value); + } + } + } + + const sqlParts = ["SELECT path, type, frontmatter, content FROM documents"]; + + if (conditions.length > 0) { + sqlParts.push("WHERE " + conditions.join(" AND ")); + } + + sqlParts.push("ORDER BY path LIMIT ? OFFSET ?"); + params.push(limit, offset); + + const sql = sqlParts.join(" "); + + const rows = db.prepare(sql).all(...params); + + return rows.map((row) => ({ + path: row.path, + type: row.type, + frontmatter: JSON.parse(row.frontmatter ?? "{}"), + content: row.content, + })); +}; + +/** + * Get all unique values for a frontmatter field. + * Useful for building filter UIs. + * + * @param {import('better-sqlite3').Database} db + * @param {string} field - Frontmatter field name + * @returns {Array} + */ +const getFieldValues = (db, field) => { + // Validate field to prevent SQL injection + if (!isValidJsonPath(field)) { + throw createError({ + ...ValidationError, + message: `Invalid field name: ${field}. Only alphanumeric characters, underscores, and dots are allowed.`, + field, + }); + } + + const sql = ` + SELECT DISTINCT json_extract(frontmatter, '$.${field}') as value + FROM documents + WHERE json_extract(frontmatter, '$.${field}') IS NOT NULL + ORDER BY value + `; + + return db + .prepare(sql) + .all() + .map((row) => row.value) + .filter((v) => v !== null); +}; + +/** + * Get all unique document types. + * + * @param {import('better-sqlite3').Database} db + * @returns {Array} + */ +const getDocumentTypes = (db) => { + const sql = `SELECT DISTINCT type FROM documents ORDER BY type`; + return db + .prepare(sql) + .all() + .map((row) => row.type); +}; + +export { searchMetadata, getFieldValues, getDocumentTypes }; diff --git a/ai/tools/search/metadata.test.js b/ai/tools/search/metadata.test.js new file mode 100644 index 0000000..990643e --- /dev/null +++ b/ai/tools/search/metadata.test.js @@ -0,0 +1,265 @@ +import { describe, test, onTestFinished } from "vitest"; +import { assert } from "riteway/vitest"; + +import { createDatabase, closeDatabase } from "../db/connection.js"; +import { initializeSchema } from "../db/schema.js"; +import { searchMetadata, getFieldValues } from "./metadata.js"; + +const setupTestDatabase = () => { + const db = createDatabase(":memory:"); + initializeSchema(db); + + // Seed test documents with varied frontmatter + const insert = db.prepare(` + INSERT INTO documents (path, type, frontmatter, content, hash) + VALUES (?, ?, ?, ?, ?) + `); + + insert.run( + "ai/rules/always-on.mdc", + "rule", + JSON.stringify({ description: "Always active rule", alwaysApply: true }), + "Content here", + "hash1", + ); + + insert.run( + "ai/rules/conditional.mdc", + "rule", + JSON.stringify({ + description: "Conditional rule", + alwaysApply: false, + globs: "**/*.js", + }), + "Content here", + "hash2", + ); + + insert.run( + "ai/rules/tagged.mdc", + "rule", + JSON.stringify({ + description: "Tagged rule", + tags: ["security", "authentication"], + }), + "Content here", + "hash3", + ); + + insert.run( + "ai/commands/test.md", + "command", + JSON.stringify({ description: "Test command" }), + "Content here", + "hash4", + ); + + onTestFinished(() => closeDatabase(db)); + + return db; +}; + +describe("search/metadata", () => { + describe("searchMetadata", () => { + test("filters by boolean frontmatter field", () => { + const db = setupTestDatabase(); + const results = searchMetadata(db, { "frontmatter.alwaysApply": true }); + + assert({ + given: "filter for alwaysApply: true", + should: "find matching documents", + actual: results.map((r) => r.path), + expected: ["ai/rules/always-on.mdc"], + }); + }); + + test("filters by document type", () => { + const db = setupTestDatabase(); + const results = searchMetadata(db, { type: "command" }); + + assert({ + given: "filter for type: command", + should: "find commands only", + actual: results.map((r) => r.path), + expected: ["ai/commands/test.md"], + }); + }); + + test("filters by string field with LIKE", () => { + const db = setupTestDatabase(); + const results = searchMetadata(db, { "frontmatter.globs": "**/*.js" }); + + assert({ + given: "filter for specific globs value", + should: "find matching document", + actual: results.length, + expected: 1, + }); + }); + + test("filters by array contains", () => { + const db = setupTestDatabase(); + const results = searchMetadata(db, { + "frontmatter.tags": { contains: "security" }, + }); + + assert({ + given: "filter for tag contains 'security'", + should: "find document with that tag", + actual: results.map((r) => r.path), + expected: ["ai/rules/tagged.mdc"], + }); + }); + + test("combines multiple filters with AND", () => { + const db = setupTestDatabase(); + const results = searchMetadata(db, { + type: "rule", + "frontmatter.alwaysApply": true, + }); + + assert({ + given: "multiple filters", + should: "apply all filters", + actual: results.length, + expected: 1, + }); + }); + + test("returns empty array for no matches", () => { + const db = setupTestDatabase(); + const results = searchMetadata(db, { type: "nonexistent" }); + + assert({ + given: "no matching documents", + should: "return empty array", + actual: results, + expected: [], + }); + }); + + test("respects limit option", () => { + const db = setupTestDatabase(); + const results = searchMetadata(db, { type: "rule" }, { limit: 2 }); + + assert({ + given: "limit of 2", + should: "return at most 2 results", + actual: results.length <= 2, + expected: true, + }); + }); + + test("returns full document metadata", () => { + const db = setupTestDatabase(); + const results = searchMetadata(db, { type: "rule" }, { limit: 1 }); + + assert({ + given: "search result", + should: "include path, type, and frontmatter", + actual: + results[0] && + "path" in results[0] && + "type" in results[0] && + "frontmatter" in results[0], + expected: true, + }); + }); + + test("rejects SQL injection in filter keys", () => { + const db = setupTestDatabase(); + + let error; + try { + searchMetadata(db, { "frontmatter.foo') OR 1=1; --": "evil" }); + } catch (err) { + error = err; + } + + assert({ + given: "filter key with SQL injection attempt", + should: "throw error with cause", + actual: error instanceof Error && error.cause !== undefined, + expected: true, + }); + + assert({ + given: "filter key with SQL injection attempt", + should: "have ValidationError cause name", + actual: error.cause.name, + expected: "ValidationError", + }); + + assert({ + given: "filter key with SQL injection attempt", + should: "have VALIDATION_ERROR code", + actual: error.cause.code, + expected: "VALIDATION_ERROR", + }); + + assert({ + given: "filter key with SQL injection attempt", + should: "include the invalid jsonPath in cause", + actual: error.cause.jsonPath, + expected: "foo') OR 1=1; --", + }); + }); + + test("allows valid nested JSON paths", () => { + const db = setupTestDatabase(); + + // Should not throw + const results = searchMetadata(db, { + "frontmatter.nested.deep_value": "test", + }); + + assert({ + given: "valid nested JSON path with underscore", + should: "not throw and return results", + actual: Array.isArray(results), + expected: true, + }); + }); + }); + + describe("getFieldValues", () => { + test("rejects SQL injection in field name", () => { + const db = setupTestDatabase(); + + let error; + try { + getFieldValues(db, "foo') OR 1=1; --"); + } catch (err) { + error = err; + } + + assert({ + given: "field name with SQL injection attempt", + should: "throw error with cause", + actual: error instanceof Error && error.cause !== undefined, + expected: true, + }); + + assert({ + given: "field name with SQL injection attempt", + should: "have ValidationError cause name", + actual: error.cause.name, + expected: "ValidationError", + }); + + assert({ + given: "field name with SQL injection attempt", + should: "have VALIDATION_ERROR code", + actual: error.cause.code, + expected: "VALIDATION_ERROR", + }); + + assert({ + given: "field name with SQL injection attempt", + should: "include the invalid field in cause", + actual: error.cause.field, + expected: "foo') OR 1=1; --", + }); + }); + }); +}); diff --git a/lib/agents-md.test.js b/lib/agents-md.test.js index c91c9a4..2f73878 100644 --- a/lib/agents-md.test.js +++ b/lib/agents-md.test.js @@ -1,8 +1,9 @@ import { assert } from "riteway/vitest"; -import { describe, test, beforeEach, afterEach } from "vitest"; +import { describe, test, onTestFinished } from "vitest"; import path from "path"; import fs from "fs-extra"; import os from "os"; +import { createId } from "@paralleldrive/cuid2"; import { ensureAgentsMd, @@ -13,20 +14,18 @@ import { REQUIRED_DIRECTIVES, } from "./agents-md.js"; -describe("agents-md", () => { - let tempDir; - - beforeEach(async () => { - // Create a unique temp directory for each test - tempDir = path.join(os.tmpdir(), `aidd-test-${Date.now()}`); - await fs.ensureDir(tempDir); - }); +const setupTestDirectory = async () => { + const tempDir = path.join(os.tmpdir(), `aidd-test-${createId()}`); + await fs.ensureDir(tempDir); - afterEach(async () => { - // Clean up temp directory + onTestFinished(async () => { await fs.remove(tempDir); }); + return tempDir; +}; + +describe("agents-md", () => { describe("hasAllDirectives", () => { test("returns true when all directives are present", () => { assert({ @@ -92,6 +91,7 @@ describe("agents-md", () => { describe("agentsFileExists", () => { test("returns false when file does not exist", async () => { + const tempDir = await setupTestDirectory(); const exists = await agentsFileExists(tempDir); assert({ @@ -103,6 +103,7 @@ describe("agents-md", () => { }); test("returns true when file exists", async () => { + const tempDir = await setupTestDirectory(); await fs.writeFile(path.join(tempDir, "AGENTS.md"), "# Test"); const exists = await agentsFileExists(tempDir); @@ -118,6 +119,7 @@ describe("agents-md", () => { describe("ensureAgentsMd", () => { test("creates AGENTS.md when it does not exist", async () => { + const tempDir = await setupTestDirectory(); const result = await ensureAgentsMd(tempDir); assert({ @@ -141,6 +143,7 @@ describe("agents-md", () => { }); test("does not modify AGENTS.md when all directives present", async () => { + const tempDir = await setupTestDirectory(); const originalContent = AGENTS_MD_CONTENT; await fs.writeFile(path.join(tempDir, "AGENTS.md"), originalContent); @@ -167,6 +170,7 @@ describe("agents-md", () => { }); test("appends directives when some are missing", async () => { + const tempDir = await setupTestDirectory(); const originalContent = "# My Custom AGENTS.md\n\nSome custom content."; await fs.writeFile(path.join(tempDir, "AGENTS.md"), originalContent); @@ -200,6 +204,7 @@ describe("agents-md", () => { }); test("does not duplicate directives if already present", async () => { + const tempDir = await setupTestDirectory(); // Create content with all directives but in custom format const customContent = `# Custom AGENTS.md diff --git a/lib/index-generator.js b/lib/index-generator.js index 480c563..1b731d5 100644 --- a/lib/index-generator.js +++ b/lib/index-generator.js @@ -155,7 +155,16 @@ const generateSubdirEntry = (subdirName) => { const generateIndexContent = async (dirPath) => { const dirName = path.basename(dirPath); const files = await getIndexableFiles(dirPath); - const subdirs = await getSubdirectories(dirPath); + const allSubdirs = await getSubdirectories(dirPath); + + // Filter subdirectories to only include those with indexable content + const subdirs = []; + for (const subdir of allSubdirs) { + const subdirPath = path.join(dirPath, subdir); + if (await hasIndexableContent(subdirPath)) { + subdirs.push(subdir); + } + } let content = `# ${dirName}\n\n`; content += `This index provides an overview of the contents in this directory.\n\n`; @@ -188,10 +197,52 @@ const generateIndexContent = async (dirPath) => { return content; }; +/** + * Check if a directory has any indexable content (markdown files or subdirectories with content). + * Recursively checks subdirectories to ensure they actually contain markdown files. + */ +const hasIndexableContent = async (dirPath, visited = new Set()) => { + // Prevent infinite recursion via symlinks + const realPath = await fs.realpath(dirPath); + if (visited.has(realPath)) { + return false; + } + visited.add(realPath); + + const files = await getIndexableFiles(dirPath); + + // If there are markdown files, we have content + if (files.length > 0) { + return true; + } + + // Check if any subdirectory has indexable content (recursive) + const subdirs = await getSubdirectories(dirPath); + for (const subdir of subdirs) { + const subdirPath = path.join(dirPath, subdir); + // Skip symlinks to prevent infinite recursion + const stats = await fs.lstat(subdirPath); + if (!stats.isSymbolicLink()) { + if (await hasIndexableContent(subdirPath, visited)) { + return true; + } + } + } + + return false; +}; + /** * Write index.md to a directory + * Returns null if directory has no indexable content (only non-markdown files) */ const writeIndex = async (dirPath) => { + // Skip directories with no markdown files or subdirectories + const hasContent = await hasIndexableContent(dirPath); + if (!hasContent) { + return null; + } + const indexPath = path.join(dirPath, "index.md"); const content = await generateIndexContent(dirPath); @@ -218,9 +269,11 @@ const generateIndexRecursive = async (dirPath, results = [], depth = 0) => { return results; } - // Generate index for current directory + // Generate index for current directory (returns null if no indexable content) const result = await writeIndex(dirPath); - results.push(result); + if (result) { + results.push(result); + } // Process subdirectories const subdirs = await getSubdirectories(dirPath); @@ -276,6 +329,7 @@ export { generateIndexRecursive, generateIndexContent, writeIndex, + hasIndexableContent, parseFrontmatter, extractTitle, getIndexableFiles, diff --git a/lib/index-generator.test.js b/lib/index-generator.test.js index 4bc4d4b..712fa34 100644 --- a/lib/index-generator.test.js +++ b/lib/index-generator.test.js @@ -7,6 +7,7 @@ import os from "os"; import { generateAllIndexes, generateIndexContent, + hasIndexableContent, parseFrontmatter, extractTitle, getIndexableFiles, @@ -159,6 +160,64 @@ alwaysApply: false }); }); + describe("hasIndexableContent", () => { + test("returns true for directory with markdown files", async () => { + await fs.writeFile(path.join(tempDir, "doc.md"), "# Doc"); + + assert({ + given: "directory with markdown file", + should: "return true", + actual: await hasIndexableContent(tempDir), + expected: true, + }); + }); + + test("returns false for empty directory", async () => { + assert({ + given: "empty directory", + should: "return false", + actual: await hasIndexableContent(tempDir), + expected: false, + }); + }); + + test("returns false for directory with only non-markdown files", async () => { + await fs.writeFile(path.join(tempDir, "script.js"), "// js"); + await fs.writeFile(path.join(tempDir, "data.json"), "{}"); + + assert({ + given: "directory with only non-markdown files", + should: "return false", + actual: await hasIndexableContent(tempDir), + expected: false, + }); + }); + + test("returns false for nested empty subdirectories", async () => { + await fs.ensureDir(path.join(tempDir, "a", "b", "c")); + await fs.ensureDir(path.join(tempDir, "x", "y")); + + assert({ + given: "directory with only nested empty subdirectories", + should: "return false", + actual: await hasIndexableContent(tempDir), + expected: false, + }); + }); + + test("returns true when deeply nested subdirectory has markdown", async () => { + await fs.ensureDir(path.join(tempDir, "a", "b", "c")); + await fs.writeFile(path.join(tempDir, "a", "b", "c", "doc.md"), "# Deep"); + + assert({ + given: "directory with markdown in deeply nested subdirectory", + should: "return true", + actual: await hasIndexableContent(tempDir), + expected: true, + }); + }); + }); + describe("generateIndexContent", () => { test("generates index with file entries", async () => { const fileContent = `--- @@ -179,19 +238,76 @@ Content here.`; }); }); - test("includes subdirectory references", async () => { + test("includes subdirectory references when subdirectory has content", async () => { await fs.ensureDir(path.join(tempDir, "subcommands")); + await fs.writeFile( + path.join(tempDir, "subcommands", "help.md"), + "# Help", + ); const content = await generateIndexContent(tempDir); assert({ - given: "directory with subdirectory", + given: "directory with subdirectory containing markdown", should: "include subdirectory reference", actual: content.includes("subcommands/index.md"), expected: true, }); }); + test("excludes subdirectory references when subdirectory has no content", async () => { + await fs.ensureDir(path.join(tempDir, "empty-subdir")); + + const content = await generateIndexContent(tempDir); + + assert({ + given: "directory with empty subdirectory", + should: "not include subdirectory reference", + actual: content.includes("empty-subdir/index.md"), + expected: false, + }); + }); + + test("excludes subdirectory with only nested empty subdirectories", async () => { + // Create a subdirectory that contains only more empty subdirectories + // but no actual markdown files anywhere in the tree + await fs.ensureDir(path.join(tempDir, "parent", "child1", "grandchild")); + await fs.ensureDir(path.join(tempDir, "parent", "child2")); + // Add a non-markdown file to prove it's not just checking for "any files" + await fs.writeFile( + path.join(tempDir, "parent", "child1", "readme.txt"), + "not markdown", + ); + + const content = await generateIndexContent(tempDir); + + assert({ + given: + "subdirectory with only nested empty directories and non-markdown files", + should: "not include subdirectory reference", + actual: content.includes("parent/index.md"), + expected: false, + }); + }); + + test("includes subdirectory when nested child has markdown content", async () => { + // Create nested structure where only the deepest level has markdown + await fs.ensureDir(path.join(tempDir, "parent", "child", "grandchild")); + await fs.writeFile( + path.join(tempDir, "parent", "child", "grandchild", "doc.md"), + "# Deep Document", + ); + + const content = await generateIndexContent(tempDir); + + assert({ + given: "subdirectory with markdown only in deeply nested child", + should: "include subdirectory reference", + actual: content.includes("parent/index.md"), + expected: true, + }); + }); + test("handles files without frontmatter", async () => { await fs.writeFile( path.join(tempDir, "plain.md"), diff --git a/package-lock.json b/package-lock.json index c11c74d..ae73d97 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,8 +11,10 @@ "dependencies": { "@paralleldrive/cuid2": "^3.1.0", "@sinclair/typebox": "^0.34.41", + "better-sqlite3": "^11.8.1", "chalk": "^4.1.2", "commander": "^11.1.0", + "dependency-cruiser": "17.3.7", "error-causes": "^3.0.2", "fs-extra": "^11.1.1", "gray-matter": "^4.0.3", @@ -23,6 +25,7 @@ }, "devDependencies": { "@eslint/eslintrc": "^3", + "@types/better-sqlite3": "^7.6.12", "@vitest/coverage-v8": "^3.2.4", "doctoc": "^2.2.1", "eslint": "^9", @@ -119,9 +122,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.10.tgz", - "integrity": "sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", + "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", "cpu": [ "ppc64" ], @@ -136,9 +139,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.10.tgz", - "integrity": "sha512-dQAxF1dW1C3zpeCDc5KqIYuZ1tgAdRXNoZP7vkBIRtKZPYe2xVr/d3SkirklCHudW1B45tGiUlz2pUWDfbDD4w==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", + "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", "cpu": [ "arm" ], @@ -153,9 +156,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.10.tgz", - "integrity": "sha512-LSQa7eDahypv/VO6WKohZGPSJDq5OVOo3UoFR1E4t4Gj1W7zEQMUhI+lo81H+DtB+kP+tDgBp+M4oNCwp6kffg==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", + "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", "cpu": [ "arm64" ], @@ -170,9 +173,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.10.tgz", - "integrity": "sha512-MiC9CWdPrfhibcXwr39p9ha1x0lZJ9KaVfvzA0Wxwz9ETX4v5CHfF09bx935nHlhi+MxhA63dKRRQLiVgSUtEg==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", + "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", "cpu": [ "x64" ], @@ -187,9 +190,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.10.tgz", - "integrity": "sha512-JC74bdXcQEpW9KkV326WpZZjLguSZ3DfS8wrrvPMHgQOIEIG/sPXEN/V8IssoJhbefLRcRqw6RQH2NnpdprtMA==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", + "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", "cpu": [ "arm64" ], @@ -204,9 +207,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.10.tgz", - "integrity": "sha512-tguWg1olF6DGqzws97pKZ8G2L7Ig1vjDmGTwcTuYHbuU6TTjJe5FXbgs5C1BBzHbJ2bo1m3WkQDbWO2PvamRcg==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", + "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", "cpu": [ "x64" ], @@ -221,9 +224,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.10.tgz", - "integrity": "sha512-3ZioSQSg1HT2N05YxeJWYR+Libe3bREVSdWhEEgExWaDtyFbbXWb49QgPvFH8u03vUPX10JhJPcz7s9t9+boWg==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", + "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", "cpu": [ "arm64" ], @@ -238,9 +241,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.10.tgz", - "integrity": "sha512-LLgJfHJk014Aa4anGDbh8bmI5Lk+QidDmGzuC2D+vP7mv/GeSN+H39zOf7pN5N8p059FcOfs2bVlrRr4SK9WxA==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", + "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", "cpu": [ "x64" ], @@ -255,9 +258,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.10.tgz", - "integrity": "sha512-oR31GtBTFYCqEBALI9r6WxoU/ZofZl962pouZRTEYECvNF/dtXKku8YXcJkhgK/beU+zedXfIzHijSRapJY3vg==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", + "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", "cpu": [ "arm" ], @@ -272,9 +275,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.10.tgz", - "integrity": "sha512-5luJWN6YKBsawd5f9i4+c+geYiVEw20FVW5x0v1kEMWNq8UctFjDiMATBxLvmmHA4bf7F6hTRaJgtghFr9iziQ==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", + "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", "cpu": [ "arm64" ], @@ -289,9 +292,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.10.tgz", - "integrity": "sha512-NrSCx2Kim3EnnWgS4Txn0QGt0Xipoumb6z6sUtl5bOEZIVKhzfyp/Lyw4C1DIYvzeW/5mWYPBFJU3a/8Yr75DQ==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", + "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", "cpu": [ "ia32" ], @@ -306,9 +309,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.10.tgz", - "integrity": "sha512-xoSphrd4AZda8+rUDDfD9J6FUMjrkTz8itpTITM4/xgerAZZcFW7Dv+sun7333IfKxGG8gAq+3NbfEMJfiY+Eg==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", + "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", "cpu": [ "loong64" ], @@ -323,9 +326,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.10.tgz", - "integrity": "sha512-ab6eiuCwoMmYDyTnyptoKkVS3k8fy/1Uvq7Dj5czXI6DF2GqD2ToInBI0SHOp5/X1BdZ26RKc5+qjQNGRBelRA==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", + "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", "cpu": [ "mips64el" ], @@ -340,9 +343,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.10.tgz", - "integrity": "sha512-NLinzzOgZQsGpsTkEbdJTCanwA5/wozN9dSgEl12haXJBzMTpssebuXR42bthOF3z7zXFWH1AmvWunUCkBE4EA==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", + "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", "cpu": [ "ppc64" ], @@ -357,9 +360,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.10.tgz", - "integrity": "sha512-FE557XdZDrtX8NMIeA8LBJX3dC2M8VGXwfrQWU7LB5SLOajfJIxmSdyL/gU1m64Zs9CBKvm4UAuBp5aJ8OgnrA==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", + "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", "cpu": [ "riscv64" ], @@ -374,9 +377,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.10.tgz", - "integrity": "sha512-3BBSbgzuB9ajLoVZk0mGu+EHlBwkusRmeNYdqmznmMc9zGASFjSsxgkNsqmXugpPk00gJ0JNKh/97nxmjctdew==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", + "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", "cpu": [ "s390x" ], @@ -391,9 +394,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.10.tgz", - "integrity": "sha512-QSX81KhFoZGwenVyPoberggdW1nrQZSvfVDAIUXr3WqLRZGZqWk/P4T8p2SP+de2Sr5HPcvjhcJzEiulKgnxtA==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", + "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", "cpu": [ "x64" ], @@ -408,9 +411,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.10.tgz", - "integrity": "sha512-AKQM3gfYfSW8XRk8DdMCzaLUFB15dTrZfnX8WXQoOUpUBQ+NaAFCP1kPS/ykbbGYz7rxn0WS48/81l9hFl3u4A==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", + "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", "cpu": [ "arm64" ], @@ -425,9 +428,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.10.tgz", - "integrity": "sha512-7RTytDPGU6fek/hWuN9qQpeGPBZFfB4zZgcz2VK2Z5VpdUxEI8JKYsg3JfO0n/Z1E/6l05n0unDCNc4HnhQGig==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", + "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", "cpu": [ "x64" ], @@ -442,9 +445,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.10.tgz", - "integrity": "sha512-5Se0VM9Wtq797YFn+dLimf2Zx6McttsH2olUBsDml+lm0GOCRVebRWUvDtkY4BWYv/3NgzS8b/UM3jQNh5hYyw==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", + "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", "cpu": [ "arm64" ], @@ -459,9 +462,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.10.tgz", - "integrity": "sha512-XkA4frq1TLj4bEMB+2HnI0+4RnjbuGZfet2gs/LNs5Hc7D89ZQBHQ0gL2ND6Lzu1+QVkjp3x1gIcPKzRNP8bXw==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", + "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", "cpu": [ "x64" ], @@ -476,9 +479,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.10.tgz", - "integrity": "sha512-AVTSBhTX8Y/Fz6OmIVBip9tJzZEUcY8WLh7I59+upa5/GPhh2/aM6bvOMQySspnCCHvFi79kMtdJS1w0DXAeag==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", + "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", "cpu": [ "arm64" ], @@ -493,9 +496,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.10.tgz", - "integrity": "sha512-fswk3XT0Uf2pGJmOpDB7yknqhVkJQkAQOcW/ccVOtfx05LkbWOaRAtn5SaqXypeKQra1QaEa841PgrSL9ubSPQ==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", + "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", "cpu": [ "x64" ], @@ -510,9 +513,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.10.tgz", - "integrity": "sha512-ah+9b59KDTSfpaCg6VdJoOQvKjI33nTaQr4UluQwW7aEwZQsbMCfTmfEO4VyewOxx4RaDT/xCy9ra2GPWmO7Kw==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", + "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", "cpu": [ "arm64" ], @@ -527,9 +530,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.10.tgz", - "integrity": "sha512-QHPDbKkrGO8/cz9LKVnJU22HOi4pxZnZhhA2HYHez5Pz4JeffhDjf85E57Oyco163GnzNCVkZK0b/n4Y0UHcSw==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", + "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", "cpu": [ "ia32" ], @@ -544,9 +547,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.10.tgz", - "integrity": "sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", + "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", "cpu": [ "x64" ], @@ -778,9 +781,9 @@ } }, "node_modules/@inquirer/ansi": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.0.tgz", - "integrity": "sha512-JWaTfCxI1eTmJ1BIv86vUfjVatOdxwD0DAVKYevY8SazeUUZtW+tNbsdejVO1GYE0GXJW1N1ahmiC3TFd+7wZA==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.2.tgz", + "integrity": "sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==", "dev": true, "license": "MIT", "engines": { @@ -788,17 +791,17 @@ } }, "node_modules/@inquirer/checkbox": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.2.4.tgz", - "integrity": "sha512-2n9Vgf4HSciFq8ttKXk+qy+GsyTXPV1An6QAwe/8bkbbqvG4VW1I/ZY1pNu2rf+h9bdzMLPbRSfcNxkHBy/Ydw==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.3.2.tgz", + "integrity": "sha512-VXukHf0RR1doGe6Sm4F0Em7SWYLTHSsbGfJdS9Ja2bX5/D5uwVOEjr07cncLROdBvmnvCATYEWlHqYmXv2IlQA==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/ansi": "^1.0.0", - "@inquirer/core": "^10.2.2", - "@inquirer/figures": "^1.0.13", - "@inquirer/type": "^3.0.8", - "yoctocolors-cjs": "^2.1.2" + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" }, "engines": { "node": ">=18" @@ -813,14 +816,14 @@ } }, "node_modules/@inquirer/confirm": { - "version": "5.1.18", - "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.18.tgz", - "integrity": "sha512-MilmWOzHa3Ks11tzvuAmFoAd/wRuaP3SwlT1IZhyMke31FKLxPiuDWcGXhU+PKveNOpAc4axzAgrgxuIJJRmLw==", + "version": "5.1.21", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.21.tgz", + "integrity": "sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^10.2.2", - "@inquirer/type": "^3.0.8" + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" }, "engines": { "node": ">=18" @@ -835,20 +838,20 @@ } }, "node_modules/@inquirer/core": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.2.2.tgz", - "integrity": "sha512-yXq/4QUnk4sHMtmbd7irwiepjB8jXU0kkFRL4nr/aDBA2mDz13cMakEWdDwX3eSCTkk03kwcndD1zfRAIlELxA==", + "version": "10.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.3.2.tgz", + "integrity": "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/ansi": "^1.0.0", - "@inquirer/figures": "^1.0.13", - "@inquirer/type": "^3.0.8", + "@inquirer/ansi": "^1.0.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", "cli-width": "^4.1.0", "mute-stream": "^2.0.0", "signal-exit": "^4.1.0", "wrap-ansi": "^6.2.0", - "yoctocolors-cjs": "^2.1.2" + "yoctocolors-cjs": "^2.1.3" }, "engines": { "node": ">=18" @@ -923,15 +926,15 @@ } }, "node_modules/@inquirer/editor": { - "version": "4.2.20", - "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.20.tgz", - "integrity": "sha512-7omh5y5bK672Q+Brk4HBbnHNowOZwrb/78IFXdrEB9PfdxL3GudQyDk8O9vQ188wj3xrEebS2M9n18BjJoI83g==", + "version": "4.2.23", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.23.tgz", + "integrity": "sha512-aLSROkEwirotxZ1pBaP8tugXRFCxW94gwrQLxXfrZsKkfjOYC1aRvAZuhpJOb5cu4IBTJdsCigUlf2iCOu4ZDQ==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^10.2.2", - "@inquirer/external-editor": "^1.0.2", - "@inquirer/type": "^3.0.8" + "@inquirer/core": "^10.3.2", + "@inquirer/external-editor": "^1.0.3", + "@inquirer/type": "^3.0.10" }, "engines": { "node": ">=18" @@ -946,15 +949,15 @@ } }, "node_modules/@inquirer/expand": { - "version": "4.0.20", - "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.20.tgz", - "integrity": "sha512-Dt9S+6qUg94fEvgn54F2Syf0Z3U8xmnBI9ATq2f5h9xt09fs2IJXSCIXyyVHwvggKWFXEY/7jATRo2K6Dkn6Ow==", + "version": "4.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.23.tgz", + "integrity": "sha512-nRzdOyFYnpeYTTR2qFwEVmIWypzdAx/sIkCMeTNTcflFOovfqUk+HcFhQQVBftAh9gmGrpFj6QcGEqrDMDOiew==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^10.2.2", - "@inquirer/type": "^3.0.8", - "yoctocolors-cjs": "^2.1.2" + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" }, "engines": { "node": ">=18" @@ -969,13 +972,13 @@ } }, "node_modules/@inquirer/external-editor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.2.tgz", - "integrity": "sha512-yy9cOoBnx58TlsPrIxauKIFQTiyH+0MK4e97y4sV9ERbI+zDxw7i2hxHLCIEGIE/8PPvDxGhgzIOTSOWcs6/MQ==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.3.tgz", + "integrity": "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==", "dev": true, "license": "MIT", "dependencies": { - "chardet": "^2.1.0", + "chardet": "^2.1.1", "iconv-lite": "^0.7.0" }, "engines": { @@ -991,9 +994,9 @@ } }, "node_modules/@inquirer/external-editor/node_modules/iconv-lite": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", - "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", "dev": true, "license": "MIT", "dependencies": { @@ -1008,9 +1011,9 @@ } }, "node_modules/@inquirer/figures": { - "version": "1.0.13", - "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.13.tgz", - "integrity": "sha512-lGPVU3yO9ZNqA7vTYz26jny41lE7yoQansmqdMLBEfqaGsmdg7V3W9mK9Pvb5IL4EVZ9GnSDGMO/cJXud5dMaw==", + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.15.tgz", + "integrity": "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==", "dev": true, "license": "MIT", "engines": { @@ -1018,14 +1021,14 @@ } }, "node_modules/@inquirer/input": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.2.4.tgz", - "integrity": "sha512-cwSGpLBMwpwcZZsc6s1gThm0J+it/KIJ+1qFL2euLmSKUMGumJ5TcbMgxEjMjNHRGadouIYbiIgruKoDZk7klw==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.3.1.tgz", + "integrity": "sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^10.2.2", - "@inquirer/type": "^3.0.8" + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" }, "engines": { "node": ">=18" @@ -1040,14 +1043,14 @@ } }, "node_modules/@inquirer/number": { - "version": "3.0.20", - "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.20.tgz", - "integrity": "sha512-bbooay64VD1Z6uMfNehED2A2YOPHSJnQLs9/4WNiV/EK+vXczf/R988itL2XLDGTgmhMF2KkiWZo+iEZmc4jqg==", + "version": "3.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.23.tgz", + "integrity": "sha512-5Smv0OK7K0KUzUfYUXDXQc9jrf8OHo4ktlEayFlelCjwMXz0299Y8OrI+lj7i4gCBY15UObk76q0QtxjzFcFcg==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^10.2.2", - "@inquirer/type": "^3.0.8" + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" }, "engines": { "node": ">=18" @@ -1062,15 +1065,15 @@ } }, "node_modules/@inquirer/password": { - "version": "4.0.20", - "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.20.tgz", - "integrity": "sha512-nxSaPV2cPvvoOmRygQR+h0B+Av73B01cqYLcr7NXcGXhbmsYfUb8fDdw2Us1bI2YsX+VvY7I7upgFYsyf8+Nug==", + "version": "4.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.23.tgz", + "integrity": "sha512-zREJHjhT5vJBMZX/IUbyI9zVtVfOLiTO66MrF/3GFZYZ7T4YILW5MSkEYHceSii/KtRk+4i3RE7E1CUXA2jHcA==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/ansi": "^1.0.0", - "@inquirer/core": "^10.2.2", - "@inquirer/type": "^3.0.8" + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" }, "engines": { "node": ">=18" @@ -1085,22 +1088,22 @@ } }, "node_modules/@inquirer/prompts": { - "version": "7.8.6", - "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.8.6.tgz", - "integrity": "sha512-68JhkiojicX9SBUD8FE/pSKbOKtwoyaVj1kwqLfvjlVXZvOy3iaSWX4dCLsZyYx/5Ur07Fq+yuDNOen+5ce6ig==", + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.10.1.tgz", + "integrity": "sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/checkbox": "^4.2.4", - "@inquirer/confirm": "^5.1.18", - "@inquirer/editor": "^4.2.20", - "@inquirer/expand": "^4.0.20", - "@inquirer/input": "^4.2.4", - "@inquirer/number": "^3.0.20", - "@inquirer/password": "^4.0.20", - "@inquirer/rawlist": "^4.1.8", - "@inquirer/search": "^3.1.3", - "@inquirer/select": "^4.3.4" + "@inquirer/checkbox": "^4.3.2", + "@inquirer/confirm": "^5.1.21", + "@inquirer/editor": "^4.2.23", + "@inquirer/expand": "^4.0.23", + "@inquirer/input": "^4.3.1", + "@inquirer/number": "^3.0.23", + "@inquirer/password": "^4.0.23", + "@inquirer/rawlist": "^4.1.11", + "@inquirer/search": "^3.2.2", + "@inquirer/select": "^4.4.2" }, "engines": { "node": ">=18" @@ -1115,15 +1118,15 @@ } }, "node_modules/@inquirer/rawlist": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.1.8.tgz", - "integrity": "sha512-CQ2VkIASbgI2PxdzlkeeieLRmniaUU1Aoi5ggEdm6BIyqopE9GuDXdDOj9XiwOqK5qm72oI2i6J+Gnjaa26ejg==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.1.11.tgz", + "integrity": "sha512-+LLQB8XGr3I5LZN/GuAHo+GpDJegQwuPARLChlMICNdwW7OwV2izlCSCxN6cqpL0sMXmbKbFcItJgdQq5EBXTw==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^10.2.2", - "@inquirer/type": "^3.0.8", - "yoctocolors-cjs": "^2.1.2" + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" }, "engines": { "node": ">=18" @@ -1138,16 +1141,16 @@ } }, "node_modules/@inquirer/search": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.1.3.tgz", - "integrity": "sha512-D5T6ioybJJH0IiSUK/JXcoRrrm8sXwzrVMjibuPs+AgxmogKslaafy1oxFiorNI4s3ElSkeQZbhYQgLqiL8h6Q==", + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.2.2.tgz", + "integrity": "sha512-p2bvRfENXCZdWF/U2BXvnSI9h+tuA8iNqtUKb9UWbmLYCRQxd8WkvwWvYn+3NgYaNwdUkHytJMGG4MMLucI1kA==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^10.2.2", - "@inquirer/figures": "^1.0.13", - "@inquirer/type": "^3.0.8", - "yoctocolors-cjs": "^2.1.2" + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" }, "engines": { "node": ">=18" @@ -1162,17 +1165,17 @@ } }, "node_modules/@inquirer/select": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.3.4.tgz", - "integrity": "sha512-Qp20nySRmfbuJBBsgPU7E/cL62Hf250vMZRzYDcBHty2zdD1kKCnoDFWRr0WO2ZzaXp3R7a4esaVGJUx0E6zvA==", + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.4.2.tgz", + "integrity": "sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/ansi": "^1.0.0", - "@inquirer/core": "^10.2.2", - "@inquirer/figures": "^1.0.13", - "@inquirer/type": "^3.0.8", - "yoctocolors-cjs": "^2.1.2" + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" }, "engines": { "node": ">=18" @@ -1187,9 +1190,9 @@ } }, "node_modules/@inquirer/type": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.8.tgz", - "integrity": "sha512-lg9Whz8onIHRthWaN1Q9EGLa/0LFJjyM8mEUbL1eTi6yMGvBf8gvyDLtxSXztQsxMvhxxNpJYrwa1YHdq+w4Jw==", + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.10.tgz", + "integrity": "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==", "dev": true, "license": "MIT", "engines": { @@ -1331,17 +1334,17 @@ } }, "node_modules/@octokit/core": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/@octokit/core/-/core-7.0.4.tgz", - "integrity": "sha512-jOT8V1Ba5BdC79sKrRWDdMT5l1R+XNHTPR6CPWzUP2EcfAcvIHZWF0eAbmRcpOOP5gVIwnqNg0C4nvh6Abc3OA==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-7.0.6.tgz", + "integrity": "sha512-DhGl4xMVFGVIyMwswXeyzdL4uXD5OGILGX5N8Y+f6W7LhC1Ze2poSNrkF/fedpVDHEEZ+PHFW0vL14I+mm8K3Q==", "dev": true, "license": "MIT", "dependencies": { "@octokit/auth-token": "^6.0.0", - "@octokit/graphql": "^9.0.1", - "@octokit/request": "^10.0.2", - "@octokit/request-error": "^7.0.0", - "@octokit/types": "^15.0.0", + "@octokit/graphql": "^9.0.3", + "@octokit/request": "^10.0.6", + "@octokit/request-error": "^7.0.2", + "@octokit/types": "^16.0.0", "before-after-hook": "^4.0.0", "universal-user-agent": "^7.0.0" }, @@ -1350,83 +1353,49 @@ } }, "node_modules/@octokit/endpoint": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-11.0.0.tgz", - "integrity": "sha512-hoYicJZaqISMAI3JfaDr1qMNi48OctWuOih1m80bkYow/ayPw6Jj52tqWJ6GEoFTk1gBqfanSoI1iY99Z5+ekQ==", + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-11.0.2.tgz", + "integrity": "sha512-4zCpzP1fWc7QlqunZ5bSEjxc6yLAlRTnDwKtgXfcI/FxxGoqedDG8V2+xJ60bV2kODqcGB+nATdtap/XYq2NZQ==", "dev": true, "license": "MIT", "dependencies": { - "@octokit/types": "^14.0.0", + "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.2" }, "engines": { "node": ">= 20" } }, - "node_modules/@octokit/endpoint/node_modules/@octokit/openapi-types": { - "version": "25.1.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-25.1.0.tgz", - "integrity": "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@octokit/endpoint/node_modules/@octokit/types": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-14.1.0.tgz", - "integrity": "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@octokit/openapi-types": "^25.1.0" - } - }, "node_modules/@octokit/graphql": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-9.0.1.tgz", - "integrity": "sha512-j1nQNU1ZxNFx2ZtKmL4sMrs4egy5h65OMDmSbVyuCzjOcwsHq6EaYjOTGXPQxgfiN8dJ4CriYHk6zF050WEULg==", + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-9.0.3.tgz", + "integrity": "sha512-grAEuupr/C1rALFnXTv6ZQhFuL1D8G5y8CN04RgrO4FIPMrtm+mcZzFG7dcBm+nq+1ppNixu+Jd78aeJOYxlGA==", "dev": true, "license": "MIT", "dependencies": { - "@octokit/request": "^10.0.2", - "@octokit/types": "^14.0.0", + "@octokit/request": "^10.0.6", + "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.0" }, "engines": { "node": ">= 20" } }, - "node_modules/@octokit/graphql/node_modules/@octokit/openapi-types": { - "version": "25.1.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-25.1.0.tgz", - "integrity": "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@octokit/graphql/node_modules/@octokit/types": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-14.1.0.tgz", - "integrity": "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@octokit/openapi-types": "^25.1.0" - } - }, "node_modules/@octokit/openapi-types": { - "version": "26.0.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-26.0.0.tgz", - "integrity": "sha512-7AtcfKtpo77j7Ts73b4OWhOZHTKo/gGY8bB3bNBQz4H+GRSWqx2yvj8TXRsbdTE0eRmYmXOEY66jM7mJ7LzfsA==", + "version": "27.0.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-27.0.0.tgz", + "integrity": "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA==", "dev": true, "license": "MIT" }, "node_modules/@octokit/plugin-paginate-rest": { - "version": "13.1.1", - "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-13.1.1.tgz", - "integrity": "sha512-q9iQGlZlxAVNRN2jDNskJW/Cafy7/XE52wjZ5TTvyhyOD904Cvx//DNyoO3J/MXJ0ve3rPoNWKEg5iZrisQSuw==", + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-14.0.0.tgz", + "integrity": "sha512-fNVRE7ufJiAA3XUrha2omTA39M6IXIc6GIZLvlbsm8QOQCYvpq/LkMNGyFlB1d8hTDzsAXa3OKtybdMAYsV/fw==", "dev": true, "license": "MIT", "dependencies": { - "@octokit/types": "^14.1.0" + "@octokit/types": "^16.0.0" }, "engines": { "node": ">= 20" @@ -1435,23 +1404,6 @@ "@octokit/core": ">=6" } }, - "node_modules/@octokit/plugin-paginate-rest/node_modules/@octokit/openapi-types": { - "version": "25.1.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-25.1.0.tgz", - "integrity": "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@octokit/plugin-paginate-rest/node_modules/@octokit/types": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-14.1.0.tgz", - "integrity": "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@octokit/openapi-types": "^25.1.0" - } - }, "node_modules/@octokit/plugin-request-log": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-6.0.0.tgz", @@ -1466,13 +1418,13 @@ } }, "node_modules/@octokit/plugin-rest-endpoint-methods": { - "version": "16.1.0", - "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-16.1.0.tgz", - "integrity": "sha512-nCsyiKoGRnhH5LkH8hJEZb9swpqOcsW+VXv1QoyUNQXJeVODG4+xM6UICEqyqe9XFr6LkL8BIiFCPev8zMDXPw==", + "version": "17.0.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-17.0.0.tgz", + "integrity": "sha512-B5yCyIlOJFPqUUeiD0cnBJwWJO8lkJs5d8+ze9QDP6SvfiXSz1BF+91+0MeI1d2yxgOhU/O+CvtiZ9jSkHhFAw==", "dev": true, "license": "MIT", "dependencies": { - "@octokit/types": "^15.0.0" + "@octokit/types": "^16.0.0" }, "engines": { "node": ">= 20" @@ -1482,15 +1434,15 @@ } }, "node_modules/@octokit/request": { - "version": "10.0.3", - "resolved": "https://registry.npmjs.org/@octokit/request/-/request-10.0.3.tgz", - "integrity": "sha512-V6jhKokg35vk098iBqp2FBKunk3kMTXlmq+PtbV9Gl3TfskWlebSofU9uunVKhUN7xl+0+i5vt0TGTG8/p/7HA==", + "version": "10.0.7", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-10.0.7.tgz", + "integrity": "sha512-v93h0i1yu4idj8qFPZwjehoJx4j3Ntn+JhXsdJrG9pYaX6j/XRz2RmasMUHtNgQD39nrv/VwTWSqK0RNXR8upA==", "dev": true, "license": "MIT", "dependencies": { - "@octokit/endpoint": "^11.0.0", - "@octokit/request-error": "^7.0.0", - "@octokit/types": "^14.0.0", + "@octokit/endpoint": "^11.0.2", + "@octokit/request-error": "^7.0.2", + "@octokit/types": "^16.0.0", "fast-content-type-parse": "^3.0.0", "universal-user-agent": "^7.0.2" }, @@ -1499,76 +1451,42 @@ } }, "node_modules/@octokit/request-error": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-7.0.0.tgz", - "integrity": "sha512-KRA7VTGdVyJlh0cP5Tf94hTiYVVqmt2f3I6mnimmaVz4UG3gQV/k4mDJlJv3X67iX6rmN7gSHCF8ssqeMnmhZg==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-7.1.0.tgz", + "integrity": "sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw==", "dev": true, "license": "MIT", "dependencies": { - "@octokit/types": "^14.0.0" + "@octokit/types": "^16.0.0" }, "engines": { "node": ">= 20" } }, - "node_modules/@octokit/request-error/node_modules/@octokit/openapi-types": { - "version": "25.1.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-25.1.0.tgz", - "integrity": "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@octokit/request-error/node_modules/@octokit/types": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-14.1.0.tgz", - "integrity": "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@octokit/openapi-types": "^25.1.0" - } - }, - "node_modules/@octokit/request/node_modules/@octokit/openapi-types": { - "version": "25.1.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-25.1.0.tgz", - "integrity": "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@octokit/request/node_modules/@octokit/types": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-14.1.0.tgz", - "integrity": "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@octokit/openapi-types": "^25.1.0" - } - }, "node_modules/@octokit/rest": { - "version": "22.0.0", - "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-22.0.0.tgz", - "integrity": "sha512-z6tmTu9BTnw51jYGulxrlernpsQYXpui1RK21vmXn8yF5bp6iX16yfTtJYGK5Mh1qDkvDOmp2n8sRMcQmR8jiA==", + "version": "22.0.1", + "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-22.0.1.tgz", + "integrity": "sha512-Jzbhzl3CEexhnivb1iQ0KJ7s5vvjMWcmRtq5aUsKmKDrRW6z3r84ngmiFKFvpZjpiU/9/S6ITPFRpn5s/3uQJw==", "dev": true, "license": "MIT", "dependencies": { - "@octokit/core": "^7.0.2", - "@octokit/plugin-paginate-rest": "^13.0.1", + "@octokit/core": "^7.0.6", + "@octokit/plugin-paginate-rest": "^14.0.0", "@octokit/plugin-request-log": "^6.0.0", - "@octokit/plugin-rest-endpoint-methods": "^16.0.0" + "@octokit/plugin-rest-endpoint-methods": "^17.0.0" }, "engines": { "node": ">= 20" } }, "node_modules/@octokit/types": { - "version": "15.0.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-15.0.0.tgz", - "integrity": "sha512-8o6yDfmoGJUIeR9OfYU0/TUJTnMPG2r68+1yEdUeG2Fdqpj8Qetg0ziKIgcBm0RW/j29H41WP37CYCEhp6GoHQ==", + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-16.0.0.tgz", + "integrity": "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg==", "dev": true, "license": "MIT", "dependencies": { - "@octokit/openapi-types": "^26.0.0" + "@octokit/openapi-types": "^27.0.0" } }, "node_modules/@paralleldrive/cuid2": { @@ -1969,6 +1887,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/better-sqlite3": { + "version": "7.6.13", + "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz", + "integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/chai": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.2.tgz", @@ -2010,6 +1938,16 @@ "@types/unist": "^2" } }, + "node_modules/@types/node": { + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.1.0.tgz", + "integrity": "sha512-t7frlewr6+cbx+9Ohpl0NOTKXZNV9xHRmNOvql47BFJKcEG1CxtxlPEEe+gR9uhVWM4DwhnvTF110mIL4yP9RA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, "node_modules/@types/parse-path": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/@types/parse-path/-/parse-path-7.0.3.tgz", @@ -2177,7 +2115,6 @@ "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", - "dev": true, "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -2190,12 +2127,41 @@ "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, "license": "MIT", "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/acorn-jsx-walk": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/acorn-jsx-walk/-/acorn-jsx-walk-2.0.0.tgz", + "integrity": "sha512-uuo6iJj4D4ygkdzd6jPtcxs8vZgDX9YFIkqczGImoypX2fQ4dVImmu3UzA4ynixCIMTrEOWW+95M2HuBaCEOVA==", + "license": "MIT" + }, + "node_modules/acorn-loose": { + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/acorn-loose/-/acorn-loose-8.5.2.tgz", + "integrity": "sha512-PPvV6g8UGMGgjrMu+n/f9E/tCSkNQ2Y97eFvuVdJfG11+xdIeDcLyNdC8SHcrHbRqkfwLASdplyR6B6sKM1U4A==", + "license": "MIT", + "dependencies": { + "acorn": "^8.15.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/agent-base": { "version": "7.1.4", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", @@ -2406,6 +2372,26 @@ "dev": true, "license": "MIT" }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/basic-ftp": { "version": "5.0.5", "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz", @@ -2423,6 +2409,17 @@ "dev": true, "license": "Apache-2.0" }, + "node_modules/better-sqlite3": { + "version": "11.10.0", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.10.0.tgz", + "integrity": "sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + } + }, "node_modules/bignumber.js": { "version": "9.3.1", "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", @@ -2432,6 +2429,26 @@ "node": "*" } }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, "node_modules/boolbase": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", @@ -2450,6 +2467,30 @@ "concat-map": "0.0.1" } }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, "node_modules/bundle-name": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", @@ -2467,19 +2508,19 @@ } }, "node_modules/c12": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/c12/-/c12-3.3.0.tgz", - "integrity": "sha512-K9ZkuyeJQeqLEyqldbYLG3wjqwpw4BVaAqvmxq3GYKK0b1A/yYQdIcJxkzAOWcNVWhJpRXAPfZFueekiY/L8Dw==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/c12/-/c12-3.3.3.tgz", + "integrity": "sha512-750hTRvgBy5kcMNPdh95Qo+XUBeGo8C7nsKSmedDmaQI+E0r82DwHeM6vBewDe4rGFbnxoa4V9pw+sPh5+Iz8Q==", "dev": true, "license": "MIT", "dependencies": { - "chokidar": "^4.0.3", + "chokidar": "^5.0.0", "confbox": "^0.2.2", "defu": "^6.1.4", - "dotenv": "^17.2.2", - "exsolve": "^1.0.7", + "dotenv": "^17.2.3", + "exsolve": "^1.0.8", "giget": "^2.0.0", - "jiti": "^2.5.1", + "jiti": "^2.6.1", "ohash": "^2.0.11", "pathe": "^2.0.3", "perfect-debounce": "^2.0.0", @@ -2487,7 +2528,7 @@ "rc9": "^2.1.2" }, "peerDependencies": { - "magicast": "^0.3.5" + "magicast": "*" }, "peerDependenciesMeta": { "magicast": { @@ -2643,9 +2684,9 @@ } }, "node_modules/chardet": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.0.tgz", - "integrity": "sha512-bNFETTG/pM5ryzQ9Ad0lJOTa6HWD/YsScAR3EnCPZRPlQh77JocYktSHOUHelyhm8IARL+o4c4F1bP5KVOjiRA==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.1.tgz", + "integrity": "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==", "dev": true, "license": "MIT" }, @@ -2704,25 +2745,31 @@ } }, "node_modules/chokidar": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", - "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", "dev": true, "license": "MIT", "dependencies": { - "readdirp": "^4.0.1" + "readdirp": "^5.0.0" }, "engines": { - "node": ">= 14.16.0" + "node": ">= 20.19.0" }, "funding": { "url": "https://paulmillr.com/funding/" } }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, "node_modules/ci-info": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.0.tgz", - "integrity": "sha512-l+2bNRMiQgcfILUi33labAZYIWlH1kWDp+ecNo5iisRKrbm0xcRyCww71/YU0Fkw0mAFpz9bJayXPjey6vkmaQ==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", + "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", "dev": true, "funding": [ { @@ -2962,6 +3009,21 @@ } } }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/deep-eql": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", @@ -3005,6 +3067,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -3123,6 +3194,61 @@ "node": ">= 14" } }, + "node_modules/dependency-cruiser": { + "version": "17.3.7", + "resolved": "https://registry.npmjs.org/dependency-cruiser/-/dependency-cruiser-17.3.7.tgz", + "integrity": "sha512-WEEOrnf0eshNirg4CMWuB7kK+qVZ+fecW6EBJa6AomEFhDDZKi3Zel1Tyl4ihcWtiSDhF+vALQb8NJS+wQiwLA==", + "license": "MIT", + "dependencies": { + "acorn": "8.15.0", + "acorn-jsx": "5.3.2", + "acorn-jsx-walk": "2.0.0", + "acorn-loose": "8.5.2", + "acorn-walk": "8.3.4", + "commander": "14.0.2", + "enhanced-resolve": "5.18.4", + "ignore": "7.0.5", + "interpret": "3.1.1", + "is-installed-globally": "1.0.0", + "json5": "2.2.3", + "picomatch": "4.0.3", + "prompts": "2.4.2", + "rechoir": "0.8.0", + "safe-regex": "2.1.1", + "semver": "7.7.3", + "tsconfig-paths-webpack-plugin": "4.2.0", + "watskeburt": "5.0.2" + }, + "bin": { + "depcruise": "bin/dependency-cruise.mjs", + "depcruise-baseline": "bin/depcruise-baseline.mjs", + "depcruise-fmt": "bin/depcruise-fmt.mjs", + "depcruise-wrap-stream-in-html": "bin/wrap-stream-in-html.mjs", + "dependency-cruise": "bin/dependency-cruise.mjs", + "dependency-cruiser": "bin/dependency-cruise.mjs" + }, + "engines": { + "node": "^20.12||^22||>=24" + } + }, + "node_modules/dependency-cruiser/node_modules/commander": { + "version": "14.0.2", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.2.tgz", + "integrity": "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/dependency-cruiser/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/destr": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", @@ -3130,6 +3256,15 @@ "dev": true, "license": "MIT" }, + "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==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "node_modules/doctoc": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/doctoc/-/doctoc-2.2.1.tgz", @@ -3297,9 +3432,9 @@ } }, "node_modules/dotenv": { - "version": "17.2.2", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.2.tgz", - "integrity": "sha512-Sf2LSQP+bOlhKWWyhFsn0UsfdK/kCWRv1iuA2gXAwt3dyNabr6QSj00I2V10pidqz69soatm9ZwZvpQMTIOd5Q==", + "version": "17.2.3", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz", + "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -3365,6 +3500,28 @@ "url": "https://github.com/fb55/encoding-sniffer?sponsor=1" } }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.18.4", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.4.tgz", + "integrity": "sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/entities": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", @@ -3549,9 +3706,9 @@ } }, "node_modules/esbuild": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.10.tgz", - "integrity": "sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", + "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -3562,32 +3719,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.10", - "@esbuild/android-arm": "0.25.10", - "@esbuild/android-arm64": "0.25.10", - "@esbuild/android-x64": "0.25.10", - "@esbuild/darwin-arm64": "0.25.10", - "@esbuild/darwin-x64": "0.25.10", - "@esbuild/freebsd-arm64": "0.25.10", - "@esbuild/freebsd-x64": "0.25.10", - "@esbuild/linux-arm": "0.25.10", - "@esbuild/linux-arm64": "0.25.10", - "@esbuild/linux-ia32": "0.25.10", - "@esbuild/linux-loong64": "0.25.10", - "@esbuild/linux-mips64el": "0.25.10", - "@esbuild/linux-ppc64": "0.25.10", - "@esbuild/linux-riscv64": "0.25.10", - "@esbuild/linux-s390x": "0.25.10", - "@esbuild/linux-x64": "0.25.10", - "@esbuild/netbsd-arm64": "0.25.10", - "@esbuild/netbsd-x64": "0.25.10", - "@esbuild/openbsd-arm64": "0.25.10", - "@esbuild/openbsd-x64": "0.25.10", - "@esbuild/openharmony-arm64": "0.25.10", - "@esbuild/sunos-x64": "0.25.10", - "@esbuild/win32-arm64": "0.25.10", - "@esbuild/win32-ia32": "0.25.10", - "@esbuild/win32-x64": "0.25.10" + "@esbuild/aix-ppc64": "0.27.2", + "@esbuild/android-arm": "0.27.2", + "@esbuild/android-arm64": "0.27.2", + "@esbuild/android-x64": "0.27.2", + "@esbuild/darwin-arm64": "0.27.2", + "@esbuild/darwin-x64": "0.27.2", + "@esbuild/freebsd-arm64": "0.27.2", + "@esbuild/freebsd-x64": "0.27.2", + "@esbuild/linux-arm": "0.27.2", + "@esbuild/linux-arm64": "0.27.2", + "@esbuild/linux-ia32": "0.27.2", + "@esbuild/linux-loong64": "0.27.2", + "@esbuild/linux-mips64el": "0.27.2", + "@esbuild/linux-ppc64": "0.27.2", + "@esbuild/linux-riscv64": "0.27.2", + "@esbuild/linux-s390x": "0.27.2", + "@esbuild/linux-x64": "0.27.2", + "@esbuild/netbsd-arm64": "0.27.2", + "@esbuild/netbsd-x64": "0.27.2", + "@esbuild/openbsd-arm64": "0.27.2", + "@esbuild/openbsd-x64": "0.27.2", + "@esbuild/openharmony-arm64": "0.27.2", + "@esbuild/sunos-x64": "0.27.2", + "@esbuild/win32-arm64": "0.27.2", + "@esbuild/win32-ia32": "0.27.2", + "@esbuild/win32-x64": "0.27.2" } }, "node_modules/escape-string-regexp": { @@ -3885,9 +4042,9 @@ } }, "node_modules/eta": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/eta/-/eta-4.0.1.tgz", - "integrity": "sha512-0h0oBEsF6qAJU7eu9ztvJoTo8D2PAq/4FvXVIQA1fek3WOTe6KPsVJycekG1+g1N6mfpblkheoGwaUhMtnlH4A==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/eta/-/eta-4.5.0.tgz", + "integrity": "sha512-qifAYjuW5AM1eEEIsFnOwB+TGqu6ynU3OKj9WbUTOtUBHFPZqL03XUW34kbp3zm19Ald+U8dEyRXaVsUck+Y1g==", "dev": true, "license": "MIT", "engines": { @@ -3937,6 +4094,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, "node_modules/expect-type": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.2.tgz", @@ -3948,9 +4114,9 @@ } }, "node_modules/exsolve": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.7.tgz", - "integrity": "sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz", + "integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==", "dev": true, "license": "MIT" }, @@ -4063,6 +4229,12 @@ "node": ">=16.0.0" } }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -4143,6 +4315,12 @@ "node": ">=0.4.x" } }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, "node_modules/fs-extra": { "version": "11.3.2", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.2.tgz", @@ -4183,7 +4361,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -4367,6 +4544,12 @@ "git-up": "^8.1.0" } }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -4402,6 +4585,30 @@ "node": ">=10.13.0" } }, + "node_modules/global-directory": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/global-directory/-/global-directory-4.0.1.tgz", + "integrity": "sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==", + "license": "MIT", + "dependencies": { + "ini": "4.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/global-directory/node_modules/ini": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.1.tgz", + "integrity": "sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==", + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/globals": { "version": "14.0.0", "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", @@ -4590,7 +4797,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -4706,6 +4912,26 @@ "node": ">=0.10.0" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -4759,22 +4985,27 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", "license": "ISC" }, "node_modules/inquirer": { - "version": "12.9.6", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-12.9.6.tgz", - "integrity": "sha512-603xXOgyfxhuis4nfnWaZrMaotNT0Km9XwwBNWUKbIDqeCY89jGr2F9YPEMiNhU6XjIP4VoWISMBFfcc5NgrTw==", + "version": "12.11.1", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-12.11.1.tgz", + "integrity": "sha512-9VF7mrY+3OmsAfjH3yKz/pLbJ5z22E23hENKw3/LNSaA/sAt3v49bDRY+Ygct1xwuKT+U+cBfTzjCPySna69Qw==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/ansi": "^1.0.0", - "@inquirer/core": "^10.2.2", - "@inquirer/prompts": "^7.8.6", - "@inquirer/type": "^3.0.8", + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/prompts": "^7.10.1", + "@inquirer/type": "^3.0.10", "mute-stream": "^2.0.0", - "run-async": "^4.0.5", + "run-async": "^4.0.6", "rxjs": "^7.8.2" }, "engines": { @@ -4804,10 +5035,19 @@ "node": ">= 0.4" } }, - "node_modules/ip-address": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz", - "integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==", + "node_modules/interpret": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", + "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/ip-address": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz", + "integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==", "dev": true, "license": "MIT", "engines": { @@ -4969,7 +5209,6 @@ "version": "2.16.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", - "dev": true, "license": "MIT", "dependencies": { "hasown": "^2.0.2" @@ -5150,6 +5389,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-installed-globally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-1.0.0.tgz", + "integrity": "sha512-K55T22lfpQ63N4KEN57jZUAaAYqYHEe8veb/TycJRk9DdSCLLcovXz/mL6mOnhQaZsQGwPhuFopdQIlqGSEjiQ==", + "license": "MIT", + "dependencies": { + "global-directory": "^4.0.1", + "is-path-inside": "^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-interactive": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", @@ -5206,6 +5461,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-path-inside": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-4.0.0.tgz", + "integrity": "sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-plain-obj": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", @@ -5515,9 +5782,9 @@ } }, "node_modules/jiti": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.0.tgz", - "integrity": "sha512-VXe6RjJkBPj0ohtqaO8vSWP3ZhAKo66fKrFNCll4BTcwljPLz03pCbaNKfzGP5MbrCYcbJ7v0nOYYwUzTEIdXQ==", + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", "dev": true, "license": "MIT", "bin": { @@ -5538,9 +5805,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, "license": "MIT", "dependencies": { @@ -5564,6 +5831,18 @@ "dev": true, "license": "MIT" }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/jsonfile": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", @@ -5595,6 +5874,15 @@ "node": ">=0.10.0" } }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -5626,9 +5914,9 @@ } }, "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", "dev": true, "license": "MIT" }, @@ -6114,16 +6402,20 @@ } }, "node_modules/mime-types": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", - "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", "dev": true, "license": "MIT", "dependencies": { "mime-db": "^1.54.0" }, "engines": { - "node": ">= 0.6" + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/mimic-fn": { @@ -6152,6 +6444,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -6169,7 +6473,6 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -6185,6 +6488,12 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, "node_modules/mock-property": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/mock-property/-/mock-property-1.1.0.tgz", @@ -6243,6 +6552,12 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -6276,6 +6591,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/node-abi": { + "version": "3.87.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.87.0.tgz", + "integrity": "sha512-+CGM1L1CgmtheLcBuleyYOn7NWPVu0s0EJH2C4puxgEZb9h8QpR9G2dBfZJOAUhi7VQxuBPMd0hiISWcTyiYyQ==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/node-fetch-native": { "version": "1.6.7", "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz", @@ -6326,32 +6653,40 @@ } }, "node_modules/nypm": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.2.tgz", - "integrity": "sha512-7eM+hpOtrKrBDCh7Ypu2lJ9Z7PNZBdi/8AT3AX8xoCj43BBVHD0hPSTEvMtkMpfs8FCqBGhxB+uToIQimA111g==", + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.4.tgz", + "integrity": "sha512-1TvCKjZyyklN+JJj2TS3P4uSQEInrM/HkkuSXsEzm1ApPgBffOn8gFguNnZf07r/1X6vlryfIqMUkJKQMzlZiw==", "dev": true, "license": "MIT", "dependencies": { - "citty": "^0.1.6", - "consola": "^3.4.2", + "citty": "^0.2.0", "pathe": "^2.0.3", - "pkg-types": "^2.3.0", - "tinyexec": "^1.0.1" + "tinyexec": "^1.0.2" }, "bin": { "nypm": "dist/cli.mjs" }, "engines": { - "node": "^14.16.0 || >=16.10.0" + "node": ">=18" } }, - "node_modules/nypm/node_modules/tinyexec": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.1.tgz", - "integrity": "sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==", + "node_modules/nypm/node_modules/citty": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/citty/-/citty-0.2.0.tgz", + "integrity": "sha512-8csy5IBFI2ex2hTVpaHN2j+LNE199AgiI7y4dMintrr8i0lQiFn+0AWMZrWdHKIgMOer65f8IThysYhoReqjWA==", "dev": true, "license": "MIT" }, + "node_modules/nypm/node_modules/tinyexec": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", + "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/object-inspect": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", @@ -6424,7 +6759,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, "license": "ISC", "dependencies": { "wrappy": "1" @@ -6788,7 +7122,6 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true, "license": "MIT" }, "node_modules/path-scurry": { @@ -6826,9 +7159,9 @@ } }, "node_modules/perfect-debounce": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-2.0.0.tgz", - "integrity": "sha512-fkEH/OBiKrqqI/yIgjR92lMfs2K8105zt/VT6+7eTjNwisrsh47CeIED9z58zI7DfKdH3uHAn25ziRZn3kgAow==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-2.1.0.tgz", + "integrity": "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==", "dev": true, "license": "MIT" }, @@ -6843,7 +7176,6 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -6903,6 +7235,32 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -6942,6 +7300,19 @@ "node": ">=6.0.0" } }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/protocols": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/protocols/-/protocols-2.0.2.tgz", @@ -6986,6 +7357,16 @@ "dev": true, "license": "MIT" }, + "node_modules/pump": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", + "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -6996,6 +7377,30 @@ "node": ">=6" } }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/rc/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/rc9": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz", @@ -7031,20 +7436,66 @@ "react": "^19.1.1" } }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/readdirp": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", - "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", + "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", "dev": true, "license": "MIT", "engines": { - "node": ">= 14.18.0" + "node": ">= 20.19.0" }, "funding": { "type": "individual", "url": "https://paulmillr.com/funding/" } }, + "node_modules/rechoir": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", + "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", + "license": "MIT", + "dependencies": { + "resolve": "^1.20.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/rechoir/node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -7068,6 +7519,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/regexp-tree": { + "version": "0.1.27", + "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.27.tgz", + "integrity": "sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==", + "license": "MIT", + "bin": { + "regexp-tree": "bin/regexp-tree" + } + }, "node_modules/regexp.prototype.flags": { "version": "1.5.4", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", @@ -7090,9 +7550,9 @@ } }, "node_modules/release-it": { - "version": "19.0.5", - "resolved": "https://registry.npmjs.org/release-it/-/release-it-19.0.5.tgz", - "integrity": "sha512-bYlUKC0TQroBKi8jQUeoxLHql4d9Fx/2EQLHPKUobXTNSiTS2WY8vlgdHZRhRjVEMyAWwyadJVKfFZnRJuRn4Q==", + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/release-it/-/release-it-19.2.4.tgz", + "integrity": "sha512-BwaJwQYUIIAKuDYvpqQTSoy0U7zIy6cHyEjih/aNaFICphGahia4cjDANuFXb7gVZ51hIK9W0io6fjNQWXqICg==", "dev": true, "funding": [ { @@ -7107,25 +7567,25 @@ "license": "MIT", "dependencies": { "@nodeutils/defaults-deep": "1.1.0", - "@octokit/rest": "22.0.0", + "@octokit/rest": "22.0.1", "@phun-ky/typeof": "2.0.3", "async-retry": "1.3.3", - "c12": "3.3.0", - "ci-info": "^4.3.0", - "eta": "4.0.1", + "c12": "3.3.3", + "ci-info": "^4.3.1", + "eta": "4.5.0", "git-url-parse": "16.1.0", - "inquirer": "12.9.6", + "inquirer": "12.11.1", "issue-parser": "7.0.1", "lodash.merge": "4.6.2", - "mime-types": "3.0.1", + "mime-types": "3.0.2", "new-github-release-url": "2.0.0", "open": "10.2.0", "ora": "9.0.0", "os-name": "6.1.0", "proxy-agent": "6.5.0", - "semver": "7.7.2", + "semver": "7.7.3", "tinyglobby": "0.2.15", - "undici": "6.21.3", + "undici": "6.23.0", "url-join": "5.0.0", "wildcard-match": "5.1.4", "yargs-parser": "21.1.1" @@ -7138,9 +7598,9 @@ } }, "node_modules/release-it/node_modules/undici": { - "version": "6.21.3", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.21.3.tgz", - "integrity": "sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==", + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.23.0.tgz", + "integrity": "sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g==", "dev": true, "license": "MIT", "engines": { @@ -7383,6 +7843,26 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/safe-push-apply": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", @@ -7400,6 +7880,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safe-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-2.1.1.tgz", + "integrity": "sha512-rx+x8AMzKb5Q5lQ95Zoi6ZbJqwCLkqi3XuJXp5P3rT8OEc6sZCJG5AE5dU3lsgRr/F4Bs31jSlVN+j5KrsGu9A==", + "license": "MIT", + "dependencies": { + "regexp-tree": "~0.1.1" + } + }, "node_modules/safe-regex-test": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", @@ -7446,10 +7935,9 @@ } }, "node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", - "dev": true, + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -7626,6 +8114,57 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "license": "MIT" + }, "node_modules/smart-buffer": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", @@ -7735,6 +8274,15 @@ "node": ">= 0.4" } }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", @@ -7898,6 +8446,15 @@ "node": ">=8" } }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/strip-bom-string": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz", @@ -7962,7 +8519,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -7987,6 +8543,19 @@ "url": "https://opencollective.com/synckit" } }, + "node_modules/tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, "node_modules/tape": { "version": "5.9.0", "resolved": "https://registry.npmjs.org/tape/-/tape-5.9.0.tgz", @@ -8024,6 +8593,34 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/test-exclude": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.1.tgz", @@ -8050,9 +8647,9 @@ } }, "node_modules/test-exclude/node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", "dev": true, "license": "ISC", "dependencies": { @@ -8176,6 +8773,35 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/tsconfig-paths": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", + "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", + "license": "MIT", + "dependencies": { + "json5": "^2.2.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tsconfig-paths-webpack-plugin": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths-webpack-plugin/-/tsconfig-paths-webpack-plugin-4.2.0.tgz", + "integrity": "sha512-zbem3rfRS8BgeNK50Zz5SIQgXzLafiHjOwUAvk/38/o1jHn/V5QAgVUcz884or7WYcPaH3N2CIfUc2u0ul7UcA==", + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "enhanced-resolve": "^5.7.0", + "tapable": "^2.2.1", + "tsconfig-paths": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -8183,6 +8809,18 @@ "dev": true, "license": "0BSD" }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -8351,15 +8989,22 @@ "license": "MIT" }, "node_modules/undici": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.16.0.tgz", - "integrity": "sha512-QEg3HPMll0o3t2ourKwOeUAZ159Kn9mx5pnzHRQO8+Wixmh88YdZRiIwat0iNzNNXn0yoEtXJqFpyW7eM8BV7g==", + "version": "7.19.2", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.19.2.tgz", + "integrity": "sha512-4VQSpGEGsWzk0VYxyB/wVX/Q7qf9t5znLRgs0dzszr9w9Fej/8RVNQ+S20vdXSAyra/bJ7ZQfGv6ZMj7UEbzSg==", "dev": true, "license": "MIT", "engines": { "node": ">=20.18.1" } }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, "node_modules/unified": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/unified/-/unified-9.2.2.tgz", @@ -8462,6 +9107,12 @@ "node": "^12.20.0 || ^14.13.1 || >=16.0.0" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, "node_modules/vfile": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/vfile/-/vfile-4.2.1.tgz", @@ -8495,13 +9146,13 @@ } }, "node_modules/vite": { - "version": "7.1.7", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.7.tgz", - "integrity": "sha512-VbA8ScMvAISJNJVbRDTJdCwqQoAareR/wutevKanhR2/1EkoXVZVkkORaYm/tNVCjP/UDTKtcw3bAkwOUdedmA==", + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "^0.25.0", + "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", @@ -8665,6 +9316,18 @@ } } }, + "node_modules/watskeburt": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/watskeburt/-/watskeburt-5.0.2.tgz", + "integrity": "sha512-8xIz2RALjwTA7kYeRtkiQ2uaFyr327T1GXJnVcGOoPuzQX2axpUXqeJPcgOEVemCWB2YveZjhWCcW/eZ3uTkZA==", + "license": "MIT", + "bin": { + "watskeburt": "dist/run-cli.js" + }, + "engines": { + "node": "^20.12||^22.13||>=24.0" + } + }, "node_modules/whatwg-encoding": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", @@ -8942,7 +9605,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true, "license": "ISC" }, "node_modules/wsl-utils": { diff --git a/package.json b/package.json index 9c7447b..cd5f166 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,11 @@ "typecheck": "tsc --noEmit && echo 'Type check complete.'", "check-status": "[ -n \"$(git status --porcelain)\" ] && { echo 'โŒ Uncommitted changes'; exit 1; } || echo 'โœ… Git status is clean.'", "prepare": "husky", - "toc": "doctoc README.md" + "toc": "doctoc README.md", + "aidd:index": "node ai/tools/cli/index-cli.js --full --deps --stats", + "aidd:index:incremental": "node ai/tools/cli/index-cli.js --deps --stats", + "aidd:query": "node ai/tools/cli/query-cli.js", + "aidd:find-related": "node ai/tools/cli/find-related-cli.js" }, "exports": { "./utils": { @@ -28,6 +32,10 @@ "./server": { "types": "./src/server/index.d.ts", "default": "./src/server/index.js" + }, + "./tools": { + "types": "./ai/tools/index.d.ts", + "default": "./ai/tools/index.js" } }, "files": [ @@ -67,8 +75,10 @@ "dependencies": { "@paralleldrive/cuid2": "^3.1.0", "@sinclair/typebox": "^0.34.41", + "better-sqlite3": "^11.8.1", "chalk": "^4.1.2", "commander": "^11.1.0", + "dependency-cruiser": "17.3.7", "error-causes": "^3.0.2", "fs-extra": "^11.1.1", "gray-matter": "^4.0.3", @@ -84,6 +94,7 @@ }, "devDependencies": { "@eslint/eslintrc": "^3", + "@types/better-sqlite3": "^7.6.12", "@vitest/coverage-v8": "^3.2.4", "doctoc": "^2.2.1", "eslint": "^9",