Skip to content

Commit 05dbd0f

Browse files
feat: Support listing packages with their licenses (#1458)
* Support for listing packages with their licenses * update README --------- Co-authored-by: Marcos Sevilla <31174242+marcossevilla@users.noreply.github.com>
1 parent 1720668 commit 05dbd0f

File tree

3 files changed

+181
-1
lines changed

3 files changed

+181
-1
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,9 @@ very_good packages check licenses --forbidden="unknown"
175175

176176
# Check licenses for certain dependencies types
177177
very_good packages check licenses --dependency-type="direct-main,transitive"
178+
179+
# Check and list licenses in the current directory
180+
very_good packages check licenses --reporter="csv"
178181
```
179182

180183
### [`very_good mcp`](https://cli.vgv.dev/docs/commands/mcp)
@@ -192,6 +195,7 @@ very_good mcp
192195
The MCP server exposes Very Good CLI functionality through the Model Context Protocol, allowing AI assistants to interact with the CLI programmatically. This enables automated project creation, testing, and package management through MCP-compatible tools.
193196

194197
**Available MCP Tools:**
198+
195199
- `create`: Create new Dart/Flutter projects (https://cli.vgv.dev/docs/category/templates)
196200
- `tests`: Run tests with optional coverage and optimization (https://cli.vgv.dev/docs/commands/test)
197201
- `packages_check_licenses`: Check packages for issues and licenses (https://cli.vgv.dev/docs/commands/check_licenses)

lib/src/commands/packages/commands/check/commands/licenses.dart

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,18 @@ class PackagesCheckLicensesCommand extends Command<int> {
103103
..addMultiOption(
104104
'skip-packages',
105105
help: 'Skip packages from having their licenses checked.',
106+
)
107+
..addOption(
108+
'reporter',
109+
help: 'Lists all licenses.',
110+
allowed: [
111+
'text',
112+
'csv',
113+
],
114+
allowedHelp: {
115+
'text': 'Lists licenses without a specific format.',
116+
'csv': 'Lists licenses in a CSV format.',
117+
},
106118
);
107119
}
108120

@@ -128,6 +140,11 @@ class PackagesCheckLicensesCommand extends Command<int> {
128140
final allowedLicenses = _argResults['allowed'] as List<String>;
129141
final forbiddenLicenses = _argResults['forbidden'] as List<String>;
130142
final skippedPackages = _argResults['skip-packages'] as List<String>;
143+
final reporterOutput = _argResults['reporter'] as String?;
144+
145+
final reporterOutputFormat = ReporterOutputFormat.fromString(
146+
reporterOutput,
147+
);
131148

132149
allowedLicenses.removeWhere((license) => license.trim().isEmpty);
133150
forbiddenLicenses.removeWhere((license) => license.trim().isEmpty);
@@ -312,6 +329,7 @@ class PackagesCheckLicensesCommand extends Command<int> {
312329
_composeReport(
313330
licenses: licenses,
314331
bannedDependencies: bannedDependencies,
332+
reporterOutputFormat: reporterOutputFormat,
315333
),
316334
);
317335

@@ -409,6 +427,7 @@ _BannedDependencyLicenseMap? _bannedDependencies({
409427
String _composeReport({
410428
required _DependencyLicenseMap licenses,
411429
required _BannedDependencyLicenseMap? bannedDependencies,
430+
ReporterOutputFormat? reporterOutputFormat,
412431
}) {
413432
final bannedLicenseTypes = bannedDependencies?.values.fold(<String>{}, (
414433
previousValue,
@@ -453,7 +472,24 @@ String _composeReport({
453472
? ''
454473
: ' of type: ${formattedLicenseTypes.toList().stringify()}';
455474

456-
return '''Retrieved $totalLicenseCount $licenseWord from ${licenses.length} $packageWord$suffix.''';
475+
final licenseBuilder = StringBuffer();
476+
if (reporterOutputFormat case final ReporterOutputFormat outputFormat) {
477+
licenseBuilder.write('\n');
478+
for (final license in licenses.entries) {
479+
if (license.value case final Set<String> dependencyLicenses) {
480+
for (final dependencyLicense in dependencyLicenses) {
481+
licenseBuilder.writeln(
482+
outputFormat.formatLicense(
483+
packageName: license.key,
484+
licenseName: dependencyLicense,
485+
),
486+
);
487+
}
488+
}
489+
}
490+
}
491+
492+
return '''Retrieved $totalLicenseCount $licenseWord from ${licenses.length} $packageWord$suffix.$licenseBuilder''';
457493
}
458494

459495
String _composeBannedReport(_BannedDependencyLicenseMap bannedDependencies) {
@@ -497,3 +533,40 @@ extension on List<Object> {
497533
return '${join(', ')} and $last';
498534
}
499535
}
536+
537+
/// Format type for listing all licenses via --reporter option.
538+
enum ReporterOutputFormat {
539+
/// List all licenses separated by a dash.
540+
///
541+
/// Example: very_good_cli - MIT
542+
text,
543+
544+
/// List all licenses in a CSV format.
545+
///
546+
/// Example: very_good_cli,MIT
547+
csv
548+
;
549+
550+
/// Convenience parsing method from user input.
551+
///
552+
/// Return desired format for valid inputs
553+
/// null for invalid inputs or unspecified input.
554+
static ReporterOutputFormat? fromString(String? value) {
555+
return switch (value) {
556+
'text' => text,
557+
'csv' => csv,
558+
_ => null,
559+
};
560+
}
561+
562+
/// Stringify the package with it's license into the desired format
563+
String formatLicense({
564+
required String packageName,
565+
required String licenseName,
566+
}) {
567+
return switch (this) {
568+
text => '$packageName - $licenseName',
569+
csv => '$packageName,$licenseName',
570+
};
571+
}
572+
}

test/src/commands/packages/commands/check/commands/licenses_test.dart

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@ const _expectedPackagesCheckLicensesUsage = [
4646
' --allowed Only allow the use of certain licenses.\n'
4747
' --forbidden Deny the use of certain licenses.\n'
4848
' --skip-packages Skip packages from having their licenses checked.\n'
49+
' --reporter Lists all licenses.\n'
50+
'\n'
51+
' [text] Lists licenses without a specific format.\n'
52+
' [csv] Lists licenses in a CSV format.\n'
4953
'\n'
5054
'Run "very_good help" to see global options.',
5155
];
@@ -1457,6 +1461,105 @@ and limitations under the License.''');
14571461
});
14581462
});
14591463

1464+
group('reporter', () {
1465+
const reporterArgument = '--reporter';
1466+
1467+
test(
1468+
'when no option is provided',
1469+
withRunner((commandRunner, logger, pubUpdater, printLogs) async {
1470+
final result = await commandRunner.run(
1471+
[...commandArguments, reporterArgument],
1472+
);
1473+
expect(result, equals(ExitCode.usage.code));
1474+
}),
1475+
);
1476+
1477+
test(
1478+
'when invalid option is provided',
1479+
withRunner((commandRunner, logger, pubUpdater, printLogs) async {
1480+
final result = await commandRunner.run(
1481+
[...commandArguments, reporterArgument, 'invalid'],
1482+
);
1483+
expect(result, equals(ExitCode.usage.code));
1484+
}),
1485+
);
1486+
test(
1487+
'text format prints packages with license separated by a dash',
1488+
withRunner((commandRunner, logger, pubUpdater, printLogs) async {
1489+
File(
1490+
path.join(tempDirectory.path, pubspecLockBasename),
1491+
).writeAsStringSync(_validPubspecLockContent);
1492+
1493+
when(() => packageConfig.packages).thenReturn({
1494+
veryGoodTestRunnerConfigPackage,
1495+
});
1496+
1497+
when(() => detectorResult.matches).thenReturn([mitLicenseMatch]);
1498+
when(() => logger.progress(any())).thenReturn(progress);
1499+
1500+
final result = await commandRunner.run(
1501+
[
1502+
...commandArguments,
1503+
reporterArgument,
1504+
'text',
1505+
tempDirectory.path,
1506+
],
1507+
);
1508+
1509+
const expectedOutput =
1510+
'''Retrieved 1 license from 1 package of type: MIT (1).\nvery_good_test_runner - MIT\n''';
1511+
1512+
verify(
1513+
() => progress.update(
1514+
'Collecting licenses from 1 out of 1 package',
1515+
),
1516+
).called(1);
1517+
verify(
1518+
() => progress.complete(expectedOutput),
1519+
).called(1);
1520+
1521+
expect(result, equals(ExitCode.success.code));
1522+
}),
1523+
);
1524+
test(
1525+
'csv format prints packages with license in a CSV format.',
1526+
withRunner((commandRunner, logger, pubUpdater, printLogs) async {
1527+
File(
1528+
path.join(tempDirectory.path, pubspecLockBasename),
1529+
).writeAsStringSync(_validPubspecLockContent);
1530+
1531+
when(() => packageConfig.packages).thenReturn({
1532+
veryGoodTestRunnerConfigPackage,
1533+
});
1534+
when(() => detectorResult.matches).thenReturn([mitLicenseMatch]);
1535+
when(() => logger.progress(any())).thenReturn(progress);
1536+
1537+
final result = await commandRunner.run(
1538+
[
1539+
...commandArguments,
1540+
reporterArgument,
1541+
'csv',
1542+
tempDirectory.path,
1543+
],
1544+
);
1545+
1546+
const expectedOutput =
1547+
'''Retrieved 1 license from 1 package of type: MIT (1).\nvery_good_test_runner,MIT\n''';
1548+
1549+
verify(
1550+
() => progress.update(
1551+
'Collecting licenses from 1 out of 1 package',
1552+
),
1553+
).called(1);
1554+
verify(
1555+
() => progress.complete(expectedOutput),
1556+
).called(1);
1557+
1558+
expect(result, equals(ExitCode.success.code));
1559+
}),
1560+
);
1561+
});
1562+
14601563
group('skip-packages', () {
14611564
const skipPackagesArgument = '--skip-packages';
14621565

0 commit comments

Comments
 (0)