blob: 7a08b962185366685de7692510569ab41346c157 [file] [log] [blame] [edit]
// 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.';
@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();
for (final package in installedPackages) {
if (package.installed == Installed.not && !all) {
continue;
}
print(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;
}