Handle whole workspace in dependency_services (#4294)

diff --git a/lib/src/command/dependency_services.dart b/lib/src/command/dependency_services.dart
index baffc2b..5f3eb72 100644
--- a/lib/src/command/dependency_services.dart
+++ b/lib/src/command/dependency_services.dart
@@ -25,6 +25,7 @@
 import '../package_name.dart';
 import '../pubspec.dart';
 import '../pubspec_utils.dart';
+import '../sdk.dart';
 import '../solver.dart';
 import '../solver/version_solver.dart';
 import '../source/git.dart';
@@ -56,6 +57,7 @@
 
   @override
   Future<void> runProtected() async {
+    _checkAtRoot(entrypoint);
     final stdinString = await utf8.decodeStream(stdin);
     final input = json.decode(stdinString.isEmpty ? '{}' : stdinString)
         as Map<String, Object?>;
@@ -65,27 +67,21 @@
       throw FormatException('"target" should be a String.');
     }
 
-    final compatiblePubspec =
-        stripDependencyOverrides(entrypoint.workspaceRoot.pubspec);
+    final compatibleWorkspace = entrypoint.workspaceRoot
+        .transformWorkspace((p) => stripDependencyOverrides(p.pubspec));
 
-    final breakingPubspec = stripVersionBounds(compatiblePubspec);
+    final breakingWorkspace = compatibleWorkspace.transformWorkspace(
+      (p) => stripVersionBounds(p.pubspec),
+    );
 
     final compatiblePackagesResult = await _tryResolve(
-      Package(
-        compatiblePubspec,
-        entrypoint.workspaceRoot.dir,
-        entrypoint.workspaceRoot.workspaceChildren,
-      ),
+      compatibleWorkspace,
       cache,
       additionalConstraints: additionalConstraints,
     );
 
     final breakingPackagesResult = await _tryResolve(
-      Package(
-        breakingPubspec,
-        entrypoint.workspaceRoot.dir,
-        entrypoint.workspaceRoot.workspaceChildren,
-      ),
+      breakingWorkspace,
       cache,
       additionalConstraints: additionalConstraints,
     );
@@ -105,22 +101,19 @@
           ?.firstWhereOrNull((element) => element.name == package.name);
       final multiBreakingVersion = breakingPackagesResult
           ?.firstWhereOrNull((element) => element.name == package.name);
-      final singleBreakingPubspec = compatiblePubspec.copyWith();
 
-      final dependencySet =
-          _dependencySetOfPackage(singleBreakingPubspec, package);
-      final kind = _kindString(compatiblePubspec, package.name);
+      final kind = _kindString(compatibleWorkspace, package.name);
       PackageId? singleBreakingVersion;
-      if (dependencySet != null) {
-        dependencySet[package.name] = package
-            .toRef()
-            .withConstraint(stripUpperBound(package.toRange().constraint));
+
+      if (kind != 'transitive') {
+        final singleBreakingWorkspace = compatibleWorkspace.transformWorkspace(
+          (p) {
+            final r = stripVersionBounds(p.pubspec, stripOnly: [package.name]);
+            return r;
+          },
+        );
         final singleBreakingPackagesResult = await _tryResolve(
-          Package(
-            singleBreakingPubspec,
-            entrypoint.workspaceRoot.dir,
-            entrypoint.workspaceRoot.workspaceChildren,
-          ),
+          singleBreakingWorkspace,
           cache,
         );
         singleBreakingVersion = singleBreakingPackagesResult
@@ -131,17 +124,15 @@
         (c) => c.range.toRef() == package.toRef() && !c.range.allows(package),
       )) {
         // Current version disallowed by restrictions.
-        final atLeastCurrentPubspec = atLeastCurrent(
-          compatiblePubspec,
-          entrypoint.lockFile.packages.values.toList(),
+        final atLeastCurrentWorkspace = compatibleWorkspace.transformWorkspace(
+          (p) => atLeastCurrent(
+            p.pubspec,
+            entrypoint.lockFile.packages.values.toList(),
+          ),
         );
 
         final smallestUpgradeResult = await _tryResolve(
-          Package(
-            atLeastCurrentPubspec,
-            entrypoint.workspaceRoot.dir,
-            entrypoint.workspaceRoot.workspaceChildren,
-          ),
+          atLeastCurrentWorkspace,
           cache,
           solveType: SolveType.downgrade,
           additionalConstraints: additionalConstraints,
@@ -156,7 +147,7 @@
         _UpgradeType upgradeType,
       ) async {
         return await _computeUpgradeSet(
-          compatiblePubspec,
+          compatibleWorkspace,
           package,
           entrypoint,
           cache,
@@ -174,8 +165,8 @@
         'latest':
             (await cache.getLatest(package.toRef(), version: package.version))
                 ?.versionOrHash(),
-        'constraint':
-            _constraintOf(compatiblePubspec, package.name)?.toString(),
+        'constraint': _constraintIntersection(compatibleWorkspace, package.name)
+            ?.toString(),
         'compatible': await computeUpgradeSet(
           compatibleVersion,
           _UpgradeType.compatible,
@@ -225,16 +216,11 @@
 
   @override
   Future<void> runProtected() async {
-    final pubspec = entrypoint.workspaceRoot.pubspec;
-
+    _checkAtRoot(entrypoint);
     final currentPackages = fileExists(entrypoint.lockFilePath)
         ? entrypoint.lockFile.packages.values.toList()
         : (await _tryResolve(
-              Package(
-                pubspec,
-                entrypoint.workspaceRoot.dir,
-                entrypoint.workspaceRoot.workspaceChildren,
-              ),
+              entrypoint.workspaceRoot,
               cache,
             ) ??
             <PackageId>[]);
@@ -246,8 +232,10 @@
       dependencies.add({
         'name': package.name,
         'version': package.versionOrHash(),
-        'kind': _kindString(pubspec, package.name),
-        'constraint': _constraintOf(pubspec, package.name)?.toString(),
+        'kind': _kindString(entrypoint.workspaceRoot, package.name),
+        'constraint':
+            _constraintIntersection(entrypoint.workspaceRoot, package.name)
+                ?.toString(),
         'source': _source(package, containingDir: directory),
       });
     }
@@ -303,7 +291,6 @@
 
   @override
   Future<void> runProtected() async {
-    YamlEditor(readTextFile(entrypoint.workspaceRoot.pubspecPath));
     final toApply = <_PackageVersion>[];
     final input = json.decode(await utf8.decodeStream(stdin));
     for (final change in input['dependencyChanges'] as Iterable) {
@@ -317,10 +304,46 @@
         ),
       );
     }
-
-    final pubspec = entrypoint.workspaceRoot.pubspec;
-    final pubspecEditor =
-        YamlEditor(readTextFile(entrypoint.workspaceRoot.pubspecPath));
+    final updatedPubspecs = <String, YamlEditor>{};
+    _checkAtRoot(entrypoint);
+    for (final package in entrypoint.workspaceRoot.transitiveWorkspace) {
+      final pubspec = package.pubspec;
+      final pubspecEditor = YamlEditor(readTextFile(package.pubspecPath));
+      for (final p in toApply) {
+        final targetConstraint = p.constraint;
+        final targetPackage = p.name;
+        final targetVersion = p.version;
+        late final section = pubspec.dependencies[targetPackage] != null
+            ? 'dependencies'
+            : 'dev_dependencies';
+        if (targetConstraint != null) {
+          final packageConfig =
+              pubspecEditor.parseAt([section, targetPackage]).value;
+          if (packageConfig == null || packageConfig is String) {
+            pubspecEditor
+                .update([section, targetPackage], targetConstraint.toString());
+          } else if (packageConfig is Map) {
+            pubspecEditor.update(
+              [section, targetPackage, 'version'],
+              targetConstraint.toString(),
+            );
+          } else {
+            fail(
+              'The dependency $targetPackage does not have a map or string as a description',
+            );
+          }
+        } else if (targetVersion != null) {
+          final constraint = _constraintOf(pubspec, targetPackage);
+          if (constraint != null && !constraint.allows(targetVersion)) {
+            pubspecEditor.update(
+              [section, targetPackage],
+              VersionConstraint.compatibleWith(targetVersion).toString(),
+            );
+          }
+        }
+        updatedPubspecs[package.dir] = pubspecEditor;
+      }
+    }
     final lockFile = fileExists(entrypoint.lockFilePath)
         ? readTextFile(entrypoint.lockFilePath)
         : null;
@@ -331,40 +354,8 @@
     for (final p in toApply) {
       final targetPackage = p.name;
       final targetVersion = p.version;
-      final targetConstraint = p.constraint;
       final targetRevision = p.gitRevision;
 
-      if (targetConstraint != null) {
-        final section = pubspec.dependencies[targetPackage] != null
-            ? 'dependencies'
-            : 'dev_dependencies';
-        final packageConfig =
-            pubspecEditor.parseAt([section, targetPackage]).value;
-        if (packageConfig == null || packageConfig is String) {
-          pubspecEditor
-              .update([section, targetPackage], targetConstraint.toString());
-        } else if (packageConfig is Map) {
-          pubspecEditor.update(
-            [section, targetPackage, 'version'],
-            targetConstraint.toString(),
-          );
-        } else {
-          fail(
-            'The dependency $targetPackage does not have a map or string as a description',
-          );
-        }
-      } else if (targetVersion != null) {
-        final constraint = _constraintOf(pubspec, targetPackage);
-        if (constraint != null && !constraint.allows(targetVersion)) {
-          final section = pubspec.dependencies[targetPackage] != null
-              ? 'dependencies'
-              : 'dev_dependencies';
-          pubspecEditor.update(
-            [section, targetPackage],
-            VersionConstraint.compatibleWith(targetVersion).toString(),
-          );
-        }
-      }
       if (lockFileEditor != null) {
         if (targetVersion != null &&
             (lockFileYaml['packages'] as Map).containsKey(targetPackage)) {
@@ -431,7 +422,14 @@
           );
     await log.errorsOnlyUnlessTerminal(
       () async {
-        final updatedPubspec = pubspecEditor.toString();
+        final updatedWorkspace = entrypoint.workspaceRoot.transformWorkspace(
+          (package) => Pubspec.parse(
+            updatedPubspecs[package.dir].toString(),
+            cache.sources,
+            location: toUri(package.pubspecPath),
+            containingDescription: RootDescription(package.dir),
+          ),
+        );
         // Resolve versions, this will update transitive dependencies that were
         // not passed in the input. And also counts as a validation of the input
         // by ensuring the resolution is valid.
@@ -442,21 +440,17 @@
         final solveResult = await resolveVersions(
           SolveType.get,
           cache,
-          Package(
-            Pubspec.parse(
-              updatedPubspec,
-              cache.sources,
-              location: toUri(entrypoint.workspaceRoot.pubspecPath),
-              containingDescription:
-                  RootDescription(entrypoint.workspaceRoot.dir),
-            ),
-            entrypoint.workspaceRoot.dir,
-            entrypoint.workspaceRoot.workspaceChildren,
-          ),
+          updatedWorkspace,
           lockFile: updatedLockfile,
         );
-        if (pubspecEditor.edits.isNotEmpty) {
-          writeTextFile(entrypoint.workspaceRoot.pubspecPath, updatedPubspec);
+        for (final package in entrypoint.workspaceRoot.transitiveWorkspace) {
+          final updatedPubspec = updatedPubspecs[package.dir]!;
+          if (updatedPubspec.edits.isNotEmpty) {
+            writeTextFile(
+              package.pubspecPath,
+              updatedPubspec.toString(),
+            );
+          }
         }
         // Only if we originally had a lock-file we write the resulting lockfile back.
         if (updatedLockfile != null) {
@@ -521,9 +515,9 @@
           final newLockFile = LockFile(
             updatedPackages,
             sdkConstraints: updatedLockfile.sdkConstraints,
-            mainDependencies: pubspec.dependencies.keys.toSet(),
-            devDependencies: pubspec.devDependencies.keys.toSet(),
-            overriddenDependencies: pubspec.dependencyOverrides.keys.toSet(),
+            mainDependencies: entrypoint.lockFile.mainDependencies,
+            devDependencies: entrypoint.lockFile.devDependencies,
+            overriddenDependencies: entrypoint.lockFile.overriddenDependencies,
           );
 
           newLockFile.writeToFile(entrypoint.lockFilePath, cache);
@@ -535,6 +529,12 @@
   }
 }
 
+void _checkAtRoot(Entrypoint entrypoint) {
+  if (entrypoint.workspaceRoot != entrypoint.workPackage) {
+    fail('Only apply dependency_services to the root of the workspace.');
+  }
+}
+
 class _PackageVersion {
   String name;
   Version? version;
@@ -681,16 +681,33 @@
   return solveResult?.packages;
 }
 
+VersionConstraint? _constraintIntersection(
+  Package workspace,
+  String packageName,
+) {
+  final constraints = workspace.transitiveWorkspace
+      .map((p) => _constraintOf(p.pubspec, packageName))
+      .whereNotNull();
+  if (constraints.isEmpty) {
+    return null;
+  }
+  return constraints
+      .reduce((a, b) => a.intersect(b))
+      .asCompatibleWithIfPossible();
+}
+
 VersionConstraint? _constraintOf(Pubspec pubspec, String packageName) {
   return (pubspec.dependencies[packageName] ??
           pubspec.devDependencies[packageName])
       ?.constraint;
 }
 
-String _kindString(Pubspec pubspec, String packageName) {
-  return pubspec.dependencies.containsKey(packageName)
+String _kindString(Package workspace, String packageName) {
+  return workspace.transitiveWorkspace
+          .any((p) => p.dependencies.containsKey(packageName))
       ? 'direct'
-      : pubspec.devDependencies.containsKey(packageName)
+      : workspace.transitiveWorkspace
+              .any((p) => p.devDependencies.containsKey(packageName))
           ? 'dev'
           : 'transitive';
 }
@@ -721,12 +738,14 @@
       key: (e) => (e as PackageId).name,
     );
   }
-  currentPackages.remove(entrypoint.workspaceRoot.name);
+  for (final p in entrypoint.workspaceRoot.transitiveWorkspace) {
+    currentPackages.remove(p.name);
+  }
   return currentPackages;
 }
 
 Future<List<Object>> _computeUpgradeSet(
-  Pubspec rootPubspec,
+  Package workspace,
   PackageId? package,
   Entrypoint entrypoint,
   SystemCache cache, {
@@ -736,16 +755,18 @@
 }) async {
   if (package == null) return [];
   final lockFile = entrypoint.lockFile;
-  final pubspec = (upgradeType == _UpgradeType.multiBreaking ||
+  final upgradedWorkspace = (upgradeType == _UpgradeType.multiBreaking ||
           upgradeType == _UpgradeType.smallestUpdate)
-      ? stripVersionBounds(rootPubspec)
-      : rootPubspec.copyWith();
+      ? workspace.transformWorkspace((p) => stripVersionBounds(p.pubspec))
+      : workspace.transformWorkspace((p) => p.pubspec.copyWith());
 
-  final dependencySet = _dependencySetOfPackage(pubspec, package);
-  if (dependencySet != null) {
-    // Force the version to be the new version.
-    dependencySet[package.name] =
-        package.toRef().withConstraint(package.toRange().constraint);
+  for (final p in upgradedWorkspace.transitiveWorkspace) {
+    final dependencySet = _dependencySetOfPackage(p.pubspec, package);
+    if (dependencySet != null) {
+      // Force the version to be the new version.
+      dependencySet[package.name] =
+          package.toRef().withConstraint(package.toRange().constraint);
+    }
   }
 
   final resolution = await tryResolveVersions(
@@ -753,11 +774,7 @@
         ? SolveType.downgrade
         : SolveType.get,
     cache,
-    Package(
-      pubspec,
-      entrypoint.workspaceRoot.dir,
-      entrypoint.workspaceRoot.workspaceChildren,
-    ),
+    upgradedWorkspace,
     lockFile: lockFile,
     additionalConstraints: additionalConstraints,
   );
@@ -766,40 +783,43 @@
   if (resolution == null) {
     return [];
   }
-
+  final workspaceNames = {
+    ...workspace.transitiveWorkspace.map((p) => p.name),
+  };
   return [
     ...resolution.packages.where((r) {
-      if (r.name == rootPubspec.name) return false;
+      if (workspaceNames.contains(r.name)) return false;
       final originalVersion = currentPackages[r.name];
       return originalVersion == null || r != originalVersion;
     }).map((p) {
-      final depset = _dependencySetOfPackage(rootPubspec, p);
-      final originalConstraint = depset?[p.name]?.constraint;
+      final constraintIntersection = _constraintIntersection(workspace, p.name);
       final currentPackage = currentPackages[p.name];
       return {
         'name': p.name,
         'version': p.versionOrHash(),
-        'kind': _kindString(pubspec, p.name),
+        'kind': _kindString(workspace, p.name),
         'source': _source(p, containingDir: entrypoint.workspaceRoot.dir),
-        'constraintBumped': originalConstraint == null
+        'constraintBumped': constraintIntersection == null
             ? null
             : upgradeType == _UpgradeType.compatible
-                ? originalConstraint.toString()
-                : _bumpConstraint(originalConstraint, p.version).toString(),
-        'constraintWidened': originalConstraint == null
+                ? constraintIntersection.toString()
+                : _bumpConstraint(constraintIntersection, p.version).toString(),
+        'constraintWidened': constraintIntersection == null
             ? null
             : upgradeType == _UpgradeType.compatible
-                ? originalConstraint.toString()
-                : _widenConstraint(originalConstraint, p.version).toString(),
-        'constraintBumpedIfNeeded': originalConstraint == null
+                ? constraintIntersection.toString()
+                : _widenConstraint(constraintIntersection, p.version)
+                    .toString(),
+        'constraintBumpedIfNeeded': constraintIntersection == null
             ? null
             : upgradeType == _UpgradeType.compatible
-                ? originalConstraint.toString()
-                : originalConstraint.allows(p.version)
-                    ? originalConstraint.toString()
-                    : _bumpConstraint(originalConstraint, p.version).toString(),
+                ? constraintIntersection.toString()
+                : constraintIntersection.allows(p.version)
+                    ? constraintIntersection.toString()
+                    : _bumpConstraint(constraintIntersection, p.version)
+                        .toString(),
         'previousVersion': currentPackage?.versionOrHash(),
-        'previousConstraint': originalConstraint?.toString(),
+        'previousConstraint': constraintIntersection?.toString(),
         'previousSource': currentPackage == null
             ? null
             : _source(
diff --git a/test/dependency_services/dependency_services_test.dart b/test/dependency_services/dependency_services_test.dart
index 11ccb68..ab3516c 100644
--- a/test/dependency_services/dependency_services_test.dart
+++ b/test/dependency_services/dependency_services_test.dart
@@ -18,24 +18,25 @@
 import '../golden_file.dart';
 import '../test_pub.dart';
 
-void manifestAndLockfile(GoldenTestContext context) {
+void manifestAndLockfile(GoldenTestContext context, List<String> workspace) {
   String catFile(String filename) {
     final path = p.join(d.sandbox, appPath, filename);
+    final normalizedFilename = p.posix.joinAll(p.split(p.normalize(filename)));
     if (File(path).existsSync()) {
       final contents = File(path).readAsLinesSync().map(filterUnstableText);
 
       return '''
-\$ cat $filename
+\$ cat $normalizedFilename
 ${contents.join('\n')}''';
     } else {
       return '''
-\$ cat $filename
-No such file $filename.''';
+\$ cat $normalizedFilename
+No such file $normalizedFilename.''';
     }
   }
 
   context.expectNextSection('''
-${catFile('pubspec.yaml')}
+${workspace.map((path) => catFile(p.join(path, 'pubspec.yaml'))).join('\n')}
 ${catFile('pubspec.lock')}
 ''');
 }
@@ -47,6 +48,7 @@
   Future<String> runDependencyServices(
     List<String> args, {
     String stdin = '',
+    Map<String, String>? environment,
   }) async {
     final buffer = StringBuffer();
     buffer.writeln('## Section ${args.join(' ')}');
@@ -61,6 +63,7 @@
       environment: {
         ...getPubTestEnvironment(),
         '_PUB_TEST_DEFAULT_HOSTED_URL': globalServer.url,
+        ...?environment,
       },
       workingDirectory: p.join(d.sandbox, appPath),
     );
@@ -96,11 +99,14 @@
 Future<void> _listReportApply(
   GoldenTestContext context,
   List<_PackageVersion> upgrades, {
+  List<String> workspace = const ['.'],
   void Function(Map)? reportAssertions,
+  Map<String, String>? environment,
 }) async {
-  manifestAndLockfile(context);
-  await context.runDependencyServices(['list']);
-  final report = await context.runDependencyServices(['report']);
+  manifestAndLockfile(context, workspace);
+  await context.runDependencyServices(['list'], environment: environment);
+  final report =
+      await context.runDependencyServices(['report'], environment: environment);
   if (reportAssertions != null) {
     reportAssertions(json.decode(report) as Map);
   }
@@ -108,8 +114,12 @@
     'dependencyChanges': upgrades,
   });
 
-  await context.runDependencyServices(['apply'], stdin: input);
-  manifestAndLockfile(context);
+  await context.runDependencyServices(
+    ['apply'],
+    stdin: input,
+    environment: environment,
+  );
+  manifestAndLockfile(context, workspace);
 }
 
 Future<void> _reportWithForbidden(
@@ -118,7 +128,7 @@
   void Function(Map)? resultAssertions,
   String? targetPackage,
 }) async {
-  manifestAndLockfile(context);
+  manifestAndLockfile(context, ['.']);
   final input = json.encode({
     'target': targetPackage,
     'disallowed': [
@@ -136,7 +146,7 @@
   }
 
   // await context.runDependencyServices(['apply'], stdin: input);
-  manifestAndLockfile(context);
+  manifestAndLockfile(context, ['.']);
 }
 
 Future<void> main() async {
@@ -628,6 +638,83 @@
       },
     );
   });
+
+  testWithGolden('can upgrade workspaces', (context) async {
+    (await servePackages())
+      ..serve('foo', '1.2.3')
+      ..serve('foo', '2.2.3', deps: {'transitive': '^1.0.0'})
+      ..serve('bar', '1.2.3')
+      ..serve('bar', '2.2.3')
+      ..serve('dev', '1.0.0')
+      ..serve('dev', '2.0.0')
+      ..serve('transitive', '1.0.0')
+      ..serveContentHashes = true;
+
+    await dir(appPath, [
+      libPubspec(
+        'myapp',
+        '1.2.3',
+        extras: {
+          'workspace': ['pkgs/a'],
+        },
+        deps: {
+          'foo': '^1.0.0',
+          'bar': '^1.0.0',
+        },
+        sdk: '^3.5.0',
+      ),
+      dir('pkgs', [
+        dir('a', [
+          libPubspec(
+            'a',
+            '1.1.1',
+            deps: {'bar': '>=1.2.0 <1.5.0'},
+            devDeps: {
+              'foo': '^1.2.0',
+              'dev': '^1.0.0',
+            },
+            resolutionWorkspace: true,
+          ),
+        ]),
+      ]),
+    ]).create();
+    await pubGet(environment: {'_PUB_TEST_SDK_VERSION': '3.5.0'});
+
+    final result = await Process.run(
+      Platform.resolvedExecutable,
+      [snapshot, 'list'],
+      environment: {
+        ...getPubTestEnvironment(),
+        '_PUB_TEST_SDK_VERSION': '3.5.0',
+      },
+      workingDirectory: p.join(d.sandbox, appPath, 'pkgs', 'a'),
+    );
+
+    expect(
+      result.stderr,
+      contains(
+        'Only apply dependency_services to the root of the workspace.',
+      ),
+    );
+    expect(result.exitCode, 1);
+
+    await _listReportApply(
+      context,
+      [_PackageVersion('foo', '2.2.3'), _PackageVersion('transitive', '1.0.0')],
+      workspace: ['.', p.join('pkgs', 'a')],
+      reportAssertions: (report) {
+        expect(
+          findChangeVersion(report, 'singleBreaking', 'foo'),
+          '2.2.3',
+        );
+        expect(
+          findChangeVersion(report, 'singleBreaking', 'transitive'),
+          '1.0.0',
+        );
+      },
+      environment: {'_PUB_TEST_SDK_VERSION': '3.5.0'},
+    );
+  });
 }
 
 dynamic findChangeVersion(dynamic json, String updateType, String name) {
diff --git a/test/testdata/goldens/dependency_services/dependency_services_test/can upgrade workspaces.txt b/test/testdata/goldens/dependency_services/dependency_services_test/can upgrade workspaces.txt
new file mode 100644
index 0000000..cf9e847
--- /dev/null
+++ b/test/testdata/goldens/dependency_services/dependency_services_test/can upgrade workspaces.txt
@@ -0,0 +1,400 @@
+# GENERATED BY: test/dependency_services/dependency_services_test.dart
+
+$ cat pubspec.yaml
+{"name":"myapp","version":"1.2.3","homepage":"https://pub.dev","description":"A package, I guess.","dependencies":{"foo":"^1.0.0","bar":"^1.0.0"},"environment":{"sdk":"^3.5.0"},"workspace":["pkgs/a"]}
+$ cat pkgs/a/pubspec.yaml
+{"name":"a","version":"1.1.1","homepage":"https://pub.dev","description":"A package, I guess.","dependencies":{"bar":">=1.2.0 <1.5.0"},"dev_dependencies":{"foo":"^1.2.0","dev":"^1.0.0"},"environment":{"sdk":"^3.5.0-0"},"resolution":"workspace"}
+$ cat pubspec.lock
+# Generated by pub
+# See https://dart.dev/tools/pub/glossary#lockfile
+packages:
+  bar:
+    dependency: "direct main"
+    description:
+      name: bar
+      sha256: $SHA256
+      url: "http://localhost:$PORT"
+    source: hosted
+    version: "1.2.3"
+  dev:
+    dependency: transitive
+    description:
+      name: dev
+      sha256: $SHA256
+      url: "http://localhost:$PORT"
+    source: hosted
+    version: "1.0.0"
+  foo:
+    dependency: "direct main"
+    description:
+      name: foo
+      sha256: $SHA256
+      url: "http://localhost:$PORT"
+    source: hosted
+    version: "1.2.3"
+sdks:
+  dart: ">=3.5.0 <4.0.0"
+-------------------------------- END OF OUTPUT ---------------------------------
+
+## Section list
+$ echo '' | dependency_services list
+{
+  "dependencies": [
+    {
+      "name": "bar",
+      "version": "1.2.3",
+      "kind": "direct",
+      "constraint": ">=1.2.0 <1.5.0",
+      "source": {
+        "type": "hosted",
+        "description": {
+          "name": "bar",
+          "url": "http://localhost:$PORT",
+          "sha256": "0b119406be305b6e65d33551008b5b72fdd810965f0df914478c940d5fe28e53"
+        }
+      }
+    },
+    {
+      "name": "dev",
+      "version": "1.0.0",
+      "kind": "dev",
+      "constraint": "^1.0.0",
+      "source": {
+        "type": "hosted",
+        "description": {
+          "name": "dev",
+          "url": "http://localhost:$PORT",
+          "sha256": "fb990b7b071a76286080ee183e9ed4cd6d5538f8e1eccce8c1f2caf50c51c1bc"
+        }
+      }
+    },
+    {
+      "name": "foo",
+      "version": "1.2.3",
+      "kind": "direct",
+      "constraint": "^1.2.0",
+      "source": {
+        "type": "hosted",
+        "description": {
+          "name": "foo",
+          "url": "http://localhost:$PORT",
+          "sha256": "b2b7fc405959806aa1f31ac7e68752534f66f66a11a280d9878ecb6cd835f01c"
+        }
+      }
+    }
+  ]
+}
+
+-------------------------------- END OF OUTPUT ---------------------------------
+
+## Section report
+$ echo '' | dependency_services report
+{
+  "dependencies": [
+    {
+      "name": "bar",
+      "version": "1.2.3",
+      "kind": "direct",
+      "source": {
+        "type": "hosted",
+        "description": {
+          "name": "bar",
+          "url": "http://localhost:$PORT",
+          "sha256": "0b119406be305b6e65d33551008b5b72fdd810965f0df914478c940d5fe28e53"
+        }
+      },
+      "latest": "2.2.3",
+      "constraint": ">=1.2.0 <1.5.0",
+      "compatible": [],
+      "singleBreaking": [
+        {
+          "name": "bar",
+          "version": "2.2.3",
+          "kind": "direct",
+          "source": {
+            "type": "hosted",
+            "description": {
+              "name": "bar",
+              "url": "http://localhost:$PORT",
+              "sha256": "18169c0899ff5f0551a80839dc1618e597a6ee4508a5065f9318dd2a2fda6455"
+            }
+          },
+          "constraintBumped": "^2.2.3",
+          "constraintWidened": ">=1.2.0 <3.0.0",
+          "constraintBumpedIfNeeded": "^2.2.3",
+          "previousVersion": "1.2.3",
+          "previousConstraint": ">=1.2.0 <1.5.0",
+          "previousSource": {
+            "type": "hosted",
+            "description": {
+              "name": "bar",
+              "url": "http://localhost:$PORT",
+              "sha256": "0b119406be305b6e65d33551008b5b72fdd810965f0df914478c940d5fe28e53"
+            }
+          }
+        }
+      ],
+      "multiBreaking": [
+        {
+          "name": "bar",
+          "version": "2.2.3",
+          "kind": "direct",
+          "source": {
+            "type": "hosted",
+            "description": {
+              "name": "bar",
+              "url": "http://localhost:$PORT",
+              "sha256": "18169c0899ff5f0551a80839dc1618e597a6ee4508a5065f9318dd2a2fda6455"
+            }
+          },
+          "constraintBumped": "^2.2.3",
+          "constraintWidened": ">=1.2.0 <3.0.0",
+          "constraintBumpedIfNeeded": "^2.2.3",
+          "previousVersion": "1.2.3",
+          "previousConstraint": ">=1.2.0 <1.5.0",
+          "previousSource": {
+            "type": "hosted",
+            "description": {
+              "name": "bar",
+              "url": "http://localhost:$PORT",
+              "sha256": "0b119406be305b6e65d33551008b5b72fdd810965f0df914478c940d5fe28e53"
+            }
+          }
+        }
+      ]
+    },
+    {
+      "name": "dev",
+      "version": "1.0.0",
+      "kind": "dev",
+      "source": {
+        "type": "hosted",
+        "description": {
+          "name": "dev",
+          "url": "http://localhost:$PORT",
+          "sha256": "fb990b7b071a76286080ee183e9ed4cd6d5538f8e1eccce8c1f2caf50c51c1bc"
+        }
+      },
+      "latest": "2.0.0",
+      "constraint": "^1.0.0",
+      "compatible": [],
+      "singleBreaking": [
+        {
+          "name": "dev",
+          "version": "2.0.0",
+          "kind": "dev",
+          "source": {
+            "type": "hosted",
+            "description": {
+              "name": "dev",
+              "url": "http://localhost:$PORT",
+              "sha256": "e3496752d80b78354cd745f8d1e381c42022b14624233c9e5306711171418d09"
+            }
+          },
+          "constraintBumped": "^2.0.0",
+          "constraintWidened": ">=1.0.0 <3.0.0",
+          "constraintBumpedIfNeeded": "^2.0.0",
+          "previousVersion": "1.0.0",
+          "previousConstraint": "^1.0.0",
+          "previousSource": {
+            "type": "hosted",
+            "description": {
+              "name": "dev",
+              "url": "http://localhost:$PORT",
+              "sha256": "fb990b7b071a76286080ee183e9ed4cd6d5538f8e1eccce8c1f2caf50c51c1bc"
+            }
+          }
+        }
+      ],
+      "multiBreaking": [
+        {
+          "name": "dev",
+          "version": "2.0.0",
+          "kind": "dev",
+          "source": {
+            "type": "hosted",
+            "description": {
+              "name": "dev",
+              "url": "http://localhost:$PORT",
+              "sha256": "e3496752d80b78354cd745f8d1e381c42022b14624233c9e5306711171418d09"
+            }
+          },
+          "constraintBumped": "^2.0.0",
+          "constraintWidened": ">=1.0.0 <3.0.0",
+          "constraintBumpedIfNeeded": "^2.0.0",
+          "previousVersion": "1.0.0",
+          "previousConstraint": "^1.0.0",
+          "previousSource": {
+            "type": "hosted",
+            "description": {
+              "name": "dev",
+              "url": "http://localhost:$PORT",
+              "sha256": "fb990b7b071a76286080ee183e9ed4cd6d5538f8e1eccce8c1f2caf50c51c1bc"
+            }
+          }
+        }
+      ]
+    },
+    {
+      "name": "foo",
+      "version": "1.2.3",
+      "kind": "direct",
+      "source": {
+        "type": "hosted",
+        "description": {
+          "name": "foo",
+          "url": "http://localhost:$PORT",
+          "sha256": "b2b7fc405959806aa1f31ac7e68752534f66f66a11a280d9878ecb6cd835f01c"
+        }
+      },
+      "latest": "2.2.3",
+      "constraint": "^1.2.0",
+      "compatible": [],
+      "singleBreaking": [
+        {
+          "name": "foo",
+          "version": "2.2.3",
+          "kind": "direct",
+          "source": {
+            "type": "hosted",
+            "description": {
+              "name": "foo",
+              "url": "http://localhost:$PORT",
+              "sha256": "a7e0832c069301a6e6ba78d792a08156fbf5bbfc04e766393ad7c98e7a27f648"
+            }
+          },
+          "constraintBumped": "^2.2.3",
+          "constraintWidened": ">=1.2.0 <3.0.0",
+          "constraintBumpedIfNeeded": "^2.2.3",
+          "previousVersion": "1.2.3",
+          "previousConstraint": "^1.2.0",
+          "previousSource": {
+            "type": "hosted",
+            "description": {
+              "name": "foo",
+              "url": "http://localhost:$PORT",
+              "sha256": "b2b7fc405959806aa1f31ac7e68752534f66f66a11a280d9878ecb6cd835f01c"
+            }
+          }
+        },
+        {
+          "name": "transitive",
+          "version": "1.0.0",
+          "kind": "transitive",
+          "source": {
+            "type": "hosted",
+            "description": {
+              "name": "transitive",
+              "url": "http://localhost:$PORT",
+              "sha256": "d705923b5a6b4c7053e6d86a35799ede6467245151ff15dfdd5719986a61d49c"
+            }
+          },
+          "constraintBumped": null,
+          "constraintWidened": null,
+          "constraintBumpedIfNeeded": null,
+          "previousVersion": null,
+          "previousConstraint": null,
+          "previousSource": null
+        }
+      ],
+      "multiBreaking": [
+        {
+          "name": "foo",
+          "version": "2.2.3",
+          "kind": "direct",
+          "source": {
+            "type": "hosted",
+            "description": {
+              "name": "foo",
+              "url": "http://localhost:$PORT",
+              "sha256": "a7e0832c069301a6e6ba78d792a08156fbf5bbfc04e766393ad7c98e7a27f648"
+            }
+          },
+          "constraintBumped": "^2.2.3",
+          "constraintWidened": ">=1.2.0 <3.0.0",
+          "constraintBumpedIfNeeded": "^2.2.3",
+          "previousVersion": "1.2.3",
+          "previousConstraint": "^1.2.0",
+          "previousSource": {
+            "type": "hosted",
+            "description": {
+              "name": "foo",
+              "url": "http://localhost:$PORT",
+              "sha256": "b2b7fc405959806aa1f31ac7e68752534f66f66a11a280d9878ecb6cd835f01c"
+            }
+          }
+        },
+        {
+          "name": "transitive",
+          "version": "1.0.0",
+          "kind": "transitive",
+          "source": {
+            "type": "hosted",
+            "description": {
+              "name": "transitive",
+              "url": "http://localhost:$PORT",
+              "sha256": "d705923b5a6b4c7053e6d86a35799ede6467245151ff15dfdd5719986a61d49c"
+            }
+          },
+          "constraintBumped": null,
+          "constraintWidened": null,
+          "constraintBumpedIfNeeded": null,
+          "previousVersion": null,
+          "previousConstraint": null,
+          "previousSource": null
+        }
+      ]
+    }
+  ]
+}
+
+-------------------------------- END OF OUTPUT ---------------------------------
+
+## Section apply
+$ echo '{"dependencyChanges":[{"name":"foo","version":"2.2.3"},{"name":"transitive","version":"1.0.0"}]}' | dependency_services apply
+{"dependencies":[]}
+
+-------------------------------- END OF OUTPUT ---------------------------------
+
+$ cat pubspec.yaml
+{"name":"myapp","version":"1.2.3","homepage":"https://pub.dev","description":"A package, I guess.","dependencies":{"foo":^2.2.3,"bar":"^1.0.0"},"environment":{"sdk":"^3.5.0"},"workspace":["pkgs/a"]}
+$ cat pkgs/a/pubspec.yaml
+{"name":"a","version":"1.1.1","homepage":"https://pub.dev","description":"A package, I guess.","dependencies":{"bar":">=1.2.0 <1.5.0"},"dev_dependencies":{"foo":^2.2.3,"dev":"^1.0.0"},"environment":{"sdk":"^3.5.0-0"},"resolution":"workspace"}
+$ cat pubspec.lock
+# Generated by pub
+# See https://dart.dev/tools/pub/glossary#lockfile
+packages:
+  bar:
+    dependency: "direct main"
+    description:
+      name: bar
+      sha256: $SHA256
+      url: "http://localhost:$PORT"
+    source: hosted
+    version: "1.2.3"
+  dev:
+    dependency: transitive
+    description:
+      name: dev
+      sha256: $SHA256
+      url: "http://localhost:$PORT"
+    source: hosted
+    version: "1.0.0"
+  foo:
+    dependency: "direct main"
+    description:
+      name: foo
+      sha256: $SHA256
+      url: "http://localhost:$PORT"
+    source: hosted
+    version: "2.2.3"
+  transitive:
+    dependency: transitive
+    description:
+      name: transitive
+      sha256: $SHA256
+      url: "http://localhost:$PORT"
+    source: hosted
+    version: "1.0.0"
+sdks:
+  dart: ">=3.5.0 <4.0.0"