diff --git a/package.json b/package.json index 20bf6c9..5ce1cf1 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,7 @@ "name": "@lytics/dev-agent", "version": "0.1.0", "private": true, + "license": "MIT", "workspaces": [ "packages/*" ], diff --git a/packages/cli/README.md b/packages/cli/README.md new file mode 100644 index 0000000..8e93b5f --- /dev/null +++ b/packages/cli/README.md @@ -0,0 +1,133 @@ +# @lytics/dev-agent-cli + +Command-line interface for dev-agent - Multi-agent code intelligence platform. + +## Installation + +```bash +npm install -g @lytics/dev-agent-cli +``` + +## Usage + +### Initialize + +Initialize dev-agent in your repository: + +```bash +dev init +``` + +This creates a `.dev-agent.json` configuration file. + +### Index Repository + +Index your repository for semantic search: + +```bash +dev index . +``` + +Options: +- `-f, --force` - Force re-index even if unchanged +- `-v, --verbose` - Show verbose output + +### Search + +Search your indexed code semantically: + +```bash +dev search "authentication logic" +``` + +Options: +- `-l, --limit ` - Maximum results (default: 10) +- `-t, --threshold ` - Minimum similarity score 0-1 (default: 0.7) +- `--json` - Output as JSON + +### Update + +Incrementally update the index with changed files: + +```bash +dev update +``` + +Options: +- `-v, --verbose` - Show verbose output + +### Stats + +Show indexing statistics: + +```bash +dev stats +``` + +Options: +- `--json` - Output as JSON + +### Clean + +Remove all indexed data: + +```bash +dev clean --force +``` + +Options: +- `-f, --force` - Skip confirmation prompt + +## Configuration + +The `.dev-agent.json` file configures the indexer: + +```json +{ + "repositoryPath": "/path/to/repo", + "vectorStorePath": ".dev-agent/vectors.lance", + "embeddingModel": "Xenova/all-MiniLM-L6-v2", + "dimension": 384, + "excludePatterns": [ + "**/node_modules/**", + "**/dist/**", + "**/.git/**" + ], + "languages": ["typescript", "javascript", "markdown"] +} +``` + +## Features + +- 🎨 **Beautiful UX** - Colored output, spinners, progress indicators +- ⚡ **Fast** - Incremental updates, efficient indexing +- 🧠 **Semantic Search** - Find code by meaning, not exact matches +- 🔧 **Configurable** - Customize patterns, languages, and more +- 📊 **Statistics** - Track indexing progress and stats + +## Examples + +```bash +# Initialize and index +dev init +dev index . + +# Search for code +dev search "user authentication flow" +dev search "database connection pool" --limit 5 + +# Keep index up to date +dev update + +# View statistics +dev stats + +# Clean and re-index +dev clean --force +dev index . --force +``` + +## License + +MIT + diff --git a/packages/cli/package.json b/packages/cli/package.json index babe612..dbfeb62 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -4,6 +4,9 @@ "private": true, "main": "./dist/index.js", "types": "./dist/index.d.ts", + "bin": { + "dev": "./dist/cli.js" + }, "exports": { ".": { "types": "./dist/index.d.ts", @@ -22,10 +25,13 @@ "test:watch": "vitest" }, "dependencies": { - "@lytics/dev-agent-core": "workspace:*" + "@lytics/dev-agent-core": "workspace:*", + "chalk": "^5.3.0", + "ora": "^8.0.1" }, "devDependencies": { - "typescript": "^5.3.3", - "commander": "^11.1.0" + "@types/node": "^22.0.0", + "commander": "^12.1.0", + "typescript": "^5.3.3" } } \ No newline at end of file diff --git a/packages/cli/src/cli.test.ts b/packages/cli/src/cli.test.ts new file mode 100644 index 0000000..4c4b398 --- /dev/null +++ b/packages/cli/src/cli.test.ts @@ -0,0 +1,75 @@ +import { describe, expect, it } from 'vitest'; +import { cleanCommand } from './commands/clean'; +import { indexCommand } from './commands/index'; +import { initCommand } from './commands/init'; +import { searchCommand } from './commands/search'; +import { statsCommand } from './commands/stats'; +import { updateCommand } from './commands/update'; + +describe('CLI Structure', () => { + it('should have init command', () => { + expect(initCommand.name()).toBe('init'); + expect(initCommand.description()).toContain('Initialize'); + }); + + it('should have index command', () => { + expect(indexCommand.name()).toBe('index'); + expect(indexCommand.description()).toContain('Index'); + }); + + it('should have search command', () => { + expect(searchCommand.name()).toBe('search'); + expect(searchCommand.description()).toContain('Search'); + }); + + it('should have update command', () => { + expect(updateCommand.name()).toBe('update'); + expect(updateCommand.description()).toContain('Update'); + }); + + it('should have stats command', () => { + expect(statsCommand.name()).toBe('stats'); + expect(statsCommand.description()).toContain('statistics'); + }); + + it('should have clean command', () => { + expect(cleanCommand.name()).toBe('clean'); + expect(cleanCommand.description()).toContain('Clean'); + }); + + describe('Command Options', () => { + it('index command should have force and verbose options', () => { + const options = indexCommand.options; + const forceOption = options.find((opt) => opt.long === '--force'); + const verboseOption = options.find((opt) => opt.long === '--verbose'); + + expect(forceOption).toBeDefined(); + expect(verboseOption).toBeDefined(); + }); + + it('search command should have limit, threshold, and json options', () => { + const options = searchCommand.options; + const limitOption = options.find((opt) => opt.long === '--limit'); + const thresholdOption = options.find((opt) => opt.long === '--threshold'); + const jsonOption = options.find((opt) => opt.long === '--json'); + + expect(limitOption).toBeDefined(); + expect(thresholdOption).toBeDefined(); + expect(jsonOption).toBeDefined(); + }); + + it('stats command should have json option', () => { + const options = statsCommand.options; + const jsonOption = options.find((opt) => opt.long === '--json'); + + expect(jsonOption).toBeDefined(); + }); + + it('clean command should have force option', () => { + const options = cleanCommand.options; + const forceOption = options.find((opt) => opt.long === '--force'); + + expect(forceOption).toBeDefined(); + }); + }); +}); diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts new file mode 100644 index 0000000..69a7257 --- /dev/null +++ b/packages/cli/src/cli.ts @@ -0,0 +1,32 @@ +#!/usr/bin/env node + +import chalk from 'chalk'; +import { Command } from 'commander'; +import { cleanCommand } from './commands/clean.js'; +import { indexCommand } from './commands/index.js'; +import { initCommand } from './commands/init.js'; +import { searchCommand } from './commands/search.js'; +import { statsCommand } from './commands/stats.js'; +import { updateCommand } from './commands/update.js'; + +const program = new Command(); + +program + .name('dev') + .description(chalk.cyan('🤖 Dev-Agent - Multi-agent code intelligence platform')) + .version('0.1.0'); + +// Register commands +program.addCommand(initCommand); +program.addCommand(indexCommand); +program.addCommand(searchCommand); +program.addCommand(updateCommand); +program.addCommand(statsCommand); +program.addCommand(cleanCommand); + +// Show help if no command provided +if (process.argv.length === 2) { + program.outputHelp(); +} + +program.parse(process.argv); diff --git a/packages/cli/src/commands/clean.ts b/packages/cli/src/commands/clean.ts new file mode 100644 index 0000000..f4eae93 --- /dev/null +++ b/packages/cli/src/commands/clean.ts @@ -0,0 +1,80 @@ +import * as fs from 'node:fs/promises'; +import * as path from 'node:path'; +import chalk from 'chalk'; +import { Command } from 'commander'; +import ora from 'ora'; +import { loadConfig } from '../utils/config.js'; +import { logger } from '../utils/logger.js'; + +export const cleanCommand = new Command('clean') + .description('Clean indexed data and cache') + .option('-f, --force', 'Skip confirmation prompt', false) + .action(async (options) => { + try { + // Load config + const config = await loadConfig(); + if (!config) { + logger.warn('No config found'); + logger.log('Nothing to clean.'); + return; + } + + const dataDir = path.dirname(config.vectorStorePath); + const stateFile = path.join(config.repositoryPath, '.dev-agent', 'indexer-state.json'); + + // Show what will be deleted + logger.log(''); + logger.log(chalk.bold('The following will be deleted:')); + logger.log(` ${chalk.cyan('Vector store:')} ${config.vectorStorePath}`); + logger.log(` ${chalk.cyan('State file:')} ${stateFile}`); + logger.log(` ${chalk.cyan('Data directory:')} ${dataDir}`); + logger.log(''); + + // Confirm unless --force + if (!options.force) { + logger.warn('This action cannot be undone!'); + logger.log(`Run with ${chalk.yellow('--force')} to skip this prompt.`); + logger.log(''); + process.exit(0); + } + + const spinner = ora('Cleaning indexed data...').start(); + + // Delete vector store + try { + await fs.rm(config.vectorStorePath, { recursive: true, force: true }); + spinner.text = 'Deleted vector store'; + } catch (error) { + logger.debug(`Vector store not found or already deleted: ${error}`); + } + + // Delete state file + try { + await fs.rm(stateFile, { force: true }); + spinner.text = 'Deleted state file'; + } catch (error) { + logger.debug(`State file not found or already deleted: ${error}`); + } + + // Delete data directory if empty + try { + const files = await fs.readdir(dataDir); + if (files.length === 0) { + await fs.rmdir(dataDir); + spinner.text = 'Deleted data directory'; + } + } catch (error) { + logger.debug(`Data directory not found or not empty: ${error}`); + } + + spinner.succeed(chalk.green('Cleaned successfully!')); + + logger.log(''); + logger.log('All indexed data has been removed.'); + logger.log(`Run ${chalk.yellow('dev index')} to re-index your repository.`); + logger.log(''); + } catch (error) { + logger.error(`Failed to clean: ${error instanceof Error ? error.message : String(error)}`); + process.exit(1); + } + }); diff --git a/packages/cli/src/commands/commands.test.ts b/packages/cli/src/commands/commands.test.ts new file mode 100644 index 0000000..3d84cea --- /dev/null +++ b/packages/cli/src/commands/commands.test.ts @@ -0,0 +1,72 @@ +import * as fs from 'node:fs/promises'; +import * as os from 'node:os'; +import * as path from 'node:path'; +import { Command } from 'commander'; +import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'; +import { cleanCommand } from './clean'; +import { initCommand } from './init'; + +describe('CLI Commands', () => { + let testDir: string; + + beforeAll(async () => { + testDir = path.join(os.tmpdir(), `cli-commands-test-${Date.now()}`); + await fs.mkdir(testDir, { recursive: true }); + }); + + afterAll(async () => { + await fs.rm(testDir, { recursive: true, force: true }); + }); + + describe('init command', () => { + it('should create config file', async () => { + const initDir = path.join(testDir, 'init-test'); + await fs.mkdir(initDir, { recursive: true }); + + // Mock process.exit to prevent test termination + const exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => undefined as never); + + // Create a program and add the command + const program = new Command(); + program.addCommand(initCommand); + + // Parse arguments + await program.parseAsync(['node', 'cli', 'init', '--path', initDir]); + + exitSpy.mockRestore(); + + // Check config file was created + const configPath = path.join(initDir, '.dev-agent.json'); + const exists = await fs + .access(configPath) + .then(() => true) + .catch(() => false); + expect(exists).toBe(true); + + // Verify config content + const content = await fs.readFile(configPath, 'utf-8'); + const config = JSON.parse(content); + expect(config.repositoryPath).toBe(path.resolve(initDir)); + expect(config.embeddingModel).toBe('Xenova/all-MiniLM-L6-v2'); + }); + + it('should have correct command name and description', () => { + expect(initCommand.name()).toBe('init'); + expect(initCommand.description()).toBe('Initialize dev-agent in the current directory'); + }); + }); + + describe('clean command', () => { + it('should have correct command name and description', () => { + expect(cleanCommand.name()).toBe('clean'); + expect(cleanCommand.description()).toBe('Clean indexed data and cache'); + }); + + it('should have force option', () => { + const options = cleanCommand.options; + const forceOption = options.find((opt) => opt.long === '--force'); + expect(forceOption).toBeDefined(); + expect(forceOption?.short).toBe('-f'); + }); + }); +}); diff --git a/packages/cli/src/commands/index.ts b/packages/cli/src/commands/index.ts index 9180002..eb687db 100644 --- a/packages/cli/src/commands/index.ts +++ b/packages/cli/src/commands/index.ts @@ -1,40 +1,85 @@ -// CLI commands -export interface CommandDefinition { - name: string; - description: string; - options: Array<{ - flag: string; - description: string; - }>; - action: (options: Record) => Promise; -} - -// Will be implemented using Commander.js -export const commands: CommandDefinition[] = [ - { - name: 'scan', - description: 'Scan repository and build context', - options: [ - { - flag: '--path ', - description: 'Path to repository', - }, - ], - action: async (options) => { - console.log('Scanning repository at:', options.path); - }, - }, - { - name: 'serve', - description: 'Start the context API server', - options: [ - { - flag: '--port ', - description: 'Port for API server', - }, - ], - action: async (options) => { - console.log('Starting API server on port:', options.port || 3000); - }, - }, -]; +import { RepositoryIndexer } from '@lytics/dev-agent-core'; +import chalk from 'chalk'; +import { Command } from 'commander'; +import ora from 'ora'; +import { getDefaultConfig, loadConfig } from '../utils/config.js'; +import { logger } from '../utils/logger.js'; + +export const indexCommand = new Command('index') + .description('Index a repository for semantic search') + .argument('[path]', 'Repository path to index', process.cwd()) + .option('-f, --force', 'Force re-index even if unchanged', false) + .option('-v, --verbose', 'Verbose output', false) + .action(async (repositoryPath: string, options) => { + const spinner = ora('Loading configuration...').start(); + + try { + // Load config or use defaults + let config = await loadConfig(); + if (!config) { + spinner.info('No config found, using defaults'); + config = getDefaultConfig(repositoryPath); + } + + // Override with command line args + config.repositoryPath = repositoryPath; + + spinner.text = 'Initializing indexer...'; + const indexer = new RepositoryIndexer(config); + await indexer.initialize(); + + spinner.text = 'Scanning repository...'; + + const startTime = Date.now(); + let lastUpdate = startTime; + + const stats = await indexer.index({ + force: options.force, + onProgress: (progress) => { + const now = Date.now(); + // Update spinner every 100ms to avoid flickering + if (now - lastUpdate > 100) { + const percent = progress.percentComplete || 0; + const currentFile = progress.currentFile ? ` ${progress.currentFile}` : ''; + spinner.text = `${progress.phase}:${currentFile} (${percent.toFixed(0)}%)`; + lastUpdate = now; + } + }, + }); + + await indexer.close(); + + const duration = ((Date.now() - startTime) / 1000).toFixed(2); + + spinner.succeed(chalk.green('Repository indexed successfully!')); + + // Show stats + logger.log(''); + logger.log(chalk.bold('Indexing Statistics:')); + logger.log(` ${chalk.cyan('Files scanned:')} ${stats.filesScanned}`); + logger.log(` ${chalk.cyan('Documents extracted:')} ${stats.documentsExtracted}`); + logger.log(` ${chalk.cyan('Documents indexed:')} ${stats.documentsIndexed}`); + logger.log(` ${chalk.cyan('Vectors stored:')} ${stats.vectorsStored}`); + logger.log(` ${chalk.cyan('Duration:')} ${duration}s`); + + if (stats.errors.length > 0) { + logger.log(''); + logger.warn(`${stats.errors.length} error(s) occurred during indexing`); + if (options.verbose) { + for (const error of stats.errors) { + logger.error(` ${error.file}: ${error.message}`); + } + } + } + + logger.log(''); + logger.log(`Now you can search with: ${chalk.yellow('dev search ""')}`); + } catch (error) { + spinner.fail('Failed to index repository'); + logger.error(error instanceof Error ? error.message : String(error)); + if (options.verbose && error instanceof Error && error.stack) { + logger.debug(error.stack); + } + process.exit(1); + } + }); diff --git a/packages/cli/src/commands/init.ts b/packages/cli/src/commands/init.ts new file mode 100644 index 0000000..53ae6f3 --- /dev/null +++ b/packages/cli/src/commands/init.ts @@ -0,0 +1,34 @@ +import chalk from 'chalk'; +import { Command } from 'commander'; +import ora from 'ora'; +import { getDefaultConfig, saveConfig } from '../utils/config.js'; +import { logger } from '../utils/logger.js'; + +export const initCommand = new Command('init') + .description('Initialize dev-agent in the current directory') + .option('-p, --path ', 'Repository path', process.cwd()) + .action(async (options) => { + const spinner = ora('Initializing dev-agent...').start(); + + try { + const config = getDefaultConfig(options.path); + + spinner.text = 'Creating configuration file...'; + await saveConfig(config, options.path); + + spinner.succeed(chalk.green('Dev-agent initialized successfully!')); + + logger.log(''); + logger.log(chalk.bold('Next steps:')); + logger.log(` ${chalk.cyan('1.')} Run ${chalk.yellow('dev index')} to index your repository`); + logger.log( + ` ${chalk.cyan('2.')} Run ${chalk.yellow('dev search ""')} to search your code` + ); + logger.log(''); + logger.log(`Configuration saved to ${chalk.cyan('.dev-agent.json')}`); + } catch (error) { + spinner.fail('Failed to initialize dev-agent'); + logger.error(error instanceof Error ? error.message : String(error)); + process.exit(1); + } + }); diff --git a/packages/cli/src/commands/search.ts b/packages/cli/src/commands/search.ts new file mode 100644 index 0000000..7f4d698 --- /dev/null +++ b/packages/cli/src/commands/search.ts @@ -0,0 +1,97 @@ +import * as path from 'node:path'; +import { RepositoryIndexer } from '@lytics/dev-agent-core'; +import chalk from 'chalk'; +import { Command } from 'commander'; +import ora from 'ora'; +import { loadConfig } from '../utils/config.js'; +import { logger } from '../utils/logger.js'; + +export const searchCommand = new Command('search') + .description('Search indexed code semantically') + .argument('', 'Search query') + .option('-l, --limit ', 'Maximum number of results', '10') + .option('-t, --threshold ', 'Minimum similarity score (0-1)', '0.7') + .option('--json', 'Output results as JSON', false) + .action(async (query: string, options) => { + const spinner = ora('Searching...').start(); + + try { + // Load config + const config = await loadConfig(); + if (!config) { + spinner.fail('No config found'); + logger.error('Run "dev init" first to initialize dev-agent'); + process.exit(1); + return; // TypeScript needs this + } + + spinner.text = 'Initializing indexer...'; + const indexer = new RepositoryIndexer(config); + await indexer.initialize(); + + spinner.text = `Searching for: ${chalk.cyan(query)}`; + + const results = await indexer.search(query, { + limit: Number.parseInt(options.limit, 10), + scoreThreshold: Number.parseFloat(options.threshold), + }); + + await indexer.close(); + + spinner.succeed(chalk.green(`Found ${results.length} result(s)`)); + + if (results.length === 0) { + logger.log(''); + logger.warn('No results found. Try:'); + logger.log(` - Lowering the threshold: ${chalk.yellow('--threshold 0.5')}`); + logger.log(` - Using different keywords`); + logger.log(` - Running ${chalk.yellow('dev update')} to refresh the index`); + return; + } + + // Output as JSON if requested + if (options.json) { + console.log(JSON.stringify(results, null, 2)); + return; + } + + // Pretty print results + logger.log(''); + for (let i = 0; i < results.length; i++) { + const result = results[i]; + const metadata = result.metadata; + const score = (result.score * 100).toFixed(1); + + // Extract file info + const file = metadata.file as string; + const relativePath = path.relative(config.repositoryPath, file); + const startLine = metadata.startLine as number; + const endLine = metadata.endLine as number; + const name = metadata.name as string; + const type = metadata.type as string; + + logger.log( + chalk.bold(`${i + 1}. ${chalk.cyan(name || type)} ${chalk.gray(`(${score}% match)`)}`) + ); + logger.log(` ${chalk.gray('File:')} ${relativePath}:${startLine}-${endLine}`); + + // Show signature if available + if (metadata.signature) { + logger.log(` ${chalk.gray('Signature:')} ${chalk.yellow(metadata.signature)}`); + } + + // Show docstring if available + if (metadata.docstring) { + const doc = String(metadata.docstring); + const truncated = doc.length > 80 ? `${doc.substring(0, 77)}...` : doc; + logger.log(` ${chalk.gray('Doc:')} ${truncated}`); + } + + logger.log(''); + } + } catch (error) { + spinner.fail('Search failed'); + logger.error(error instanceof Error ? error.message : String(error)); + process.exit(1); + } + }); diff --git a/packages/cli/src/commands/stats.ts b/packages/cli/src/commands/stats.ts new file mode 100644 index 0000000..78f8122 --- /dev/null +++ b/packages/cli/src/commands/stats.ts @@ -0,0 +1,75 @@ +import { RepositoryIndexer } from '@lytics/dev-agent-core'; +import chalk from 'chalk'; +import { Command } from 'commander'; +import ora from 'ora'; +import { loadConfig } from '../utils/config.js'; +import { logger } from '../utils/logger.js'; + +export const statsCommand = new Command('stats') + .description('Show indexing statistics') + .option('--json', 'Output stats as JSON', false) + .action(async (options) => { + const spinner = ora('Loading statistics...').start(); + + try { + // Load config + const config = await loadConfig(); + if (!config) { + spinner.fail('No config found'); + logger.error('Run "dev init" first to initialize dev-agent'); + process.exit(1); + return; // TypeScript needs this + } + + const indexer = new RepositoryIndexer(config); + await indexer.initialize(); + + const stats = await indexer.getStats(); + await indexer.close(); + + spinner.stop(); + + if (!stats) { + logger.warn('No indexing statistics available'); + logger.log(`Run ${chalk.yellow('dev index')} to index your repository first`); + return; + } + + // Output as JSON if requested + if (options.json) { + console.log(JSON.stringify(stats, null, 2)); + return; + } + + // Pretty print stats + logger.log(''); + logger.log(chalk.bold.cyan('📊 Indexing Statistics')); + logger.log(''); + logger.log(`${chalk.cyan('Repository:')} ${config.repositoryPath}`); + logger.log(`${chalk.cyan('Vector Store:')} ${config.vectorStorePath}`); + logger.log(''); + logger.log(`${chalk.cyan('Files Indexed:')} ${stats.filesScanned}`); + logger.log(`${chalk.cyan('Documents Extracted:')} ${stats.documentsExtracted}`); + logger.log(`${chalk.cyan('Vectors Stored:')} ${stats.vectorsStored}`); + logger.log(''); + + if (stats.startTime && stats.endTime) { + const duration = (stats.duration / 1000).toFixed(2); + logger.log( + `${chalk.cyan('Last Indexed:')} ${new Date(stats.startTime).toLocaleString()}` + ); + logger.log(`${chalk.cyan('Duration:')} ${duration}s`); + } + + if (stats.errors && stats.errors.length > 0) { + logger.log(''); + logger.warn(`${stats.errors.length} error(s) during last indexing`); + } + + logger.log(''); + } catch (error) { + spinner.fail('Failed to load statistics'); + logger.error(error instanceof Error ? error.message : String(error)); + process.exit(1); + } + }); diff --git a/packages/cli/src/commands/update.ts b/packages/cli/src/commands/update.ts new file mode 100644 index 0000000..0f03167 --- /dev/null +++ b/packages/cli/src/commands/update.ts @@ -0,0 +1,83 @@ +import { RepositoryIndexer } from '@lytics/dev-agent-core'; +import chalk from 'chalk'; +import { Command } from 'commander'; +import ora from 'ora'; +import { loadConfig } from '../utils/config.js'; +import { logger } from '../utils/logger.js'; + +export const updateCommand = new Command('update') + .description('Update index with changed files') + .option('-v, --verbose', 'Verbose output', false) + .action(async (options) => { + const spinner = ora('Checking for changes...').start(); + + try { + // Load config + const config = await loadConfig(); + if (!config) { + spinner.fail('No config found'); + logger.error('Run "dev init" first to initialize dev-agent'); + process.exit(1); + return; // TypeScript needs this + } + + spinner.text = 'Initializing indexer...'; + const indexer = new RepositoryIndexer(config); + await indexer.initialize(); + + spinner.text = 'Detecting changed files...'; + + const startTime = Date.now(); + let lastUpdate = startTime; + + const stats = await indexer.update({ + onProgress: (progress) => { + const now = Date.now(); + if (now - lastUpdate > 100) { + const percent = progress.percentComplete || 0; + const currentFile = progress.currentFile ? ` ${progress.currentFile}` : ''; + spinner.text = `${progress.phase}:${currentFile} (${percent.toFixed(0)}%)`; + lastUpdate = now; + } + }, + }); + + await indexer.close(); + + const duration = ((Date.now() - startTime) / 1000).toFixed(2); + + if (stats.filesScanned === 0) { + spinner.succeed(chalk.green('Index is up to date!')); + logger.log(''); + logger.log('No changes detected since last index.'); + } else { + spinner.succeed(chalk.green('Index updated successfully!')); + + // Show stats + logger.log(''); + logger.log(chalk.bold('Update Statistics:')); + logger.log(` ${chalk.cyan('Files updated:')} ${stats.filesScanned}`); + logger.log(` ${chalk.cyan('Documents re-indexed:')} ${stats.documentsIndexed}`); + logger.log(` ${chalk.cyan('Duration:')} ${duration}s`); + + if (stats.errors.length > 0) { + logger.log(''); + logger.warn(`${stats.errors.length} error(s) occurred during update`); + if (options.verbose) { + for (const error of stats.errors) { + logger.error(` ${error.file}: ${error.message}`); + } + } + } + } + + logger.log(''); + } catch (error) { + spinner.fail('Failed to update index'); + logger.error(error instanceof Error ? error.message : String(error)); + if (options.verbose && error instanceof Error && error.stack) { + logger.debug(error.stack); + } + process.exit(1); + } + }); diff --git a/packages/cli/src/utils/config.test.ts b/packages/cli/src/utils/config.test.ts new file mode 100644 index 0000000..46b4fb4 --- /dev/null +++ b/packages/cli/src/utils/config.test.ts @@ -0,0 +1,121 @@ +import * as fs from 'node:fs/promises'; +import * as os from 'node:os'; +import * as path from 'node:path'; +import { afterAll, beforeAll, describe, expect, it } from 'vitest'; +import { findConfigFile, getDefaultConfig, loadConfig, saveConfig } from './config'; + +describe('Config Utilities', () => { + let testDir: string; + + beforeAll(async () => { + testDir = path.join(os.tmpdir(), `cli-config-test-${Date.now()}`); + await fs.mkdir(testDir, { recursive: true }); + }); + + afterAll(async () => { + await fs.rm(testDir, { recursive: true, force: true }); + }); + + describe('getDefaultConfig', () => { + it('should return default configuration', () => { + const config = getDefaultConfig('/test/path'); + + expect(config.repositoryPath).toBe(path.resolve('/test/path')); + expect(config.vectorStorePath).toContain('.dev-agent/vectors.lance'); + expect(config.embeddingModel).toBe('Xenova/all-MiniLM-L6-v2'); + expect(config.dimension).toBe(384); + expect(config.excludePatterns).toContain('**/node_modules/**'); + expect(config.languages).toContain('typescript'); + }); + + it('should use current directory if no path provided', () => { + const config = getDefaultConfig(); + expect(config.repositoryPath).toBe(process.cwd()); + }); + }); + + describe('saveConfig', () => { + it('should save config to file', async () => { + const config = getDefaultConfig(testDir); + await saveConfig(config, testDir); + + const configPath = path.join(testDir, '.dev-agent.json'); + const exists = await fs + .access(configPath) + .then(() => true) + .catch(() => false); + expect(exists).toBe(true); + }); + + it('should save valid JSON', async () => { + const config = getDefaultConfig(testDir); + await saveConfig(config, testDir); + + const configPath = path.join(testDir, '.dev-agent.json'); + const content = await fs.readFile(configPath, 'utf-8'); + const parsed = JSON.parse(content); + + expect(parsed.repositoryPath).toBe(config.repositoryPath); + expect(parsed.embeddingModel).toBe(config.embeddingModel); + }); + }); + + describe('findConfigFile', () => { + it('should find config file in current directory', async () => { + const config = getDefaultConfig(testDir); + await saveConfig(config, testDir); + + const found = await findConfigFile(testDir); + expect(found).toBe(path.join(testDir, '.dev-agent.json')); + }); + + it('should find config file in parent directory', async () => { + const subDir = path.join(testDir, 'sub', 'nested'); + await fs.mkdir(subDir, { recursive: true }); + + const config = getDefaultConfig(testDir); + await saveConfig(config, testDir); + + const found = await findConfigFile(subDir); + expect(found).toBe(path.join(testDir, '.dev-agent.json')); + }); + + it('should return null if no config found', async () => { + // Use a completely separate temp directory to avoid finding parent configs + const isolatedDir = path.join(os.tmpdir(), `isolated-test-${Date.now()}`); + await fs.mkdir(isolatedDir, { recursive: true }); + + try { + const found = await findConfigFile(isolatedDir); + expect(found).toBeNull(); + } finally { + await fs.rm(isolatedDir, { recursive: true, force: true }); + } + }); + }); + + describe('loadConfig', () => { + it('should load config from file', async () => { + const config = getDefaultConfig(testDir); + await saveConfig(config, testDir); + + const loaded = await loadConfig(path.join(testDir, '.dev-agent.json')); + expect(loaded).toBeDefined(); + expect(loaded?.repositoryPath).toBe(config.repositoryPath); + expect(loaded?.embeddingModel).toBe(config.embeddingModel); + }); + + it('should return null if config not found', async () => { + const loaded = await loadConfig('/nonexistent/path/.dev-agent.json'); + expect(loaded).toBeNull(); + }); + + it('should handle invalid JSON gracefully', async () => { + const invalidPath = path.join(testDir, '.dev-agent-invalid.json'); + await fs.writeFile(invalidPath, 'invalid json{{{', 'utf-8'); + + const loaded = await loadConfig(invalidPath); + expect(loaded).toBeNull(); + }); + }); +}); diff --git a/packages/cli/src/utils/config.ts b/packages/cli/src/utils/config.ts new file mode 100644 index 0000000..1fd1412 --- /dev/null +++ b/packages/cli/src/utils/config.ts @@ -0,0 +1,82 @@ +import * as fs from 'node:fs/promises'; +import * as path from 'node:path'; +import { logger } from './logger.js'; + +export interface DevAgentConfig { + repositoryPath: string; + vectorStorePath: string; + embeddingModel?: string; + dimension?: number; + excludePatterns?: string[]; + includePatterns?: string[]; + languages?: string[]; +} + +const CONFIG_FILE_NAME = '.dev-agent.json'; +const DEFAULT_VECTOR_STORE_PATH = '.dev-agent/vectors.lance'; + +export async function findConfigFile(startDir: string = process.cwd()): Promise { + let currentDir = path.resolve(startDir); + const root = path.parse(currentDir).root; + + while (currentDir !== root) { + const configPath = path.join(currentDir, CONFIG_FILE_NAME); + try { + await fs.access(configPath); + return configPath; + } catch { + // Config not found, go up one directory + currentDir = path.dirname(currentDir); + } + } + + return null; +} + +export async function loadConfig(configPath?: string): Promise { + try { + const finalPath = configPath || (await findConfigFile()); + + if (!finalPath) { + return null; + } + + const content = await fs.readFile(finalPath, 'utf-8'); + return JSON.parse(content) as DevAgentConfig; + } catch (error) { + logger.error( + `Failed to load config: ${error instanceof Error ? error.message : String(error)}` + ); + return null; + } +} + +export async function saveConfig( + config: DevAgentConfig, + targetDir: string = process.cwd() +): Promise { + const configPath = path.join(targetDir, CONFIG_FILE_NAME); + + try { + await fs.writeFile(configPath, JSON.stringify(config, null, 2), 'utf-8'); + logger.success(`Config saved to ${chalk.cyan(configPath)}`); + } catch (error) { + throw new Error( + `Failed to save config: ${error instanceof Error ? error.message : String(error)}` + ); + } +} + +export function getDefaultConfig(repositoryPath: string = process.cwd()): DevAgentConfig { + return { + repositoryPath: path.resolve(repositoryPath), + vectorStorePath: path.join(repositoryPath, DEFAULT_VECTOR_STORE_PATH), + embeddingModel: 'Xenova/all-MiniLM-L6-v2', + dimension: 384, + excludePatterns: ['**/node_modules/**', '**/dist/**', '**/.git/**', '**/coverage/**'], + languages: ['typescript', 'javascript', 'markdown'], + }; +} + +// Fix: Import chalk at the top +import chalk from 'chalk'; diff --git a/packages/cli/src/utils/logger.test.ts b/packages/cli/src/utils/logger.test.ts new file mode 100644 index 0000000..0eaaf3a --- /dev/null +++ b/packages/cli/src/utils/logger.test.ts @@ -0,0 +1,63 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { logger } from './logger'; + +describe('Logger', () => { + let consoleLogSpy: ReturnType; + + beforeEach(() => { + consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); + }); + + afterEach(() => { + consoleLogSpy.mockRestore(); + }); + + it('should log info messages', () => { + logger.info('test message'); + expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('ℹ'), 'test message'); + }); + + it('should log success messages', () => { + logger.success('test success'); + expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('✔'), 'test success'); + }); + + it('should log error messages', () => { + logger.error('test error'); + expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('✖'), 'test error'); + }); + + it('should log warning messages', () => { + logger.warn('test warning'); + expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('⚠'), 'test warning'); + }); + + it('should log plain messages', () => { + logger.log('plain message'); + expect(consoleLogSpy).toHaveBeenCalledWith('plain message'); + }); + + it('should only log debug when DEBUG env is set', () => { + const originalDebug = process.env.DEBUG; + + // Without DEBUG + delete process.env.DEBUG; + logger.debug('debug message'); + expect(consoleLogSpy).not.toHaveBeenCalled(); + + // With DEBUG + process.env.DEBUG = 'true'; + logger.debug('debug message 2'); + expect(consoleLogSpy).toHaveBeenCalledWith( + expect.stringContaining('🐛'), + expect.stringContaining('debug message 2') + ); + + // Restore + if (originalDebug !== undefined) { + process.env.DEBUG = originalDebug; + } else { + delete process.env.DEBUG; + } + }); +}); diff --git a/packages/cli/src/utils/logger.ts b/packages/cli/src/utils/logger.ts new file mode 100644 index 0000000..d443a9e --- /dev/null +++ b/packages/cli/src/utils/logger.ts @@ -0,0 +1,29 @@ +import chalk from 'chalk'; + +export const logger = { + info: (message: string) => { + console.log(chalk.blue('ℹ'), message); + }, + + success: (message: string) => { + console.log(chalk.green('✔'), message); + }, + + error: (message: string) => { + console.log(chalk.red('✖'), message); + }, + + warn: (message: string) => { + console.log(chalk.yellow('⚠'), message); + }, + + log: (message: string) => { + console.log(message); + }, + + debug: (message: string) => { + if (process.env.DEBUG) { + console.log(chalk.gray('🐛'), chalk.gray(message)); + } + }, +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4f18746..22185ae 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -44,10 +44,19 @@ importers: '@lytics/dev-agent-core': specifier: workspace:* version: link:../core + chalk: + specifier: ^5.3.0 + version: 5.6.2 + ora: + specifier: ^8.0.1 + version: 8.2.0 devDependencies: + '@types/node': + specifier: ^22.0.0 + version: 22.19.1 commander: - specifier: ^11.1.0 - version: 11.1.0 + specifier: ^12.1.0 + version: 12.1.0 typescript: specifier: ^5.3.3 version: 5.9.3 @@ -948,7 +957,6 @@ packages: /@lancedb/lancedb@0.22.3(apache-arrow@18.1.0): resolution: {integrity: sha512-nRC0fkg+d7dzCtudKHT+VH7znk6KUXRZyuS6HJYNnIrbvXBxaT6wAPjEbf70KTuqvP2znj48Zg+kiwRqkRnAJw==} engines: {node: '>= 18'} - cpu: [x64, arm64] os: [darwin, linux, win32] peerDependencies: apache-arrow: '>=15.0.0 <=18.1.0' @@ -1268,7 +1276,7 @@ packages: /@types/conventional-commits-parser@5.0.2: resolution: {integrity: sha512-BgT2szDXnVypgpNxOK8aL5SGjUdaQbC++WZNjF1Qge3Og2+zhHj+RWhmehLhYyvQwqAmvezruVfOf8+3m74W+g==} dependencies: - '@types/node': 24.7.0 + '@types/node': 24.10.1 dev: true /@types/debug@4.1.12: @@ -1308,17 +1316,17 @@ packages: undici-types: 6.21.0 dev: false + /@types/node@22.19.1: + resolution: {integrity: sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ==} + dependencies: + undici-types: 6.21.0 + dev: true + /@types/node@24.10.1: resolution: {integrity: sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==} dependencies: undici-types: 7.16.0 - /@types/node@24.7.0: - resolution: {integrity: sha512-IbKooQVqUBrlzWTi79E8Fw78l8k1RNtlDDNWsFZs7XonuQSJ8oNYfEeclhprUldXISRMLzBpILuKgPlIxm+/Yw==} - dependencies: - undici-types: 7.14.0 - dev: true - /@types/unist@3.0.3: resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} @@ -1448,6 +1456,11 @@ packages: engines: {node: '>=8'} dev: true + /ansi-regex@6.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} + engines: {node: '>=12'} + dev: false + /ansi-styles@4.3.0: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} @@ -1657,7 +1670,6 @@ packages: /chalk@5.6.2: resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} - dev: true /character-entities@2.0.2: resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} @@ -1676,6 +1688,18 @@ packages: engines: {node: '>=8'} dev: true + /cli-cursor@5.0.0: + resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} + engines: {node: '>=18'} + dependencies: + restore-cursor: 5.1.0 + dev: false + + /cli-spinners@2.9.2: + resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} + engines: {node: '>=6'} + dev: false + /cliui@8.0.1: resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} engines: {node: '>=12'} @@ -1733,9 +1757,9 @@ packages: typical: 7.3.0 dev: false - /commander@11.1.0: - resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==} - engines: {node: '>=16'} + /commander@12.1.0: + resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} + engines: {node: '>=18'} dev: true /compare-func@2.0.0: @@ -1878,6 +1902,10 @@ packages: is-obj: 2.0.0 dev: true + /emoji-regex@10.6.0: + resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} + dev: false + /emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} dev: true @@ -2099,6 +2127,11 @@ packages: engines: {node: 6.* || 8.* || >= 10.*} dev: true + /get-east-asian-width@1.4.0: + resolution: {integrity: sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==} + engines: {node: '>=18'} + dev: false + /git-raw-commits@4.0.0: resolution: {integrity: sha512-ICsMM1Wk8xSGMowkOmPrzo2Fgmfo4bMHLNX6ytHjajRJUqvHOw/TFapQ+QG75c3X/tTDDhOSRPGC52dDbNM8FQ==} engines: {node: '>=16'} @@ -2246,6 +2279,11 @@ packages: dependencies: is-extglob: 2.1.1 + /is-interactive@2.0.0: + resolution: {integrity: sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==} + engines: {node: '>=12'} + dev: false + /is-number@7.0.0: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} @@ -2279,6 +2317,16 @@ packages: text-extensions: 2.4.0 dev: true + /is-unicode-supported@1.3.0: + resolution: {integrity: sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==} + engines: {node: '>=12'} + dev: false + + /is-unicode-supported@2.1.0: + resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==} + engines: {node: '>=18'} + dev: false + /is-windows@1.0.2: resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==} engines: {node: '>=0.10.0'} @@ -2426,6 +2474,14 @@ packages: resolution: {integrity: sha512-sReKOYJIJf74dhJONhU4e0/shzi1trVbSWDOhKYE5XV2O+H7Sb2Dihwuc7xWxVl+DgFPyTqIN3zMfT9cq5iWDg==} dev: true + /log-symbols@6.0.0: + resolution: {integrity: sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==} + engines: {node: '>=18'} + dependencies: + chalk: 5.6.2 + is-unicode-supported: 1.3.0 + dev: false + /long@4.0.0: resolution: {integrity: sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==} dev: false @@ -2692,6 +2748,11 @@ packages: braces: 3.0.3 picomatch: 2.3.1 + /mimic-function@5.0.1: + resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} + engines: {node: '>=18'} + dev: false + /mimic-response@3.1.0: resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} engines: {node: '>=10'} @@ -2746,6 +2807,13 @@ packages: wrappy: 1.0.2 dev: false + /onetime@7.0.0: + resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} + engines: {node: '>=18'} + dependencies: + mimic-function: 5.0.1 + dev: false + /onnx-proto@4.0.4: resolution: {integrity: sha512-aldMOB3HRoo6q/phyB6QRQxSt895HNNw82BNyZ2CMh4bjeKv7g/c+VpAFtJuEMVfYLMbRx61hbuqnKceLeDcDA==} dependencies: @@ -2776,6 +2844,21 @@ packages: platform: 1.3.6 dev: false + /ora@8.2.0: + resolution: {integrity: sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==} + engines: {node: '>=18'} + dependencies: + chalk: 5.6.2 + cli-cursor: 5.0.0 + cli-spinners: 2.9.2 + is-interactive: 2.0.0 + is-unicode-supported: 2.1.0 + log-symbols: 6.0.0 + stdin-discarder: 0.2.2 + string-width: 7.2.0 + strip-ansi: 7.1.2 + dev: false + /outdent@0.5.0: resolution: {integrity: sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q==} dev: true @@ -3048,6 +3131,14 @@ packages: engines: {node: '>=8'} dev: true + /restore-cursor@5.1.0: + resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} + engines: {node: '>=18'} + dependencies: + onetime: 7.0.0 + signal-exit: 4.1.0 + dev: false + /reusify@1.1.0: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} @@ -3140,7 +3231,6 @@ packages: /signal-exit@4.1.0: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} - dev: true /simple-concat@1.0.1: resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} @@ -3199,6 +3289,11 @@ packages: resolution: {integrity: sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==} dev: true + /stdin-discarder@0.2.2: + resolution: {integrity: sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==} + engines: {node: '>=18'} + dev: false + /streamx@2.23.0: resolution: {integrity: sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==} dependencies: @@ -3219,6 +3314,15 @@ packages: strip-ansi: 6.0.1 dev: true + /string-width@7.2.0: + resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} + engines: {node: '>=18'} + dependencies: + emoji-regex: 10.6.0 + get-east-asian-width: 1.4.0 + strip-ansi: 7.1.2 + dev: false + /string_decoder@1.3.0: resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} dependencies: @@ -3232,6 +3336,13 @@ packages: ansi-regex: 5.0.1 dev: true + /strip-ansi@7.1.2: + resolution: {integrity: sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==} + engines: {node: '>=12'} + dependencies: + ansi-regex: 6.2.2 + dev: false + /strip-bom@3.0.0: resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} engines: {node: '>=4'} @@ -3452,11 +3563,6 @@ packages: /undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} - dev: false - - /undici-types@7.14.0: - resolution: {integrity: sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA==} - dev: true /undici-types@7.16.0: resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==}