Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ elevenlabs agents branches list --agent <agent_id> [--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"]
Expand Down
44 changes: 42 additions & 2 deletions src/__tests__/cli.e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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 () => {
Expand Down
5 changes: 1 addition & 4 deletions src/agents/commands/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 1 addition & 4 deletions src/auth/commands/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
17 changes: 1 addition & 16 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down
5 changes: 1 addition & 4 deletions src/components/commands/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
109 changes: 103 additions & 6 deletions src/tests/commands/impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string[]> {
const root = path.resolve(directory);
if (!(await fs.pathExists(root))) {
return [];
}

const discovered: string[] = [];

async function walk(currentDirectory: string): Promise<void> {
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<number> {
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<TestConfigFile>(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<boolean> {
const readline = await import('readline');
Expand Down Expand Up @@ -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<void> {
async function pushTests(testId?: string, dryRun = false, configDir = 'test_configs'): Promise<void> {
// 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<TestsConfig>(testsConfigPath)
: { tests: [] };

const discoveredCount = await registerDiscoveredTestConfigs(testsConfig, configDir);
if (discoveredCount > 0) {
console.log(`Discovered ${discoveredCount} test config(s) in ${configDir}`);
}

const testsConfig = await readConfig<TestsConfig>(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;
Expand All @@ -118,7 +215,7 @@ async function pushTests(testId?: string, dryRun = false): Promise<void> {

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;
Expand Down Expand Up @@ -147,7 +244,7 @@ async function pushTests(testId?: string, dryRun = false): Promise<void> {
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;
}

Expand Down
7 changes: 2 additions & 5 deletions src/tests/commands/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ function printTestsHelp() {
const commands = [
{ name: 'add <name>', description: 'Add a new test', options: ['--template <template> Test template type (default: \'basic-llm\')'] },
{ name: 'delete [test_id]', description: 'Delete a test locally and from ElevenLabs', options: ['--all Delete all tests'] },
{ name: 'push', description: 'Push tests to ElevenLabs API', options: ['--dry-run Show what would be done without making changes'] },
{ name: 'push', description: 'Push tests to ElevenLabs API', options: ['--dry-run Show what would be done without making changes', '--config-dir <directory> Scan directory for configs not listed in tests.json'] },
{ name: 'pull', description: 'Pull tests from ElevenLabs', options: ['--test <test_id> Specific test ID to pull', '--output-dir <directory> Output directory for configs (default: \'test_configs\')', '--dry-run Show what would be done without making changes', '--update Update existing items only, skip new', '--all Pull all (new + existing)'] },
];
for (const cmd of commands) {
Expand All @@ -34,12 +34,9 @@ export function createTestsCommand(): Command {
const tests = new Command('tests');
tests.description('Manage ElevenLabs tests');

// Disable default help
tests.helpOption(false);
tests.helpOption('-h, --help', 'Display help information');
tests.addHelpCommand(false);

// Add custom help option
tests.option('-h, --help', 'Display help information');
tests.option('--human-friendly', 'Enable interactive terminal UI');

// Custom action when tests command is run without subcommands
Expand Down
12 changes: 10 additions & 2 deletions src/tests/commands/push.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,23 @@
import { Command } from 'commander';
import { pushTests } from './impl.js';

interface PushTestsOptions {
dryRun: boolean;
configDir: string;
ui: boolean;
humanFriendly?: boolean;
}

export function createPushCommand(): Command {
return new Command('push')
.description('Push tests to ElevenLabs API')
.option('--dry-run', 'Show what would be done without making changes', false)
.option('--config-dir <directory>', 'Directory to scan for test configs not listed in tests.json', 'test_configs')
.option('--no-ui', 'Disable interactive UI (default, kept for backwards compatibility)')
.option('--human-friendly', 'Enable interactive terminal UI')
.action(async (options: { dryRun: boolean; ui: boolean; humanFriendly?: boolean }) => {
.action(async (options: PushTestsOptions) => {
try {
await pushTests(undefined, options.dryRun);
await pushTests(undefined, options.dryRun, options.configDir);
} catch (error) {
console.error(`Error during push: ${error}`);
process.exit(1);
Expand Down
5 changes: 1 addition & 4 deletions src/tools/commands/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,9 @@ export function createToolsCommand(): Command {
const tools = new Command('tools');
tools.description('Manage ElevenLabs tools');

// Disable default help
tools.helpOption(false);
tools.helpOption('-h, --help', 'Display help information');
tools.addHelpCommand(false);

// Add custom help option
tools.option('-h, --help', 'Display help information');
tools.option('--human-friendly', 'Enable interactive terminal UI');

// Custom action when tools command is run without subcommands
Expand Down
Loading