├── lib ├── src │ ├── sources │ │ ├── export.dart │ │ ├── pubspec.dart │ │ └── rps_yaml.dart │ ├── scripts_parser │ │ ├── events │ │ │ ├── export.dart │ │ │ └── execution │ │ │ │ ├── export.dart │ │ │ │ ├── execution_event.dart │ │ │ │ ├── hook_executed.dart │ │ │ │ ├── command_referenced.dart │ │ │ │ └── command_executed.dart │ │ ├── sources │ │ │ ├── export.dart │ │ │ └── scripts_source.dart │ │ ├── exceptions │ │ │ ├── export.dart │ │ │ └── script_parser_exception.dart │ │ ├── export.dart │ │ ├── context.dart │ │ └── scripts_parser.dart │ ├── utils │ │ ├── date_utils.dart │ │ └── rps_package.dart │ ├── cli │ │ ├── commands │ │ │ ├── command.dart │ │ │ ├── list.dart │ │ │ └── run_command.dart │ │ ├── exceptions │ │ │ └── cli_exception.dart │ │ ├── cli_options │ │ │ ├── cli_option.dart │ │ │ ├── version.dart │ │ │ ├── upgrade.dart │ │ │ └── help.dart │ │ ├── cli.dart │ │ └── console.dart │ ├── exceptions │ │ └── rps_exception.dart │ └── bindings │ │ └── execute.dart └── rps.dart ├── rps_lib ├── rps.def ├── rps.h ├── .gitignore ├── CMakeLists.txt └── rps.cpp ├── stonks.jpg ├── native ├── rps_x64.dll ├── librps.dylib ├── librps_x64.so └── librps_aarch64.so ├── .gitignore ├── test ├── mocks │ ├── script_source.mock.dart │ └── stream_sink_controller.dart └── scripts_parser │ └── scripts_parser_test.dart ├── pubspec.yaml ├── LICENSE ├── analysis_options.yaml ├── bin └── rps.dart ├── CHANGELOG.md ├── README.md └── pubspec.lock /lib/src/sources/export.dart: -------------------------------------------------------------------------------- 1 | export 'rps_yaml.dart'; 2 | -------------------------------------------------------------------------------- /rps_lib/rps.def: -------------------------------------------------------------------------------- 1 | LIBRARY rsp 2 | EXPORTS 3 | execute -------------------------------------------------------------------------------- /stonks.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gonuit/rps/HEAD/stonks.jpg -------------------------------------------------------------------------------- /lib/src/scripts_parser/events/export.dart: -------------------------------------------------------------------------------- 1 | export 'execution/export.dart'; 2 | -------------------------------------------------------------------------------- /lib/src/scripts_parser/sources/export.dart: -------------------------------------------------------------------------------- 1 | export 'scripts_source.dart'; 2 | -------------------------------------------------------------------------------- /native/rps_x64.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gonuit/rps/HEAD/native/rps_x64.dll -------------------------------------------------------------------------------- /rps_lib/rps.h: -------------------------------------------------------------------------------- 1 | extern "C" 2 | { 3 | int execute(char *command); 4 | } 5 | -------------------------------------------------------------------------------- /lib/src/scripts_parser/exceptions/export.dart: -------------------------------------------------------------------------------- 1 | export 'script_parser_exception.dart'; 2 | -------------------------------------------------------------------------------- /native/librps.dylib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gonuit/rps/HEAD/native/librps.dylib -------------------------------------------------------------------------------- /native/librps_x64.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gonuit/rps/HEAD/native/librps_x64.so -------------------------------------------------------------------------------- /native/librps_aarch64.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gonuit/rps/HEAD/native/librps_aarch64.so -------------------------------------------------------------------------------- /lib/src/scripts_parser/sources/scripts_source.dart: -------------------------------------------------------------------------------- 1 | abstract class ScriptsSource { 2 | dynamic getScripts(); 3 | } 4 | -------------------------------------------------------------------------------- /lib/src/scripts_parser/events/execution/export.dart: -------------------------------------------------------------------------------- 1 | export 'command_executed.dart'; 2 | export 'command_referenced.dart'; 3 | export 'execution_event.dart'; 4 | export 'hook_executed.dart'; 5 | -------------------------------------------------------------------------------- /lib/src/scripts_parser/exceptions/script_parser_exception.dart: -------------------------------------------------------------------------------- 1 | class ScriptParserException implements Exception { 2 | final String message; 3 | 4 | ScriptParserException(this.message); 5 | } 6 | -------------------------------------------------------------------------------- /lib/src/scripts_parser/export.dart: -------------------------------------------------------------------------------- 1 | export 'context.dart'; 2 | export 'scripts_parser.dart'; 3 | export 'events/export.dart'; 4 | export 'sources/scripts_source.dart'; 5 | export 'exceptions/export.dart'; 6 | -------------------------------------------------------------------------------- /lib/src/utils/date_utils.dart: -------------------------------------------------------------------------------- 1 | extension DateTimeExtension on DateTime { 2 | bool isSameDay(DateTime other) { 3 | return year == other.year && month == other.month && day == other.day; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /lib/src/scripts_parser/events/execution/execution_event.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | import 'package:rps/rps.dart'; 3 | 4 | @immutable 5 | abstract class ExecutionEvent { 6 | Context get context; 7 | String get path; 8 | String? get command; 9 | } 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Files and directories created by pub 2 | .dart_tool/ 3 | .packages 4 | .DS_Store 5 | .vscode/ 6 | rps.yaml 7 | 8 | # Conventional directory for build outputs 9 | build/ 10 | 11 | # Directory created by dartdoc 12 | doc/api/ 13 | 14 | rps.config 15 | coverage/** -------------------------------------------------------------------------------- /lib/rps.dart: -------------------------------------------------------------------------------- 1 | export 'src/cli/commands/run_command.dart'; 2 | export 'src/bindings/execute.dart'; 3 | export 'src/cli/console.dart'; 4 | export 'src/exceptions/rps_exception.dart'; 5 | export 'src/sources/pubspec.dart'; 6 | export 'src/scripts_parser/export.dart'; 7 | export 'src/sources/rps_yaml.dart'; 8 | -------------------------------------------------------------------------------- /lib/src/cli/commands/command.dart: -------------------------------------------------------------------------------- 1 | import 'package:rps/rps.dart'; 2 | 3 | abstract class Command { 4 | const Command(); 5 | 6 | String get description; 7 | String get name; 8 | String? get tooltip; 9 | bool match(List arguments); 10 | Future run(Console console, List arguments); 11 | } 12 | -------------------------------------------------------------------------------- /lib/src/cli/exceptions/cli_exception.dart: -------------------------------------------------------------------------------- 1 | class CliException implements Exception { 2 | final String message; 3 | final Exception? error; 4 | final StackTrace? stackTrace; 5 | final int exitCode; 6 | 7 | CliException( 8 | this.message, { 9 | this.exitCode = 1, 10 | this.error, 11 | this.stackTrace, 12 | }); 13 | } 14 | -------------------------------------------------------------------------------- /lib/src/exceptions/rps_exception.dart: -------------------------------------------------------------------------------- 1 | class RpsException implements Exception { 2 | final String message; 3 | final Exception? error; 4 | final StackTrace? stackTrace; 5 | 6 | RpsException(this.message, [this.error, this.stackTrace]); 7 | 8 | @override 9 | String toString() { 10 | return 'RpsException: $message'; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/mocks/script_source.mock.dart: -------------------------------------------------------------------------------- 1 | import 'package:rps/rps.dart'; 2 | import 'package:yaml/yaml.dart'; 3 | 4 | class MockedScriptSource implements ScriptsSource { 5 | final String pubspecContent; 6 | 7 | MockedScriptSource(this.pubspecContent); 8 | 9 | @override 10 | dynamic getScripts() { 11 | return loadYaml(pubspecContent)['scripts']; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /rps_lib/.gitignore: -------------------------------------------------------------------------------- 1 | # ffi 2 | *.o 3 | *.dylib 4 | a.out 5 | .vscode/ 6 | .packages 7 | sdk-version 8 | *snapshot* 9 | pubspec.lock 10 | CMakeCache.txt 11 | CMakeFiles 12 | cmake_install.cmake 13 | Makefile 14 | primitives/primitives_library/primitives_test 15 | structs/structs_library/structs_test 16 | *.recipe 17 | *.tlog 18 | *.log 19 | *.pdb 20 | *.vcxproj 21 | *.vcxproj.filters 22 | *.vcxproj.user 23 | *.sln 24 | *.ilk 25 | .vs 26 | Debug 27 | Release 28 | rps_test 29 | 30 | # Windows 31 | *.obj 32 | *.dll 33 | *.lib 34 | *.exe 35 | *.exp -------------------------------------------------------------------------------- /lib/src/cli/cli_options/cli_option.dart: -------------------------------------------------------------------------------- 1 | import 'package:rps/rps.dart'; 2 | import 'package:rps/src/cli/cli.dart'; 3 | import 'package:collection/collection.dart'; 4 | 5 | abstract class CliOption { 6 | const CliOption(); 7 | 8 | String get description; 9 | String? get short; 10 | String get name; 11 | 12 | bool match(List arguments) { 13 | String? first = arguments.firstOrNull; 14 | if (first == null) return false; 15 | first = first.trim(); 16 | return (short != null && first == '-$short') || first == '--$name'; 17 | } 18 | 19 | Future run(Cli cli, Console console, List arguments); 20 | } 21 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: rps 2 | description: rps (Run Pubspec Script) allows you to define and run scripts from pubspec.yaml. 3 | version: 0.9.1 4 | repository: https://github.com/gonuit/rps 5 | homepage: https://github.com/gonuit/rps 6 | issue_tracker: https://github.com/gonuit/rps/issues 7 | 8 | executables: 9 | rps: 10 | 11 | environment: 12 | sdk: ">=3.0.0 <4.0.0" 13 | 14 | dependencies: 15 | yaml: ^3.1.2 16 | path: ^1.8.3 17 | ffi: ^2.1.0 18 | meta: ^1.16.0 19 | http_api: ^0.9.0 20 | pub_semver: ^2.1.4 21 | collection: ^1.18.0 22 | 23 | dev_dependencies: 24 | flutter_lints: ^5.0.0 25 | test: ^1.25.8 26 | -------------------------------------------------------------------------------- /lib/src/cli/cli_options/version.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:rps/rps.dart'; 4 | import 'package:rps/src/cli/cli.dart'; 5 | import 'package:rps/src/cli/cli_options/cli_option.dart'; 6 | 7 | class VersionOption extends CliOption { 8 | const VersionOption(); 9 | 10 | @override 11 | String get description => 'Prints rps version.'; 12 | 13 | @override 14 | String get name => 'version'; 15 | 16 | @override 17 | String? get short => null; 18 | 19 | @override 20 | Future run(Cli cli, Console console, List arguments) async { 21 | console 22 | ..writeln() 23 | ..writeln('📝 rps version: ${bold(cli.package.version.toString())}') 24 | ..writeln(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /lib/src/scripts_parser/events/execution/hook_executed.dart: -------------------------------------------------------------------------------- 1 | import 'package:rps/rps.dart'; 2 | 3 | class HookExecuted extends ExecutionEvent { 4 | @override 5 | final Context context; 6 | 7 | final String name; 8 | 9 | @override 10 | String get path => context.path.join(' '); 11 | @override 12 | final String command; 13 | 14 | HookExecuted({ 15 | required this.context, 16 | required this.command, 17 | required this.name, 18 | }); 19 | 20 | @override 21 | bool operator ==(Object other) { 22 | return other is HookExecuted && 23 | other.command == command && 24 | other.path == path && 25 | other.name == name; 26 | } 27 | 28 | @override 29 | int get hashCode => Object.hash(path, command, name, runtimeType); 30 | } 31 | -------------------------------------------------------------------------------- /lib/src/scripts_parser/events/execution/command_referenced.dart: -------------------------------------------------------------------------------- 1 | import 'package:rps/rps.dart'; 2 | 3 | class CommandReferenced extends ExecutionEvent { 4 | @override 5 | final Context context; 6 | 7 | final String label; 8 | 9 | @override 10 | String get path => context.basePath.join(' '); 11 | @override 12 | final String command; 13 | 14 | final bool isHook; 15 | 16 | CommandReferenced({ 17 | required this.context, 18 | required this.command, 19 | required this.label, 20 | this.isHook = false, 21 | }); 22 | 23 | @override 24 | bool operator ==(Object other) { 25 | return other is CommandReferenced && 26 | other.command == command && 27 | other.path == path && 28 | other.label == label; 29 | } 30 | 31 | @override 32 | int get hashCode => Object.hash(path, command, label, runtimeType); 33 | } 34 | -------------------------------------------------------------------------------- /test/mocks/stream_sink_controller.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:rps/rps.dart'; 4 | 5 | class StreamSinkController implements StringSink { 6 | StreamSinkController(); 7 | 8 | List get lines => const LineSplitter().convert(_writes.join('')); 9 | List get plainLines => lines.map(removeAnsiEscapeSequences).toList(); 10 | final _writes = []; 11 | 12 | @override 13 | void write(Object? obj) { 14 | _writes.add(obj.toString()); 15 | } 16 | 17 | @override 18 | void writeAll(Iterable objects, [String separator = ""]) { 19 | _writes.add(objects.join(separator)); 20 | } 21 | 22 | @override 23 | void writeln([Object? obj = ""]) { 24 | _writes.add('$obj\n'); 25 | } 26 | 27 | @override 28 | void writeCharCode(int charCode) { 29 | _writes.add(String.fromCharCode(charCode)); 30 | } 31 | 32 | void clear() { 33 | _writes.clear(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /rps_lib/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.15 FATAL_ERROR) 2 | 3 | set (CMAKE_CXX_STANDARD 17) 4 | set(CMAKE_OSX_ARCHITECTURES "x86_64;arm64" ) 5 | 6 | 7 | project(rps_lib VERSION 0.2.0 LANGUAGES CXX) 8 | add_library(rps_lib SHARED rps.cpp rps.def) 9 | add_executable(rps_test rps.cpp) 10 | 11 | # Set /MT or /MTd compiler flag for MSVC using MSVC_RUNTIME_LIBRARY 12 | if(MSVC) 13 | # For the library 14 | set_property(TARGET rps_lib PROPERTY 15 | MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") 16 | 17 | # For the executable 18 | set_property(TARGET rps_test PROPERTY 19 | MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") 20 | endif() 21 | 22 | set_target_properties(rps_lib PROPERTIES 23 | LINK_FLAGS_RELEASE -x 24 | PUBLIC_HEADER rps.h 25 | VERSION ${PROJECT_VERSION} 26 | SOVERSION 1 27 | OUTPUT_NAME "rps" 28 | # XCODE_ATTRIBUTE_CODE_SIGN_IDENTITY "Hex_Identity_ID_Goes_Here" 29 | ) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Kamil Klyta 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /lib/src/cli/cli_options/upgrade.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:rps/rps.dart'; 4 | import 'package:rps/src/cli/cli.dart'; 5 | import 'package:rps/src/cli/cli_options/cli_option.dart'; 6 | 7 | import '../exceptions/cli_exception.dart'; 8 | 9 | class UpgradeOption extends CliOption { 10 | const UpgradeOption(); 11 | 12 | @override 13 | String get description => 'Updates rps to latest version.'; 14 | 15 | @override 16 | String get name => 'upgrade'; 17 | 18 | @override 19 | String? get short => 'u'; 20 | 21 | @override 22 | Future run(Cli cli, Console console, List arguments) async { 23 | const command = 'dart pub global activate rps'; 24 | console.write( 25 | '${boldBlue('\n⏳ Upgrading rps package...')}\n\n' 26 | '${boldGreen(r'$')} ${bold(command)}\n\n', 27 | ); 28 | 29 | final exitCode = await execute(command); 30 | if (exitCode > 0) { 31 | throw CliException( 32 | 'Failed to update the rps package. ' 33 | 'Command ended with a non zero exit code.', 34 | exitCode: exitCode, 35 | ); 36 | } else { 37 | console 38 | ..writeln() 39 | ..writeln(boldGreen('✓ rps updated successfully!')) 40 | ..writeln(); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /lib/src/sources/pubspec.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'package:path/path.dart' as p; 3 | import 'package:rps/rps.dart'; 4 | import 'package:yaml/yaml.dart'; 5 | 6 | class Pubspec implements ScriptsSource { 7 | static const filename = 'pubspec.yaml'; 8 | 9 | final Directory directory; 10 | final Map parsed; 11 | 12 | Pubspec._(this.directory, this.parsed); 13 | 14 | factory Pubspec.load(Directory directory) { 15 | bool isPubspecFile(File file) => p.basename(file.path) == Pubspec.filename; 16 | 17 | final pubspecFile = directory.listSync().whereType().firstWhere( 18 | isPubspecFile, 19 | orElse: () => throw RpsException( 20 | 'Cannot find $filename file in the current directory ' 21 | '(${Directory.current.path}).', 22 | ), 23 | ); 24 | 25 | try { 26 | final pubspecString = pubspecFile.readAsStringSync(); 27 | final parsed = Map.unmodifiable(loadYaml(pubspecString)); 28 | 29 | return Pubspec._(directory, parsed); 30 | } on Exception catch (err, st) { 31 | throw RpsException('Pubspec file cannot be parsed', err, st); 32 | } 33 | } 34 | 35 | String get packageVersion => parsed['version']!; 36 | String get packageName => parsed['name']!; 37 | 38 | @override 39 | dynamic getScripts() { 40 | dynamic scripts = parsed['scripts']; 41 | if (scripts == null) { 42 | throw RpsException('Missing "scripts" field in the $filename file.'); 43 | } 44 | return scripts; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # This file configures the analyzer, which statically analyzes Dart code to 2 | # check for errors, warnings, and lints. 3 | # 4 | # The issues identified by the analyzer are surfaced in the UI of Dart-enabled 5 | # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be 6 | # invoked from the command line by running `flutter analyze`. 7 | 8 | # The following line activates a set of recommended lints for Flutter apps, 9 | # packages, and plugins designed to encourage good coding practices. 10 | include: package:flutter_lints/flutter.yaml 11 | 12 | linter: 13 | # The lint rules applied to this project can be customized in the 14 | # section below to disable rules from the `package:flutter_lints/flutter.yaml` 15 | # included above or to enable additional rules. A list of all available lints 16 | # and their documentation is published at 17 | # https://dart-lang.github.io/linter/lints/index.html. 18 | # 19 | # Instead of disabling a lint rule for the entire project in the 20 | # section below, it can also be suppressed for a single line of code 21 | # or a specific dart file by using the `// ignore: name_of_lint` and 22 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file 23 | # producing the lint. 24 | rules: 25 | avoid_print: false # Uncomment to disable the `avoid_print` rule 26 | # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule 27 | 28 | # Additional information about this file can be found at 29 | # https://dart.dev/guides/language/analysis-options 30 | -------------------------------------------------------------------------------- /lib/src/cli/cli.dart: -------------------------------------------------------------------------------- 1 | import 'package:rps/rps.dart'; 2 | import 'package:rps/src/cli/commands/command.dart'; 3 | import 'package:rps/src/cli/cli_options/cli_option.dart'; 4 | import 'package:collection/collection.dart'; 5 | import 'package:rps/src/cli/exceptions/cli_exception.dart'; 6 | import 'package:rps/src/utils/rps_package.dart'; 7 | 8 | class Cli { 9 | final RpsPackage package; 10 | final Console console; 11 | final List commands; 12 | final List options; 13 | final CliOption? fallback; 14 | 15 | Cli({ 16 | required this.package, 17 | required this.console, 18 | required this.commands, 19 | required this.options, 20 | required this.fallback, 21 | }); 22 | 23 | Future run(List arguments) async { 24 | try { 25 | final option = 26 | options.firstWhereOrNull((option) => option.match(arguments)); 27 | if (option != null) { 28 | return option.run(this, console, arguments); 29 | } 30 | final command = 31 | commands.firstWhereOrNull((command) => command.match(arguments)); 32 | if (command != null) { 33 | return command.run(console, arguments); 34 | } 35 | 36 | if (fallback != null) { 37 | console 38 | ..writeln('${bold(yellow('Warning!'))} No command has been matched.') 39 | ..writeln(); 40 | fallback!.run(this, console, arguments); 41 | } else { 42 | throw CliException('No command has been matched.'); 43 | } 44 | } catch (_) { 45 | await console.flush(); 46 | rethrow; 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /lib/src/cli/commands/list.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:rps/rps.dart'; 4 | import 'package:rps/src/cli/commands/command.dart'; 5 | 6 | class LsCommand extends Command { 7 | final FutureOr Function() _getScriptsSource; 8 | 9 | LsCommand({ 10 | required FutureOr Function() getScriptsSource, 11 | }) : _getScriptsSource = getScriptsSource; 12 | 13 | @override 14 | String get description => 'List all commands.'; 15 | 16 | @override 17 | String get name => 'ls'; 18 | 19 | @override 20 | String get tooltip => 'ls'; 21 | 22 | @override 23 | bool match(List arguments) { 24 | return arguments.isNotEmpty && arguments[0] == 'ls'; 25 | } 26 | 27 | @override 28 | Future run(Console console, List arguments) async { 29 | final source = await _getScriptsSource(); 30 | final parser = ScriptsParser(source: source); 31 | final commands = parser.listCommands().toList(); 32 | if (commands.isNotEmpty) { 33 | console.writeln('${bold('Commands')}:'); 34 | for (final command in commands) { 35 | console 36 | ..write(' ') 37 | ..write(lightBlue(command.path)) 38 | ..write(' ') 39 | ..writeln('(${gray(command.command)})'); 40 | if (command.error != null) { 41 | console 42 | ..write(' ') 43 | ..writeln(red(command.error!)); 44 | } 45 | if (command.description != null) { 46 | console 47 | ..write(' ') 48 | ..writeln(command.description); 49 | } 50 | } 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /lib/src/sources/rps_yaml.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'package:path/path.dart' as p; 3 | import 'package:rps/rps.dart'; 4 | import 'package:yaml/yaml.dart'; 5 | 6 | /// A script source implementation that loads scripts from the `rps.yaml` file. 7 | class RpsYaml implements ScriptsSource { 8 | static const filename = 'rps.yaml'; 9 | 10 | final Directory directory; 11 | final Map parsed; 12 | 13 | RpsYaml._(this.directory, this.parsed); 14 | 15 | /// Returns `true` if the `rps.yaml` file 16 | /// is present in the provided [directory]. 17 | static bool exists(Directory directory) { 18 | final filePath = p.join(directory.path, filename); 19 | final rpsFile = File(filePath); 20 | return rpsFile.existsSync(); 21 | } 22 | 23 | factory RpsYaml.load(Directory directory) { 24 | bool isScriptsFile(File file) => p.basename(file.path) == RpsYaml.filename; 25 | 26 | final file = directory.listSync().whereType().firstWhere( 27 | isScriptsFile, 28 | orElse: () => throw RpsException( 29 | 'Cannot find $filename file in the current directory ' 30 | '(${Directory.current.path}).', 31 | ), 32 | ); 33 | 34 | try { 35 | final string = file.readAsStringSync(); 36 | final parsed = Map.unmodifiable(loadYaml(string)); 37 | 38 | return RpsYaml._(directory, parsed); 39 | } on Exception catch (err, st) { 40 | throw RpsException('Scripts file cannot be parsed', err, st); 41 | } 42 | } 43 | 44 | @override 45 | dynamic getScripts() { 46 | dynamic scripts = parsed['scripts']; 47 | if (scripts == null) { 48 | throw RpsException('Missing scripts in the $filename file.'); 49 | } 50 | return scripts; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /lib/src/bindings/execute.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ffi'; 2 | import 'dart:isolate'; 3 | 4 | import 'package:ffi/ffi.dart'; 5 | import 'package:path/path.dart' as path; 6 | import 'package:rps/rps.dart'; 7 | 8 | typedef ExecuteNative = Int32 Function(Pointer command); 9 | typedef Execute = int Function(Pointer command); 10 | typedef ExecuteFunction = Future Function(String command); 11 | 12 | Future execute( 13 | String command, { 14 | bool verbose = false, 15 | StringSink? out, 16 | }) async { 17 | final bindings = { 18 | Abi.windowsX64: 'rps_x64.dll', 19 | Abi.linuxX64: 'librps_x64.so', 20 | Abi.linuxArm64: 'librps_aarch64.so', 21 | Abi.macosX64: 'librps.dylib', 22 | Abi.macosArm64: 'librps.dylib', 23 | }; 24 | 25 | const rootLibrary = 'package:rps/rps.dart'; 26 | final uri = await Isolate.resolvePackageUri(Uri.parse(rootLibrary)); 27 | if (uri == null) { 28 | throw RpsException('Cannot load the library.'); 29 | } 30 | 31 | final platform = Abi.current(); 32 | if (verbose) { 33 | out?.writeln("Running on platform: $platform"); 34 | } 35 | 36 | String? libraryName = bindings[platform]; 37 | 38 | if (verbose) { 39 | out?.writeln("Dynamic library file selected: $libraryName"); 40 | } 41 | 42 | if (libraryName == null) { 43 | throw RpsException( 44 | 'Current platform ($platform) is currently not supported.'); 45 | } 46 | 47 | final root = path.fromUri(uri.resolve(path.join('..', 'native')).path); 48 | final libraryPath = path.join(root, libraryName); 49 | 50 | if (verbose) { 51 | out?.writeln("Dynamic library path: $libraryPath"); 52 | } 53 | 54 | final dylib = DynamicLibrary.open(libraryPath); 55 | final execute = dylib.lookupFunction('execute'); 56 | 57 | final code = execute(command.toNativeUtf8()); 58 | 59 | return code; 60 | } 61 | -------------------------------------------------------------------------------- /lib/src/cli/cli_options/help.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:rps/rps.dart'; 4 | import 'package:rps/src/cli/cli.dart'; 5 | import 'package:rps/src/cli/cli_options/cli_option.dart'; 6 | import 'package:rps/src/utils/rps_package.dart'; 7 | 8 | class HelpOption extends CliOption { 9 | final Console console; 10 | final RpsPackage package; 11 | 12 | HelpOption({ 13 | required this.console, 14 | required this.package, 15 | }); 16 | 17 | @override 18 | String get description => 'Prints help.'; 19 | 20 | @override 21 | String get name => 'help'; 22 | 23 | @override 24 | String get short => 'h'; 25 | 26 | @override 27 | Future run(Cli cli, Console console, List arguments) async { 28 | console 29 | ..writeln( 30 | '${bold('Run Pubspec Script')} (${boldGreen('rps')}) ${bold("v${package.version}")}') 31 | ..writeln(); 32 | // ..writeln('${bold('Options')}:') 33 | // ..writeln(' -v, --version - prints version.') 34 | // ..writeln(' -h, --help - prints help.') 35 | // ..writeln(' -u, --upgrade - upgrades rps package.'); 36 | 37 | final options = cli.options; 38 | if (options.isNotEmpty) { 39 | console.writeln('${bold('Options')}:'); 40 | for (final option in options) { 41 | final short = option.short; 42 | 43 | if (short != null) { 44 | console.writeln( 45 | ' -${option.short}, --${option.name} - ${option.description}'); 46 | } else { 47 | console.writeln(' --${option.name} - ${option.description}'); 48 | } 49 | } 50 | } 51 | 52 | final commands = cli.commands; 53 | if (commands.isNotEmpty) { 54 | console.writeln('${bold('Commands')}:'); 55 | for (final command in commands) { 56 | console.writeln( 57 | ' ${command.tooltip ?? command.name} - ${command.description}'); 58 | } 59 | } 60 | 61 | console.writeln(); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /bin/rps.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:rps/src/cli/cli.dart'; 4 | import 'package:rps/src/cli/cli_options/help.dart'; 5 | import 'package:rps/src/cli/cli_options/upgrade.dart'; 6 | import 'package:rps/src/cli/cli_options/version.dart'; 7 | import 'package:rps/src/cli/commands/list.dart'; 8 | import 'package:rps/src/cli/exceptions/cli_exception.dart'; 9 | import 'package:rps/src/utils/rps_package.dart'; 10 | import 'package:rps/src/bindings/execute.dart' as bindings; 11 | import 'package:rps/rps.dart'; 12 | 13 | void main(List args) async { 14 | final console = Console(sink: stdout); 15 | 16 | try { 17 | final package = await RpsPackage.load(); 18 | try { 19 | final versions = await package.getVersions(); 20 | if (versions.hasUpdate) { 21 | console.writeBordered([ 22 | 'Update available ${gray(versions.current.toString())} → ${green(versions.latest.toString())}', 23 | 'Run ${lightBlue('dart pub global activate rps')} to update', 24 | ]); 25 | await Future.delayed(const Duration(seconds: 2)); 26 | } 27 | } on Exception { 28 | // ignore 29 | } 30 | 31 | ScriptsSource loadScriptSource() { 32 | final cur = Directory.current; 33 | if (RpsYaml.exists(cur)) { 34 | return RpsYaml.load(Directory.current); 35 | } else { 36 | return Pubspec.load(Directory.current); 37 | } 38 | } 39 | 40 | final help = HelpOption(console: console, package: package); 41 | final cli = Cli( 42 | package: package, 43 | console: console, 44 | commands: [ 45 | LsCommand(getScriptsSource: loadScriptSource), 46 | RunCommand( 47 | getScriptsSource: loadScriptSource, 48 | execute: bindings.execute, 49 | ), 50 | ], 51 | options: [ 52 | help, 53 | const VersionOption(), 54 | const UpgradeOption(), 55 | ], 56 | fallback: help, 57 | ); 58 | 59 | await cli.run(args); 60 | } on RpsException catch (err) { 61 | stderr.writeln("${boldRed('Error!')} ${err.message}"); 62 | await stderr.flush(); 63 | exit(1); 64 | } on CliException catch (err) { 65 | stderr.writeln("${boldRed('Error!')} ${err.message}"); 66 | await stderr.flush(); 67 | exit(err.exitCode); 68 | } catch (err, st) { 69 | stderr.writeln("${boldRed('Error!')} $err\n$st"); 70 | await stderr.flush(); 71 | exit(1); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /rps_lib/rps.cpp: -------------------------------------------------------------------------------- 1 | #include "rps.h" 2 | 3 | // windows 4 | #ifdef _WIN32 5 | #include 6 | #include 7 | // linux and macos 8 | #else 9 | #include 10 | #include 11 | #include 12 | #include 13 | #endif 14 | 15 | int main() 16 | { 17 | return 0; 18 | } 19 | 20 | // windows 21 | #ifdef _WIN32 22 | 23 | intptr_t handle; 24 | 25 | BOOL WINAPI CtrlHandler(DWORD fdwCtrlType) 26 | { 27 | switch (fdwCtrlType) 28 | { 29 | case CTRL_C_EVENT: 30 | case CTRL_CLOSE_EVENT: 31 | case CTRL_BREAK_EVENT: 32 | case CTRL_LOGOFF_EVENT: 33 | case CTRL_SHUTDOWN_EVENT: 34 | return TRUE; 35 | default: 36 | return FALSE; 37 | } 38 | } 39 | 40 | int runCommand(const char *command) 41 | { 42 | 43 | SetConsoleCtrlHandler(CtrlHandler, TRUE); 44 | 45 | handle = _spawnlp(_P_NOWAIT, "cmd", "/c", command, (char *)NULL); 46 | 47 | int statusCode = 0; 48 | _cwait(&statusCode, handle, NULL); 49 | return statusCode; 50 | } 51 | // linux and macos 52 | #else 53 | int pid; 54 | 55 | void ctrlCHandler(int signum) 56 | { 57 | // send SIGKILL signal to the child process 58 | kill(pid, SIGKILL); 59 | } 60 | 61 | int runCommand(const char *command) 62 | { 63 | 64 | pid = fork(); 65 | 66 | if (pid == -1) 67 | { 68 | return 1; 69 | } 70 | else if (pid == 0) 71 | { 72 | execlp("bash", "bash", "-c", command, (char *)NULL); 73 | // Nothing below this line should be executed by child process. If so, 74 | // it means that the execlp function wasn't successfull. 75 | exit(1); 76 | } 77 | else 78 | { 79 | 80 | // Register ctrlc handler to kill child process before closing parent process. 81 | struct sigaction sigIntHandler; 82 | sigIntHandler.sa_handler = ctrlCHandler; 83 | sigemptyset(&sigIntHandler.sa_mask); 84 | sigIntHandler.sa_flags = 0; 85 | sigaction(SIGINT, &sigIntHandler, NULL); 86 | 87 | int exitCode = 0; 88 | int returnedPid = waitpid(pid, &exitCode, 0); 89 | // -1 on error. 90 | if (returnedPid == -1) 91 | { 92 | return 1; 93 | } 94 | else 95 | { 96 | return exitCode; 97 | } 98 | } 99 | } 100 | #endif 101 | 102 | int execute(char *command) 103 | { 104 | if (command == NULL) 105 | { 106 | return 1; 107 | } 108 | 109 | return runCommand(command); 110 | } 111 | -------------------------------------------------------------------------------- /lib/src/cli/commands/run_command.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:rps/rps.dart'; 4 | import 'package:collection/collection.dart'; 5 | import 'package:rps/src/cli/commands/command.dart'; 6 | 7 | class RunCommand implements Command { 8 | final FutureOr Function() _getScriptsSource; 9 | final ExecuteFunction execute; 10 | 11 | RunCommand({ 12 | required this.execute, 13 | required FutureOr Function() getScriptsSource, 14 | }) : _getScriptsSource = getScriptsSource; 15 | 16 | @override 17 | String get description => 18 | 'This runs an arbitrary command from a pubspec\'s "scripts" object. ' 19 | 'If no "command" is provided, it will list the available scripts.'; 20 | 21 | @override 22 | String get name => 'run'; 23 | 24 | @override 25 | String? get tooltip => 'run [arguments]'; 26 | 27 | @override 28 | bool match(List arguments) { 29 | return true; 30 | } 31 | 32 | @override 33 | Future run(Console console, List arguments) async { 34 | final source = await _getScriptsSource(); 35 | final parser = ScriptsParser(source: source); 36 | 37 | final List events; 38 | try { 39 | /// Remove command name (run) from arguments list 40 | events = parser.getCommandsToExecute(arguments.firstOrNull == name 41 | ? arguments.skip(1).toList() 42 | : arguments); 43 | } on ScriptParserException catch (err) { 44 | throw RpsException(err.message, err); 45 | } 46 | 47 | if (events.isEmpty) { 48 | throw RpsException( 49 | 'Missing script. Command: "${arguments.join(' ')}".', 50 | ); 51 | } 52 | 53 | for (final event in events) { 54 | if (event is CommandExecuted) { 55 | final basePath = event.context.basePath; 56 | if (event.isHook) { 57 | basePath.last = blue(basePath.last.substring(1)); 58 | } 59 | console.writeln('${boldGreen('>')} ${basePath.join(' ')}'); 60 | final command = event.compile(); 61 | console.writeln('${boldGreen(r'$')} ${bold(command)}\n'); 62 | final exitCode = await execute(command); 63 | 64 | if (exitCode > 0) { 65 | throw RpsException('Command ended with a non zero exit code.'); 66 | } 67 | console.writeln(); 68 | } else if (event is CommandReferenced) { 69 | final basePath = event.context.basePath; 70 | if (event.isHook) { 71 | basePath.last = blue(basePath.last.substring(1)); 72 | } 73 | console.writeln('${boldGreen('>')} ${basePath.join(' ')}'); 74 | console.writeln('${boldGreen(r'$ rps')} ${bold(event.command)}\n'); 75 | } else if (event is HookExecuted) { 76 | // final basePath = event.context.basePath; 77 | // console.writeln('${boldGreen('>')} ${basePath.sublist(0, basePath.length - 1).join(' ')} ${blue(event.name)}'); 78 | } 79 | } 80 | 81 | return 0; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /lib/src/scripts_parser/context.dart: -------------------------------------------------------------------------------- 1 | import 'package:collection/collection.dart'; 2 | 3 | class Context { 4 | final dynamic current; 5 | final String? key; 6 | 7 | /// Parent context of this node. 8 | final Context? parent; 9 | 10 | /// Each time the reference is triggered, 11 | /// the context starts from the `base` node. 12 | /// 13 | /// It has a [current] state equal to `root`, but is not a terminating node 14 | /// because it maintains the context between references. 15 | /// 16 | /// `root` - is the first node of the execution. 17 | /// `base` - is the first node of execution or after reference. 18 | final bool isBase; 19 | 20 | /// Initial context node. The entry point of the run command. 21 | bool get isRoot => parent == null; 22 | 23 | Context({ 24 | required this.key, 25 | required this.parent, 26 | required this.current, 27 | required this.isBase, 28 | }); 29 | 30 | /// Initial context node. The entry point of the run command. 31 | Context.root(this.current) 32 | : parent = null, 33 | isBase = true, 34 | key = null; 35 | 36 | List get path { 37 | final path = []; 38 | 39 | Context? context = this; 40 | while (context != null) { 41 | final key = context.key; 42 | if (key != null) { 43 | path.add(key); 44 | } 45 | context = context.parent; 46 | } 47 | return path.reversed.toList(); 48 | } 49 | 50 | List get basePath { 51 | final path = []; 52 | 53 | Context? context = this; 54 | while (context != null && !context.isBase) { 55 | final key = context.key; 56 | if (key != null) { 57 | path.add(key); 58 | } 59 | context = context.parent; 60 | } 61 | return path.reversed.toList(); 62 | } 63 | 64 | /// Get the initial context node from the context tree. 65 | Context get root { 66 | Context context = this; 67 | while (context.parent != null) { 68 | context = context.parent!; 69 | } 70 | return context; 71 | } 72 | 73 | Context next(String key) { 74 | return Context( 75 | key: key, 76 | parent: this, 77 | isBase: false, 78 | current: current[key], 79 | ); 80 | } 81 | 82 | /// Takes a portion of the context tree 83 | /// from the current node (leaf) to the base. 84 | Context toBase() { 85 | final stack = []; 86 | Context context = this; 87 | while (context.parent != null) { 88 | stack.add(context); 89 | if (context.isBase) break; 90 | context = context.parent!; 91 | } 92 | 93 | Context? getParent(List stack) { 94 | final context = stack.firstOrNull; 95 | if (context == null) return null; 96 | return Context( 97 | current: context.current, 98 | isBase: stack.length == 1, 99 | key: context.key, 100 | parent: getParent(stack.skip(1).toList()), 101 | ); 102 | } 103 | 104 | return getParent(stack)!; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /lib/src/scripts_parser/events/execution/command_executed.dart: -------------------------------------------------------------------------------- 1 | import 'package:rps/rps.dart'; 2 | import 'dart:math' as math; 3 | 4 | class CommandExecuted extends ExecutionEvent { 5 | @override 6 | final String command; 7 | 8 | @override 9 | final Context context; 10 | 11 | final List arguments; 12 | 13 | @override 14 | String get path => context.path.join(' '); 15 | final String? description; 16 | 17 | final String? error; 18 | 19 | final bool isHook; 20 | 21 | static final _positionalArgumentsRegexp = RegExp(r'\$\{\s{0,}[0-9]+\s{0,}\}'); 22 | 23 | CommandExecuted({ 24 | required this.command, 25 | required this.context, 26 | List? arguments, 27 | this.description, 28 | this.isHook = false, 29 | this.error, 30 | }) : arguments = arguments ?? const []; 31 | 32 | /// Escape backslashes, single and double quotes for shell safety 33 | /// and enclose in quotes only if necessary: contains spaces or quotes 34 | String? _serializeArguments(List arguments) { 35 | if (arguments.isEmpty) return null; 36 | 37 | return arguments.map((arg) { 38 | String escaped = arg 39 | .replaceAll(r'\', r'\\') 40 | .replaceAll('"', r'\"') 41 | .replaceAll("'", r"\'"); 42 | 43 | if (escaped != arg) { 44 | return '"$escaped"'; 45 | } 46 | return escaped; 47 | }).join(' '); 48 | } 49 | 50 | /// Compiles the command. Returns the command ready for execution. 51 | String compile() { 52 | final argumentsInCommand = _positionalArgumentsRegexp.allMatches(command); 53 | 54 | if (argumentsInCommand.isNotEmpty) { 55 | if (isHook) { 56 | throw RpsException( 57 | 'The script "$path" defines a positional argument(s), ' 58 | 'but hooks do not support positional arguments.', 59 | ); 60 | } 61 | 62 | final usedArguments = {}; 63 | final filledCommand = command.replaceAllMapped( 64 | _positionalArgumentsRegexp, 65 | (match) { 66 | final content = match.group(0)!; 67 | final value = content.substring(2, content.length - 1).trim(); 68 | final argumentIndex = int.tryParse(value); 69 | if (argumentIndex == null) { 70 | throw RpsException( 71 | "Bad argument script ($content). " 72 | "Only positional arguments are supported.", 73 | ); 74 | } else if (argumentIndex >= arguments.length) { 75 | throw RpsException( 76 | 'The script "$path" defines a positional argument $content, ' 77 | 'but ${arguments.length} positional argument(s) are given.', 78 | ); 79 | } else { 80 | usedArguments.add(argumentIndex); 81 | return arguments[argumentIndex]; 82 | } 83 | }, 84 | ); 85 | 86 | final lastUsed = usedArguments.reduce(math.max); 87 | if (lastUsed > usedArguments.length) { 88 | final unusedArguments = List.generate(lastUsed, (index) => index) 89 | .where((e) => !usedArguments.contains(e)); 90 | 91 | throw RpsException( 92 | 'The script defines unused positional argument(s): ' 93 | '${unusedArguments.map((a) => '\${$a}').join(', ')}', 94 | ); 95 | } 96 | 97 | return [ 98 | filledCommand, 99 | _serializeArguments(arguments.sublist(lastUsed + 1)) 100 | ].nonNulls.join(' '); 101 | } else { 102 | return [command, _serializeArguments(arguments)].nonNulls.join(' '); 103 | } 104 | } 105 | 106 | @override 107 | bool operator ==(Object other) { 108 | return other is CommandExecuted && 109 | other.command == command && 110 | other.path == path; 111 | } 112 | 113 | @override 114 | int get hashCode => Object.hash(path, command, runtimeType); 115 | } 116 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.9.1 2 | - Used static linking of the CRT library on Windows instead of dynamic - solves Windows problems when the CRT library was not present. 3 | ## 0.9.0 4 | - Added support for an external script file (`rps.yaml`), thanks to [@masreplay](https://github.com/masreplay) in [#24](https://github.com/gonuit/rps/pull/24). 5 | ## 0.8.2 6 | - Fix arguments serialization. escape backslashes, single and double quotes for shell safety and enclose in quotes if necessary: contains spaces or quotes. 7 | ## 0.8.1 8 | - Added support for Linux Arm64 (aarch64) architecture. 9 | - Improved Abi handling 10 | ## 0.8.0 11 | ### BREAKING CHANGES 12 | - Support for positional arguments: `${0}`, `${1}` ... 13 | - To use references use `rps` instead of `$` prefix. 14 | ### Additional changes 15 | - Added list command: `rps ls` 16 | - Lists all available commands 17 | - Added upgrade command: `rps -u` / `rps --upgrade` 18 | - Improved help command `rps -h` / `rps --help` 19 | - Updated readme documentation. 20 | ## 0.7.0 21 | 22 | ### BREAKING CHANGES 23 | 24 | - A special `$script` key has been introduced with a neat 💪 platform recognition feature. 25 | 26 | Do you work on multiple platforms? Need to use many different commands and always forget which one to use? This is what you've been waiting for! 27 | 28 | ```yaml 29 | scripts: 30 | where-am-i: 31 | $script: 32 | $windows: echo "You are on Windows!" 33 | $linux: echo "You are on Linux!" 34 | $macos: echo "You are on MacOs!" 35 | $default: echo "You are on... something else?" 36 | ``` 37 | 38 | ``` 39 | user@MacBook-Pro Desktop % rps where-am-i 40 | > where-am-i 41 | $ echo "You are on MacOs!" 42 | 43 | You are on MacOs! 44 | 45 | user@MacBook-Pro Desktop % 46 | ``` 47 | 48 | This can be useful for commands like `rm -rf`, which in Windows.... `rd /s /q`, you know what I mean, it can be helpful, right? 49 | 50 | - Added support for script references. From now on it is possible to call another rps command directly from a defined script. 51 | 52 | ```yaml 53 | scripts: 54 | app: 55 | clean: flutter clean 56 | clean: $app clean 57 | c: $clean 58 | clear: $c 59 | delete: $clear 60 | ``` 61 | 62 | This is just a simple proxy example, but you can also use it in `$before` and `$after` hooks to chain multiple scripts together. 63 | 64 | - ⚠️ The `before-` and `after-` hooks has been removed. Instead use the `$before` and `$after` keys. This will help keep hook scripts grouped in a project with multiple scripts defined. 65 | 66 | ```yaml 67 | scripts: 68 | hooks: 69 | $before: echo "before" 70 | $script: echo "script" 71 | $after: echo "after" 72 | ``` 73 | 74 | Execute by calling the `rps hooks`. 75 | 76 | It is also possible to link multiple scripts together! 77 | 78 | ```yaml 79 | scripts: 80 | get: flutter pub get 81 | test: 82 | $before: $get 83 | $script: flutter test 84 | build: 85 | $before: $test 86 | $script: flutter build apk 87 | ``` 88 | 89 | You don't have to worry about cyclic references, RPS will keep track of them and notify you in case of a problem. 90 | 91 | ## 0.6.5 92 | 93 | - Update dependencies 94 | - Longer description in pubspec.yaml 95 | - pedantic (deprecated) replaced by flutter_lints 96 | 97 | ## 0.6.4 98 | 99 | - Fixed readme example 100 | 101 | ## 0.6.3 102 | 103 | - Minor bug fixes and improvements. 🚧 104 | 105 | ## 0.6.2 106 | 107 | - Exposed executables. 108 | 109 | ## 0.6.1 110 | 111 | - Minor bug fixes and improvements. 🚧 112 | - Added colorful logs. 🎨 113 | - Shared with the world via Github and pub.dev! 🌎 114 | 115 | ## 0.6.0 116 | 117 | - Added optional `before-` and `after-` hooks support. 118 | 119 | ## 0.5.1 120 | 121 | - A `--version` flag has been added. 122 | 123 | ## 0.5.0 124 | 125 | - Move towards native implementation. 126 | - Added native support for linux (x86_64). 127 | 128 | ## 0.4.0 129 | 130 | - Move towards native implementation. 131 | - Added native support for windows (x86_64). 132 | 133 | ## 0.3.1 134 | 135 | - Move towards native implementation. 136 | - Added native support for macos (arm64). 137 | 138 | ## 0.3.0 139 | 140 | - Move towards native implementation. 141 | - Added native support for macos (x86_64). 142 | 143 | ## 0.2.0 144 | 145 | - Added basic logs. 146 | - Added two-way communication with the child process through dart `Process`. 147 | 148 | ## 0.1.0 149 | 150 | - Initial version. 151 | -------------------------------------------------------------------------------- /lib/src/cli/console.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'dart:math' as math; 3 | 4 | String gray(String text) => '\x1B[30m$text\x1B[0m'; 5 | String red(String text) => '\x1B[31m$text\x1B[0m'; 6 | String green(String text) => '\x1B[32m$text\x1B[0m'; 7 | String yellow(String text) => '\x1B[33m$text\x1B[0m'; 8 | String blue(String text) => '\x1B[34m$text\x1B[0m'; 9 | String violet(String text) => '\x1B[35m$text\x1B[0m'; 10 | String lightBlue(String text) => '\x1B[36m$text\x1B[0m'; 11 | String white(String text) => '\x1B[37m$text\x1B[0m'; 12 | String bold(String text) => '\x1b[1m$text\x1b[0m'; 13 | String boldGreen(String text) => bold(green(text)); 14 | String boldRed(String text) => bold(red(text)); 15 | String boldBlue(String text) => bold(blue(text)); 16 | 17 | class BorderCharacters { 18 | final String topLeft; 19 | final String top; 20 | final String topRight; 21 | final String right; 22 | final String bottomRight; 23 | final String bottom; 24 | final String bottomLeft; 25 | final String left; 26 | final String empty; 27 | 28 | const BorderCharacters({ 29 | required this.topLeft, 30 | required this.top, 31 | required this.topRight, 32 | required this.right, 33 | required this.bottomRight, 34 | required this.bottom, 35 | required this.bottomLeft, 36 | required this.left, 37 | required this.empty, 38 | }); 39 | 40 | const BorderCharacters.basic() 41 | : topLeft = '┌', 42 | top = '─', 43 | topRight = '┐', 44 | right = '│', 45 | bottomRight = '┘', 46 | bottom = '─', 47 | bottomLeft = '└', 48 | left = '│', 49 | empty = ' '; 50 | 51 | String getTopBorder(int length) { 52 | return topLeft + top * length + topRight; 53 | } 54 | 55 | String getBottomBorder(int length) { 56 | return bottomLeft + bottom * length + bottomRight; 57 | } 58 | 59 | String getEmptyLine(int length) { 60 | return left + empty * length + right; 61 | } 62 | } 63 | 64 | String removeAnsiEscapeSequences(String input) { 65 | final ansiEscapePattern = RegExp(r'\x1B\[[0-9;]*[a-zA-Z]'); 66 | return input.replaceAll(ansiEscapePattern, ''); 67 | } 68 | 69 | enum Alignment { left, center, right } 70 | 71 | class Console implements StringSink { 72 | final StringSink _sink; 73 | 74 | Console({required StringSink sink}) : _sink = sink; 75 | 76 | /// Function to remove ANSI escape sequences from a string 77 | 78 | /// Function to calculate the visible length of a string (excluding ANSI escape sequences) 79 | int visibleLength(String input) { 80 | return removeAnsiEscapeSequences(input).length; 81 | } 82 | 83 | void writeBordered( 84 | List lines, { 85 | int horizontalPadding = 2, 86 | int verticalPadding = 1, 87 | BorderCharacters border = const BorderCharacters.basic(), 88 | Alignment alignment = Alignment.center, 89 | }) { 90 | if (horizontalPadding < 0) { 91 | throw ArgumentError.value( 92 | horizontalPadding, 93 | 'horizontalPadding', 94 | 'horizontalPadding cannot be lower than 0', 95 | ); 96 | } 97 | if (verticalPadding < 0) { 98 | throw ArgumentError.value( 99 | verticalPadding, 100 | 'verticalPadding', 101 | 'verticalPadding cannot be lower than 0', 102 | ); 103 | } 104 | 105 | final lineLengths = lines.map((line) => visibleLength(line)).toList(); 106 | final maxLength = lineLengths.fold( 107 | 0, (prevLength, length) => math.max(length, prevLength)); 108 | final horizontalLength = maxLength + horizontalPadding * 2; 109 | 110 | writeln(border.getTopBorder(horizontalLength)); 111 | for (int y = 0; y < verticalPadding; y++) { 112 | writeln(border.getEmptyLine(horizontalLength)); 113 | } 114 | for (int i = 0; i < lines.length; i++) { 115 | final line = lines[i]; 116 | final length = lineLengths[i]; 117 | 118 | final int paddingTotal; 119 | final int paddingStart; 120 | final int paddingEnd; 121 | 122 | switch (alignment) { 123 | case Alignment.left: 124 | paddingTotal = maxLength - length; 125 | paddingStart = horizontalPadding; 126 | paddingEnd = paddingTotal + horizontalPadding; 127 | break; 128 | case Alignment.center: 129 | paddingTotal = maxLength - length; 130 | final halfPadding = (paddingTotal ~/ 2); 131 | paddingStart = halfPadding + horizontalPadding; 132 | paddingEnd = (paddingTotal - halfPadding) + horizontalPadding; 133 | break; 134 | case Alignment.right: 135 | paddingTotal = maxLength - length; 136 | paddingStart = horizontalPadding + paddingTotal; 137 | paddingEnd = horizontalPadding; 138 | break; 139 | } 140 | writeln( 141 | '${border.left}${border.empty * paddingStart}$line${border.empty * paddingEnd}${border.right}'); 142 | } 143 | for (int y = 0; y < verticalPadding; y++) { 144 | writeln(border.getEmptyLine(horizontalLength)); 145 | } 146 | writeln(border.getBottomBorder(horizontalLength)); 147 | } 148 | 149 | @override 150 | void write(Object? object) { 151 | _sink.write(object); 152 | } 153 | 154 | @override 155 | void writeAll(Iterable objects, [String separator = ""]) { 156 | _sink.writeAll(objects, separator); 157 | } 158 | 159 | @override 160 | void writeCharCode(int charCode) { 161 | _sink.writeCharCode(charCode); 162 | } 163 | 164 | @override 165 | void writeln([Object? object = ""]) { 166 | _sink.writeln(object); 167 | } 168 | 169 | Future flush() async { 170 | final sink = _sink; 171 | if (sink is IOSink) { 172 | await sink.flush(); 173 | } 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /lib/src/utils/rps_package.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:io'; 3 | import 'dart:isolate'; 4 | 5 | import 'package:http_api/http_api.dart'; 6 | import 'package:pub_semver/pub_semver.dart'; 7 | import 'package:rps/rps.dart'; 8 | import 'package:path/path.dart' as p; 9 | import 'package:rps/src/utils/date_utils.dart'; 10 | 11 | class PubDevApiException { 12 | final int? statusCode; 13 | final String? message; 14 | 15 | PubDevApiException(this.statusCode, [this.message]); 16 | 17 | factory PubDevApiException.fromResponse( 18 | Response response, 19 | ) { 20 | if (response.bodyBytes != null) { 21 | final data = jsonDecode(response.body); 22 | final error = data['error']; 23 | if (error is Map) { 24 | return PubDevApiException( 25 | response.statusCode!, 26 | error['message'], 27 | ); 28 | } 29 | } 30 | 31 | return PubDevApiException(response.statusCode); 32 | } 33 | } 34 | 35 | class PubDevApi extends BaseApi { 36 | PubDevApi() : super(Uri.parse('https://pub.dev/api')); 37 | 38 | Future getLastVersion(String packageName) async { 39 | final response = await get('/packages/$packageName'); 40 | 41 | if (response.ok) { 42 | final data = jsonDecode(response.body); 43 | return data['latest']['version']; 44 | } else { 45 | throw PubDevApiException.fromResponse(response); 46 | } 47 | } 48 | } 49 | 50 | class PackageVersions { 51 | final Version latest; 52 | final Version current; 53 | 54 | PackageVersions({ 55 | required this.latest, 56 | required this.current, 57 | }); 58 | 59 | bool get hasUpdate => latest > current; 60 | } 61 | 62 | class RpsConfig { 63 | final File _file; 64 | RpsConfigData get data => _data; 65 | RpsConfigData _data; 66 | 67 | RpsConfig._({ 68 | required File file, 69 | required RpsConfigData data, 70 | }) : _data = data, 71 | _file = file; 72 | 73 | factory RpsConfig.load(Directory directory) { 74 | final configFile = File(p.join(directory.path, 'rps.config')); 75 | 76 | RpsConfig createInitial() { 77 | final config = RpsConfigData.initial(); 78 | configFile.writeAsStringSync( 79 | const JsonEncoder.withIndent(' ').convert(config.toJson()), 80 | flush: true, 81 | ); 82 | 83 | return RpsConfig._(file: configFile, data: config); 84 | } 85 | 86 | if (!configFile.existsSync()) { 87 | return createInitial(); 88 | } else { 89 | try { 90 | final data = configFile.readAsStringSync(); 91 | final config = RpsConfigData.fromJson(jsonDecode(data)); 92 | return RpsConfig._(file: configFile, data: config); 93 | } on Exception catch (err) { 94 | stdout.write("Cannot read configuration. Fallback to default.\n$err"); 95 | return createInitial(); 96 | } 97 | } 98 | } 99 | 100 | void update(RpsConfigData data) { 101 | _file.writeAsStringSync( 102 | const JsonEncoder.withIndent(' ').convert(data.toJson()), 103 | flush: true, 104 | ); 105 | _data = data; 106 | } 107 | } 108 | 109 | class RpsConfigData { 110 | final DateTime? updateCheckedAt; 111 | final Version? latestVersion; 112 | 113 | RpsConfigData({ 114 | required this.updateCheckedAt, 115 | required this.latestVersion, 116 | }); 117 | 118 | factory RpsConfigData.initial() => RpsConfigData( 119 | updateCheckedAt: null, 120 | latestVersion: null, 121 | ); 122 | 123 | RpsConfigData copyWith({ 124 | DateTime? updateCheckedAt, 125 | Version? latestVersion, 126 | }) => 127 | RpsConfigData( 128 | updateCheckedAt: updateCheckedAt ?? this.updateCheckedAt, 129 | latestVersion: latestVersion ?? this.latestVersion, 130 | ); 131 | 132 | static RpsConfigData fromJson(Map json) { 133 | return RpsConfigData( 134 | updateCheckedAt: json['updateCheckedAt'] != null 135 | ? DateTime.parse(json['updateCheckedAt']) 136 | : null, 137 | latestVersion: json['latestVersion'] != null 138 | ? Version.parse(json['latestVersion']) 139 | : null, 140 | ); 141 | } 142 | 143 | Map toJson() { 144 | return { 145 | 'updateCheckedAt': updateCheckedAt?.toIso8601String(), 146 | 'latestVersion': latestVersion.toString(), 147 | }; 148 | } 149 | } 150 | 151 | class RpsPackage { 152 | final Pubspec _pubspec; 153 | final PubDevApi _api; 154 | final RpsConfig _config; 155 | 156 | Version get version => Version.parse(_pubspec.packageVersion); 157 | 158 | RpsPackage({ 159 | required Pubspec pubspec, 160 | required PubDevApi api, 161 | required RpsConfig lockFile, 162 | }) : _pubspec = pubspec, 163 | _api = api, 164 | _config = lockFile; 165 | 166 | static Future load() async { 167 | Future getPackageDirectory() async { 168 | const rootLibrary = 'package:rps/rps.dart'; 169 | final uri = await Isolate.resolvePackageUri(Uri.parse(rootLibrary)); 170 | if (uri == null) { 171 | print('Library cannot be loaded.'); 172 | exit(1); 173 | } 174 | 175 | final root = uri.resolve('..'); 176 | return Directory.fromUri(root); 177 | } 178 | 179 | final directory = await getPackageDirectory(); 180 | 181 | return RpsPackage( 182 | pubspec: Pubspec.load(directory), 183 | api: PubDevApi(), 184 | lockFile: RpsConfig.load(directory), 185 | ); 186 | } 187 | 188 | Future getLatestPackageVersion() async { 189 | final cachedVersion = _config.data.latestVersion; 190 | final cacheDate = _config.data.updateCheckedAt; 191 | final now = DateTime.now(); 192 | 193 | // Return cached if valid 194 | if (cachedVersion != null && 195 | cacheDate != null && 196 | cacheDate.isSameDay(now)) { 197 | return cachedVersion; 198 | } 199 | 200 | final version = await _api.getLastVersion('rps'); 201 | final parsedVersion = Version.parse(version); 202 | _config.update(_config.data.copyWith( 203 | updateCheckedAt: DateTime.now(), 204 | latestVersion: parsedVersion, 205 | )); 206 | return parsedVersion; 207 | } 208 | 209 | Future getVersions() async { 210 | final latest = await getLatestPackageVersion() 211 | .timeout(const Duration(milliseconds: 300)); 212 | 213 | return PackageVersions( 214 | latest: latest, 215 | current: version, 216 | ); 217 | } 218 | 219 | void dispose() { 220 | _api.dispose(); 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /lib/src/scripts_parser/scripts_parser.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'package:collection/collection.dart'; 3 | import 'package:rps/rps.dart'; 4 | 5 | abstract class ScriptsParser { 6 | static const beforeKey = r'$before'; 7 | static const afterKey = r'$after'; 8 | static const scriptKey = r'$script'; 9 | static const descriptionKey = r'$description'; 10 | static const defaultScriptKey = r'$default'; 11 | 12 | List listCommands(); 13 | List getCommandsToExecute(List arguments); 14 | 15 | factory ScriptsParser({required ScriptsSource source}) = _ScriptParser; 16 | } 17 | 18 | class _ScriptParser implements ScriptsParser { 19 | final ScriptsSource _source; 20 | 21 | _ScriptParser({required ScriptsSource source}) : _source = source; 22 | 23 | @override 24 | List listCommands() { 25 | var scripts = _source.getScripts(); 26 | final context = Context.root(scripts); 27 | return _listCommands(context: context).toList(); 28 | } 29 | 30 | Iterable _listCommands({ 31 | required Context context, 32 | }) sync* { 33 | final current = context.current; 34 | 35 | if (current is String) { 36 | if (context.isRoot) { 37 | // todo: load file with scripts 38 | } else { 39 | yield CommandExecuted( 40 | command: current, 41 | context: context, 42 | ); 43 | } 44 | } else if (current is Map) { 45 | if (_hasScriptKey(current)) { 46 | final script = current[ScriptsParser.scriptKey]; 47 | final description = current[ScriptsParser.descriptionKey]; 48 | if (script is String) { 49 | yield CommandExecuted( 50 | command: script, 51 | context: context, 52 | description: description, 53 | ); 54 | } else if (script is Map) { 55 | final platformKey = '\$${Platform.operatingSystem}'; 56 | final command = 57 | script[platformKey] ?? script[ScriptsParser.defaultScriptKey]; 58 | if (command is! String) { 59 | yield CommandExecuted( 60 | command: '-', 61 | context: context, 62 | description: description, 63 | error: 64 | 'No platform script key for the command: "${context.path}". ' 65 | 'Consider adding the key for the current ' 66 | 'platform: "$platformKey" or the default script ' 67 | 'key: "${ScriptsParser.defaultScriptKey}".', 68 | ); 69 | } else { 70 | yield CommandExecuted( 71 | command: command, 72 | context: context, 73 | description: description, 74 | ); 75 | } 76 | } 77 | } else { 78 | for (final key in current.keys) { 79 | yield* _listCommands(context: context.next(key)); 80 | } 81 | } 82 | } else { 83 | yield CommandExecuted( 84 | command: current.toString(), 85 | context: context, 86 | error: 'Invalid command. Cannot use type ' 87 | '${current.runtimeType} ($current) as a command.', 88 | ); 89 | } 90 | } 91 | 92 | @override 93 | List getCommandsToExecute(List arguments) { 94 | var scripts = _source.getScripts(); 95 | var context = Context.root(scripts); 96 | // If command is not specified, fallback to run 97 | if (arguments.isEmpty) { 98 | context = context.next('run'); 99 | } 100 | 101 | final events = {}; 102 | for (final event 103 | in _getCommandsToExecute(context: context, arguments: arguments)) { 104 | final added = events.add(event); 105 | if (!added) { 106 | throw ScriptParserException( 107 | 'Script cycle detected: ${[ 108 | ...events.map((e) => e.path), 109 | event.path 110 | ].join(' → ')}', 111 | ); 112 | } 113 | } 114 | 115 | return events.toList(); 116 | } 117 | 118 | Iterable _getCommandsToExecute({ 119 | required Context context, 120 | required List arguments, 121 | }) sync* { 122 | final current = context.current; 123 | 124 | if (current == null) { 125 | // no command 126 | return; 127 | } else if (current is String) { 128 | if (context.isRoot) { 129 | // todo Load file with scripts 130 | throw RpsException( 131 | 'The root key "scripts" cannot contain commands.', 132 | ); 133 | } else { 134 | yield* _handleCommand( 135 | command: current, 136 | context: context, 137 | arguments: arguments, 138 | ); 139 | } 140 | } else if (current is Map) { 141 | // if it is a Map, it may contain hooks. 142 | yield* _handleHooks(context, () sync* { 143 | if (_hasScriptKey(current)) { 144 | final script = current[ScriptsParser.scriptKey]; 145 | if (script is String) { 146 | yield* _handleCommand( 147 | command: script, 148 | context: context, 149 | arguments: arguments, 150 | ); 151 | } else if (script is Map) { 152 | final platformKey = '\$${Platform.operatingSystem}'; 153 | final command = 154 | script[platformKey] ?? script[ScriptsParser.defaultScriptKey]; 155 | if (command is! String) { 156 | throw RpsException( 157 | 'No platform script key for the command: "${context.path}". ' 158 | 'Consider adding the key for the current ' 159 | 'platform: "$platformKey" or the default script ' 160 | 'key: "${ScriptsParser.defaultScriptKey}".', 161 | ); 162 | } else { 163 | yield* _handleCommand( 164 | command: command, 165 | context: context, 166 | arguments: arguments, 167 | ); 168 | } 169 | } 170 | } else { 171 | final nextKey = arguments.firstOrNull; 172 | if (nextKey == null) { 173 | throw RpsException( 174 | 'Missing script. Command: "${context.path}" ' 175 | 'is not a full path.', 176 | ); 177 | } else { 178 | final remainingArguments = arguments.skip(1).toList(); 179 | yield* _getCommandsToExecute( 180 | context: context.next(nextKey), 181 | arguments: remainingArguments, 182 | ); 183 | } 184 | } 185 | }); 186 | } else { 187 | throw RpsException( 188 | 'Invalid command. Cannot use type ' 189 | '${current.runtimeType} ($current) as a command.', 190 | ); 191 | } 192 | } 193 | 194 | Iterable _handleHooks( 195 | Context context, 196 | Iterable Function() handler, 197 | ) sync* { 198 | final current = context.current; 199 | final beforeHook = current[ScriptsParser.beforeKey]; 200 | if (beforeHook is String) { 201 | final hookContext = context.next(ScriptsParser.beforeKey); 202 | yield HookExecuted( 203 | command: beforeHook, 204 | context: hookContext, 205 | name: 'before', 206 | ); 207 | yield* _handleCommand( 208 | isHook: true, 209 | command: beforeHook, 210 | context: hookContext, 211 | // arguments are not passed to to the hooks 212 | arguments: null, 213 | ); 214 | } 215 | 216 | yield* handler(); 217 | 218 | final afterHook = current[ScriptsParser.afterKey]; 219 | if (afterHook is String) { 220 | final hookContext = context.next(ScriptsParser.afterKey); 221 | yield HookExecuted( 222 | command: afterHook, 223 | context: hookContext, 224 | name: 'after', 225 | ); 226 | yield* _handleCommand( 227 | command: afterHook, 228 | context: hookContext, 229 | // arguments are not passed to to the hooks 230 | arguments: null, 231 | isHook: true, 232 | ); 233 | } 234 | } 235 | 236 | Iterable _handleCommand({ 237 | required Context context, 238 | required String command, 239 | List? arguments, 240 | bool isHook = false, 241 | }) sync* { 242 | if (command.startsWith(r'rps ')) { 243 | final referencedCommand = command.substring(4); 244 | 245 | yield CommandReferenced( 246 | command: referencedCommand, 247 | context: context, 248 | label: context.key!, 249 | isHook: isHook, 250 | ); 251 | 252 | /// Ref should start from root. 253 | yield* _getCommandsToExecute( 254 | context: Context( 255 | key: null, 256 | parent: context, 257 | isBase: true, 258 | current: context.root.current, 259 | ), 260 | arguments: [ 261 | ...referencedCommand.split(RegExp(r'\s+')), 262 | ...?arguments, 263 | ], 264 | ); 265 | } else { 266 | yield CommandExecuted( 267 | command: command, 268 | context: context, 269 | arguments: arguments, 270 | isHook: isHook, 271 | ); 272 | } 273 | } 274 | 275 | bool _hasScriptKey(Map scripts) { 276 | return scripts.containsKey(ScriptsParser.scriptKey); 277 | } 278 | } 279 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Run Pubspec Script (RPS) 2 | 3 | RPS is a dart script manager that allows you to define and use scripts from the _pubspec.yaml_ file. 4 | 5 | 6 | ## Features 7 | 8 | - [🔦 Listing commands](#-listing-commands) 9 | - [🪆 Nesting](#-nesting) 10 | - [🔗 References](#-references) 11 | - [🪝 Hooks](#-hooks) 12 | - [💻 Platform specific scripts](#-platform-specific-scripts) 13 | - [🎯 Positional arguments](#-positional-arguments) 14 | - [📝 External scripts file](#-external-scripts-file) 15 | 16 | ## Quick start 🚀 17 | 18 | 1. Install this package. 19 | ```bash 20 | dart pub global activate rps 21 | ``` 22 | 2. Define scripts inside the `pubspec.yaml` or create an [`rps.yaml` file]((#-external-scripts-file)). 23 | ```yaml 24 | scripts: 25 | gen: flutter pub run build_runner build --delete-conflicting-outputs 26 | ``` 27 | 3. Use your custom command. 28 | ```bash 29 | rps gen 30 | # you can also provide additional arguments 31 | rps gen --verbose 32 | ``` 33 | 4. Safe a time and become a power user! 😈 34 | 35 | Less time typing long commands more time watching funny cats. 🐈 36 | 37 | # Features 38 | 39 | ## 🔦 Listing commands 40 | 41 | Not sure what command to choose? Add a description to your scripts: 42 | 43 | ```yaml 44 | scripts: 45 | build: 46 | web: 47 | $script: flutter build web --flavor production -t lib/main.dart 48 | $description: Builds a web application 49 | android: 50 | $script: flutter build apk --flavor production -t lib/main.dart 51 | $description: Builds an Android app 52 | ``` 53 | 54 | Then type `rps ls` to preview all of them: 55 | 56 | ``` 57 | Commands: 58 | build web (flutter build web --flavor production -t lib/main.dart) 59 | Builds a web application 60 | build android (flutter build apk --flavor production -t lib/main.dart) 61 | Builds an Android app 62 | ``` 63 | 64 | ## 🪆 Nesting 65 | 66 | Nest your commands to group them contextually and make them easier to understand. 67 | 68 | ```yaml 69 | scripts: 70 | build: 71 | web: flutter build web --flavor production -t lib/main.dart 72 | android: 73 | apk: flutter build apk --flavor production -t lib/main.dart 74 | appbundle: flutter build appbundle --flavor production -t lib/main.dart 75 | ios: 76 | ipa: flutter build ipa --flavor production -t lib/main.dart 77 | ios: flutter build ios --flavor production -t lib/main.dart 78 | ``` 79 | 80 | Use with ease 😏 81 | 82 | ``` 83 | rps build android apk 84 | ``` 85 | 86 | ## 🔗 References 87 | 88 | Sometimes you may want to create a command alias (eg. a shorter version of it) or simply pass the execution of a hook to another command. This is where references come to the rescue! ⛑ 89 | 90 | References are defined as a scripts that must begin with `rps` prefix. 91 | 92 | ```yaml 93 | scripts: 94 | generator: 95 | build: flutter pub run build_runner build --delete-conflicting-outputs 96 | watch: flutter pub run build_runner watch --delete-conflicting-outputs 97 | # short aliases: 98 | gb: rps generator build 99 | # They will not trigger an additional rps process. 100 | gw: rps generator watch 101 | ``` 102 | 103 | References can also be used with `$before` and `$after` hooks. 104 | 105 | ## 🪝 Hooks 106 | 107 | 108 | To enable hooks define the `$before` and/or `$after` keys for your script command. 109 | 110 | ```yaml 111 | scripts: 112 | hello: 113 | $before: echo "hello before" # executed before the $script 114 | $script: echo "hello script" 115 | $after: echo "hello after" # executed after $script 116 | ``` 117 | 118 | All hooks will be executed in the specified order when calling the `rps hello` command. 119 | 120 | You can also combine multiple scripts using references! 121 | 122 | ```yaml 123 | scripts: 124 | get: flutter pub get 125 | test: 126 | # equivalent of "rps run get" 127 | $before: $get 128 | $script: flutter test 129 | build: 130 | # equivalent of "rps run test" 131 | $before: $test 132 | $script: flutter build apk 133 | ``` 134 | 135 | You can also nest hooks to provide an easy-to-read workflow for all grouped commands! 136 | 137 | ```yaml 138 | # Order when executing: "rps generator build" 139 | scripts: 140 | generator: 141 | $before: dart pub get # 1. 142 | $after: dart analyze # 5. 143 | build: 144 | $before: echo "Building JSON models..." # 2. 145 | $script: dart pub run build_runner build # 3. 146 | $after: echo "JSON models built successfully..." # 4. 147 | ``` 148 | 149 | You don't have to worry about cyclic references 🔄, RPS will keep track of them and notify you in case of a problem 😏. 150 | 151 | ## 💻 Platform specific scripts 152 | 153 | Do you work on multiple platforms? Need to use many different commands and always forget which one to use? This is what you've been waiting for! 🛠 154 | 155 | Just use one command on all supported platforms 💪. 156 | 157 | ```yaml 158 | scripts: 159 | where-am-i: 160 | $script: 161 | $windows: echo "You are on Windows!" 162 | $linux: echo "You are on Linux!" 163 | $macos: echo "You are on MacOs!" 164 | $default: echo "You are on... something else?" 165 | ``` 166 | 167 | ``` 168 | user@MacBook-Pro Desktop % rps where-am-i 169 | > where-am-i 170 | $ echo "You are on MacOs!" 171 | 172 | You are on MacOs! 173 | ``` 174 | 175 | This can be useful for commands like `rm -rf`, which in Windows.... `rd /s /q`, you know what I mean, it can be helpful, right? 176 | 177 | ## 🎯 Positional arguments 178 | 179 | Arguments can be passed by adding them to the end of the command - simple, but sometimes it is useful to give them in a specific place, here positional arguments come to the rescue. 180 | 181 | Positional arguments are defined as numbers in parentheses: `${0}`, `${1}`, `${3}` and so on.... 182 | 183 | For example, you can use the following command to specify the target to be built. 184 | 185 | ```yaml 186 | scripts: 187 | build: flutter build ${0} -t lib/main.dart --release 188 | ``` 189 | 190 | Now just run `rps build apk`. 191 | 192 | You can still add trailing arguments, such as `rps build apk --flavor funny` what will execute the following command `flutter build apk -t lib/main.dart --release --flavor funny`. 193 | 194 | ## 📝 External scripts file 195 | 196 | If you prefer to keep your scripts separate from your `pubspec.yaml`, you can define them in an external **`rps.yaml`** file. When an `rps.yaml` file is present in your project's root directory, rps will use it as the scripts source **instead of the scripts in `pubspec.yaml`**. 197 | 198 | To use an external scripts file: 199 | 1. Create an `rps.yaml` file in the root of your project. 200 | 2. Define your scripts under the `scripts` key, just like you would in `pubspec.yaml`. 201 | ```yaml 202 | scripts: 203 | clean: 204 | $script: 205 | $default: rm -rf ./build 206 | $windows: rd /s /q build 207 | build: 208 | release: flutter build apk --release 209 | debug: flutter build apk --debug 210 | ``` 211 | 3. Run your scripts using rps as usual: 212 | ```bash 213 | rps clean 214 | rps build release 215 | ``` 216 | Using `rps.yaml` allows you to keep your `pubspec.yaml` file focused on dependencies and package configuration, while managing your scripts in a dedicated file. 217 | 218 | ## 🔎 Overview example 219 | 220 | ```yaml 221 | name: my_great_app 222 | version: 1.0.0 223 | 224 | scripts: 225 | # run is a default script. To use it, simply type 226 | # in the command line: "rps" - that's all! 227 | run: "flutter run -t lib/main_development.dart --flavor development" 228 | # you can define more commands like this: "rps gen" 229 | gen: "flutter pub run build_runner watch --delete-conflicting-outputs" 230 | # and even nest them! 231 | build: 232 | # You can use hooks to! (and even nest them!) 233 | $before: flutter pub get 234 | $after: echo "Build done!" 235 | android: 236 | # rps build android apk 237 | apk: 238 | $before: echo "Building android apk..." 239 | $script: "flutter build --release apk --flavor production" 240 | # rps build android appbundle 241 | appbundle: "flutter build --release appbundle --flavor production" 242 | # and so on... 243 | # too long command? no problem! define alias using reference syntax! 244 | bab: rps build android appbundle 245 | # as simple as typing "rps baa" 246 | baa: rps build android apk 247 | # some commands may vary from platform to platform 248 | # but that's not a problem 249 | clear: 250 | # use the $script key to define platform specific scripts 251 | $script: 252 | # define the default script 253 | $default: rm -rf ./app/cache 254 | # And different script for the windows platform. 255 | $windows: rd /s /q app\cache 256 | # now "rps clear" will work on any platform! 257 | # want to run multi line script, here is an example 258 | reset: | 259 | flutter clean 260 | flutter pub get 261 | 262 | # the rest of your pubspec file... 263 | dependencies: 264 | path: ^1.7.0 265 | ``` 266 | 267 | ___ 268 | 269 | ## Supported platforms 270 | 271 | | Platform | x64 | Arm64 | 272 | | :------- | :---: | :---: | 273 | | Linux | ✅ | ✅ | 274 | | Windows | ✅ | ❌ | 275 | | MacOs | ✅ | ✅ | 276 | 277 | 278 | --- 279 | 280 | ## 📈 Motivation 281 | 282 | I got bored of typing the same long commands over and over again... I even got bored of pressing that boring (and even small on a MacBook) up arrow key to find my previous command in history. So, as befits a programmer, to save time, I spent more time writing this library than searching/writing commands for the last year... 283 | 284 | ![stonks](./stonks.jpg) 285 | 286 | --- 287 | 288 | Hey you! This package is still in development (errors and API changes can still occur 🐛😏). 289 | -------------------------------------------------------------------------------- /test/scripts_parser/scripts_parser_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:rps/rps.dart'; 4 | import 'package:test/test.dart'; 5 | 6 | import '../mocks/script_source.mock.dart'; 7 | import '../mocks/stream_sink_controller.dart'; 8 | 9 | const mockedPubspecYaml = r''' 10 | name: my_package 11 | version: 3.1.0 12 | scripts: 13 | echo: echo "echo 123" 14 | hook-1: 15 | $before: echo "before hook-1" 16 | $script: echo "hook-1" 17 | $after: echo "after hook-1" 18 | hook-2: 19 | $before: echo "before hook-2" 20 | hook-2-nested: 21 | $before: echo "before hook-2-nested" 22 | $script: echo "hook-2-nested" 23 | $after: echo "after hook-2-nested" 24 | $after: echo "after hook-2" 25 | ref: rps echo 26 | hook-ref: 27 | $before: rps echo 28 | $script: echo "hook-ref" 29 | $after: rps echo 30 | echo-args: echo 31 | echo-args-nested: 32 | echo: echo 33 | echo-args-hooks: 34 | $before: echo 35 | echo: 36 | $before: echo 37 | $script: echo 38 | $after: echo 39 | $after: echo 40 | echo-positional: echo 1=${1} 0=${0} 41 | echo-ref: rps echo-positional 42 | echo-hook-positional: 43 | $before: echo ${0} 44 | $script: echo ${0} 45 | $after: echo ${0} 46 | 47 | '''; 48 | 49 | void main() { 50 | final consoleSink = StreamSinkController(); 51 | final executions = []; 52 | 53 | ExecuteFunction execute(int exitCode) => (String command) { 54 | executions.add(command); 55 | return Future.value(exitCode); 56 | }; 57 | 58 | setUp(() { 59 | executions.clear(); 60 | consoleSink.clear(); 61 | }); 62 | 63 | group('run command', () { 64 | final run = RunCommand( 65 | getScriptsSource: () => MockedScriptSource(mockedPubspecYaml), 66 | execute: execute(0), 67 | ); 68 | 69 | test('Correctly executes command', () async { 70 | final exitCode = await run.run(Console(sink: consoleSink), ['echo']); 71 | expect(exitCode, equals(0)); 72 | expect(executions, equals(['echo "echo 123"'])); 73 | expect( 74 | consoleSink.plainLines, 75 | equals([ 76 | '> echo', 77 | '\$ echo "echo 123"', 78 | '', 79 | '', 80 | ]), 81 | ); 82 | }); 83 | 84 | test('Correctly executes hooks', () async { 85 | final exitCode = await run.run(Console(sink: consoleSink), ['hook-1']); 86 | expect(exitCode, equals(0)); 87 | expect( 88 | executions, 89 | equals([ 90 | 'echo "before hook-1"', 91 | 'echo "hook-1"', 92 | 'echo "after hook-1"', 93 | ]), 94 | ); 95 | expect( 96 | consoleSink.plainLines, 97 | equals([ 98 | '> hook-1 before', 99 | '\$ echo "before hook-1"', 100 | '', 101 | '', 102 | '> hook-1', 103 | '\$ echo "hook-1"', 104 | '', 105 | '', 106 | '> hook-1 after', 107 | '\$ echo "after hook-1"', 108 | '', 109 | '', 110 | ]), 111 | ); 112 | }); 113 | 114 | test('Correctly executes nested hooks', () async { 115 | final exitCode = await run 116 | .run(Console(sink: consoleSink), ['hook-2', 'hook-2-nested']); 117 | expect(exitCode, equals(0)); 118 | expect( 119 | executions, 120 | equals([ 121 | 'echo "before hook-2"', 122 | 'echo "before hook-2-nested"', 123 | 'echo "hook-2-nested"', 124 | 'echo "after hook-2-nested"', 125 | 'echo "after hook-2"' 126 | ]), 127 | ); 128 | expect( 129 | consoleSink.plainLines, 130 | equals([ 131 | '> hook-2 before', 132 | '\$ echo "before hook-2"', 133 | '', 134 | '', 135 | '> hook-2 hook-2-nested before', 136 | '\$ echo "before hook-2-nested"', 137 | '', 138 | '', 139 | '> hook-2 hook-2-nested', 140 | '\$ echo "hook-2-nested"', 141 | '', 142 | '', 143 | '> hook-2 hook-2-nested after', 144 | '\$ echo "after hook-2-nested"', 145 | '', 146 | '', 147 | '> hook-2 after', 148 | '\$ echo "after hook-2"', 149 | '', 150 | '', 151 | ]), 152 | ); 153 | }); 154 | 155 | test('Correctly executes referenced command', () async { 156 | final exitCode = await run.run(Console(sink: consoleSink), ['ref']); 157 | expect(exitCode, equals(0)); 158 | expect( 159 | executions, 160 | equals(['echo "echo 123"']), 161 | ); 162 | expect( 163 | consoleSink.plainLines, 164 | equals([ 165 | '> ref', 166 | '\$ rps echo', 167 | '', 168 | '> echo', 169 | '\$ echo "echo 123"', 170 | '', 171 | '', 172 | ]), 173 | ); 174 | }); 175 | 176 | test('Correctly executes references in hooks', () async { 177 | final exitCode = await run.run( 178 | Console(sink: consoleSink), 179 | ['hook-ref'], 180 | ); 181 | expect(exitCode, equals(0)); 182 | expect( 183 | executions, 184 | equals([ 185 | 'echo "echo 123"', 186 | 'echo "hook-ref"', 187 | 'echo "echo 123"', 188 | ]), 189 | ); 190 | expect( 191 | consoleSink.plainLines, 192 | equals([ 193 | '> hook-ref before', 194 | '\$ rps echo', 195 | '', 196 | '> echo', 197 | '\$ echo "echo 123"', 198 | '', 199 | '', 200 | '> hook-ref', 201 | '\$ echo "hook-ref"', 202 | '', 203 | '', 204 | '> hook-ref after', 205 | '\$ rps echo', 206 | '', 207 | '> echo', 208 | '\$ echo "echo 123"', 209 | '', 210 | '', 211 | ]), 212 | ); 213 | }); 214 | 215 | test('Correctly pass additional arguments and options', () async { 216 | final exitCode = await run.run( 217 | Console(sink: consoleSink), 218 | ['echo-args', '123', '-v', '--help'], 219 | ); 220 | expect(exitCode, equals(0)); 221 | expect( 222 | executions, 223 | equals([ 224 | 'echo 123 -v --help', 225 | ]), 226 | ); 227 | expect( 228 | consoleSink.plainLines, 229 | equals([ 230 | '> echo-args', 231 | '\$ echo 123 -v --help', 232 | '', 233 | '', 234 | ]), 235 | ); 236 | }); 237 | 238 | test('Correctly pass additional arguments and options to nested scripts', 239 | () async { 240 | final exitCode = await run.run( 241 | Console(sink: consoleSink), 242 | ['echo-args-nested', 'echo', '123', '-v', '--help'], 243 | ); 244 | expect(exitCode, equals(0)); 245 | expect( 246 | executions, 247 | equals([ 248 | 'echo 123 -v --help', 249 | ]), 250 | ); 251 | expect( 252 | consoleSink.plainLines, 253 | equals([ 254 | '> echo-args-nested echo', 255 | '\$ echo 123 -v --help', 256 | '', 257 | '', 258 | ]), 259 | ); 260 | }); 261 | 262 | test('Do not pass arguments to hooks', () async { 263 | final exitCode = await run.run( 264 | Console(sink: consoleSink), 265 | ['echo-args-hooks', 'echo', '123', '-v', '--help'], 266 | ); 267 | expect(exitCode, equals(0)); 268 | expect( 269 | executions, 270 | equals([ 271 | 'echo', 272 | 'echo', 273 | 'echo 123 -v --help', 274 | 'echo', 275 | 'echo', 276 | ]), 277 | ); 278 | expect( 279 | consoleSink.plainLines, 280 | equals([ 281 | '> echo-args-hooks before', 282 | '\$ echo', 283 | '', 284 | '', 285 | '> echo-args-hooks echo before', 286 | '\$ echo', 287 | '', 288 | '', 289 | '> echo-args-hooks echo', 290 | '\$ echo 123 -v --help', 291 | '', 292 | '', 293 | '> echo-args-hooks echo after', 294 | '\$ echo', 295 | '', 296 | '', 297 | '> echo-args-hooks after', 298 | '\$ echo', 299 | '', 300 | '' 301 | ]), 302 | ); 303 | }); 304 | 305 | test('Correctly pass positional arguments', () async { 306 | final exitCode = await run.run( 307 | Console(sink: consoleSink), 308 | ['echo-positional', 'zero', 'one'], 309 | ); 310 | expect(exitCode, equals(0)); 311 | expect( 312 | executions, 313 | equals([ 314 | 'echo 1=one 0=zero', 315 | ]), 316 | ); 317 | expect( 318 | consoleSink.plainLines, 319 | equals([ 320 | '> echo-positional', 321 | '\$ echo 1=one 0=zero', 322 | '', 323 | '', 324 | ]), 325 | ); 326 | }); 327 | 328 | test('Pass positional arguments to references', () async { 329 | final exitCode = await run.run( 330 | Console(sink: consoleSink), 331 | ['echo-ref', 'zero', 'one'], 332 | ); 333 | expect(exitCode, equals(0)); 334 | expect( 335 | executions, 336 | equals([ 337 | 'echo 1=one 0=zero', 338 | ]), 339 | ); 340 | expect( 341 | consoleSink.plainLines, 342 | equals([ 343 | '> echo-ref', 344 | '\$ rps echo-positional', 345 | '', 346 | '> echo-positional', 347 | '\$ echo 1=one 0=zero', 348 | '', 349 | '', 350 | ]), 351 | ); 352 | }); 353 | 354 | test('Throw error when positional arguments are passed to hooks.', 355 | () async { 356 | RpsException? exception; 357 | try { 358 | await run.run( 359 | Console(sink: consoleSink), 360 | ['echo-hook-positional', 'zero'], 361 | ); 362 | } on RpsException catch (err) { 363 | exception = err; 364 | } 365 | 366 | expect(exception, isNotNull); 367 | expect( 368 | exception!.message, 369 | r'The script "echo-hook-positional $before" defines a positional argument(s), but hooks do not support positional arguments.', 370 | ); 371 | expect( 372 | executions, 373 | equals([]), 374 | ); 375 | expect( 376 | consoleSink.plainLines, 377 | equals([ 378 | '> echo-hook-positional before', 379 | ]), 380 | ); 381 | }); 382 | }); 383 | } 384 | -------------------------------------------------------------------------------- /pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://dart.dev/tools/pub/glossary#lockfile 3 | packages: 4 | _fe_analyzer_shared: 5 | dependency: transitive 6 | description: 7 | name: _fe_analyzer_shared 8 | sha256: ae92f5d747aee634b87f89d9946000c2de774be1d6ac3e58268224348cd0101a 9 | url: "https://pub.dev" 10 | source: hosted 11 | version: "61.0.0" 12 | analyzer: 13 | dependency: transitive 14 | description: 15 | name: analyzer 16 | sha256: ea3d8652bda62982addfd92fdc2d0214e5f82e43325104990d4f4c4a2a313562 17 | url: "https://pub.dev" 18 | source: hosted 19 | version: "5.13.0" 20 | args: 21 | dependency: transitive 22 | description: 23 | name: args 24 | sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596 25 | url: "https://pub.dev" 26 | source: hosted 27 | version: "2.4.2" 28 | async: 29 | dependency: transitive 30 | description: 31 | name: async 32 | sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" 33 | url: "https://pub.dev" 34 | source: hosted 35 | version: "2.11.0" 36 | boolean_selector: 37 | dependency: transitive 38 | description: 39 | name: boolean_selector 40 | sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" 41 | url: "https://pub.dev" 42 | source: hosted 43 | version: "2.1.1" 44 | collection: 45 | dependency: "direct main" 46 | description: 47 | name: collection 48 | sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a 49 | url: "https://pub.dev" 50 | source: hosted 51 | version: "1.18.0" 52 | convert: 53 | dependency: transitive 54 | description: 55 | name: convert 56 | sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" 57 | url: "https://pub.dev" 58 | source: hosted 59 | version: "3.1.1" 60 | coverage: 61 | dependency: transitive 62 | description: 63 | name: coverage 64 | sha256: ac86d3abab0f165e4b8f561280ff4e066bceaac83c424dd19f1ae2c2fcd12ca9 65 | url: "https://pub.dev" 66 | source: hosted 67 | version: "1.7.1" 68 | crypto: 69 | dependency: transitive 70 | description: 71 | name: crypto 72 | sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab 73 | url: "https://pub.dev" 74 | source: hosted 75 | version: "3.0.3" 76 | ffi: 77 | dependency: "direct main" 78 | description: 79 | name: ffi 80 | sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878" 81 | url: "https://pub.dev" 82 | source: hosted 83 | version: "2.1.0" 84 | file: 85 | dependency: transitive 86 | description: 87 | name: file 88 | sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" 89 | url: "https://pub.dev" 90 | source: hosted 91 | version: "7.0.0" 92 | flutter_lints: 93 | dependency: "direct dev" 94 | description: 95 | name: flutter_lints 96 | sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1" 97 | url: "https://pub.dev" 98 | source: hosted 99 | version: "5.0.0" 100 | frontend_server_client: 101 | dependency: transitive 102 | description: 103 | name: frontend_server_client 104 | sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612" 105 | url: "https://pub.dev" 106 | source: hosted 107 | version: "3.2.0" 108 | glob: 109 | dependency: transitive 110 | description: 111 | name: glob 112 | sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" 113 | url: "https://pub.dev" 114 | source: hosted 115 | version: "2.1.2" 116 | http: 117 | dependency: transitive 118 | description: 119 | name: http 120 | sha256: "5895291c13fa8a3bd82e76d5627f69e0d85ca6a30dcac95c4ea19a5d555879c2" 121 | url: "https://pub.dev" 122 | source: hosted 123 | version: "0.13.6" 124 | http_api: 125 | dependency: "direct main" 126 | description: 127 | name: http_api 128 | sha256: a89d7dc95269e09179501c895b0688734351e3b8c8613c8215116d3948a494e0 129 | url: "https://pub.dev" 130 | source: hosted 131 | version: "0.9.0" 132 | http_multi_server: 133 | dependency: transitive 134 | description: 135 | name: http_multi_server 136 | sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" 137 | url: "https://pub.dev" 138 | source: hosted 139 | version: "3.2.1" 140 | http_parser: 141 | dependency: transitive 142 | description: 143 | name: http_parser 144 | sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" 145 | url: "https://pub.dev" 146 | source: hosted 147 | version: "4.0.2" 148 | io: 149 | dependency: transitive 150 | description: 151 | name: io 152 | sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" 153 | url: "https://pub.dev" 154 | source: hosted 155 | version: "1.0.4" 156 | js: 157 | dependency: transitive 158 | description: 159 | name: js 160 | sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 161 | url: "https://pub.dev" 162 | source: hosted 163 | version: "0.6.7" 164 | lints: 165 | dependency: transitive 166 | description: 167 | name: lints 168 | sha256: "3315600f3fb3b135be672bf4a178c55f274bebe368325ae18462c89ac1e3b413" 169 | url: "https://pub.dev" 170 | source: hosted 171 | version: "5.0.0" 172 | logging: 173 | dependency: transitive 174 | description: 175 | name: logging 176 | sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" 177 | url: "https://pub.dev" 178 | source: hosted 179 | version: "1.2.0" 180 | matcher: 181 | dependency: transitive 182 | description: 183 | name: matcher 184 | sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb 185 | url: "https://pub.dev" 186 | source: hosted 187 | version: "0.12.16+1" 188 | meta: 189 | dependency: "direct main" 190 | description: 191 | name: meta 192 | sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c 193 | url: "https://pub.dev" 194 | source: hosted 195 | version: "1.16.0" 196 | mime: 197 | dependency: transitive 198 | description: 199 | name: mime 200 | sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e 201 | url: "https://pub.dev" 202 | source: hosted 203 | version: "1.0.4" 204 | node_preamble: 205 | dependency: transitive 206 | description: 207 | name: node_preamble 208 | sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db" 209 | url: "https://pub.dev" 210 | source: hosted 211 | version: "2.0.2" 212 | objectid: 213 | dependency: transitive 214 | description: 215 | name: objectid 216 | sha256: "22fa972000d3256f10d06323a9dcbf4b564fb03fdb9024399e3a6c1d9902f914" 217 | url: "https://pub.dev" 218 | source: hosted 219 | version: "2.1.0" 220 | package_config: 221 | dependency: transitive 222 | description: 223 | name: package_config 224 | sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" 225 | url: "https://pub.dev" 226 | source: hosted 227 | version: "2.1.0" 228 | path: 229 | dependency: "direct main" 230 | description: 231 | name: path 232 | sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" 233 | url: "https://pub.dev" 234 | source: hosted 235 | version: "1.8.3" 236 | pool: 237 | dependency: transitive 238 | description: 239 | name: pool 240 | sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" 241 | url: "https://pub.dev" 242 | source: hosted 243 | version: "1.5.1" 244 | pub_semver: 245 | dependency: "direct main" 246 | description: 247 | name: pub_semver 248 | sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" 249 | url: "https://pub.dev" 250 | source: hosted 251 | version: "2.1.4" 252 | shelf: 253 | dependency: transitive 254 | description: 255 | name: shelf 256 | sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 257 | url: "https://pub.dev" 258 | source: hosted 259 | version: "1.4.1" 260 | shelf_packages_handler: 261 | dependency: transitive 262 | description: 263 | name: shelf_packages_handler 264 | sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e" 265 | url: "https://pub.dev" 266 | source: hosted 267 | version: "3.0.2" 268 | shelf_static: 269 | dependency: transitive 270 | description: 271 | name: shelf_static 272 | sha256: a41d3f53c4adf0f57480578c1d61d90342cd617de7fc8077b1304643c2d85c1e 273 | url: "https://pub.dev" 274 | source: hosted 275 | version: "1.1.2" 276 | shelf_web_socket: 277 | dependency: transitive 278 | description: 279 | name: shelf_web_socket 280 | sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1" 281 | url: "https://pub.dev" 282 | source: hosted 283 | version: "1.0.4" 284 | source_map_stack_trace: 285 | dependency: transitive 286 | description: 287 | name: source_map_stack_trace 288 | sha256: "84cf769ad83aa6bb61e0aa5a18e53aea683395f196a6f39c4c881fb90ed4f7ae" 289 | url: "https://pub.dev" 290 | source: hosted 291 | version: "2.1.1" 292 | source_maps: 293 | dependency: transitive 294 | description: 295 | name: source_maps 296 | sha256: "708b3f6b97248e5781f493b765c3337db11c5d2c81c3094f10904bfa8004c703" 297 | url: "https://pub.dev" 298 | source: hosted 299 | version: "0.10.12" 300 | source_span: 301 | dependency: transitive 302 | description: 303 | name: source_span 304 | sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" 305 | url: "https://pub.dev" 306 | source: hosted 307 | version: "1.10.0" 308 | stack_trace: 309 | dependency: transitive 310 | description: 311 | name: stack_trace 312 | sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" 313 | url: "https://pub.dev" 314 | source: hosted 315 | version: "1.11.1" 316 | stream_channel: 317 | dependency: transitive 318 | description: 319 | name: stream_channel 320 | sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 321 | url: "https://pub.dev" 322 | source: hosted 323 | version: "2.1.2" 324 | string_scanner: 325 | dependency: transitive 326 | description: 327 | name: string_scanner 328 | sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" 329 | url: "https://pub.dev" 330 | source: hosted 331 | version: "1.2.0" 332 | term_glyph: 333 | dependency: transitive 334 | description: 335 | name: term_glyph 336 | sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 337 | url: "https://pub.dev" 338 | source: hosted 339 | version: "1.2.1" 340 | test: 341 | dependency: "direct dev" 342 | description: 343 | name: test 344 | sha256: "713a8789d62f3233c46b4a90b174737b2c04cb6ae4500f2aa8b1be8f03f5e67f" 345 | url: "https://pub.dev" 346 | source: hosted 347 | version: "1.25.8" 348 | test_api: 349 | dependency: transitive 350 | description: 351 | name: test_api 352 | sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c" 353 | url: "https://pub.dev" 354 | source: hosted 355 | version: "0.7.3" 356 | test_core: 357 | dependency: transitive 358 | description: 359 | name: test_core 360 | sha256: "12391302411737c176b0b5d6491f466b0dd56d4763e347b6714efbaa74d7953d" 361 | url: "https://pub.dev" 362 | source: hosted 363 | version: "0.6.5" 364 | typed_data: 365 | dependency: transitive 366 | description: 367 | name: typed_data 368 | sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c 369 | url: "https://pub.dev" 370 | source: hosted 371 | version: "1.3.2" 372 | vm_service: 373 | dependency: transitive 374 | description: 375 | name: vm_service 376 | sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 377 | url: "https://pub.dev" 378 | source: hosted 379 | version: "13.0.0" 380 | watcher: 381 | dependency: transitive 382 | description: 383 | name: watcher 384 | sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" 385 | url: "https://pub.dev" 386 | source: hosted 387 | version: "1.1.0" 388 | web_socket_channel: 389 | dependency: transitive 390 | description: 391 | name: web_socket_channel 392 | sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b 393 | url: "https://pub.dev" 394 | source: hosted 395 | version: "2.4.0" 396 | webkit_inspection_protocol: 397 | dependency: transitive 398 | description: 399 | name: webkit_inspection_protocol 400 | sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572" 401 | url: "https://pub.dev" 402 | source: hosted 403 | version: "1.2.1" 404 | yaml: 405 | dependency: "direct main" 406 | description: 407 | name: yaml 408 | sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" 409 | url: "https://pub.dev" 410 | source: hosted 411 | version: "3.1.2" 412 | sdks: 413 | dart: ">=3.5.0 <4.0.0" 414 | --------------------------------------------------------------------------------