feat(cli): add CLI entry point with commander and start command
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
import type { Command } from 'commander';
|
||||
|
||||
export function registerConfigCommand(program: Command): void {
|
||||
program
|
||||
.command('config')
|
||||
.description('Show resolved configuration (secrets redacted)')
|
||||
.option('-c, --config <path>', 'Config file path')
|
||||
.action(async (_opts: { config?: string }) => {
|
||||
console.error('Not yet implemented');
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import type { Command } from 'commander';
|
||||
|
||||
export function registerDoctorCommand(program: Command): void {
|
||||
program
|
||||
.command('doctor')
|
||||
.description('Validate configuration and check system health')
|
||||
.option('-c, --config <path>', 'Config file path')
|
||||
.action(async (_opts: { config?: string }) => {
|
||||
console.error('Not yet implemented');
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { createProgram } from './index.js';
|
||||
|
||||
describe('CLI program', () => {
|
||||
it('creates a commander program with expected commands', () => {
|
||||
const program = createProgram();
|
||||
const commandNames = program.commands.map((c) => c.name());
|
||||
|
||||
expect(commandNames).toContain('start');
|
||||
expect(commandNames).toContain('tui');
|
||||
expect(commandNames).toContain('send');
|
||||
expect(commandNames).toContain('sessions');
|
||||
expect(commandNames).toContain('doctor');
|
||||
expect(commandNames).toContain('config');
|
||||
});
|
||||
|
||||
it('has version info', () => {
|
||||
const program = createProgram();
|
||||
expect(program.version()).toBeDefined();
|
||||
});
|
||||
|
||||
it('has description', () => {
|
||||
const program = createProgram();
|
||||
expect(program.description()).toContain('AI');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
#!/usr/bin/env node
|
||||
import { Command } from 'commander';
|
||||
import { registerStartCommand } from './start.js';
|
||||
import { registerSendCommand } from './send.js';
|
||||
import { registerSessionsCommand } from './sessions.js';
|
||||
import { registerDoctorCommand } from './doctor.js';
|
||||
import { registerConfigCommand } from './config-cmd.js';
|
||||
import { registerTuiCommand } from './tui.js';
|
||||
|
||||
export function createProgram(): Command {
|
||||
const program = new Command();
|
||||
|
||||
program
|
||||
.name('flynn')
|
||||
.description('Flynn — self-hosted personal AI agent')
|
||||
.version('0.1.0');
|
||||
|
||||
registerStartCommand(program);
|
||||
registerTuiCommand(program);
|
||||
registerSendCommand(program);
|
||||
registerSessionsCommand(program);
|
||||
registerDoctorCommand(program);
|
||||
registerConfigCommand(program);
|
||||
|
||||
return program;
|
||||
}
|
||||
|
||||
// Only run when executed directly (not imported in tests)
|
||||
const isDirectRun = process.argv[1] &&
|
||||
(process.argv[1].endsWith('/cli/index.js') ||
|
||||
process.argv[1].endsWith('/cli/index.ts'));
|
||||
|
||||
if (isDirectRun) {
|
||||
const program = createProgram();
|
||||
program.parse(process.argv);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import type { Command } from 'commander';
|
||||
|
||||
export function registerSendCommand(program: Command): void {
|
||||
program
|
||||
.command('send <message>')
|
||||
.description('Send a one-shot message and print the response')
|
||||
.option('-c, --config <path>', 'Config file path')
|
||||
.action(async (_message: string, _opts: { config?: string }) => {
|
||||
console.error('Not yet implemented');
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import type { Command } from 'commander';
|
||||
|
||||
export function registerSessionsCommand(program: Command): void {
|
||||
program
|
||||
.command('sessions')
|
||||
.description('List active sessions')
|
||||
.option('-c, --config <path>', 'Config file path')
|
||||
.action(async (_opts: { config?: string }) => {
|
||||
console.error('Not yet implemented');
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
@@ -76,8 +76,9 @@ models:
|
||||
models: { default: { provider: 'anthropic', model: 'claude' } },
|
||||
};
|
||||
const redacted = redactSecrets(config);
|
||||
expect(redacted.telegram.bot_token).toBe('***');
|
||||
expect(redacted.telegram.allowed_chat_ids).toEqual([123]);
|
||||
const telegram = redacted.telegram as Record<string, unknown>;
|
||||
expect(telegram.bot_token).toBe('***');
|
||||
expect(telegram.allowed_chat_ids).toEqual([123]);
|
||||
});
|
||||
|
||||
it('redacts api_key in models', () => {
|
||||
@@ -88,7 +89,8 @@ models:
|
||||
},
|
||||
};
|
||||
const redacted = redactSecrets(config);
|
||||
expect(redacted.models.default.api_key).toBe('***');
|
||||
const models = redacted.models as Record<string, Record<string, unknown>>;
|
||||
expect(models.default.api_key).toBe('***');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import type { Command } from 'commander';
|
||||
import { loadConfigSafe, getConfigPath } from './shared.js';
|
||||
import { existsSync } from 'fs';
|
||||
|
||||
export function registerStartCommand(program: Command): void {
|
||||
program
|
||||
.command('start')
|
||||
.description('Start the Flynn daemon')
|
||||
.option('-c, --config <path>', 'Config file path')
|
||||
.action(async (opts: { config?: string }) => {
|
||||
const configPath = opts.config ?? getConfigPath();
|
||||
|
||||
if (!existsSync(configPath)) {
|
||||
console.error(`Config file not found: ${configPath}`);
|
||||
console.error('Run "flynn doctor" to diagnose, or create a config at ~/.config/flynn/config.yaml');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log('Flynn starting...');
|
||||
console.log(`Loading config from: ${configPath}`);
|
||||
|
||||
const { config, error } = loadConfigSafe(configPath);
|
||||
if (!config) {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Dynamic import to avoid loading daemon code for other commands
|
||||
const { startDaemon } = await import('../daemon/index.js');
|
||||
const daemon = await startDaemon(config);
|
||||
|
||||
console.log(`Allowed Telegram chat IDs: ${config.telegram.allowed_chat_ids.join(', ')}`);
|
||||
|
||||
// Keep process alive
|
||||
await new Promise<void>((resolve) => {
|
||||
daemon.lifecycle.onShutdown(async () => resolve());
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import type { Command } from 'commander';
|
||||
|
||||
export function registerTuiCommand(program: Command): void {
|
||||
program
|
||||
.command('tui')
|
||||
.description('Launch the interactive TUI')
|
||||
.option('-f, --fullscreen', 'Start in fullscreen mode')
|
||||
.option('-c, --config <path>', 'Config file path')
|
||||
.action(async (_opts: { fullscreen?: boolean; config?: string }) => {
|
||||
console.error('Not yet implemented');
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user