From 024c181f695f9d9ed20c4f3c826065801c4b63fc Mon Sep 17 00:00:00 2001 From: Rob Becker Date: Tue, 26 May 2026 16:38:01 -0600 Subject: [PATCH 1/7] Add pm asdf/pub-get workflow improvements --- CHANGELOG.md | 15 +- README.md | 24 ++- bin/pm.dart | 311 +++++++++++++++++++++++++++++++++--- lib/src/pubspec.yaml.g.dart | 24 +-- pubspec.yaml | 2 +- test/pm_test.dart | 280 +++++++++++++++++++++++++++++++- 6 files changed, 617 insertions(+), 39 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ba2c093..b514f27 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,16 @@ +## [1.2.3] - 2026-05-26 +### Added +- Added `pm set-asdf-dart` to read `environment.sdk` from `pubspec.yaml` and run `asdf set dart` for Dart 2 (`2.19.6`) or Dart 3. +- Added `--dart-3-version` to `pm set-asdf-dart` so the Dart 3 version can be overridden (default: `3.11.6`). +- Added global `--pub-get` to run `dart pub get` in directories where a command modified `pubspec.yaml`. + +### Updated +- Updated `pm` tests to cover `set-asdf-dart` behavior for Dart 2, Dart 3, and custom `--dart-3-version` values. +- Updated README command docs and examples for `set-asdf-dart` and `--dart-3-version`. +- Updated `pm` tests and docs for `--pub-get` behavior on changed and unchanged pubspecs. +- Updated `set-asdf-dart` to support recursive mode (`-r`) across discovered `pubspec.yaml` files. +- Updated `tighten -r` to use each pubspec directory's `pubspec.lock` by default. + ## [1.2.0] - 2026-04-13 ### Added - Added `pm remove` to remove one or more packages from `dependencies` and `dev_dependencies`. @@ -41,4 +54,4 @@ ### Added - Initial Version -[1.0.0]: https://github.com/robrbecker/replace/releases/tag/1.0.0 \ No newline at end of file +[1.0.0]: https://github.com/robrbecker/replace/releases/tag/1.0.0 diff --git a/README.md b/README.md index 5a25864..f5b777c 100644 --- a/README.md +++ b/README.md @@ -90,6 +90,7 @@ Global options: - `-r, --recursive` Recurse through subdirectories and process all pubspec.yaml files - `--fail-on-parse-error` Exit with non-zero if any pubspec.yaml cannot be parsed - `--[no-]tighten` Tighten is enabled by default. Use `--no-tighten` to keep explicit range output. +- `--pub-get` Run `dart pub get` in each directory where `pubspec.yaml` was modified by the command Commands: @@ -99,6 +100,7 @@ Commands: - `raise-max` Raise the maximum bound (exclusive) of a version range - `lower-max` Lower the maximum bound (exclusive) of a version range - `set-sdk` Set `environment.sdk` constraint exactly as provided +- `set-asdf-dart` Read `environment.sdk` and run `asdf set dart ` (Dart 3, default `3.11.6`, overridable with `--dart-3-version`) or `asdf set dart 2.19.6` (Dart 2) - `raise-min-sdk` Raise the minimum `environment.sdk` bound (inclusive) - `raise-max-sdk` Raise the maximum `environment.sdk` bound (exclusive) - `tighten` Raise all dependency minimums to resolved versions from `pubspec.lock` (or a provided lockfile path) @@ -110,10 +112,12 @@ Notes: - SDK commands update `environment.sdk` only - `set` accepts any valid Dart version constraint, for example `^1.9.0` or `'>=1.9.0 <2.0.0'` - `set-sdk` accepts any valid Dart SDK constraint, for example `'>=3.3.0 <4.0.0'` +- `set-asdf-dart` uses `environment.sdk` from the current `pubspec.yaml` (or all discovered pubspec files with `-r`) and accepts optional `--dart-3-version` (default `3.11.6`) - `raise-min`, `raise-max`, and `lower-max` expect a specific semantic version, for example `1.9.1` - `raise-min-sdk` and `raise-max-sdk` expect a specific semantic version, for example `3.4.0` - Tightening is enabled by default and only rewrites when the updated range is exactly equivalent to a caret constraint -- `tighten` reads `pubspec.lock` in the current directory by default and applies the equivalent of `raise-min --tighten` for each locked package +- `--pub-get` only runs in directories where a command actually changed `pubspec.yaml` +- `tighten` reads `pubspec.lock` in the current directory by default (or each pubspec directory when used with `-r`) and applies the equivalent of `raise-min --tighten` for each locked package - `'>=3.0.0 <4.0.0'` becomes `^3.0.0` - `'>=0.18.2 <0.19.0'` becomes `^0.18.2` - `'>=1.2.3 <4.0.0'` stays as a range (not equivalent to a caret constraint) @@ -216,6 +220,12 @@ Lower max version and fail if any pubspec is malformed: pm lower-max path 2.5.0 -r --fail-on-parse-error ``` +Raise minimum and run `dart pub get` for each modified pubspec directory: + +```sh +pm raise-min path 1.9.1 -r --pub-get +``` + Remove multiple dependencies in a single command: ```sh @@ -250,6 +260,18 @@ Set SDK constraint: pm set-sdk '>=3.3.0 <4.0.0' ``` +Set local asdf Dart version from SDK constraint: + +```sh +pm set-asdf-dart +``` + +Set local asdf Dart version with an overridden Dart 3 target: + +```sh +pm set-asdf-dart --dart-3-version 3.12.1 +``` + Raise SDK minimum recursively across a monorepo: ```sh diff --git a/bin/pm.dart b/bin/pm.dart index 465c67d..f955be2 100644 --- a/bin/pm.dart +++ b/bin/pm.dart @@ -14,9 +14,16 @@ const _commandRaiseMin = 'raise-min'; const _commandRaiseMinSdk = 'raise-min-sdk'; const _commandRemove = 'remove'; const _commandSet = 'set'; +const _commandSetAsdfDart = 'set-asdf-dart'; const _commandSetSdk = 'set-sdk'; const _commandTighten = 'tighten'; +const _dart2Version = '2.19.6'; +const _dart3Version = '3.11.6'; +const _asdfExecutableEnv = 'PM_ASDF_BIN'; +const _dartExecutableEnv = 'PM_DART_BIN'; +const _dart3VersionOption = 'dart-3-version'; + const _usageHeader = 'Usage: dart run pm [arguments]'; Future main(List args) async { @@ -65,6 +72,8 @@ Future _run(List args) async { return 64; } + final runPubGet = results['pub-get'] as bool; + if (commandName == _commandTighten) { final rest = command.rest; if (rest.length > 1) { @@ -78,37 +87,52 @@ Future _run(List args) async { final failOnParseError = results['fail-on-parse-error'] as bool; final recursive = results['recursive'] as bool; - final lockfilePath = rest.isEmpty ? 'pubspec.lock' : rest.single.trim(); - if (lockfilePath.isEmpty) { + final requestedLockfilePath = rest.isEmpty ? null : rest.single.trim(); + if (requestedLockfilePath != null && requestedLockfilePath.isEmpty) { stderr.writeln('Lockfile path must not be empty.'); return 64; } - final lockfile = File(lockfilePath); - if (!lockfile.existsSync()) { - stderr.writeln('Lockfile not found: ${lockfile.path}'); - return 64; - } - - Map lockedVersions; - try { - lockedVersions = _readLockedDependencyVersions(lockfile); - } on FormatException catch (e) { - stderr.writeln('Unable to parse ${lockfile.path}: ${e.message}'); - return 64; - } - - if (lockedVersions.isEmpty) { - return 0; - } - final pubspecFiles = _findPubspecFiles(recursive: recursive); if (pubspecFiles.isEmpty) { return 0; } var hadParseError = false; + final changedPubspecDirectories = {}; + final lockedVersionsCache = >{}; for (final path in pubspecFiles) { + final pubspecDirectory = File(path).parent; + final lockfilePath = requestedLockfilePath == null + ? '${pubspecDirectory.path}${Platform.pathSeparator}pubspec.lock' + : requestedLockfilePath; + final lockfile = File(lockfilePath); + if (!lockfile.existsSync()) { + if (recursive && requestedLockfilePath == null) { + continue; + } + stderr.writeln('Lockfile not found: ${lockfile.path}'); + return 64; + } + + final lockfileCacheKey = lockfile.absolute.path; + Map lockedVersions; + if (lockedVersionsCache.containsKey(lockfileCacheKey)) { + lockedVersions = lockedVersionsCache[lockfileCacheKey]!; + } else { + try { + lockedVersions = _readLockedDependencyVersions(lockfile); + } on FormatException catch (e) { + stderr.writeln('Unable to parse ${lockfile.path}: ${e.message}'); + return 64; + } + lockedVersionsCache[lockfileCacheKey] = lockedVersions; + } + + if (lockedVersions.isEmpty) { + continue; + } + PubSpec pubspec; try { pubspec = PubSpec.loadFromPath(path); @@ -155,8 +179,9 @@ Future _run(List args) async { } pubspec.saveTo(path); + changedPubspecDirectories.add(pubspecDirectory.path); for (final message in updateMessages) { - stdout.writeln(message); + stdout.writeln('$path: $message'); } } @@ -164,6 +189,14 @@ Future _run(List args) async { return 1; } + final pubGetExitCode = await _runPubGetForDirectories( + enabled: runPubGet, + directories: changedPubspecDirectories, + ); + if (pubGetExitCode != 0) { + return pubGetExitCode; + } + return 0; } @@ -197,6 +230,7 @@ Future _run(List args) async { } var hadParseError = false; + final changedPubspecDirectories = {}; for (final path in pubspecFiles) { PubSpec pubspec; try { @@ -223,8 +257,9 @@ Future _run(List args) async { } pubspec.saveTo(path); + changedPubspecDirectories.add(File(path).parent.path); for (final message in updateMessages) { - stdout.writeln(message); + stdout.writeln('$path: $message'); } } @@ -232,6 +267,151 @@ Future _run(List args) async { return 1; } + final pubGetExitCode = await _runPubGetForDirectories( + enabled: runPubGet, + directories: changedPubspecDirectories, + ); + if (pubGetExitCode != 0) { + return pubGetExitCode; + } + + return 0; + } + + if (commandName == _commandSetAsdfDart) { + final rest = command.rest; + if (rest.isNotEmpty) { + stderr.writeln('Command "$commandName" does not accept arguments.'); + stderr.writeln(''); + _printUsage(parser); + return 64; + } + + final requestedDart3Version = + (command[_dart3VersionOption] as String).trim(); + if (requestedDart3Version.isEmpty) { + stderr.writeln('Option --$_dart3VersionOption must not be empty.'); + return 64; + } + + final semver.Version dart2Probe; + final semver.Version dart3Probe; + try { + dart2Probe = semver.Version.parse(_dart2Version); + dart3Probe = semver.Version.parse(requestedDart3Version); + } on FormatException catch (e) { + stderr.writeln( + 'Invalid version for --$_dart3VersionOption "$requestedDart3Version": ${e.message}', + ); + return 64; + } + + final recursive = results['recursive'] as bool; + final failOnParseError = results['fail-on-parse-error'] as bool; + final pubspecFiles = _findPubspecFiles(recursive: recursive); + if (pubspecFiles.isEmpty) { + if (recursive) { + return 0; + } + final pubspecPath = + '${Directory.current.path}${Platform.pathSeparator}pubspec.yaml'; + stderr.writeln('pubspec.yaml not found: $pubspecPath'); + return 64; + } + + final asdfExecutable = Platform.environment[_asdfExecutableEnv] ?? 'asdf'; + var hadParseError = false; + for (final pubspecPath in pubspecFiles) { + final String sdkConstraintText; + try { + final pubspec = PubSpec.loadFromPath(pubspecPath); + sdkConstraintText = pubspec.environment.sdk.trim(); + } catch (e) { + hadParseError = true; + stderr.writeln('Unable to parse $pubspecPath: $e'); + if (failOnParseError) { + return 1; + } + continue; + } + + if (sdkConstraintText.isEmpty) { + hadParseError = true; + stderr.writeln('$pubspecPath environment.sdk must not be empty.'); + if (failOnParseError) { + return 1; + } + continue; + } + + final semver.VersionConstraint sdkConstraint; + try { + sdkConstraint = semver.VersionConstraint.parse( + _stripMatchingQuotes(sdkConstraintText), + ); + } on FormatException catch (e) { + hadParseError = true; + stderr.writeln( + 'Invalid SDK constraint "$sdkConstraintText" in $pubspecPath: ${e.message}', + ); + if (failOnParseError) { + return 1; + } + continue; + } + + final supportsDart2 = sdkConstraint.allows(dart2Probe); + final supportsDart3 = sdkConstraint.allows(dart3Probe); + + late String dartVersionToSet; + if (!supportsDart2 && supportsDart3) { + dartVersionToSet = requestedDart3Version; + } else if (supportsDart2) { + dartVersionToSet = _dart2Version; + } else { + hadParseError = true; + stderr.writeln( + 'Unable to determine Dart major from environment.sdk "$sdkConstraintText" in $pubspecPath.', + ); + if (failOnParseError) { + return 1; + } + continue; + } + + final targetDirectory = File(pubspecPath).parent.path; + final setResult = await Process.run( + asdfExecutable, + ['set', 'dart', dartVersionToSet], + workingDirectory: targetDirectory, + ); + + final commandOutput = (setResult.stdout as String).trim(); + if (commandOutput.isNotEmpty) { + stdout.writeln(commandOutput); + } + + final commandError = (setResult.stderr as String).trim(); + if (commandError.isNotEmpty) { + stderr.writeln(commandError); + } + + if (setResult.exitCode != 0) { + stderr.writeln( + 'Failed to run: asdf set dart $dartVersionToSet in $targetDirectory', + ); + return setResult.exitCode; + } + + stdout.writeln( + 'Set Dart to $dartVersionToSet in $targetDirectory based on SDK constraint $sdkConstraintText', + ); + } + + if (failOnParseError && hadParseError) { + return 1; + } + return 0; } @@ -304,6 +484,7 @@ Future _run(List args) async { } var hadParseError = false; + final changedPubspecDirectories = {}; for (final path in pubspecFiles) { PubSpec pubspec; try { @@ -356,8 +537,9 @@ Future _run(List args) async { } pubspec.saveTo(path); + changedPubspecDirectories.add(File(path).parent.path); for (final message in updateMessages) { - stdout.writeln(message); + stdout.writeln('$path: $message'); } } @@ -365,6 +547,72 @@ Future _run(List args) async { return 1; } + final pubGetExitCode = await _runPubGetForDirectories( + enabled: runPubGet, + directories: changedPubspecDirectories, + ); + if (pubGetExitCode != 0) { + return pubGetExitCode; + } + + return 0; +} + +Future _runPubGetForDirectories({ + required bool enabled, + required Set directories, +}) async { + if (!enabled || directories.isEmpty) { + return 0; + } + + final dartExecutable = Platform.environment[_dartExecutableEnv] ?? 'dart'; + final sortedDirectories = directories.toList()..sort(); + for (final directory in sortedDirectories) { + final versionResult = await Process.run( + dartExecutable, + ['--version'], + workingDirectory: directory, + ); + + final versionOutput = (versionResult.stdout as String).trim(); + if (versionOutput.isNotEmpty) { + stdout.writeln(versionOutput); + } + + final versionError = (versionResult.stderr as String).trim(); + if (versionError.isNotEmpty) { + stderr.writeln(versionError); + } + + if (versionResult.exitCode != 0) { + stderr.writeln('Failed to run: dart --version in $directory'); + return versionResult.exitCode; + } + + stdout.writeln('Running dart pub get in $directory'); + final result = await Process.run( + dartExecutable, + ['pub', 'get'], + workingDirectory: directory, + ); + + final commandOutput = (result.stdout as String).trim(); + if (commandOutput.isNotEmpty) { + stdout.writeln(commandOutput); + } + + final commandError = (result.stderr as String).trim(); + if (commandError.isNotEmpty) { + stderr.writeln(commandError); + } + + if (result.exitCode != 0) { + stderr.writeln('Failed to run: dart pub get in $directory'); + return result.exitCode; + } + } + return 0; } @@ -589,6 +837,13 @@ ArgParser _buildParser() { defaultsTo: true, help: 'When updating range constraints, rewrite equivalent ranges as caret constraints (for example >=3.0.0 <4.0.0 to ^3.0.0). Disable with --no-tighten.', + ) + ..addFlag( + 'pub-get', + aliases: ['pubget'], + negatable: false, + help: + 'Run dart pub get in each directory containing a pubspec.yaml that was modified by the command.', ); for (final command in [ @@ -599,12 +854,20 @@ ArgParser _buildParser() { _commandRaiseMin, _commandRaiseMinSdk, _commandSet, + _commandSetAsdfDart, _commandSetSdk, _commandTighten, ]) { parser.addCommand(command); } + parser.commands[_commandSetAsdfDart]?.addOption( + _dart3VersionOption, + defaultsTo: _dart3Version, + help: + 'Dart 3 version to set when environment.sdk requires Dart 3 (default: $_dart3Version).', + ); + return parser; } @@ -627,6 +890,8 @@ void _printUsage(ArgParser parser) { stdout.writeln('(sdk)'); stdout.writeln( ' set-sdk Set the SDK version constraint in environment.sdk.'); + stdout.writeln( + ' set-asdf-dart Set local asdf Dart version from environment.sdk (Dart 3 -> --dart-3-version, default 3.11.6; Dart 2 -> 2.19.6).'); stdout.writeln( ' raise-max-sdk Raise the maximum allowed SDK version (exclusive) in environment.sdk.'); stdout.writeln( diff --git a/lib/src/pubspec.yaml.g.dart b/lib/src/pubspec.yaml.g.dart index 5ce94c0..5fc138a 100644 --- a/lib/src/pubspec.yaml.g.dart +++ b/lib/src/pubspec.yaml.g.dart @@ -92,13 +92,13 @@ sealed class Pubspec { static const PubspecVersion version = ( /// Non-canonical string representation of the version as provided /// in the pubspec.yaml file. - representation: r'1.2.0', + representation: r'1.2.3', /// Returns a 'canonicalized' representation /// of the application version. /// This represents the version string in accordance with /// Semantic Versioning (SemVer) standards. - canonical: r'1.2.0', + canonical: r'1.2.3', /// MAJOR version when you make incompatible API changes. /// The major version number: 1 in "1.2.3". @@ -111,7 +111,7 @@ sealed class Pubspec { /// PATCH version when you make backward compatible bug fixes. /// The patch version number: 3 in "1.2.3". - patch: 0, + patch: 3, /// The pre-release identifier: "foo" in "1.2.3-foo". preRelease: [], @@ -123,13 +123,13 @@ sealed class Pubspec { /// Build date and time (UTC) static final DateTime timestamp = DateTime.utc( 2026, - 4, - 13, - 20, - 38, - 6, - 354, - 898, + 5, + 26, + 22, + 35, + 1, + 915, + 827, ); /// Name @@ -164,8 +164,7 @@ sealed class Pubspec { /// Think of the description as the sales pitch for your package. /// Users see it when they [browse for packages](https://pub.dev/packages). /// The description is plain text: no markdown or HTML. - static const String description = - r'An easy to use cross-platform regex replace command line util.'; + static const String description = r'An easy to use cross-platform regex replace command line util.'; /// Homepage /// @@ -485,4 +484,5 @@ sealed class Pubspec { 'pm': r'', }, }; + } diff --git a/pubspec.yaml b/pubspec.yaml index 11404b3..fa44109 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: replace -version: 1.2.2 +version: 1.2.3 description: An easy to use cross-platform regex replace command line util. repository: https://github.com/robrbecker/replace diff --git a/test/pm_test.dart b/test/pm_test.dart index 9556711..a573db3 100644 --- a/test/pm_test.dart +++ b/test/pm_test.dart @@ -112,6 +112,167 @@ void main() { expect(result.stderr.toString(), contains('Unable to parse')); }); + test('set-asdf-dart uses Dart 3 when sdk requires Dart 3', () async { + final workDir = _writePubspecFixture(scratchRoot, ''' +name: sdk_dart3_fixture +version: 0.0.1 +environment: + sdk: '>=3.0.0 <4.0.0' +'''); + + final asdfBinary = _createFakeAsdfBin(workDir, expectedVersion: '3.11.6'); + final result = await _runPm( + ['set-asdf-dart'], + workingDirectory: workDir.path, + environment: { + 'PM_ASDF_BIN': asdfBinary.path, + }, + ); + + expect(result.exitCode, 0, reason: result.stderr.toString()); + expect(result.stdout.toString(), contains('Set Dart to 3.11.6')); + expect(_readAsdfLog(workDir), equals('set dart 3.11.6\n')); + }); + + test('set-asdf-dart uses Dart 2 when sdk allows Dart 2', () async { + final workDir = _writePubspecFixture(scratchRoot, ''' +name: sdk_dart2_fixture +version: 0.0.1 +environment: + sdk: '>=2.19.0 <3.0.0' +'''); + + final asdfBinary = _createFakeAsdfBin(workDir, expectedVersion: '2.19.6'); + final result = await _runPm( + ['set-asdf-dart'], + workingDirectory: workDir.path, + environment: { + 'PM_ASDF_BIN': asdfBinary.path, + }, + ); + + expect(result.exitCode, 0, reason: result.stderr.toString()); + expect(result.stdout.toString(), contains('Set Dart to 2.19.6')); + expect(_readAsdfLog(workDir), equals('set dart 2.19.6\n')); + }); + + test('set-asdf-dart allows overriding Dart 3 version', () async { + final workDir = _writePubspecFixture(scratchRoot, ''' +name: sdk_dart3_override_fixture +version: 0.0.1 +environment: + sdk: '>=3.12.0 <4.0.0' +'''); + + final asdfBinary = _createFakeAsdfBin(workDir, expectedVersion: '3.12.1'); + final result = await _runPm( + ['set-asdf-dart', '--dart-3-version', '3.12.1'], + workingDirectory: workDir.path, + environment: { + 'PM_ASDF_BIN': asdfBinary.path, + }, + ); + + expect(result.exitCode, 0, reason: result.stderr.toString()); + expect(result.stdout.toString(), contains('Set Dart to 3.12.1')); + expect(_readAsdfLog(workDir), equals('set dart 3.12.1\n')); + }); + + test('set-asdf-dart supports recursive mode', () async { + final workDir = _writePubspecFixture(scratchRoot, ''' +name: sdk_recursive_set_asdf_root +version: 0.0.1 +environment: + sdk: '>=3.0.0 <4.0.0' +'''); + final nestedDir = Directory(p.join(workDir.path, 'packages', 'nested')) + ..createSync(recursive: true); + File(p.join(nestedDir.path, 'pubspec.yaml')).writeAsStringSync(''' +name: sdk_recursive_set_asdf_nested +version: 0.0.1 +environment: + sdk: '>=3.0.0 <4.0.0' +'''); + + final asdfBinary = _createCwdLoggingAsdfBin(workDir); + final result = await _runPm( + ['set-asdf-dart', '-r'], + workingDirectory: workDir.path, + environment: { + 'PM_ASDF_BIN': asdfBinary.path, + }, + ); + + expect(result.exitCode, 0, reason: result.stderr.toString()); + final calls = _readAsdfCwdLog(workDir) + .split('\n') + .where((line) => line.trim().isNotEmpty) + .toList(); + expect(calls, hasLength(2)); + expect(calls.any((line) => line.endsWith(workDir.path)), isTrue); + expect(calls.any((line) => line.endsWith(nestedDir.path)), isTrue); + }); + + test('--pub-get runs dart pub get when pubspec is modified', () async { + final workDir = _copyFixture('basic', scratchRoot); + final dartBinary = _createFakeDartBin(workDir); + + final result = await _runPm( + ['set', 'path', '1.9.1', '--pub-get'], + workingDirectory: workDir.path, + environment: { + 'PM_DART_BIN': dartBinary.path, + }, + ); + + expect(result.exitCode, 0, reason: result.stderr.toString()); + expect(result.stdout.toString(), contains('Dart SDK version: fake')); + final lines = _readDartPubGetLog(workDir) + .split('\n') + .where((line) => line.trim().isNotEmpty) + .toList(); + expect(lines, hasLength(1)); + expect(p.basename(lines.single), equals(p.basename(workDir.path))); + }); + + test('--pub-get does not run dart pub get when pubspec is unchanged', + () async { + final workDir = _copyFixture('basic', scratchRoot); + final dartBinary = _createFakeDartBin(workDir); + + final result = await _runPm( + ['set', 'path', '^1.9.0', '--pub-get'], + workingDirectory: workDir.path, + environment: { + 'PM_DART_BIN': dartBinary.path, + }, + ); + + expect(result.exitCode, 0, reason: result.stderr.toString()); + expect(_readDartPubGetLog(workDir), isEmpty); + }); + + test('--pubget alias runs dart pub get when pubspec is modified', () async { + final workDir = _copyFixture('basic', scratchRoot); + final dartBinary = _createFakeDartBin(workDir); + + final result = await _runPm( + ['set', 'path', '1.9.1', '--pubget'], + workingDirectory: workDir.path, + environment: { + 'PM_DART_BIN': dartBinary.path, + }, + ); + + expect(result.exitCode, 0, reason: result.stderr.toString()); + final lines = _readDartPubGetLog(workDir) + .split('\n') + .where((line) => line.trim().isNotEmpty) + .toList(); + expect(lines, hasLength(1)); + expect(p.basename(lines.single), equals(p.basename(workDir.path))); + }); + test('set updates plain and hosted dependency styles', () async { final workDir = _copyFixture('basic', scratchRoot); @@ -384,6 +545,41 @@ dependencies: expect(_hasConstraint(content, 'version', '>=0.2.3 <1.0.0'), isTrue); expect(_hasConstraint(content, 'test', '^1.25.15'), isTrue); }); + + test('tighten recursive uses each pubspec directory lockfile', () async { + final workDir = _copyFixture('tighten_basic', scratchRoot); + final nestedDir = Directory(p.join(workDir.path, 'packages', 'nested')) + ..createSync(recursive: true); + File(p.join(nestedDir.path, 'pubspec.yaml')).writeAsStringSync(''' +name: nested_tighten_fixture +version: 0.0.1 +environment: + sdk: '>=3.0.0 <4.0.0' +dependencies: + path: '>=1.8.0 <2.0.0' +'''); + + _writeTightenLockfile(workDir, pathVersion: '1.9.1'); + _writeTightenLockfile( + nestedDir, + pathVersion: '1.10.0', + ); + + final result = await _runPm( + ['tighten', '-r'], + workingDirectory: workDir.path, + ); + + expect(result.exitCode, 0, reason: result.stderr.toString()); + + final rootContent = + File(p.join(workDir.path, 'pubspec.yaml')).readAsStringSync(); + final nestedContent = + File(p.join(nestedDir.path, 'pubspec.yaml')).readAsStringSync(); + + expect(_hasConstraint(rootContent, 'path', '^1.9.1'), isTrue); + expect(_hasConstraint(nestedContent, 'path', '^1.10.0'), isTrue); + }); }); } @@ -436,6 +632,7 @@ dependencies: void _writeTightenLockfile( Directory root, { String relativePath = 'pubspec.lock', + String pathVersion = '1.9.1', }) { File(p.join(root.path, relativePath)) ..createSync(recursive: true) @@ -466,7 +663,7 @@ packages: sha256: "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" url: "https://pub.dev" source: hosted - version: "1.9.1" + version: "$pathVersion" test: dependency: "direct dev" description: @@ -498,6 +695,7 @@ void _copyDirectory(Directory source, Directory target) { Future _runPm( List args, { required String workingDirectory, + Map? environment, }) { final packageConfig = p.join(_repoRoot.path, '.dart_tool', 'package_config.json'); @@ -507,9 +705,89 @@ Future _runPm( 'dart', ['--packages=$packageConfig', script, ...args], workingDirectory: workingDirectory, + environment: environment, ); } +File _createFakeAsdfBin(Directory workDir, {required String expectedVersion}) { + final asdfScript = File(p.join(workDir.path, 'asdf_stub.sh')); + final logPath = p.join(workDir.path, '.asdf_calls.log'); + + asdfScript.writeAsStringSync(''' +#!/usr/bin/env bash +set -euo pipefail +printf '%s %s %s\n' "\${1:-}" "\${2:-}" "\${3:-}" >> "$logPath" +if [[ "\${1:-}" != "set" || "\${2:-}" != "dart" || "\${3:-}" != "$expectedVersion" ]]; then + echo "unexpected args: $expectedVersion expected" >&2 + exit 9 +fi +'''); + Process.runSync('chmod', ['+x', asdfScript.path]); + return asdfScript; +} + +String _readAsdfLog(Directory workDir) { + final logFile = File(p.join(workDir.path, '.asdf_calls.log')); + if (!logFile.existsSync()) { + return ''; + } + return logFile.readAsStringSync(); +} + +File _createCwdLoggingAsdfBin(Directory workDir) { + final asdfScript = File(p.join(workDir.path, 'asdf_cwd_stub.sh')); + final logPath = p.join(workDir.path, '.asdf_cwd_calls.log'); + + asdfScript.writeAsStringSync(''' +#!/usr/bin/env bash +set -euo pipefail +if [[ "\${1:-}" != "set" || "\${2:-}" != "dart" ]]; then + echo "unexpected args: \${1:-} \${2:-}" >&2 + exit 11 +fi +pwd >> "$logPath" +'''); + Process.runSync('chmod', ['+x', asdfScript.path]); + return asdfScript; +} + +String _readAsdfCwdLog(Directory workDir) { + final logFile = File(p.join(workDir.path, '.asdf_cwd_calls.log')); + if (!logFile.existsSync()) { + return ''; + } + return logFile.readAsStringSync(); +} + +File _createFakeDartBin(Directory workDir) { + final dartScript = File(p.join(workDir.path, 'dart_stub.sh')); + final logPath = p.join(workDir.path, '.dart_pub_get_calls.log'); + + dartScript.writeAsStringSync(''' +#!/usr/bin/env bash +set -euo pipefail +if [[ "\${1:-}" == "--version" ]]; then + echo "Dart SDK version: fake" + exit 0 +fi +if [[ "\${1:-}" != "pub" || "\${2:-}" != "get" ]]; then + echo "unexpected args: \${1:-} \${2:-}" >&2 + exit 10 +fi +pwd >> "$logPath" +'''); + Process.runSync('chmod', ['+x', dartScript.path]); + return dartScript; +} + +String _readDartPubGetLog(Directory workDir) { + final logFile = File(p.join(workDir.path, '.dart_pub_get_calls.log')); + if (!logFile.existsSync()) { + return ''; + } + return logFile.readAsStringSync(); +} + Directory get _repoRoot => Directory.current; String _readRootPackageVersion() { From 11ad8c018bd1109ac3435135c50ad3c5949f52cf Mon Sep 17 00:00:00 2001 From: Rob Becker Date: Tue, 26 May 2026 16:39:57 -0600 Subject: [PATCH 2/7] Enhance pm workflows for asdf, pub get, and recursive tighten behavior ## Summary - add `pm set-asdf-dart` to select and set Dart via `asdf` from `environment.sdk` - add `--dart-3-version` override (default `3.11.6`) for Dart 3 selection - add global `--pub-get`/`--pubget` to run `dart --version` then `dart pub get` for modified pubspec directories - include modified `pubspec.yaml` path in update output messages - make `set-asdf-dart` honor `-r` and `--fail-on-parse-error` - make `tighten -r` use each package directory's `pubspec.lock` by default - update docs/changelog and bump version metadata to `1.2.3` ## Testing - `test/pm_test.dart` (full suite) --- .tool-versions | 1 + 1 file changed, 1 insertion(+) create mode 100644 .tool-versions diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 0000000..21c3972 --- /dev/null +++ b/.tool-versions @@ -0,0 +1 @@ +dart 3.11.6 From 3619e9fcf7b30676266f51b6d4dec34dad717d8f Mon Sep 17 00:00:00 2001 From: Rob Becker Date: Tue, 26 May 2026 16:43:57 -0600 Subject: [PATCH 3/7] format --- .tool-versions | 2 +- lib/src/pubspec.yaml.g.dart | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.tool-versions b/.tool-versions index 21c3972..0c9a32a 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1 +1 @@ -dart 3.11.6 +dart 3.12.1 diff --git a/lib/src/pubspec.yaml.g.dart b/lib/src/pubspec.yaml.g.dart index 5fc138a..b9775f3 100644 --- a/lib/src/pubspec.yaml.g.dart +++ b/lib/src/pubspec.yaml.g.dart @@ -164,7 +164,8 @@ sealed class Pubspec { /// Think of the description as the sales pitch for your package. /// Users see it when they [browse for packages](https://pub.dev/packages). /// The description is plain text: no markdown or HTML. - static const String description = r'An easy to use cross-platform regex replace command line util.'; + static const String description = + r'An easy to use cross-platform regex replace command line util.'; /// Homepage /// @@ -484,5 +485,4 @@ sealed class Pubspec { 'pm': r'', }, }; - } From b1d825fa0227f2f6d6d7f3211a5d4333770809f1 Mon Sep 17 00:00:00 2001 From: Rob Becker Date: Tue, 26 May 2026 16:47:38 -0600 Subject: [PATCH 4/7] format and update description --- lib/src/pubspec.yaml.g.dart | 8 ++++---- pubspec.yaml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/src/pubspec.yaml.g.dart b/lib/src/pubspec.yaml.g.dart index b9775f3..60848d3 100644 --- a/lib/src/pubspec.yaml.g.dart +++ b/lib/src/pubspec.yaml.g.dart @@ -126,10 +126,10 @@ sealed class Pubspec { 5, 26, 22, - 35, - 1, - 915, - 827, + 45, + 39, + 772, + 949, ); /// Name diff --git a/pubspec.yaml b/pubspec.yaml index fa44109..32a24b5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: replace version: 1.2.3 -description: An easy to use cross-platform regex replace command line util. +description: An easy to use cross-platform regex replace command line util and pm CLI to modify pubspec.yaml files. repository: https://github.com/robrbecker/replace environment: From 30f6cb6980a765414ccb7f7bae6aac694298c664 Mon Sep 17 00:00:00 2001 From: Rob Becker Date: Tue, 26 May 2026 16:51:03 -0600 Subject: [PATCH 5/7] format again --- lib/src/pubspec.yaml.g.dart | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/src/pubspec.yaml.g.dart b/lib/src/pubspec.yaml.g.dart index 60848d3..d769538 100644 --- a/lib/src/pubspec.yaml.g.dart +++ b/lib/src/pubspec.yaml.g.dart @@ -126,10 +126,10 @@ sealed class Pubspec { 5, 26, 22, - 45, - 39, - 772, - 949, + 50, + 38, + 418, + 139, ); /// Name @@ -165,7 +165,7 @@ sealed class Pubspec { /// Users see it when they [browse for packages](https://pub.dev/packages). /// The description is plain text: no markdown or HTML. static const String description = - r'An easy to use cross-platform regex replace command line util.'; + r'An easy to use cross-platform regex replace command line util and pm CLI to modify pubspec.yaml files.'; /// Homepage /// From 52730d41e9c718bd599263851d04fb4793ccf6e3 Mon Sep 17 00:00:00 2001 From: Rob Becker Date: Tue, 26 May 2026 17:02:44 -0600 Subject: [PATCH 6/7] ignore generated version file --- analysis_options.yaml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/analysis_options.yaml b/analysis_options.yaml index c065a24..858f034 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1,3 +1,7 @@ analyzer: exclude: - - test/fixtures/** \ No newline at end of file + - test/fixtures/** + +formatter: + exclude: + - "lib/src/pubspec.yaml.g.dart" \ No newline at end of file From 0125bcd6cf49ae2e923a77bccf37db47a06e709b Mon Sep 17 00:00:00 2001 From: Rob Becker Date: Tue, 26 May 2026 17:10:31 -0600 Subject: [PATCH 7/7] update CI --- .github/workflows/ci.yml | 12 ------------ .tool-versions | 1 - analysis_options.yaml | 4 ---- lib/src/pubspec.yaml.g.dart | 10 +++++----- 4 files changed, 5 insertions(+), 22 deletions(-) delete mode 100644 .tool-versions diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e8fb3af..71fa090 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,18 +22,6 @@ jobs: - name: Install dependencies run: dart pub get - - name: Build generated files - run: dart run build_runner build --delete-conflicting-outputs - - - name: Verify generated files are up to date - run: | - if [[ -n "$(git status --porcelain)" ]]; then - echo "Generated files are not up to date." - echo "Run: dart run build_runner build --delete-conflicting-outputs" - git --no-pager diff - # exit 1 - fi - - name: Analyze run: dart analyze . diff --git a/.tool-versions b/.tool-versions deleted file mode 100644 index 0c9a32a..0000000 --- a/.tool-versions +++ /dev/null @@ -1 +0,0 @@ -dart 3.12.1 diff --git a/analysis_options.yaml b/analysis_options.yaml index 858f034..1206125 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1,7 +1,3 @@ analyzer: exclude: - test/fixtures/** - -formatter: - exclude: - - "lib/src/pubspec.yaml.g.dart" \ No newline at end of file diff --git a/lib/src/pubspec.yaml.g.dart b/lib/src/pubspec.yaml.g.dart index d769538..d572fca 100644 --- a/lib/src/pubspec.yaml.g.dart +++ b/lib/src/pubspec.yaml.g.dart @@ -125,11 +125,11 @@ sealed class Pubspec { 2026, 5, 26, - 22, - 50, - 38, - 418, - 139, + 23, + 8, + 40, + 314, + 946, ); /// Name