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
4 changes: 3 additions & 1 deletion packages/vgv_ai_cli/lib/src/command_runner.dart
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,9 @@ ${lightYellow.wrap('Update available!')} ${lightCyan.wrap(packageVersion)} \u219
Run ${lightCyan.wrap('$executableName update')} to update''');
}
} on Exception catch (_) {
_logger.err('Failed to check for updates.');
// Best effort: the check runs after every command, so a user who is
// offline, or behind a proxy that blocks pub.dev, should not be told
// about it.
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import 'dart:io' show Platform;

import 'package:args/command_runner.dart';
import 'package:collection/collection.dart';
import 'package:file/file.dart';
Expand Down Expand Up @@ -44,10 +42,13 @@ class DoctorCommand({
final Translator _translator = const Translator(),
final Reconciler _reconciler = const Reconciler(),
}) extends Command<int> with HarnessSelection {
/// {@macro doctor_command}
this {
addHarnessOptions('Check the Very Good AI tooling for');
}

/// The name the CLI registers `doctor` under, so other code can name it
/// without constructing one.
static const String commandName = 'doctor';

/// The harnesses that load the shared skills, so whose `install` fixes
Expand All @@ -66,7 +67,7 @@ class DoctorCommand({

@override
Future<int> run() async {
final home = _home ?? userHome(_environment ?? Platform.environment);
final home = commandHome(_home, _environment);
if (home == null) {
_logger.err(
'Could not find your home directory. Set HOME and run doctor again.',
Expand Down Expand Up @@ -105,8 +106,8 @@ class DoctorCommand({
..info('Run ${lightCyan.wrap('$fix')} to fix everything install can.');
if (fix.force) {
_logger.info(
'--force applies to every harness listed, replacing your values '
"wherever they conflict with VGV's.",
'${lightCyan.wrap('--force')} applies to every harness listed, '
"replacing your values wherever they conflict with VGV's.",
);
}
}
Expand Down Expand Up @@ -180,7 +181,7 @@ class DoctorCommand({
manifest,
selected,
).entries) {
switch (await _resolver.resolve(home, only: only)) {
switch (await _resolver.resolve(home, only: only, log: _logger.detail)) {
case ResolvedAssets(:final assets):
final translation = await _translator.translate(
assets: assets,
Expand Down
4 changes: 4 additions & 0 deletions packages/vgv_ai_cli/lib/src/commands/harness_selection.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ mixin HarnessSelection on Command<int> {
'only',
help: '$action $only',
allowed: Harness.values.map((harness) => harness.name),
allowedHelp: {
for (final harness in Harness.values)
harness.name: '${harness.displayName}.',
},
);

// Opting out matters more than opting in: the default is every harness
Expand Down
16 changes: 10 additions & 6 deletions packages/vgv_ai_cli/lib/src/commands/install_command.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import 'dart:io' show Platform;

import 'package:args/command_runner.dart';
import 'package:collection/collection.dart';
import 'package:file/file.dart';
Expand Down Expand Up @@ -35,6 +33,7 @@ class InstallCommand({
final Translator _translator = const Translator(),
final Reconciler _reconciler = const Reconciler(),
}) extends Command<int> with HarnessSelection {
/// {@macro install_command}
this {
argParser
..addFlag(
Expand All @@ -57,6 +56,8 @@ class InstallCommand({
addHarnessOptions('Configure the Very Good AI tooling for');
}

/// The name the CLI registers `install` under, so other code can name it
/// without constructing one.
static const String commandName = 'install';

/// The outcomes that leave a file with VGV's contributions in it.
Expand All @@ -80,9 +81,7 @@ class InstallCommand({
String get name => commandName;

/// The directory install configures, or null when it cannot be found.
Directory? get home {
return _home ?? userHome(_environment ?? Platform.environment);
}
Directory? get home => commandHome(_home, _environment);

@override
Future<int> run() async {
Expand Down Expand Up @@ -116,6 +115,7 @@ class InstallCommand({
_logger,
nothingDone: 'Installed nothing',
done: 'written',
retry: 'install',
);
final manifest = store.load();
if (manifest == null) return ExitCode.ioError.code;
Expand Down Expand Up @@ -232,7 +232,11 @@ class InstallCommand({
Future<ResolvedAssets?> _resolveAssets(Directory home) async {
final progress = _logger.progress('Resolving plugins');

switch (await _resolver.resolve(home, only: _selectedPlugins)) {
switch (await _resolver.resolve(
home,
only: _selectedPlugins,
log: _logger.detail,
)) {
case final ResolvedAssets resolved:
progress.complete(
'Resolved ${resolved.assets.skills.length} skills and '
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,28 @@ class const ReportingManifestStore(

/// What the command did to the files it records, such as `written`.
required final String _done,

/// The command to run again once the manifest can be read, such as
/// `install`.
required final String _retry,
}) {
/// {@macro reporting_manifest_store}
this;

/// The manifest in the store, or null, having said why, when it cannot be
/// read or was written by a newer version.
InstallManifest? load() {
try {
final manifest = _store.load();
if (manifest == null) {
_logger.err(
'$_nothingDone, because ${ManifestStore.unreadable}: '
'${_store.file.path}',
);
_logger
..err(
'$_nothingDone, because ${ManifestStore.unreadable}: '
'${_store.file.path}',
)
..err(
'Run ${lightCyan.wrap('vgv_ai update')}, then run $_retry again.',
);
}
return manifest;
} on FileSystemException catch (error) {
Expand Down
8 changes: 5 additions & 3 deletions packages/vgv_ai_cli/lib/src/commands/uninstall_command.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import 'dart:io' show Platform;

import 'package:args/command_runner.dart';
import 'package:file/file.dart';
import 'package:mason_logger/mason_logger.dart';
Expand Down Expand Up @@ -32,6 +30,7 @@ class UninstallCommand({
final Map<String, String>? _environment,
final ManifestAuditor _auditor = const ManifestAuditor(),
}) extends Command<int> with HarnessSelection {
/// {@macro uninstall_command}
this {
argParser
..addFlag(
Expand All @@ -50,6 +49,8 @@ class UninstallCommand({
);
}

/// The name the CLI registers `uninstall` under, so other code can name it
/// without constructing one.
static const String commandName = 'uninstall';

/// The outcomes where something the user changed was kept.
Expand Down Expand Up @@ -78,7 +79,7 @@ class UninstallCommand({
final only = onlyHarnesses;
final selected = only ?? Harness.values.where(isEnabled).toSet();

final home = _home ?? userHome(_environment ?? Platform.environment);
final home = commandHome(_home, _environment);
if (home == null) {
_logger.err(
'Could not find your home directory. Set HOME and run uninstall '
Expand All @@ -92,6 +93,7 @@ class UninstallCommand({
_logger,
nothingDone: 'Removed nothing',
done: 'removed',
retry: 'uninstall',
);
final manifest = store.load();
if (manifest == null) return ExitCode.ioError.code;
Expand Down
7 changes: 7 additions & 0 deletions packages/vgv_ai_cli/lib/src/commands/user_home.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import 'dart:io' show Platform;

import 'package:file/file.dart';
import 'package:file/local.dart';

Expand All @@ -6,3 +8,8 @@ Directory? userHome(Map<String, String> environment) {
final home = environment['HOME'] ?? environment['USERPROFILE'];
return home == null ? null : const LocalFileSystem().directory(home);
}

/// The directory a command works on: [override] when one was injected, and
/// otherwise the home directory [environment], or the real one, names.
Directory? commandHome(Directory? override, Map<String, String>? environment) =>
override ?? userHome(environment ?? Platform.environment);
12 changes: 11 additions & 1 deletion packages/vgv_ai_cli/lib/src/resolve/plugin_resolver.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,25 @@ class const PluginResolver({
final PluginParser _parser = const ClaudePluginParser(),
final List<PluginRef> _plugins = vgvPlugins,
}) {
/// {@macro plugin_resolver}
this;

/// Resolves the plugins [only] names for [home], or every pinned one when
/// it names none, stopping at the first that cannot be resolved or parsed:
/// a half-configured harness is worse than an unconfigured one.
Future<AssetResolution> resolve(Directory home, {Set<String>? only}) async {
///
/// [log] receives the git command line and its stderr, for `--verbose`.
Future<AssetResolution> resolve(
Directory home, {
Set<String>? only,
LogDetail? log,
}) async {
final sources = [
LocalCachePluginSource(home: home),
GitPluginSource(
cacheRoot: home.childDirectory('.vgv_ai').childDirectory('plugins'),
runGit: (args) => _runProcess('git', args),
log: log,
),
];

Expand Down
77 changes: 77 additions & 0 deletions packages/vgv_ai_cli/test/helpers/test_runner.dart
Comment thread
marcossevilla marked this conversation as resolved.
Resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import 'package:mason_logger/mason_logger.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pub_updater/pub_updater.dart';
import 'package:vgv_ai_cli/src/command_runner.dart';
import 'package:vgv_ai_cli/src/commands/commands.dart';
import 'package:vgv_ai_cli/src/version.dart';

class _MockLogger extends Mock implements Logger;

class _MockProgress extends Mock implements Progress;

class _MockPubUpdater extends Mock implements PubUpdater;

/// {@template test_runner}
/// The mocked collaborators a command test runs against, stubbed the way
/// every one of them needs: the CLI is already up to date, and the progress
/// [logger] hands out records its messages in [progressLogs].
///
/// Tests that need another answer re-stub [logger], [progress] or
/// [pubUpdater] themselves.
/// {@endtemplate}
class TestRunner() {
/// {@macro test_runner}
this {
when(() => logger.progress(any())).thenReturn(progress);
when(() => progress.complete(any())).thenAnswer((invocation) {
final message = invocation.positionalArguments.first as String?;
if (message != null) progressLogs.add(message);
});
when(() => pubUpdater.getLatestVersion(any()))
.thenAnswer((_) async => packageVersion);
when(
() => pubUpdater.isUpToDate(
packageName: any(named: 'packageName'),
currentVersion: any(named: 'currentVersion'),
),
).thenAnswer((_) async => true);
}

/// The logger the runner and its commands write to.
final Logger logger = _MockLogger();

/// The progress [logger] returns.
final Progress progress = _MockProgress();

/// The updater the runner checks the latest version with.
final PubUpdater pubUpdater = _MockPubUpdater();

/// What [progress] was completed with, in order.
final List<String> progressLogs = <String>[];

/// A runner carrying [logger] and [pubUpdater], with the given commands
/// replacing the ones that would otherwise reach the real home directory.
VgvAiCliCommandRunner build({
InstallCommand? installCommand,
DoctorCommand? doctorCommand,
UninstallCommand? uninstallCommand,
}) => VgvAiCliCommandRunner(
logger: logger,
pubUpdater: pubUpdater,
installCommand: installCommand,
doctorCommand: doctorCommand,
uninstallCommand: uninstallCommand,
);

/// Runs [args] through a runner from [build].
Future<int> run(
List<String> args, {
InstallCommand? installCommand,
DoctorCommand? doctorCommand,
UninstallCommand? uninstallCommand,
}) => build(
installCommand: installCommand,
doctorCommand: doctorCommand,
uninstallCommand: uninstallCommand,
).run(args);
}
36 changes: 9 additions & 27 deletions packages/vgv_ai_cli/test/src/command_runner_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,7 @@ import 'package:test/test.dart';
import 'package:vgv_ai_cli/src/command_runner.dart';
import 'package:vgv_ai_cli/src/version.dart';

class _MockLogger extends Mock implements Logger;

class _MockProgress extends Mock implements Progress;

class _MockPubUpdater extends Mock implements PubUpdater;
import '../helpers/test_runner.dart';

class _FlagCommand() extends Command<int> {
this {
Expand Down Expand Up @@ -46,17 +42,10 @@ Run ${lightCyan.wrap('$executableName update')} to update''';
late VgvAiCliCommandRunner commandRunner;

