diff --git a/packages/ipuaro/CHANGELOG.md b/packages/ipuaro/CHANGELOG.md index a98f1b1..98ba515 100644 --- a/packages/ipuaro/CHANGELOG.md +++ b/packages/ipuaro/CHANGELOG.md @@ -5,6 +5,64 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.15.0] - 2025-12-01 - CLI Entry Point + +### Added + +- **Onboarding Module (0.15.3)** + - `checkRedis()`: Validates Redis connection with helpful error messages + - `checkOllama()`: Validates Ollama availability with install instructions + - `checkModel()`: Checks if LLM model is available, offers to pull if missing + - `checkProjectSize()`: Warns if project has >10K files + - `runOnboarding()`: Runs all pre-flight checks before starting + +- **Start Command (0.15.1)** + - Full TUI startup with dependency injection + - Integrates onboarding checks before launch + - Interactive model pull prompt if model missing + - Redis, storage, LLM, and tools initialization + - Clean shutdown with disconnect on exit + +- **Init Command (0.15.1)** + - Creates `.ipuaro.json` configuration file + - Default template with Redis, LLM, and edit settings + - `--force` option to overwrite existing config + - Helpful output showing available options + +- **Index Command (0.15.1)** + - Standalone project indexing without TUI + - File scanning with progress output + - AST parsing with error handling + - Metadata analysis and storage + - Symbol index and dependency graph building + - Duration and statistics reporting + +- **CLI Options (0.15.2)** + - `--auto-apply`: Enable auto-apply mode for edits + - `--model `: Override LLM model + - `--help`: Show help + - `--version`: Show version + +- **Tools Setup Helper** + - `registerAllTools()`: Registers all 18 tools with the registry + - Clean separation from CLI logic + +### Changed + +- **CLI Architecture** + - Refactored from placeholder to full implementation + - Commands in separate modules under `src/cli/commands/` + - Dynamic version from package.json + - `start` command is now default (runs with `ipuaro` or `ipuaro start`) + +### Technical Details + +- Total tests: 1372 (29 new CLI tests) +- Coverage: ~98% maintained (CLI excluded from coverage thresholds) +- New test files: onboarding.test.ts, init.test.ts, tools-setup.test.ts + +--- + ## [0.14.0] - 2025-12-01 - Commands ### Added diff --git a/packages/ipuaro/ROADMAP.md b/packages/ipuaro/ROADMAP.md index 1e074be..a238b7a 100644 --- a/packages/ipuaro/ROADMAP.md +++ b/packages/ipuaro/ROADMAP.md @@ -1182,10 +1182,10 @@ Tab // Path autocomplete --- -## Version 0.15.0 - CLI Entry Point 🚪 ⬜ +## Version 0.15.0 - CLI Entry Point 🚪 āœ… **Priority:** HIGH -**Status:** NEXT MILESTONE +**Status:** Complete (v0.15.0 released) ### 0.15.1 - CLI Commands @@ -1219,14 +1219,14 @@ ipuaro index // Index only (no TUI) ``` **Tests:** -- [ ] E2E tests for CLI +- [x] Unit tests for CLI commands (29 tests) --- ## Version 0.16.0 - Error Handling āš ļø ⬜ **Priority:** HIGH -**Status:** Partial — IpuaroError exists (v0.1.0), need full error matrix implementation +**Status:** NEXT MILESTONE — IpuaroError exists (v0.1.0), need full error matrix implementation ### 0.16.1 - Error Types @@ -1347,4 +1347,4 @@ sessions:list # List **Last Updated:** 2025-12-01 **Target Version:** 1.0.0 -**Current Version:** 0.14.0 \ No newline at end of file +**Current Version:** 0.15.0 \ No newline at end of file diff --git a/packages/ipuaro/package.json b/packages/ipuaro/package.json index 22222b7..27b3d54 100644 --- a/packages/ipuaro/package.json +++ b/packages/ipuaro/package.json @@ -1,6 +1,6 @@ { "name": "@samiyev/ipuaro", - "version": "0.14.0", + "version": "0.15.0", "description": "Local AI agent for codebase operations with infinite context feeling", "author": "Fozilbek Samiyev ", "license": "MIT", diff --git a/packages/ipuaro/src/cli/commands/index-cmd.ts b/packages/ipuaro/src/cli/commands/index-cmd.ts new file mode 100644 index 0000000..4ac4bb3 --- /dev/null +++ b/packages/ipuaro/src/cli/commands/index-cmd.ts @@ -0,0 +1,250 @@ +/** + * Index command implementation. + * Indexes project without starting TUI. + */ + +import * as fs from "node:fs/promises" +import * as path from "node:path" +import { RedisClient } from "../../infrastructure/storage/RedisClient.js" +import { RedisStorage } from "../../infrastructure/storage/RedisStorage.js" +import { generateProjectName } from "../../infrastructure/storage/schema.js" +import { FileScanner } from "../../infrastructure/indexer/FileScanner.js" +import { ASTParser } from "../../infrastructure/indexer/ASTParser.js" +import { MetaAnalyzer } from "../../infrastructure/indexer/MetaAnalyzer.js" +import { IndexBuilder } from "../../infrastructure/indexer/IndexBuilder.js" +import { createFileData } from "../../domain/value-objects/FileData.js" +import type { FileAST } from "../../domain/value-objects/FileAST.js" +import { type Config, DEFAULT_CONFIG } from "../../shared/constants/config.js" +import { md5 } from "../../shared/utils/hash.js" +import { checkRedis } from "./onboarding.js" + +type Language = "ts" | "tsx" | "js" | "jsx" + +/** + * Result of index command. + */ +export interface IndexResult { + success: boolean + filesIndexed: number + filesSkipped: number + errors: string[] + duration: number +} + +/** + * Progress callback for indexing. + */ +export type IndexProgressCallback = ( + phase: "scanning" | "parsing" | "analyzing" | "storing", + current: number, + total: number, + currentFile?: string, +) => void + +/** + * Execute the index command. + */ +export async function executeIndex( + projectPath: string, + config: Config = DEFAULT_CONFIG, + onProgress?: IndexProgressCallback, +): Promise { + const startTime = Date.now() + const resolvedPath = path.resolve(projectPath) + const projectName = generateProjectName(resolvedPath) + const errors: string[] = [] + + console.warn(`šŸ“ Indexing project: ${resolvedPath}`) + console.warn(` Project name: ${projectName}\n`) + + const redisResult = await checkRedis(config.redis) + if (!redisResult.ok) { + console.error(`āŒ ${redisResult.error ?? "Redis unavailable"}`) + return { + success: false, + filesIndexed: 0, + filesSkipped: 0, + errors: [redisResult.error ?? "Redis unavailable"], + duration: Date.now() - startTime, + } + } + + let redisClient: RedisClient | null = null + + try { + redisClient = new RedisClient(config.redis) + await redisClient.connect() + + const storage = new RedisStorage(redisClient, projectName) + const scanner = new FileScanner({ + onProgress: (progress): void => { + onProgress?.("scanning", progress.current, progress.total, progress.currentFile) + }, + }) + const astParser = new ASTParser() + const metaAnalyzer = new MetaAnalyzer(resolvedPath) + const indexBuilder = new IndexBuilder(resolvedPath) + + console.warn("šŸ” Scanning files...") + const files = await scanner.scanAll(resolvedPath) + console.warn(` Found ${String(files.length)} files\n`) + + if (files.length === 0) { + console.warn("āš ļø No files found to index.") + return { + success: true, + filesIndexed: 0, + filesSkipped: 0, + errors: [], + duration: Date.now() - startTime, + } + } + + console.warn("šŸ“ Parsing files...") + const allASTs = new Map() + const fileContents = new Map() + let parsed = 0 + let skipped = 0 + + for (const file of files) { + const fullPath = path.join(resolvedPath, file.path) + const language = getLanguage(file.path) + + if (!language) { + skipped++ + continue + } + + try { + const content = await fs.readFile(fullPath, "utf-8") + const ast = astParser.parse(content, language) + + if (ast.parseError) { + errors.push( + `Parse error in ${file.path}: ${ast.parseErrorMessage ?? "unknown"}`, + ) + skipped++ + continue + } + + allASTs.set(file.path, ast) + fileContents.set(file.path, content) + parsed++ + + onProgress?.("parsing", parsed + skipped, files.length, file.path) + + if ((parsed + skipped) % 50 === 0) { + process.stdout.write( + `\r Parsed ${String(parsed)} files (${String(skipped)} skipped)...`, + ) + } + } catch (error) { + const message = error instanceof Error ? error.message : String(error) + errors.push(`Error reading ${file.path}: ${message}`) + skipped++ + } + } + console.warn(`\r Parsed ${String(parsed)} files (${String(skipped)} skipped) \n`) + + console.warn("šŸ“Š Analyzing metadata...") + let analyzed = 0 + for (const [filePath, ast] of allASTs) { + const content = fileContents.get(filePath) ?? "" + const meta = metaAnalyzer.analyze( + path.join(resolvedPath, filePath), + ast, + content, + allASTs, + ) + + const fileData = createFileData({ + lines: content.split("\n"), + hash: md5(content), + size: content.length, + lastModified: Date.now(), + }) + + await storage.setFile(filePath, fileData) + await storage.setAST(filePath, ast) + await storage.setMeta(filePath, meta) + + analyzed++ + onProgress?.("analyzing", analyzed, allASTs.size, filePath) + + if (analyzed % 50 === 0) { + process.stdout.write( + `\r Analyzed ${String(analyzed)}/${String(allASTs.size)} files...`, + ) + } + } + console.warn(`\r Analyzed ${String(analyzed)} files \n`) + + console.warn("šŸ—ļø Building indexes...") + onProgress?.("storing", 0, 2) + const symbolIndex = indexBuilder.buildSymbolIndex(allASTs) + const depsGraph = indexBuilder.buildDepsGraph(allASTs) + + await storage.setSymbolIndex(symbolIndex) + await storage.setDepsGraph(depsGraph) + onProgress?.("storing", 2, 2) + + const duration = Date.now() - startTime + const durationSec = (duration / 1000).toFixed(2) + + console.warn(`āœ… Indexing complete in ${durationSec}s`) + console.warn(` Files indexed: ${String(parsed)}`) + console.warn(` Files skipped: ${String(skipped)}`) + console.warn(` Symbols: ${String(symbolIndex.size)}`) + + if (errors.length > 0) { + console.warn(`\nāš ļø ${String(errors.length)} errors occurred:`) + for (const error of errors.slice(0, 5)) { + console.warn(` - ${error}`) + } + if (errors.length > 5) { + console.warn(` ... and ${String(errors.length - 5)} more`) + } + } + + return { + success: true, + filesIndexed: parsed, + filesSkipped: skipped, + errors, + duration, + } + } catch (error) { + const message = error instanceof Error ? error.message : String(error) + console.error(`āŒ Indexing failed: ${message}`) + return { + success: false, + filesIndexed: 0, + filesSkipped: 0, + errors: [message], + duration: Date.now() - startTime, + } + } finally { + if (redisClient) { + await redisClient.disconnect() + } + } +} + +/** + * Get language from file extension. + */ +function getLanguage(filePath: string): Language | null { + const ext = path.extname(filePath).toLowerCase() + switch (ext) { + case ".ts": + return "ts" + case ".tsx": + return "tsx" + case ".js": + return "js" + case ".jsx": + return "jsx" + default: + return null + } +} diff --git a/packages/ipuaro/src/cli/commands/index.ts b/packages/ipuaro/src/cli/commands/index.ts new file mode 100644 index 0000000..fbb59dd --- /dev/null +++ b/packages/ipuaro/src/cli/commands/index.ts @@ -0,0 +1,18 @@ +/** + * CLI commands module. + */ + +export { executeStart, type StartOptions, type StartResult } from "./start.js" +export { executeInit, type InitOptions, type InitResult } from "./init.js" +export { executeIndex, type IndexResult, type IndexProgressCallback } from "./index-cmd.js" +export { + runOnboarding, + checkRedis, + checkOllama, + checkModel, + checkProjectSize, + pullModel, + type OnboardingResult, + type OnboardingOptions, +} from "./onboarding.js" +export { registerAllTools } from "./tools-setup.js" diff --git a/packages/ipuaro/src/cli/commands/init.ts b/packages/ipuaro/src/cli/commands/init.ts new file mode 100644 index 0000000..6301f60 --- /dev/null +++ b/packages/ipuaro/src/cli/commands/init.ts @@ -0,0 +1,114 @@ +/** + * Init command implementation. + * Creates .ipuaro.json configuration file. + */ + +import * as fs from "node:fs/promises" +import * as path from "node:path" + +/** + * Default configuration template for .ipuaro.json + */ +const CONFIG_TEMPLATE = { + $schema: "https://raw.githubusercontent.com/samiyev/puaros/main/packages/ipuaro/schema.json", + redis: { + host: "localhost", + port: 6379, + db: 0, + }, + llm: { + model: "qwen2.5-coder:7b-instruct", + temperature: 0.1, + host: "http://localhost:11434", + }, + project: { + ignorePatterns: [], + }, + edit: { + autoApply: false, + }, +} + +/** + * Options for init command. + */ +export interface InitOptions { + force?: boolean +} + +/** + * Result of init command. + */ +export interface InitResult { + success: boolean + filePath?: string + error?: string + skipped?: boolean +} + +/** + * Execute the init command. + * Creates a .ipuaro.json file in the specified directory. + */ +export async function executeInit( + projectPath = ".", + options: InitOptions = {}, +): Promise { + const resolvedPath = path.resolve(projectPath) + const configPath = path.join(resolvedPath, ".ipuaro.json") + + try { + const exists = await fileExists(configPath) + + if (exists && !options.force) { + console.warn(`āš ļø Configuration file already exists: ${configPath}`) + console.warn(" Use --force to overwrite.") + return { + success: true, + skipped: true, + filePath: configPath, + } + } + + const dirExists = await fileExists(resolvedPath) + if (!dirExists) { + await fs.mkdir(resolvedPath, { recursive: true }) + } + + const content = JSON.stringify(CONFIG_TEMPLATE, null, 4) + await fs.writeFile(configPath, content, "utf-8") + + console.warn(`āœ… Created ${configPath}`) + console.warn("\nConfiguration options:") + console.warn(" redis.host - Redis server host (default: localhost)") + console.warn(" redis.port - Redis server port (default: 6379)") + console.warn(" llm.model - Ollama model name (default: qwen2.5-coder:7b-instruct)") + console.warn(" llm.temperature - LLM temperature (default: 0.1)") + console.warn(" edit.autoApply - Auto-apply edits without confirmation (default: false)") + console.warn("\nRun `ipuaro` to start the AI agent.") + + return { + success: true, + filePath: configPath, + } + } catch (error) { + const message = error instanceof Error ? error.message : String(error) + console.error(`āŒ Failed to create configuration: ${message}`) + return { + success: false, + error: message, + } + } +} + +/** + * Check if a file or directory exists. + */ +async function fileExists(filePath: string): Promise { + try { + await fs.access(filePath) + return true + } catch { + return false + } +} diff --git a/packages/ipuaro/src/cli/commands/onboarding.ts b/packages/ipuaro/src/cli/commands/onboarding.ts new file mode 100644 index 0000000..b44c03c --- /dev/null +++ b/packages/ipuaro/src/cli/commands/onboarding.ts @@ -0,0 +1,290 @@ +/** + * Onboarding checks for CLI. + * Validates environment before starting ipuaro. + */ + +import { RedisClient } from "../../infrastructure/storage/RedisClient.js" +import { OllamaClient } from "../../infrastructure/llm/OllamaClient.js" +import { FileScanner } from "../../infrastructure/indexer/FileScanner.js" +import type { LLMConfig, RedisConfig } from "../../shared/constants/config.js" + +/** + * Result of onboarding checks. + */ +export interface OnboardingResult { + success: boolean + redisOk: boolean + ollamaOk: boolean + modelOk: boolean + projectOk: boolean + fileCount: number + errors: string[] + warnings: string[] +} + +/** + * Options for onboarding checks. + */ +export interface OnboardingOptions { + redisConfig: RedisConfig + llmConfig: LLMConfig + projectPath: string + maxFiles?: number + skipRedis?: boolean + skipOllama?: boolean + skipModel?: boolean + skipProject?: boolean +} + +const DEFAULT_MAX_FILES = 10_000 + +/** + * Check Redis availability. + */ +export async function checkRedis(config: RedisConfig): Promise<{ + ok: boolean + error?: string +}> { + const client = new RedisClient(config) + + try { + await client.connect() + const pingOk = await client.ping() + await client.disconnect() + + if (!pingOk) { + return { + ok: false, + error: "Redis ping failed. Server may be overloaded.", + } + } + + return { ok: true } + } catch (error) { + const message = error instanceof Error ? error.message : String(error) + return { + ok: false, + error: `Cannot connect to Redis: ${message} + +Redis is required for ipuaro to store project indexes and session data. + +Install Redis: + macOS: brew install redis && brew services start redis + Ubuntu: sudo apt install redis-server && sudo systemctl start redis + Docker: docker run -d -p 6379:6379 redis`, + } + } +} + +/** + * Check Ollama availability. + */ +export async function checkOllama(config: LLMConfig): Promise<{ + ok: boolean + error?: string +}> { + const client = new OllamaClient(config) + + try { + const available = await client.isAvailable() + + if (!available) { + return { + ok: false, + error: `Cannot connect to Ollama at ${config.host} + +Ollama is required for ipuaro to process your requests using local LLMs. + +Install Ollama: + macOS: brew install ollama && ollama serve + Linux: curl -fsSL https://ollama.com/install.sh | sh && ollama serve + Manual: https://ollama.com/download + +After installing, ensure Ollama is running with: ollama serve`, + } + } + + return { ok: true } + } catch (error) { + const message = error instanceof Error ? error.message : String(error) + return { + ok: false, + error: `Ollama check failed: ${message}`, + } + } +} + +/** + * Check model availability. + */ +export async function checkModel(config: LLMConfig): Promise<{ + ok: boolean + needsPull: boolean + error?: string +}> { + const client = new OllamaClient(config) + + try { + const hasModel = await client.hasModel(config.model) + + if (!hasModel) { + return { + ok: false, + needsPull: true, + error: `Model "${config.model}" is not installed. + +Would you like to pull it? This may take a few minutes. +Run: ollama pull ${config.model}`, + } + } + + return { ok: true, needsPull: false } + } catch (error) { + const message = error instanceof Error ? error.message : String(error) + return { + ok: false, + needsPull: false, + error: `Model check failed: ${message}`, + } + } +} + +/** + * Pull model from Ollama. + */ +export async function pullModel( + config: LLMConfig, + onProgress?: (status: string) => void, +): Promise<{ ok: boolean; error?: string }> { + const client = new OllamaClient(config) + + try { + onProgress?.(`Pulling model "${config.model}"...`) + await client.pullModel(config.model) + onProgress?.(`Model "${config.model}" pulled successfully.`) + return { ok: true } + } catch (error) { + const message = error instanceof Error ? error.message : String(error) + return { + ok: false, + error: `Failed to pull model: ${message}`, + } + } +} + +/** + * Check project size. + */ +export async function checkProjectSize( + projectPath: string, + maxFiles: number = DEFAULT_MAX_FILES, +): Promise<{ + ok: boolean + fileCount: number + warning?: string +}> { + const scanner = new FileScanner() + + try { + const files = await scanner.scanAll(projectPath) + const fileCount = files.length + + if (fileCount > maxFiles) { + return { + ok: true, + fileCount, + warning: `Project has ${fileCount.toLocaleString()} files (>${maxFiles.toLocaleString()}). +This may take a while to index and use more memory. + +Consider: + 1. Running ipuaro in a subdirectory: ipuaro ./src + 2. Adding patterns to .gitignore to exclude unnecessary files + 3. Using a smaller project for better performance`, + } + } + + if (fileCount === 0) { + return { + ok: false, + fileCount: 0, + warning: `No supported files found in "${projectPath}". + +ipuaro supports: .ts, .tsx, .js, .jsx, .json, .yaml, .yml + +Ensure you're running ipuaro in a project directory with source files.`, + } + } + + return { ok: true, fileCount } + } catch (error) { + const message = error instanceof Error ? error.message : String(error) + return { + ok: false, + fileCount: 0, + warning: `Failed to scan project: ${message}`, + } + } +} + +/** + * Run all onboarding checks. + */ +export async function runOnboarding(options: OnboardingOptions): Promise { + const errors: string[] = [] + const warnings: string[] = [] + const maxFiles = options.maxFiles ?? DEFAULT_MAX_FILES + + let redisOk = true + let ollamaOk = true + let modelOk = true + let projectOk = true + let fileCount = 0 + + if (!options.skipRedis) { + const redisResult = await checkRedis(options.redisConfig) + redisOk = redisResult.ok + if (!redisOk && redisResult.error) { + errors.push(redisResult.error) + } + } + + if (!options.skipOllama) { + const ollamaResult = await checkOllama(options.llmConfig) + ollamaOk = ollamaResult.ok + if (!ollamaOk && ollamaResult.error) { + errors.push(ollamaResult.error) + } + } + + if (!options.skipModel && ollamaOk) { + const modelResult = await checkModel(options.llmConfig) + modelOk = modelResult.ok + if (!modelOk && modelResult.error) { + errors.push(modelResult.error) + } + } + + if (!options.skipProject) { + const projectResult = await checkProjectSize(options.projectPath, maxFiles) + projectOk = projectResult.ok + fileCount = projectResult.fileCount + if (projectResult.warning) { + if (projectResult.ok) { + warnings.push(projectResult.warning) + } else { + errors.push(projectResult.warning) + } + } + } + + return { + success: redisOk && ollamaOk && modelOk && projectOk && errors.length === 0, + redisOk, + ollamaOk, + modelOk, + projectOk, + fileCount, + errors, + warnings, + } +} diff --git a/packages/ipuaro/src/cli/commands/start.ts b/packages/ipuaro/src/cli/commands/start.ts new file mode 100644 index 0000000..430de16 --- /dev/null +++ b/packages/ipuaro/src/cli/commands/start.ts @@ -0,0 +1,162 @@ +/** + * Start command implementation. + * Launches the ipuaro TUI. + */ + +import * as path from "node:path" +import * as readline from "node:readline" +import { render } from "ink" +import React from "react" +import { App, type AppDependencies } from "../../tui/App.js" +import { RedisClient } from "../../infrastructure/storage/RedisClient.js" +import { RedisStorage } from "../../infrastructure/storage/RedisStorage.js" +import { RedisSessionStorage } from "../../infrastructure/storage/RedisSessionStorage.js" +import { OllamaClient } from "../../infrastructure/llm/OllamaClient.js" +import { ToolRegistry } from "../../infrastructure/tools/registry.js" +import { generateProjectName } from "../../infrastructure/storage/schema.js" +import { type Config, DEFAULT_CONFIG } from "../../shared/constants/config.js" +import { checkModel, pullModel, runOnboarding } from "./onboarding.js" +import { registerAllTools } from "./tools-setup.js" + +/** + * Options for start command. + */ +export interface StartOptions { + autoApply?: boolean + model?: string +} + +/** + * Result of start command. + */ +export interface StartResult { + success: boolean + error?: string +} + +/** + * Execute the start command. + */ +export async function executeStart( + projectPath: string, + options: StartOptions, + config: Config = DEFAULT_CONFIG, +): Promise { + const resolvedPath = path.resolve(projectPath) + const projectName = generateProjectName(resolvedPath) + + const llmConfig = { + ...config.llm, + model: options.model ?? config.llm.model, + } + + console.warn("šŸ” Running pre-flight checks...\n") + + const onboardingResult = await runOnboarding({ + redisConfig: config.redis, + llmConfig, + projectPath: resolvedPath, + }) + + for (const warning of onboardingResult.warnings) { + console.warn(`āš ļø ${warning}\n`) + } + + if (!onboardingResult.success) { + for (const error of onboardingResult.errors) { + console.error(`āŒ ${error}\n`) + } + + if (!onboardingResult.modelOk && onboardingResult.ollamaOk) { + const shouldPull = await promptYesNo( + `Would you like to pull "${llmConfig.model}"? (y/n): `, + ) + + if (shouldPull) { + const pullResult = await pullModel(llmConfig, console.warn) + if (!pullResult.ok) { + console.error(`āŒ ${pullResult.error ?? "Unknown error"}`) + return { success: false, error: pullResult.error } + } + + const recheckModel = await checkModel(llmConfig) + if (!recheckModel.ok) { + console.error("āŒ Model still not available after pull.") + return { success: false, error: "Model pull failed" } + } + } else { + return { success: false, error: "Model not available" } + } + } else { + return { + success: false, + error: onboardingResult.errors.join("\n"), + } + } + } + + console.warn(`āœ… All checks passed. Found ${String(onboardingResult.fileCount)} files.\n`) + console.warn("šŸš€ Starting ipuaro...\n") + + const redisClient = new RedisClient(config.redis) + + try { + await redisClient.connect() + + const storage = new RedisStorage(redisClient, projectName) + const sessionStorage = new RedisSessionStorage(redisClient) + const llm = new OllamaClient(llmConfig) + const tools = new ToolRegistry() + + registerAllTools(tools) + + const deps: AppDependencies = { + storage, + sessionStorage, + llm, + tools, + } + + const handleExit = (): void => { + void redisClient.disconnect() + } + + const { waitUntilExit } = render( + React.createElement(App, { + projectPath: resolvedPath, + autoApply: options.autoApply ?? config.edit.autoApply, + deps, + onExit: handleExit, + }), + ) + + await waitUntilExit() + await redisClient.disconnect() + + return { success: true } + } catch (error) { + const message = error instanceof Error ? error.message : String(error) + console.error(`āŒ Failed to start ipuaro: ${message}`) + await redisClient.disconnect() + return { success: false, error: message } + } +} + +/** + * Simple yes/no prompt for CLI. + */ +async function promptYesNo(question: string): Promise { + return new Promise((resolve) => { + process.stdout.write(question) + + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }) + + rl.once("line", (answer: string) => { + rl.close() + resolve(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes") + }) + }) +} diff --git a/packages/ipuaro/src/cli/commands/tools-setup.ts b/packages/ipuaro/src/cli/commands/tools-setup.ts new file mode 100644 index 0000000..1aeece2 --- /dev/null +++ b/packages/ipuaro/src/cli/commands/tools-setup.ts @@ -0,0 +1,59 @@ +/** + * Tool registration helper for CLI. + * Registers all 18 tools with the tool registry. + */ + +import type { IToolRegistry } from "../../application/interfaces/IToolRegistry.js" + +import { GetLinesTool } from "../../infrastructure/tools/read/GetLinesTool.js" +import { GetFunctionTool } from "../../infrastructure/tools/read/GetFunctionTool.js" +import { GetClassTool } from "../../infrastructure/tools/read/GetClassTool.js" +import { GetStructureTool } from "../../infrastructure/tools/read/GetStructureTool.js" + +import { EditLinesTool } from "../../infrastructure/tools/edit/EditLinesTool.js" +import { CreateFileTool } from "../../infrastructure/tools/edit/CreateFileTool.js" +import { DeleteFileTool } from "../../infrastructure/tools/edit/DeleteFileTool.js" + +import { FindReferencesTool } from "../../infrastructure/tools/search/FindReferencesTool.js" +import { FindDefinitionTool } from "../../infrastructure/tools/search/FindDefinitionTool.js" + +import { GetDependenciesTool } from "../../infrastructure/tools/analysis/GetDependenciesTool.js" +import { GetDependentsTool } from "../../infrastructure/tools/analysis/GetDependentsTool.js" +import { GetComplexityTool } from "../../infrastructure/tools/analysis/GetComplexityTool.js" +import { GetTodosTool } from "../../infrastructure/tools/analysis/GetTodosTool.js" + +import { GitStatusTool } from "../../infrastructure/tools/git/GitStatusTool.js" +import { GitDiffTool } from "../../infrastructure/tools/git/GitDiffTool.js" +import { GitCommitTool } from "../../infrastructure/tools/git/GitCommitTool.js" + +import { RunCommandTool } from "../../infrastructure/tools/run/RunCommandTool.js" +import { RunTestsTool } from "../../infrastructure/tools/run/RunTestsTool.js" + +/** + * Register all 18 tools with the tool registry. + */ +export function registerAllTools(registry: IToolRegistry): void { + registry.register(new GetLinesTool()) + registry.register(new GetFunctionTool()) + registry.register(new GetClassTool()) + registry.register(new GetStructureTool()) + + registry.register(new EditLinesTool()) + registry.register(new CreateFileTool()) + registry.register(new DeleteFileTool()) + + registry.register(new FindReferencesTool()) + registry.register(new FindDefinitionTool()) + + registry.register(new GetDependenciesTool()) + registry.register(new GetDependentsTool()) + registry.register(new GetComplexityTool()) + registry.register(new GetTodosTool()) + + registry.register(new GitStatusTool()) + registry.register(new GitDiffTool()) + registry.register(new GitCommitTool()) + + registry.register(new RunCommandTool()) + registry.register(new RunTestsTool()) +} diff --git a/packages/ipuaro/src/cli/index.ts b/packages/ipuaro/src/cli/index.ts index ecf2690..9752420 100644 --- a/packages/ipuaro/src/cli/index.ts +++ b/packages/ipuaro/src/cli/index.ts @@ -1,44 +1,63 @@ #!/usr/bin/env node +/** + * ipuaro CLI entry point. + * Local AI agent for codebase operations with infinite context feeling. + */ + +import { createRequire } from "node:module" import { Command } from "commander" +import { executeStart } from "./commands/start.js" +import { executeInit } from "./commands/init.js" +import { executeIndex } from "./commands/index-cmd.js" +import { loadConfig } from "../shared/config/loader.js" + +const require = createRequire(import.meta.url) +const pkg = require("../../package.json") as { version: string } const program = new Command() program .name("ipuaro") .description("Local AI agent for codebase operations with infinite context feeling") - .version("0.1.0") + .version(pkg.version) program - .command("start") + .command("start", { isDefault: true }) .description("Start ipuaro TUI in the current directory") .argument("[path]", "Project path", ".") .option("--auto-apply", "Enable auto-apply mode for edits") - .option("--model ", "Override LLM model", "qwen2.5-coder:7b-instruct") - .action((path: string, options: { autoApply?: boolean; model?: string }) => { - const model = options.model ?? "default" - const autoApply = options.autoApply ?? false - console.warn(`Starting ipuaro in ${path}...`) - console.warn(`Model: ${model}`) - console.warn(`Auto-apply: ${autoApply ? "enabled" : "disabled"}`) - console.warn("\nNot implemented yet. Coming in version 0.11.0!") + .option("--model ", "Override LLM model") + .action(async (projectPath: string, options: { autoApply?: boolean; model?: string }) => { + const config = loadConfig(projectPath) + const result = await executeStart(projectPath, options, config) + if (!result.success) { + process.exit(1) + } }) program .command("init") .description("Create .ipuaro.json config file") - .action(() => { - console.warn("Creating .ipuaro.json...") - console.warn("\nNot implemented yet. Coming in version 0.17.0!") + .argument("[path]", "Project path", ".") + .option("--force", "Overwrite existing config file") + .action(async (projectPath: string, options: { force?: boolean }) => { + const result = await executeInit(projectPath, options) + if (!result.success) { + process.exit(1) + } }) program .command("index") .description("Index project without starting TUI") .argument("[path]", "Project path", ".") - .action((path: string) => { - console.warn(`Indexing ${path}...`) - console.warn("\nNot implemented yet. Coming in version 0.3.0!") + .action(async (projectPath: string) => { + const config = loadConfig(projectPath) + const result = await executeIndex(projectPath, config) + if (!result.success) { + process.exit(1) + } }) program.parse() diff --git a/packages/ipuaro/tests/unit/cli/commands/init.test.ts b/packages/ipuaro/tests/unit/cli/commands/init.test.ts new file mode 100644 index 0000000..800c68f --- /dev/null +++ b/packages/ipuaro/tests/unit/cli/commands/init.test.ts @@ -0,0 +1,117 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest" +import * as fs from "node:fs/promises" +import * as path from "node:path" +import { executeInit } from "../../../../src/cli/commands/init.js" + +vi.mock("node:fs/promises") + +describe("executeInit", () => { + const testPath = "/test/project" + const configPath = path.join(testPath, ".ipuaro.json") + + beforeEach(() => { + vi.clearAllMocks() + vi.spyOn(console, "warn").mockImplementation(() => {}) + vi.spyOn(console, "error").mockImplementation(() => {}) + }) + + afterEach(() => { + vi.restoreAllMocks() + }) + + it("should create .ipuaro.json file successfully", async () => { + vi.mocked(fs.access).mockRejectedValue(new Error("ENOENT")) + vi.mocked(fs.mkdir).mockResolvedValue(undefined) + vi.mocked(fs.writeFile).mockResolvedValue(undefined) + + const result = await executeInit(testPath) + + expect(result.success).toBe(true) + expect(result.filePath).toBe(configPath) + expect(fs.writeFile).toHaveBeenCalledWith( + configPath, + expect.stringContaining('"redis"'), + "utf-8", + ) + }) + + it("should skip existing file without force option", async () => { + vi.mocked(fs.access).mockResolvedValue(undefined) + + const result = await executeInit(testPath) + + expect(result.success).toBe(true) + expect(result.skipped).toBe(true) + expect(fs.writeFile).not.toHaveBeenCalled() + }) + + it("should overwrite existing file with force option", async () => { + vi.mocked(fs.access).mockResolvedValue(undefined) + vi.mocked(fs.writeFile).mockResolvedValue(undefined) + + const result = await executeInit(testPath, { force: true }) + + expect(result.success).toBe(true) + expect(result.skipped).toBeUndefined() + expect(fs.writeFile).toHaveBeenCalled() + }) + + it("should handle write errors", async () => { + vi.mocked(fs.access).mockRejectedValue(new Error("ENOENT")) + vi.mocked(fs.mkdir).mockResolvedValue(undefined) + vi.mocked(fs.writeFile).mockRejectedValue(new Error("Permission denied")) + + const result = await executeInit(testPath) + + expect(result.success).toBe(false) + expect(result.error).toContain("Permission denied") + }) + + it("should create parent directories if needed", async () => { + vi.mocked(fs.access) + .mockRejectedValueOnce(new Error("ENOENT")) + .mockRejectedValueOnce(new Error("ENOENT")) + vi.mocked(fs.mkdir).mockResolvedValue(undefined) + vi.mocked(fs.writeFile).mockResolvedValue(undefined) + + const result = await executeInit(testPath) + + expect(result.success).toBe(true) + expect(fs.mkdir).toHaveBeenCalledWith(expect.any(String), { recursive: true }) + }) + + it("should use current directory as default", async () => { + vi.mocked(fs.access).mockRejectedValue(new Error("ENOENT")) + vi.mocked(fs.mkdir).mockResolvedValue(undefined) + vi.mocked(fs.writeFile).mockResolvedValue(undefined) + + const result = await executeInit() + + expect(result.success).toBe(true) + expect(result.filePath).toContain(".ipuaro.json") + }) + + it("should include expected config sections", async () => { + vi.mocked(fs.access).mockRejectedValue(new Error("ENOENT")) + vi.mocked(fs.mkdir).mockResolvedValue(undefined) + vi.mocked(fs.writeFile).mockResolvedValue(undefined) + + await executeInit(testPath) + + const writeCall = vi.mocked(fs.writeFile).mock.calls[0] + const content = writeCall[1] as string + const config = JSON.parse(content) as { + redis: unknown + llm: unknown + edit: unknown + } + + expect(config).toHaveProperty("redis") + expect(config).toHaveProperty("llm") + expect(config).toHaveProperty("edit") + expect(config.redis).toHaveProperty("host", "localhost") + expect(config.redis).toHaveProperty("port", 6379) + expect(config.llm).toHaveProperty("model", "qwen2.5-coder:7b-instruct") + expect(config.edit).toHaveProperty("autoApply", false) + }) +}) diff --git a/packages/ipuaro/tests/unit/cli/commands/onboarding.test.ts b/packages/ipuaro/tests/unit/cli/commands/onboarding.test.ts new file mode 100644 index 0000000..82b0516 --- /dev/null +++ b/packages/ipuaro/tests/unit/cli/commands/onboarding.test.ts @@ -0,0 +1,353 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest" +import { + checkRedis, + checkOllama, + checkModel, + checkProjectSize, + runOnboarding, +} from "../../../../src/cli/commands/onboarding.js" +import { RedisClient } from "../../../../src/infrastructure/storage/RedisClient.js" +import { OllamaClient } from "../../../../src/infrastructure/llm/OllamaClient.js" +import { FileScanner } from "../../../../src/infrastructure/indexer/FileScanner.js" + +vi.mock("../../../../src/infrastructure/storage/RedisClient.js") +vi.mock("../../../../src/infrastructure/llm/OllamaClient.js") +vi.mock("../../../../src/infrastructure/indexer/FileScanner.js") + +describe("onboarding", () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + afterEach(() => { + vi.restoreAllMocks() + }) + + describe("checkRedis", () => { + it("should return ok when Redis connects and pings successfully", async () => { + const mockConnect = vi.fn().mockResolvedValue(undefined) + const mockPing = vi.fn().mockResolvedValue(true) + const mockDisconnect = vi.fn().mockResolvedValue(undefined) + + vi.mocked(RedisClient).mockImplementation( + () => + ({ + connect: mockConnect, + ping: mockPing, + disconnect: mockDisconnect, + }) as unknown as RedisClient, + ) + + const result = await checkRedis({ + host: "localhost", + port: 6379, + db: 0, + keyPrefix: "ipuaro:", + }) + + expect(result.ok).toBe(true) + expect(result.error).toBeUndefined() + expect(mockConnect).toHaveBeenCalled() + expect(mockPing).toHaveBeenCalled() + expect(mockDisconnect).toHaveBeenCalled() + }) + + it("should return error when Redis connection fails", async () => { + vi.mocked(RedisClient).mockImplementation( + () => + ({ + connect: vi.fn().mockRejectedValue(new Error("Connection refused")), + }) as unknown as RedisClient, + ) + + const result = await checkRedis({ + host: "localhost", + port: 6379, + db: 0, + keyPrefix: "ipuaro:", + }) + + expect(result.ok).toBe(false) + expect(result.error).toContain("Cannot connect to Redis") + }) + + it("should return error when ping fails", async () => { + vi.mocked(RedisClient).mockImplementation( + () => + ({ + connect: vi.fn().mockResolvedValue(undefined), + ping: vi.fn().mockResolvedValue(false), + disconnect: vi.fn().mockResolvedValue(undefined), + }) as unknown as RedisClient, + ) + + const result = await checkRedis({ + host: "localhost", + port: 6379, + db: 0, + keyPrefix: "ipuaro:", + }) + + expect(result.ok).toBe(false) + expect(result.error).toContain("Redis ping failed") + }) + }) + + describe("checkOllama", () => { + it("should return ok when Ollama is available", async () => { + vi.mocked(OllamaClient).mockImplementation( + () => + ({ + isAvailable: vi.fn().mockResolvedValue(true), + }) as unknown as OllamaClient, + ) + + const result = await checkOllama({ + model: "qwen2.5-coder:7b-instruct", + contextWindow: 128_000, + temperature: 0.1, + host: "http://localhost:11434", + timeout: 120_000, + }) + + expect(result.ok).toBe(true) + expect(result.error).toBeUndefined() + }) + + it("should return error when Ollama is not available", async () => { + vi.mocked(OllamaClient).mockImplementation( + () => + ({ + isAvailable: vi.fn().mockResolvedValue(false), + }) as unknown as OllamaClient, + ) + + const result = await checkOllama({ + model: "qwen2.5-coder:7b-instruct", + contextWindow: 128_000, + temperature: 0.1, + host: "http://localhost:11434", + timeout: 120_000, + }) + + expect(result.ok).toBe(false) + expect(result.error).toContain("Cannot connect to Ollama") + }) + }) + + describe("checkModel", () => { + it("should return ok when model is available", async () => { + vi.mocked(OllamaClient).mockImplementation( + () => + ({ + hasModel: vi.fn().mockResolvedValue(true), + }) as unknown as OllamaClient, + ) + + const result = await checkModel({ + model: "qwen2.5-coder:7b-instruct", + contextWindow: 128_000, + temperature: 0.1, + host: "http://localhost:11434", + timeout: 120_000, + }) + + expect(result.ok).toBe(true) + expect(result.needsPull).toBe(false) + }) + + it("should return needsPull when model is not available", async () => { + vi.mocked(OllamaClient).mockImplementation( + () => + ({ + hasModel: vi.fn().mockResolvedValue(false), + }) as unknown as OllamaClient, + ) + + const result = await checkModel({ + model: "qwen2.5-coder:7b-instruct", + contextWindow: 128_000, + temperature: 0.1, + host: "http://localhost:11434", + timeout: 120_000, + }) + + expect(result.ok).toBe(false) + expect(result.needsPull).toBe(true) + expect(result.error).toContain("not installed") + }) + }) + + describe("checkProjectSize", () => { + it("should return ok when file count is within limits", async () => { + vi.mocked(FileScanner).mockImplementation( + () => + ({ + scanAll: vi.fn().mockResolvedValue( + Array.from({ length: 100 }, (_, i) => ({ + path: `file${String(i)}.ts`, + type: "file" as const, + size: 1000, + lastModified: Date.now(), + })), + ), + }) as unknown as FileScanner, + ) + + const result = await checkProjectSize("/test/path") + + expect(result.ok).toBe(true) + expect(result.fileCount).toBe(100) + expect(result.warning).toBeUndefined() + }) + + it("should return warning when file count exceeds limit", async () => { + vi.mocked(FileScanner).mockImplementation( + () => + ({ + scanAll: vi.fn().mockResolvedValue( + Array.from({ length: 15000 }, (_, i) => ({ + path: `file${String(i)}.ts`, + type: "file" as const, + size: 1000, + lastModified: Date.now(), + })), + ), + }) as unknown as FileScanner, + ) + + const result = await checkProjectSize("/test/path", 10_000) + + expect(result.ok).toBe(true) + expect(result.fileCount).toBe(15000) + expect(result.warning).toContain("15") + expect(result.warning).toContain("000 files") + }) + + it("should return error when no files found", async () => { + vi.mocked(FileScanner).mockImplementation( + () => + ({ + scanAll: vi.fn().mockResolvedValue([]), + }) as unknown as FileScanner, + ) + + const result = await checkProjectSize("/test/path") + + expect(result.ok).toBe(false) + expect(result.fileCount).toBe(0) + expect(result.warning).toContain("No supported files found") + }) + }) + + describe("runOnboarding", () => { + it("should return success when all checks pass", async () => { + vi.mocked(RedisClient).mockImplementation( + () => + ({ + connect: vi.fn().mockResolvedValue(undefined), + ping: vi.fn().mockResolvedValue(true), + disconnect: vi.fn().mockResolvedValue(undefined), + }) as unknown as RedisClient, + ) + + vi.mocked(OllamaClient).mockImplementation( + () => + ({ + isAvailable: vi.fn().mockResolvedValue(true), + hasModel: vi.fn().mockResolvedValue(true), + }) as unknown as OllamaClient, + ) + + vi.mocked(FileScanner).mockImplementation( + () => + ({ + scanAll: vi.fn().mockResolvedValue([{ path: "file.ts" }]), + }) as unknown as FileScanner, + ) + + const result = await runOnboarding({ + redisConfig: { host: "localhost", port: 6379, db: 0, keyPrefix: "ipuaro:" }, + llmConfig: { + model: "test", + contextWindow: 128_000, + temperature: 0.1, + host: "http://localhost:11434", + timeout: 120_000, + }, + projectPath: "/test/path", + }) + + expect(result.success).toBe(true) + expect(result.redisOk).toBe(true) + expect(result.ollamaOk).toBe(true) + expect(result.modelOk).toBe(true) + expect(result.projectOk).toBe(true) + expect(result.errors).toHaveLength(0) + }) + + it("should return failure when Redis fails", async () => { + vi.mocked(RedisClient).mockImplementation( + () => + ({ + connect: vi.fn().mockRejectedValue(new Error("Connection refused")), + }) as unknown as RedisClient, + ) + + vi.mocked(OllamaClient).mockImplementation( + () => + ({ + isAvailable: vi.fn().mockResolvedValue(true), + hasModel: vi.fn().mockResolvedValue(true), + }) as unknown as OllamaClient, + ) + + vi.mocked(FileScanner).mockImplementation( + () => + ({ + scanAll: vi.fn().mockResolvedValue([{ path: "file.ts" }]), + }) as unknown as FileScanner, + ) + + const result = await runOnboarding({ + redisConfig: { host: "localhost", port: 6379, db: 0, keyPrefix: "ipuaro:" }, + llmConfig: { + model: "test", + contextWindow: 128_000, + temperature: 0.1, + host: "http://localhost:11434", + timeout: 120_000, + }, + projectPath: "/test/path", + }) + + expect(result.success).toBe(false) + expect(result.redisOk).toBe(false) + expect(result.errors.length).toBeGreaterThan(0) + }) + + it("should skip checks when skip options are set", async () => { + const result = await runOnboarding({ + redisConfig: { host: "localhost", port: 6379, db: 0, keyPrefix: "ipuaro:" }, + llmConfig: { + model: "test", + contextWindow: 128_000, + temperature: 0.1, + host: "http://localhost:11434", + timeout: 120_000, + }, + projectPath: "/test/path", + skipRedis: true, + skipOllama: true, + skipModel: true, + skipProject: true, + }) + + expect(result.success).toBe(true) + expect(result.redisOk).toBe(true) + expect(result.ollamaOk).toBe(true) + expect(result.modelOk).toBe(true) + expect(result.projectOk).toBe(true) + }) + }) +}) diff --git a/packages/ipuaro/tests/unit/cli/commands/tools-setup.test.ts b/packages/ipuaro/tests/unit/cli/commands/tools-setup.test.ts new file mode 100644 index 0000000..34ab6be --- /dev/null +++ b/packages/ipuaro/tests/unit/cli/commands/tools-setup.test.ts @@ -0,0 +1,111 @@ +import { describe, it, expect } from "vitest" +import { registerAllTools } from "../../../../src/cli/commands/tools-setup.js" +import { ToolRegistry } from "../../../../src/infrastructure/tools/registry.js" + +describe("registerAllTools", () => { + it("should register all 18 tools", () => { + const registry = new ToolRegistry() + + registerAllTools(registry) + + expect(registry.size).toBe(18) + }) + + it("should register all read tools", () => { + const registry = new ToolRegistry() + + registerAllTools(registry) + + expect(registry.has("get_lines")).toBe(true) + expect(registry.has("get_function")).toBe(true) + expect(registry.has("get_class")).toBe(true) + expect(registry.has("get_structure")).toBe(true) + }) + + it("should register all edit tools", () => { + const registry = new ToolRegistry() + + registerAllTools(registry) + + expect(registry.has("edit_lines")).toBe(true) + expect(registry.has("create_file")).toBe(true) + expect(registry.has("delete_file")).toBe(true) + }) + + it("should register all search tools", () => { + const registry = new ToolRegistry() + + registerAllTools(registry) + + expect(registry.has("find_references")).toBe(true) + expect(registry.has("find_definition")).toBe(true) + }) + + it("should register all analysis tools", () => { + const registry = new ToolRegistry() + + registerAllTools(registry) + + expect(registry.has("get_dependencies")).toBe(true) + expect(registry.has("get_dependents")).toBe(true) + expect(registry.has("get_complexity")).toBe(true) + expect(registry.has("get_todos")).toBe(true) + }) + + it("should register all git tools", () => { + const registry = new ToolRegistry() + + registerAllTools(registry) + + expect(registry.has("git_status")).toBe(true) + expect(registry.has("git_diff")).toBe(true) + expect(registry.has("git_commit")).toBe(true) + }) + + it("should register all run tools", () => { + const registry = new ToolRegistry() + + registerAllTools(registry) + + expect(registry.has("run_command")).toBe(true) + expect(registry.has("run_tests")).toBe(true) + }) + + it("should register tools with correct categories", () => { + const registry = new ToolRegistry() + + registerAllTools(registry) + + const readTools = registry.getByCategory("read") + const editTools = registry.getByCategory("edit") + const searchTools = registry.getByCategory("search") + const analysisTools = registry.getByCategory("analysis") + const gitTools = registry.getByCategory("git") + const runTools = registry.getByCategory("run") + + expect(readTools.length).toBe(4) + expect(editTools.length).toBe(3) + expect(searchTools.length).toBe(2) + expect(analysisTools.length).toBe(4) + expect(gitTools.length).toBe(3) + expect(runTools.length).toBe(2) + }) + + it("should register tools with requiresConfirmation flag", () => { + const registry = new ToolRegistry() + + registerAllTools(registry) + + const confirmationTools = registry.getConfirmationTools() + const safeTools = registry.getSafeTools() + + expect(confirmationTools.length).toBeGreaterThan(0) + expect(safeTools.length).toBeGreaterThan(0) + + const confirmNames = confirmationTools.map((t) => t.name) + expect(confirmNames).toContain("edit_lines") + expect(confirmNames).toContain("create_file") + expect(confirmNames).toContain("delete_file") + expect(confirmNames).toContain("git_commit") + }) +}) diff --git a/packages/ipuaro/vitest.config.ts b/packages/ipuaro/vitest.config.ts index 0aaefc1..3fbf4b4 100644 --- a/packages/ipuaro/vitest.config.ts +++ b/packages/ipuaro/vitest.config.ts @@ -15,6 +15,7 @@ export default defineConfig({ "src/**/*.test.ts", "src/tui/**/*.ts", "src/tui/**/*.tsx", + "src/cli/**/*.ts", ], thresholds: { lines: 95,