fix: init-config command not shown in help

This commit is contained in:
insleker 2026-04-05 17:40:24 +08:00
parent 86ca0fc4a6
commit 1f2a803a7f
4 changed files with 155 additions and 101 deletions

1
.gitignore vendored
View File

@ -12,3 +12,4 @@ agent-orchestrator.yaml
/skills/*
!/skills/dart-gherkin-tests
*.g.dart
**/*.exe

View File

@ -1,123 +1,152 @@
import 'dart:io';
import 'package:args/args.dart';
import 'package:args/command_runner.dart';
import 'package:code_work_spawner/code_work_spawner.dart';
import 'package:logging/logging.dart';
Future<void> main(List<String> arguments) async {
_configureLogging();
final parser = ArgParser()
..addFlag(
'once',
abbr: '1',
help: 'Run a single poll 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',
)
..addFlag('help', abbr: 'h', negatable: false);
final initConfigParser = parser.addCommand('init-config')
..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,
)
..addFlag('help', abbr: 'h', negatable: false);
final results = parser.parse(arguments);
final command = results.command;
if (command?.name == 'init-config') {
if (command!['help'] as bool) {
stdout.writeln(initConfigParser.usage);
return;
}
await _runInitConfig(command);
return;
}
if (results['help'] as bool) {
stdout.writeln(parser.usage);
return;
}
final config = await AppConfig.load(results['config'] as String);
final logger = Logger('code_work_spawner.cli');
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);
logger.info(
'project=${project.key} '
'repo=${project.repo} '
'path=${project.path} '
'exists=${await repoDirectory.exists()} '
'enabled=${project.issueAssistant.enabled}',
);
}
final app = await IssueAssistantApp.open(
config: config,
databasePath: results['db'] as String,
ghCommand: results['gh-command'] as String,
);
final runner = _CodeWorkSpawnerCommandRunner();
try {
if (results['once'] as bool) {
await app.runOnce();
return;
}
await app.run();
} finally {
await app.close();
await runner.run(arguments);
} on UsageException catch (error) {
stdout.writeln(error);
exitCode = 64;
}
}
Future<void> _runInitConfig(ArgResults command) async {
final outputPath = command['output'] as String?;
final force = command['force'] as bool;
final content = AppConfig.renderInitialConfig();
class _CodeWorkSpawnerCommandRunner extends CommandRunner<void> {
_CodeWorkSpawnerCommandRunner()
: super(
'code_work_spawner',
'GitHub issue thread assistant with an Agent Orchestrator style YAML config.',
) {
argParser
..addFlag(
'once',
abbr: '1',
help: 'Run a single poll 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',
);
if (outputPath == null) {
stdout.writeln(content);
return;
addCommand(_InitConfigCommand());
}
final outputFile = File(outputPath);
if (await outputFile.exists() && !force) {
stderr.writeln(
'Refusing to overwrite existing file: $outputPath. Use --force to replace it.',
@override
Future<void> runCommand(ArgResults topLevelResults) async {
if (topLevelResults['help'] as bool) {
stdout.writeln(usage);
return;
}
if (topLevelResults.command != null) {
await super.runCommand(topLevelResults);
return;
}
final config = await AppConfig.load(topLevelResults['config'] as String);
final logger = Logger('code_work_spawner.cli');
logger.info(
'loaded config=${config.configPath} '
'projects=${config.projects.length} '
'worktreeDir=${config.worktreeDir ?? "(system temp)"}',
);
exitCode = 2;
return;
for (final project in config.projects.values) {
final repoDirectory = Directory(project.path);
logger.info(
'project=${project.key} '
'repo=${project.repo} '
'path=${project.path} '
'exists=${await repoDirectory.exists()} '
'enabled=${project.issueAssistant.enabled}',
);
}
final app = await IssueAssistantApp.open(
config: config,
databasePath: topLevelResults['db'] as String,
ghCommand: topLevelResults['gh-command'] as String,
);
try {
if (topLevelResults['once'] as bool) {
await app.runOnce();
return;
}
await app.run();
} finally {
await app.close();
}
}
}
class _InitConfigCommand extends Command<void> {
@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,
);
}
await outputFile.parent.create(recursive: true);
await outputFile.writeAsString('$content\n');
stdout.writeln('Wrote starter config to ${outputFile.path}');
@override
Future<void> 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() {

View File

@ -21,6 +21,7 @@ dependencies:
meta: ^1.16.0
github: ^9.25.0
git: ^2.3.2
nyxx: ^6.8.1
dev_dependencies:
build_runner: ^2.6.0

View File

@ -13,6 +13,29 @@ void main() {
/// So that I can bootstrap configuration without reading the code first
/// ```
group('code_work_spawner init-config', () {
/// ```gherkin
/// Scenario: Show init-config in top-level help
/// Given the CLI is executed with the top-level help flag
/// When the process completes successfully
/// Then stdout lists the init-config command in the available commands section
/// ```
test('Show init-config in top-level help', () async {
// Given the CLI is executed with the top-level help flag.
final result = await Process.run(Platform.resolvedExecutable, [
'run',
'bin/code_work_spawner.dart',
'--help',
], workingDirectory: Directory.current.path);
// When the process completes successfully.
expect(result.exitCode, 0, reason: '${result.stderr}');
// Then stdout lists the init-config command in the available commands section.
final stdoutText = result.stdout as String;
expect(stdoutText, contains('Available commands:'));
expect(stdoutText, contains('init-config'));
});
/// ```gherkin
/// Scenario: Print the starter config to stdout
/// Given the CLI is executed with the init-config command