setUp(() {
pubUpdater = _MockPubUpdater();

when(() => pubUpdater.getLatestVersion(any()))
.thenAnswer((_) async => packageVersion);

logger = _MockLogger();

commandRunner = VgvAiCliCommandRunner(
logger: logger,
pubUpdater: pubUpdater,
);
final runner = TestRunner();
pubUpdater = runner.pubUpdater;
logger = runner.logger;
commandRunner = runner.build();
});

test('shows update message when newer version exists', () async {
Expand All @@ -68,12 +57,13 @@ Run ${lightCyan.wrap('$executableName update')} to update''';
verify(() => logger.info(updatePrompt)).called(1);
});

test('shows error message when failed to check for updates', () async {
test('stays quiet when the update check fails', () async {
when(() => pubUpdater.getLatestVersion(any()))
.thenThrow(Exception('Failed to check for updates'));

await commandRunner.run(['--version']);
verify(() => logger.err('Failed to check for updates.')).called(1);
final result = await commandRunner.run(['--version']);
expect(result, equals(ExitCode.success.code));
verifyNever(() => logger.err(any()));
});

test('Does not show update message when the shell calls the '
Expand Down Expand Up @@ -104,14 +94,6 @@ Run ${lightCyan.wrap('$executableName update')} to update''';
),
).thenAnswer((_) async => true);

final progress = _MockProgress();
final progressLogs = <String>[];
when(() => progress.complete(any())).thenAnswer((answer) {
final message = answer.positionalArguments.elementAt(0) as String?;
if (message != null) progressLogs.add(message);
});
when(() => logger.progress(any())).thenReturn(progress);

final result = await commandRunner.run(['update']);
expect(result, ExitCode.success.code);
verifyNever(() => logger.info(updatePrompt));
Expand Down
Loading
Loading