diff --git a/README.md b/README.md index dce4593..4357240 100644 --- a/README.md +++ b/README.md @@ -174,7 +174,7 @@ elevenlabs agents branches list --agent [--include-archived] elevenlabs tools push [--tool "Tool Name"] [--dry-run] # Sync tests -elevenlabs tests push [--dry-run] +elevenlabs tests push [--dry-run] [--config-dir test_configs] # Check status elevenlabs agents status [--agent "Agent Name"] diff --git a/src/__tests__/cli.e2e.test.ts b/src/__tests__/cli.e2e.test.ts index 7f88357..b4ed19c 100644 --- a/src/__tests__/cli.e2e.test.ts +++ b/src/__tests__/cli.e2e.test.ts @@ -179,6 +179,19 @@ describe("CLI End-to-End Tests", () => { expect(result.exitCode).toBe(0); expect(result.stdout).toMatch(/\d+\.\d+\.\d+/); }); + + it.each([ + ["agents push", ["agents", "push", "--help"]], + ["tests push", ["tests", "push", "--help"]], + ["tools push", ["tools", "push", "--help"]], + ])("should show %s help without running the command", async (_name, args) => { + const result = await runCli(args); + + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain("Usage:"); + expect(result.stderr).not.toContain("Error during"); + expect(result.stderr).not.toContain("not found"); + }); }); describe("[local] init command", () => { @@ -320,9 +333,10 @@ describe("CLI End-to-End Tests", () => { it("should show help for push-tools command", async () => { const result = await runCli(["tools", "push", "--help", "--no-ui"]); - // Command should be recognized, even if help doesn't work perfectly - // The important thing is it's not "unknown command" + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain("Usage:"); expect(result.stderr).not.toContain("unknown command"); + expect(result.stderr).not.toContain("Error during"); }); it("should recognize push-tools command with dry-run option", async () => { @@ -357,6 +371,32 @@ describe("CLI End-to-End Tests", () => { }); }); + describe("[local] tests push command", () => { + it("should dry-run discovered test configs without tests.json", async () => { + const configPath = path.join(tempDir, "test_configs", "nested", "folder-test.json"); + await fs.ensureDir(path.dirname(configPath)); + await fs.writeJson(configPath, { + name: "Folder Test", + type: "llm", + chat_history: [ + { role: "user", time_in_call_secs: 1, message: "Hello" }, + ], + success_condition: "Agent responds helpfully", + }); + await fs.writeJson(path.join(tempDir, "test_configs", "metadata.json"), { + name: "metadata-only", + }); + + const result = await runCli(["tests", "push", "--dry-run"]); + + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain("Discovered 1 test config(s)"); + expect(result.stdout).toContain("[DRY RUN] Would create test: Folder Test"); + expect(result.stdout).toContain("Skipping non-test JSON file"); + expect(result.stderr).not.toContain("tests.json not found"); + }); + }); + // Tests that require API key - Auth Flow describeIfApiKey("[integration read] auth flow", () => { beforeEach(async () => { diff --git a/src/agents/commands/index.ts b/src/agents/commands/index.ts index fa98d26..4defd68 100644 --- a/src/agents/commands/index.ts +++ b/src/agents/commands/index.ts @@ -48,12 +48,9 @@ export function createAgentsCommand(): Command { const agents = new Command('agents'); agents.description('Manage ElevenLabs agents'); - // Disable default help - agents.helpOption(false); + agents.helpOption('-h, --help', 'Display help information'); agents.addHelpCommand(false); - // Add custom help option - agents.option('-h, --help', 'Display help information'); agents.option('--human-friendly', 'Enable interactive terminal UI'); // Custom action when agents command is run without subcommands diff --git a/src/auth/commands/index.ts b/src/auth/commands/index.ts index d51a341..5f6722d 100644 --- a/src/auth/commands/index.ts +++ b/src/auth/commands/index.ts @@ -28,12 +28,9 @@ export function createAuthCommand(): Command { const auth = new Command('auth'); auth.description('Authentication and configuration commands'); - // Disable default help - auth.helpOption(false); + auth.helpOption('-h, --help', 'Display help information'); auth.addHelpCommand(false); - // Add custom help option - auth.option('-h, --help', 'Display help information'); auth.option('--human-friendly', 'Enable interactive terminal UI'); // Custom action when auth command is run without subcommands diff --git a/src/cli.ts b/src/cli.ts index 0946ef4..35a7713 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -52,22 +52,7 @@ program .description('ElevenLabs CLI') .version(version) .option('--human-friendly', 'Enable interactive terminal UI') - .configureHelp({ - // Override the default help to use our Ink UI - formatHelp: () => '' - }) - .helpOption('-h, --help', 'Display help information') - .on('option:help', async () => { - if (process.argv.includes('--human-friendly')) { - const { waitUntilExit } = render( - React.createElement(HelpView) - ); - await waitUntilExit(); - } else { - printPlainHelp(); - } - process.exit(0); - }); + .helpOption('-h, --help', 'Display help information'); // Add new command groups program.addCommand(createAuthCommand()); diff --git a/src/components/commands/index.ts b/src/components/commands/index.ts index 5e696ae..f72ec37 100644 --- a/src/components/commands/index.ts +++ b/src/components/commands/index.ts @@ -22,12 +22,9 @@ export function createComponentsCommand(): Command { const components = new Command('components'); components.description('Import components from the ElevenLabs UI registry (https://ui.elevenlabs.io)'); - // Disable default help - components.helpOption(false); + components.helpOption('-h, --help', 'Display help information'); components.addHelpCommand(false); - // Add custom help option - components.option('-h, --help', 'Display help information'); components.option('--human-friendly', 'Enable interactive terminal UI'); // Custom action when components command is run without subcommands diff --git a/src/tests/commands/impl.ts b/src/tests/commands/impl.ts index 99b5d33..acdd497 100644 --- a/src/tests/commands/impl.ts +++ b/src/tests/commands/impl.ts @@ -23,6 +23,95 @@ interface TestsConfig { tests: TestDefinition[]; } +interface TestConfigFile { + id?: unknown; + name?: unknown; + type?: unknown; + chat_history?: unknown; + success_condition?: unknown; +} + +function toConfigPath(filePath: string): string { + const relativePath = path.relative(process.cwd(), filePath); + if ( + relativePath && + relativePath !== '..' && + !relativePath.startsWith(`..${path.sep}`) && + !path.isAbsolute(relativePath) + ) { + return relativePath.split(path.sep).join(path.posix.sep); + } + return filePath; +} + +function isTestConfigFile(config: TestConfigFile): boolean { + return Array.isArray(config.chat_history) || typeof config.success_condition === 'string'; +} + +async function discoverJsonFiles(directory: string): Promise { + const root = path.resolve(directory); + if (!(await fs.pathExists(root))) { + return []; + } + + const discovered: string[] = []; + + async function walk(currentDirectory: string): Promise { + const entries = await fs.readdir(currentDirectory, { withFileTypes: true }); + for (const entry of entries) { + const fullPath = path.join(currentDirectory, entry.name); + if (entry.isDirectory()) { + await walk(fullPath); + } else if (entry.isFile() && entry.name.endsWith('.json')) { + discovered.push(fullPath); + } + } + } + + await walk(root); + return discovered + .sort((a, b) => a.localeCompare(b)) + .map(toConfigPath); +} + +async function registerDiscoveredTestConfigs(testsConfig: TestsConfig, configDir: string): Promise { + const existingConfigPaths = new Set( + testsConfig.tests.map(test => path.resolve(test.config)) + ); + + let added = 0; + const configFiles = await discoverJsonFiles(configDir); + + for (const configPath of configFiles) { + if (existingConfigPaths.has(path.resolve(configPath))) { + continue; + } + + let testConfig: TestConfigFile; + try { + testConfig = await readConfig(configPath); + } catch (error) { + console.log(`Warning: Skipping unreadable test config ${configPath}: ${error}`); + continue; + } + + if (!isTestConfigFile(testConfig)) { + console.log(`Warning: Skipping non-test JSON file ${configPath}`); + continue; + } + + testsConfig.tests.push({ + config: configPath, + id: typeof testConfig.id === 'string' ? testConfig.id : undefined, + type: typeof testConfig.type === 'string' ? testConfig.type : undefined + }); + existingConfigPaths.add(path.resolve(configPath)); + added++; + } + + return added; +} + // Helper function for prompting confirmation async function promptForConfirmation(message: string): Promise { const readline = await import('readline'); @@ -97,14 +186,22 @@ async function addTest(name: string, templateType: string = "basic-llm"): Promis process.exit(1); } } -async function pushTests(testId?: string, dryRun = false): Promise { +async function pushTests(testId?: string, dryRun = false, configDir = 'test_configs'): Promise { // Load tests configuration const testsConfigPath = path.resolve(TESTS_CONFIG_FILE); - if (!(await fs.pathExists(testsConfigPath))) { - throw new Error('tests.json not found. Run \'elevenlabs add-test\' first.'); + const testsConfigExists = await fs.pathExists(testsConfigPath); + const testsConfig = testsConfigExists + ? await readConfig(testsConfigPath) + : { tests: [] }; + + const discoveredCount = await registerDiscoveredTestConfigs(testsConfig, configDir); + if (discoveredCount > 0) { + console.log(`Discovered ${discoveredCount} test config(s) in ${configDir}`); } - const testsConfig = await readConfig(testsConfigPath); + if (!testsConfigExists && testsConfig.tests.length === 0) { + throw new Error(`tests.json not found and no test configs found in ${configDir}. Run 'elevenlabs tests add' first.`); + } // Filter tests by test ID if specified let testsToProcess = testsConfig.tests; @@ -118,7 +215,7 @@ async function pushTests(testId?: string, dryRun = false): Promise { console.log(`Pushing ${testsToProcess.length} test(s) to ElevenLabs...`); - let changesMade = false; + let changesMade = discoveredCount > 0 && !dryRun; for (const testDef of testsToProcess) { const configPath = testDef.config; @@ -147,7 +244,7 @@ async function pushTests(testId?: string, dryRun = false): Promise { console.log(`${testDefName}: Will push (force override)`); if (dryRun) { - console.log(`[DRY RUN] Would update test: ${testDefName}`); + console.log(`[DRY RUN] Would ${testId ? 'update' : 'create'} test: ${testDefName}`); continue; } diff --git a/src/tests/commands/index.ts b/src/tests/commands/index.ts index 49a4e76..d150625 100644 --- a/src/tests/commands/index.ts +++ b/src/tests/commands/index.ts @@ -15,7 +15,7 @@ function printTestsHelp() { const commands = [ { name: 'add ', description: 'Add a new test', options: ['--template