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
20 changes: 14 additions & 6 deletions tool/lib/src/commands/base_skill_command.dart
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,20 @@ abstract class BaseSkillCommand extends BaseYamlCommand {
super.outputDir,
this.environment,
}) {
argParser.addOption(
'thinking-budget',
help:
'The token budget for the model to "think". Defaults to ${GeminiService.defaultThinkingBudget} (recommended for technical documentation).',
defaultsTo: GeminiService.defaultThinkingBudget.toString(),
);
argParser
..addOption(
'thinking-budget',
help:
'The token budget for the model to "think". Defaults to ${GeminiService.defaultThinkingBudget} (recommended for technical documentation).',
defaultsTo: GeminiService.defaultThinkingBudget.toString(),
)
..addFlag(
'dry-run',
abbr: 'n',
help:
'Simulate the command without making API calls or modifying files.',
negatable: false,
);
}

/// The HTTP client used for fetching resources.
Expand Down
10 changes: 10 additions & 0 deletions tool/lib/src/commands/generate_skill_command.dart
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,16 @@ class GenerateSkillCommand extends BaseSkillCommand {
return;
}

final dryRun = argResults?['dry-run'] as bool? ?? false;
if (dryRun) {
logger
..info(' [DRY RUN] Would generate skill: ${skill.name}')
..info(
' [DRY RUN] Prompt size: ${combinedMarkdown.split(' ').length} tokens.',
);
return;
}

