| // Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file | |
| // for details. All rights reserved. Use of this source code is governed by a | |
| // BSD-style license that can be found in the LICENSE file. | |
| import 'dart:io'; | |
| import 'package:dartdev/src/core.dart'; | |
| import 'package:dartdev/src/install/file_system.dart'; | |
| import 'package:dartdev/src/install/pub_formats.dart'; | |
| import 'package:pub_formats/pub_formats.dart'; | |
| class InstalledCommand extends DartdevCommand { | |
| static const cmdName = 'installed'; | |
| static const cmdDescription = 'List globally installed Dart CLI tools.'; | |
| static const noToolsInstalledMessage = 'No Dart CLI tools installed.'; | |
| static const noActiveToolsInstalledMessage = | |
| 'No active Dart CLI tools installed. ' | |
| 'Run "dart installed --all" to show inactive tools.'; | |
| @override | |
| CommandCategory get commandCategory => CommandCategory.global; | |
| InstalledCommand({bool verbose = false}) | |
| : super(cmdName, cmdDescription, verbose) { | |
| argParser.addFlag( | |
| 'all', | |
| abbr: 'a', | |
| help: '''Also list packages which are currently not active. | |
| Active package have executables on `PATH`. | |
| App bundles of packages on disk which have no executables | |
| on `PATH` are non-active.''', | |
| ); | |
| } | |
| @override | |
| Future<int> run() async { | |
| final argResults = this.argResults!; | |
| final all = argResults.flag('all'); | |
| final installedPackages = getInstalledPackages(); | |
| final packagesToShow = [ | |
| for (final package in installedPackages) | |
| if (all || package.installed != Installed.not) package, | |
| ]; | |
| if (packagesToShow.isEmpty) { | |
| if (all || installedPackages.isEmpty) { | |
| log.stdout(noToolsInstalledMessage); | |
| } else { | |
| log.stdout(noActiveToolsInstalledMessage); | |
| } | |
| } else { | |
| for (final package in packagesToShow) { | |
| log.stdout(package.toString()); | |
| } | |
| } | |
| return 0; | |
| } | |
| static List<InstalledPackage> getInstalledPackages() { | |
| final allAppBundles = DartInstallDirectory().allAppBundlesSync(); | |
| final result = <InstalledPackage>[]; | |
| for (final appBundleDir in allAppBundles) { | |
| final packageName = appBundleDir.packageName; | |
| final lockFile = appBundleDir.pubspecLock; | |
| final pubspecLock = PubspecLockFile.loadSync(lockFile); | |
| final lockInfo = pubspecLock.packages!.entries | |
| .where( | |
| (entry) => | |
| entry.value.dependency == DependencyTypeSyntax.directMain, | |
| ) | |
| .single | |
| .value; | |
| final binaries = appBundleDir.executablesSync; | |
| var foundBinary = false; | |
| var missingBinary = false; | |
| for (final binary in binaries) { | |
| final link = binary.onPath; | |
| if (!link.existsSync()) { | |
| missingBinary = true; | |
| } else { | |
| if (link.targetSync().equals(binary)) { | |
| foundBinary = true; | |
| } else { | |
| missingBinary = true; | |
| } | |
| } | |
| } | |
| final lastModified = lockFile.lastModifiedSync(); | |
| result.add( | |
| InstalledPackage( | |
| name: packageName, | |
| appBundle: appBundleDir.directory, | |
| installed: switch ((foundBinary, missingBinary)) { | |
| (_, false) => Installed.fully, | |
| (true, true) => Installed.partial, | |
| (false, true) => Installed.not, | |
| }, | |
| lockInfo: lockInfo, | |
| lastModified: lastModified, | |
| ), | |
| ); | |
| } | |
| return result; | |
| } | |
| } | |
| class InstalledPackage { | |
| final String name; | |
| final Directory appBundle; | |
| final Installed installed; | |
| final PackageSyntax lockInfo; | |
| final DateTime lastModified; | |
| InstalledPackage({ | |
| required this.appBundle, | |
| required this.installed, | |
| required this.lastModified, | |
| required this.lockInfo, | |
| required this.name, | |
| }); | |
| @override | |
| String toString() { | |
| var result = '$name ${lockInfo.version}'; | |
| switch (lockInfo.source) { | |
| case PackageSourceSyntax.git: | |
| final description = GitPackageDescriptionSyntax.fromJson( | |
| lockInfo.description.json, | |
| ); | |
| final url = description.url; | |
| final resolvedRef = description.resolvedRef.substring(0, 8); | |
| result += ' from Git repository "$url" at "$resolvedRef"'; | |
| case PackageSourceSyntax.hosted: | |
| break; | |
| case PackageSourceSyntax.path$: | |
| final description = PathPackageDescriptionSyntax.fromJson( | |
| lockInfo.description.json, | |
| ); | |
| final path = description.path$; | |
| result += ' from "$path" at $lastModified'; | |
| default: | |
| result += ' from an unknown source "${lockInfo.source.name}"'; | |
| } | |
| switch (installed) { | |
| case Installed.fully: | |
| break; | |
| case Installed.partial: | |
| result += ' (partially active)'; | |
| case Installed.not: | |
| result += ' (not active)'; | |
| } | |
| return result; | |
| } | |
| } | |
| enum Installed { fully, partial, not } |