import 'dart:io'; import 'package:args/args.dart'; import 'package:args/command_runner.dart'; import 'package:code_work_spawner/code_work_spawner.dart'; Future main(List arguments) async { final runner = _CodeWorkSpawnerCommandRunner(); try { await runner.run(arguments); } on UsageException catch (error) { stdout.writeln(error); exitCode = 64; } } class _CodeWorkSpawnerCommandRunner extends CommandRunner { _CodeWorkSpawnerCommandRunner() : super( 'code_work_spawner', 'Repository issue thread assistant with an Agent Orchestrator style YAML config.', ) { argParser ..addFlag( 'once', abbr: '1', help: 'Run a single reconciliation cycle and exit.', negatable: false, ) ..addOption( 'config', abbr: 'c', help: 'Path to agent-orchestrator.yaml.', defaultsTo: 'agent-orchestrator.yaml', ) ..addOption( 'db', help: 'Path to the SQLite database file.', defaultsTo: '.code_work_spawner.sqlite3', ) ..addOption( 'gh-command', help: 'Path to the gh executable.', defaultsTo: 'gh', ) ..addOption( 'glab-command', help: 'Path to the glab executable.', defaultsTo: 'glab', ) ..addOption( 'tea-command', help: 'Path to the tea executable.', defaultsTo: 'tea', ) ..addOption( 'gosmee-command', help: 'Path to the gosmee executable.', defaultsTo: 'gosmee', ); addCommand(_InitConfigCommand()); } @override Future runCommand(ArgResults topLevelResults) async { if (topLevelResults['help'] as bool) { stdout.writeln(usage); return; } if (topLevelResults.command != null) { await super.runCommand(topLevelResults); return; } final configPath = topLevelResults['config'] as String; AppEnvironment.loadForConfig(configPath); _configureLogging(); final config = await AppConfig.load(configPath); final logger = AppLogger('code_work_spawner.cli'); logger.debug( 'startup once=${topLevelResults["once"]} ' 'config=${topLevelResults["config"]} ' 'db=${topLevelResults["db"]}', ); logger.info( 'loaded config=${config.configPath} ' 'projects=${config.projects.length} ' 'worktreeDir=${config.worktreeDir ?? "(system temp)"}', ); for (final project in config.projects.values) { final repoDirectory = Directory(project.path); final eventSource = project.issueAssistant.eventSource; logger.info( 'project=${project.key} ' 'repo=${project.repo} ' 'path=${project.path} ' 'exists=${await repoDirectory.exists()} ' 'enabled=${project.issueAssistant.enabled} ' 'tracker_provider=${project.provider.name} ' 'scm_provider=${project.scm.provider.name} ' 'event_source=${_eventSourceKindValue(eventSource.type)}' '${_cliEventSourceSettings(eventSource)}', ); } final app = await IssueAssistantApp.open( config: config, databasePath: topLevelResults['db'] as String, ghCommand: topLevelResults['gh-command'] as String, glabCommand: topLevelResults['glab-command'] as String, gosmeeCommand: topLevelResults['gosmee-command'] as String, teaCommand: topLevelResults['tea-command'] as String, ); try { if (topLevelResults['once'] as bool) { await app.runOnce(); return; } await app.run(); } finally { await app.close(); } } } String _cliEventSourceSettings(IssueEventSourceConfig eventSource) { return switch (eventSource) { PollingIssueEventSourceConfig(:final pollInterval) => ' poll_interval=$pollInterval', ChannelIssueEventSourceConfig( :final reconciliation, :final reconcileInterval, ) => ' reconciliation=${_reconciliationKindValue(reconciliation)} ' 'reconcile_interval=$reconcileInterval', }; } String _eventSourceKindValue(IssueTrackerEventSourceKind value) { return switch (value) { IssueTrackerEventSourceKind.ghPolling => 'gh/polling', IssueTrackerEventSourceKind.glabPolling => 'glab/polling', IssueTrackerEventSourceKind.teaPolling => 'tea/polling', IssueTrackerEventSourceKind.opPolling => 'op/polling', IssueTrackerEventSourceKind.ghGosmee => 'gh/gosmee', IssueTrackerEventSourceKind.glabGosmee => 'glab/gosmee', IssueTrackerEventSourceKind.ghWebhookForward => 'gh/webhook-forward', IssueTrackerEventSourceKind.teaGosmee => 'tea/gosmee', }; } String _reconciliationKindValue(IssueTrackerReconciliationKind value) { return switch (value) { IssueTrackerReconciliationKind.startupOnly => 'startup-only', IssueTrackerReconciliationKind.continuous => 'continuous', }; } class _InitConfigCommand extends Command { @override String get name => 'init-config'; @override String get description => 'Print or write a starter config file.'; @override String get invocation => 'code_work_spawner init-config [arguments]'; _InitConfigCommand() { argParser ..addOption( 'output', abbr: 'o', help: 'Write the starter config to a file.', ) ..addFlag( 'force', abbr: 'f', help: 'Overwrite the output file if it already exists.', negatable: false, ); } @override Future run() async { final outputPath = argResults!['output'] as String?; final force = argResults!['force'] as bool; final content = AppConfig.renderInitialConfig(); if (outputPath == null) { stdout.writeln(content); return; } final outputFile = File(outputPath); if (await outputFile.exists() && !force) { stderr.writeln( 'Refusing to overwrite existing file: $outputPath. Use --force to replace it.', ); exitCode = 2; return; } await outputFile.parent.create(recursive: true); await outputFile.writeAsString('$content\n'); stdout.writeln('Wrote starter config to ${outputFile.path}'); } } void _configureLogging() { AppLogger.configure( minimumLevel: AppLogger.resolveMinimumLevel(AppEnvironment.variables), useColors: stdout.hasTerminal, ); }