final generatedContent = await gemini.generateSkillContent(
combinedMarkdown,
skill.name,
Expand Down
8 changes: 4 additions & 4 deletions tool/lib/src/commands/update_readme_command.dart
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,9 @@ class UpdateReadmeCommand extends BaseYamlCommand {
final content = readmeFile.readAsStringSync();

// Generate the table
final buffer = StringBuffer();
buffer.writeln('| Skill | Description |');
buffer.writeln('|---|---|');
final buffer = StringBuffer()
..writeln('| Skill | Description |')
..writeln('|---|---|');

// Sort skills by name for consistency
final sortedSkills = List<SkillParams>.from(skills)
Expand All @@ -70,7 +70,7 @@ class UpdateReadmeCommand extends BaseYamlCommand {

// Find where to insert the table
final sectionRegex = RegExp(
r'^## (Available Skills|List of Skills|Skills List|Skill Index)',
'^## (Available Skills|List of Skills|Skills List|Skill Index)',
caseSensitive: false,
multiLine: true,
);
Expand Down
10 changes: 10 additions & 0 deletions tool/lib/src/commands/update_skill_command.dart
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,16 @@ class UpdateSkillCommand extends BaseSkillCommand {
return;
}

final dryRun = argResults?['dry-run'] as bool? ?? false;
if (dryRun) {
logger
..info(' [DRY RUN] Would update skill: ${skill.name}')
..info(
' [DRY RUN] Original file size: ${existingContent.split(' ').length} tokens -> Raw content size: ${combinedMarkdown.split(' ').length} tokens.',
);
return;
}

final generatedContent = await gemini.updateSkillContent(
existingContent,
combinedMarkdown,
Expand Down
10 changes: 10 additions & 0 deletions tool/lib/src/commands/validate_skill_command.dart
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,16 @@ class ValidateSkillCommand extends BaseSkillCommand {
'Unknown';

// Compare
final dryRun = argResults?['dry-run'] as bool? ?? false;
if (dryRun) {
logger
..info(' [DRY RUN] Would validate skill: ${skill.name}')
..info(
' [DRY RUN] existing file size: ${existingSkillFileContent.split(' ').length} tokens -> new fetched content size: ${markdown.split(' ').length} tokens.',
);
return;
}

logger.info(' Comparing versions...');
final result = await gemini.validateExistingSkillContent(
markdown,
Expand Down
89 changes: 75 additions & 14 deletions tool/test/generate_skills_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@ void main() {
group('GenerateSkillsCommand', () {
late CommandRunner<void> runner;
late Directory tempDir;
late File videoFile;
late File inputYamlFile;

setUp(() async {
tempDir = await Directory.systemTemp.createTemp('skills_gen_test');
videoFile = File(p.join(tempDir.path, 'input.yaml'));
inputYamlFile = File(p.join(tempDir.path, 'input.yaml'));
runner = CommandRunner<void>('skills', 'Test runner');
});

Expand All @@ -31,7 +31,7 @@ void main() {

test('generates skill from YAML input with dart-docs- prefix', () async {
// Create input YAML in a file named dart_dev.yaml to trigger prefixing
videoFile = File(p.join(tempDir.path, 'dart_dev.yaml'));
inputYamlFile = File(p.join(tempDir.path, 'dart_dev.yaml'));
final inputData = [
{
'name': 'foo',
Expand All @@ -44,7 +44,7 @@ void main() {
'resources': ['https://example.com/'],
},
];
videoFile.writeAsStringSync(jsonEncode(inputData));
inputYamlFile.writeAsStringSync(jsonEncode(inputData));

final geminiRequests = <String>[];
// Mock HTTP Client
Expand Down Expand Up @@ -90,7 +90,7 @@ void main() {
runner.addCommand(command);

// Run command
await runner.run(['generate-skill', videoFile.path]);
await runner.run(['generate-skill', inputYamlFile.path]);

// Just verify file creation for now
final skillDirFoo = Directory(p.join(tempDir.path, 'foo'));
Expand All @@ -108,7 +108,7 @@ void main() {
});

test('logs progress and summary', () async {
videoFile = File(p.join(tempDir.path, 'dart_dev.yaml'));
inputYamlFile = File(p.join(tempDir.path, 'dart_dev.yaml'));
final inputData = [
{
'name': 'success',
Expand All @@ -121,7 +121,7 @@ void main() {
'resources': ['https://example.com/fail_404'],
},
];
videoFile.writeAsStringSync(jsonEncode(inputData));
inputYamlFile.writeAsStringSync(jsonEncode(inputData));

final logs = <String>[];
final sub = Logger.root.onRecord.listen((record) {
Expand Down Expand Up @@ -165,7 +165,7 @@ void main() {
);
runner.addCommand(command);

await runner.run(['generate-skill', videoFile.path]);
await runner.run(['generate-skill', inputYamlFile.path]);

// Verify Logs
expect(logs, contains(contains('Generating skill: success...')));
Expand Down Expand Up @@ -337,7 +337,7 @@ void main() {
'resources': <String>[],
},
];
final videoFile = File(p.join(tempDir.path, 'empty_fetch.yaml'))
final inputYamlFile = File(p.join(tempDir.path, 'empty_fetch.yaml'))
..writeAsStringSync(jsonEncode(inputData));

final logs = <String>[];
Expand All @@ -357,7 +357,7 @@ void main() {
);
runner.addCommand(command);

await runner.run(['generate-skill', videoFile.path]);
await runner.run(['generate-skill', inputYamlFile.path]);
expect(
logs,
contains(' No content fetched for empty-fetch. Skipping.'),
Expand All @@ -373,7 +373,7 @@ void main() {
'resources': ['https://example.com/source'],
},
];
final videoFile = File(p.join(tempDir.path, 'empty_gemini.yaml'))
final inputYamlFile = File(p.join(tempDir.path, 'empty_gemini.yaml'))
..writeAsStringSync(jsonEncode(inputData));

final logs = <String>[];
Expand Down Expand Up @@ -412,7 +412,7 @@ void main() {
);
runner.addCommand(command);

await runner.run(['generate-skill', videoFile.path]);
await runner.run(['generate-skill', inputYamlFile.path]);
expect(logs, contains(' Failed to generate content for empty-gemini'));
});

Expand All @@ -424,7 +424,7 @@ void main() {
'resources': ['https://example.com/source'],
},
];
final videoFile = File(p.join(tempDir.path, 'exception_gemini.yaml'))
final inputYamlFile = File(p.join(tempDir.path, 'exception_gemini.yaml'))
..writeAsStringSync(jsonEncode(inputData));

final logs = <String>[];
Expand All @@ -444,7 +444,7 @@ void main() {
);
runner.addCommand(command);

await runner.run(['generate-skill', videoFile.path]);
await runner.run(['generate-skill', inputYamlFile.path]);
expect(
logs,
contains(
Expand All @@ -454,5 +454,66 @@ void main() {
),
);
});

test('dry run fetches content but skips Gemini and file writes', () async {
final inputData = [
{
'name': 'dry-run-skill',
'description': 'Description',
'resources': ['https://example.com/source'],
},
];
final inputYamlFile = File(p.join(tempDir.path, 'dry_run.yaml'))
..writeAsStringSync(jsonEncode(inputData));

final logs = <String>[];
final sub = Logger.root.onRecord.listen(
(record) => logs.add(record.message),
);
addTearDown(sub.cancel);

var geminiCalled = false;
final mockClient = MockClient((request) async {
if (request.url.toString() == 'https://example.com/source') {
return http.Response('<html>Content</html>', 200);
}
if (request.url.toString().contains('generativelanguage')) {
geminiCalled = true;
return http.Response(
jsonEncode({
'candidates': [
{
'content': {
'parts': [
{'text': 'Gen'},
],
},
},
],
}),
200,
);
}
return http.Response('Error', 500);
});

final command = GenerateSkillCommand(
environment: {'GEMINI_API_KEY': 'test-key'},
httpClient: mockClient,
outputDir: tempDir,
);
runner.addCommand(command);

await runner.run(['generate-skill', '--dry-run', inputYamlFile.path]);

expect(geminiCalled, isFalse);
expect(
logs,
contains(contains('[DRY RUN] Would generate skill: dry-run-skill')),
);

final skillDir = Directory(p.join(tempDir.path, 'dry-run-skill'));
expect(skillDir.existsSync(), isFalse);
});
});
}
63 changes: 63 additions & 0 deletions tool/test/update_skills_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -174,5 +174,68 @@ void main() {
);
},
);

test('dry run fetches content but skips Gemini and file writes', () async {
final inputData = [
{
'name': 'dry-update',
'description': 'Description',
'resources': ['https://example.com/source'],
},
];
inputYamlFile.writeAsStringSync(jsonEncode(inputData));

final skillDir = Directory(p.join(tempDir.path, 'dry-update'))
..createSync();
final skillFile = File(p.join(skillDir.path, 'SKILL.md'))
..writeAsStringSync('Existing content');

final logs = <String>[];
final sub = Logger.root.onRecord.listen((record) {
logs.add(record.message);
});
addTearDown(sub.cancel);

var geminiCalled = false;
final mockClient = MockClient((request) async {
if (request.url.toString() == 'https://example.com/source') {
return http.Response('<html>New Content</html>', 200);
}
if (request.url.toString().contains('generativelanguage')) {
geminiCalled = true;
return http.Response(
jsonEncode({
'candidates': [
{
'content': {
'parts': [
{'text': 'Gen'},
],
},
},
],
}),
200,
);
}
return http.Response('Error', 500);
});

final command = UpdateSkillCommand(
environment: {'GEMINI_API_KEY': 'test-key'},
httpClient: mockClient,
outputDir: tempDir,
);
runner.addCommand(command);

await runner.run(['update-skill', '--dry-run', inputYamlFile.path]);

expect(geminiCalled, isFalse);
expect(
logs,
contains(contains('[DRY RUN] Would update skill: dry-update')),
);
expect(skillFile.readAsStringSync(), equals('Existing content'));
});
});
}
Loading
Loading