Merge pull request #1209 from dart-lang/merge-package_config-package

Merge `package:package_config`
diff --git a/.github/ISSUE_TEMPLATE/io.md b/.github/ISSUE_TEMPLATE/io.md
new file mode 100644
index 0000000..5646f0f
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/io.md
@@ -0,0 +1,5 @@
+---
+name: "package:io"
+about: "Create a bug or file a feature request against package:io."
+labels: "package:io"
+---
\ No newline at end of file
diff --git a/.github/labeler.yml b/.github/labeler.yml
index 31b8b47..39d78aa 100644
--- a/.github/labeler.yml
+++ b/.github/labeler.yml
@@ -64,6 +64,10 @@
   - changed-files:
     - any-glob-to-any-file: 'pkgs/html/**'
 
+'package:io':
+  - changed-files:
+    - any-glob-to-any-file: 'pkgs/io/**'
+
 'package:json_rpc_2':
   - changed-files:
     - any-glob-to-any-file: 'pkgs/json_rpc_2/**'
diff --git a/.github/workflows/io.yaml b/.github/workflows/io.yaml
new file mode 100644
index 0000000..0c719a6
--- /dev/null
+++ b/.github/workflows/io.yaml
@@ -0,0 +1,72 @@
+name: package:io
+
+on:
+  # Run on PRs and pushes to the default branch.
+  push:
+    branches: [ main ]
+    paths:
+      - '.github/workflows/io.yml'
+      - 'pkgs/io/**'
+  pull_request:
+    branches: [ main ]
+    paths:
+      - '.github/workflows/io.yml'
+      - 'pkgs/io/**'
+  schedule:
+    - cron: "0 0 * * 0"
+
+env:
+  PUB_ENVIRONMENT: bot.github
+
+
+defaults:
+  run:
+    working-directory: pkgs/io/
+
+
+jobs:
+  # Check code formatting and static analysis on a single OS (linux)
+  # against Dart dev and stable.
+  analyze:
+    runs-on: ubuntu-latest
+    strategy:
+      fail-fast: false
+      matrix:
+        sdk: [dev, 3.4]
+    steps:
+      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+      - uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94
+        with:
+          sdk: ${{ matrix.sdk }}
+      - id: install
+        name: Install dependencies
+        run: dart pub get
+      - name: Check formatting
+        run: dart format --output=none --set-exit-if-changed .
+        if: always() && steps.install.outcome == 'success'
+      - name: Analyze code
+        run: dart analyze --fatal-infos
+        if: always() && steps.install.outcome == 'success'
+
+  # Run tests on a matrix consisting of two dimensions:
+  # 1. OS: ubuntu-latest, (macos-latest, windows-latest)
+  # 2. release channel: dev, stable
+  test:
+    needs: analyze
+    runs-on: ${{ matrix.os }}
+    strategy:
+      fail-fast: false
+      matrix:
+        # Add macos-latest and/or windows-latest if relevant for this package.
+        os: [ubuntu-latest]
+        sdk: [dev, 3.4]
+    steps:
+      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+      - uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94
+        with:
+          sdk: ${{ matrix.sdk }}
+      - id: install
+        name: Install dependencies
+        run: dart pub get
+      - run: dart test
+        if: always() && steps.install.outcome == 'success'
diff --git a/README.md b/README.md
index 01360e9..0acab2c 100644
--- a/README.md
+++ b/README.md
@@ -29,6 +29,7 @@
 | [file_testing](pkgs/file_testing/) | Testing utilities for package:file. | [![package issues](https://img.shields.io/badge/package:file_testing-4774bc)](https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Afile_testing) | [![pub package](https://img.shields.io/pub/v/file_testing.svg)](https://pub.dev/packages/file_testing) |
 | [graphs](pkgs/graphs/) | Graph algorithms that operate on graphs in any representation. | [![package issues](https://img.shields.io/badge/package:graphs-4774bc)](https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Agraphs) | [![pub package](https://img.shields.io/pub/v/graphs.svg)](https://pub.dev/packages/graphs) |
 | [html](pkgs/html/) | APIs for parsing and manipulating HTML content outside the browser. | [![package issues](https://img.shields.io/badge/package:html-4774bc)](https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Ahtml) | [![pub package](https://img.shields.io/pub/v/html.svg)](https://pub.dev/packages/html) |
+| [io](pkgs/io/) | Utilities for the Dart VM Runtime including support for ANSI colors, file copying, and standard exit code values. | [![pub package](https://img.shields.io/pub/v/io.svg)](https://pub.dev/packages/io) |
 | [json_rpc_2](pkgs/json_rpc_2/) | Utilities to write a client or server using the JSON-RPC 2.0 spec. | [![package issues](https://img.shields.io/badge/package:json_rpc_2-4774bc)](https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Ajson_rpc_2) | [![pub package](https://img.shields.io/pub/v/json_rpc_2.svg)](https://pub.dev/packages/json_rpc_2) |
 | [mime](pkgs/mime/) | Utilities for handling media (MIME) types, including determining a type from a file extension and file contents. | [![package issues](https://img.shields.io/badge/package:mime-4774bc)](https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Amime) | [![pub package](https://img.shields.io/pub/v/mime.svg)](https://pub.dev/packages/mime) |
 | [oauth2](pkgs/oauth2/) | A client library for authenticating with a remote service via OAuth2 on behalf of a user, and making authorized HTTP requests with the user's OAuth2 credentials. | [![package issues](https://img.shields.io/badge/package:oauth2-4774bc)](https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Aoauth2) | [![pub package](https://img.shields.io/pub/v/oauth2.svg)](https://pub.dev/packages/oauth2) |
diff --git a/pkgs/io/.gitignore b/pkgs/io/.gitignore
new file mode 100644
index 0000000..01d42c0
--- /dev/null
+++ b/pkgs/io/.gitignore
@@ -0,0 +1,4 @@
+.dart_tool/
+.pub/
+.packages
+pubspec.lock
diff --git a/pkgs/io/AUTHORS b/pkgs/io/AUTHORS
new file mode 100644
index 0000000..ff09364
--- /dev/null
+++ b/pkgs/io/AUTHORS
@@ -0,0 +1,7 @@
+# Below is a list of people and organizations that have contributed
+# to the project. Names should be added to the list like so:
+#
+#   Name/Organization <email address>
+
+Google Inc.
+
diff --git a/pkgs/io/CHANGELOG.md b/pkgs/io/CHANGELOG.md
new file mode 100644
index 0000000..e0631fa
--- /dev/null
+++ b/pkgs/io/CHANGELOG.md
@@ -0,0 +1,119 @@
+## 1.0.5
+
+* Require Dart 3.4.
+* Move to `dart-lang/tools` monorepo.
+
+## 1.0.4
+
+* Updates to the readme.
+
+## 1.0.3
+
+* Revert `meta` constraint to `^1.3.0`.
+
+## 1.0.2
+
+* Update `meta` constraint to `>=1.3.0 <3.0.0`.
+
+## 1.0.1
+
+* Update code examples to call the unified `dart` developer tool.
+
+## 1.0.0
+
+* Migrate this package to null-safety.
+* Require Dart >=2.12.
+
+## 0.3.5
+
+* Require Dart >=2.1.
+* Remove dependency on `package:charcode`.
+
+## 0.3.4
+
+* Fix a number of issues affecting the package score on `pub.dev`.
+
+## 0.3.3
+
+* Updates for Dart 2 constants. Require at least Dart `2.0.0-dev.54`.
+
+* Fix the type of `StartProcess` typedef to match `Process.start` from
+  `dart:io`.
+
+## 0.3.2+1
+
+* `ansi.dart`
+
+  * The "forScript" code paths now ignore the `ansiOutputEnabled` value. Affects
+    the `escapeForScript` property on `AnsiCode` and the `wrap` and `wrapWith`
+    functions when `forScript` is true.
+
+## 0.3.2
+
+* `ansi.dart`
+
+  * Added `forScript` named argument to top-level `wrapWith` function.
+
+  * `AnsiCode`
+
+    * Added `String get escapeForScript` property.
+
+    * Added `forScript` named argument to `wrap` function.
+
+## 0.3.1
+
+- Added `SharedStdIn.nextLine` (similar to `readLineSync`) and `lines`:
+
+```dart
+main() async {
+  // Prints the first line entered on stdin.
+  print(await sharedStdIn.nextLine());
+
+  // Prints all remaining lines.
+  await for (final line in sharedStdIn.lines) {
+    print(line);
+  }
+}
+```
+
+- Added a `copyPath` and `copyPathSync` function, similar to `cp -R`.
+
+- Added a dependency on `package:path`.
+
+- Added the remaining missing arguments to `ProcessManager.spawnX` which
+  forward to `Process.start`. It is now an interchangeable function for running
+  a process.
+
+## 0.3.0
+
+- **BREAKING CHANGE**: The `arguments` argument to `ProcessManager.spawn` is
+  now positional (not named) and required. This makes it more similar to the
+  built-in `Process.start`, and easier to use as a drop in replacement:
+
+```dart
+main() {
+  processManager.spawn('dart', ['--version']);
+}
+```
+
+- Fixed a bug where processes created from `ProcessManager.spawn` could not
+  have their `stdout`/`stderr` read through their respective getters (a runtime
+  error was always thrown).
+
+- Added `ProcessMangaer#spawnBackground`, which does not forward `stdin`.
+
+- Added `ProcessManager#spawnDetached`, which does not forward any I/O.
+
+- Added the `shellSplit()` function, which parses a list of arguments in the
+  same manner as [the POSIX shell][what_is_posix_shell].
+
+[what_is_posix_shell]: https://pubs.opengroup.org/onlinepubs/9699919799/utilities/contents.html
+
+## 0.2.0
+
+- Initial commit of...
+   - `FutureOr<bool> String isExecutable(path)`.
+   - `ExitCode`
+   - `ProcessManager` and `Spawn`
+   - `sharedStdIn` and `SharedStdIn`
+   - `ansi.dart` library with support for formatting terminal output
diff --git a/pkgs/io/LICENSE b/pkgs/io/LICENSE
new file mode 100644
index 0000000..03af64a
--- /dev/null
+++ b/pkgs/io/LICENSE
@@ -0,0 +1,27 @@
+Copyright 2017, the Dart project authors. 
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above
+      copyright notice, this list of conditions and the following
+      disclaimer in the documentation and/or other materials provided
+      with the distribution.
+    * Neither the name of Google LLC nor the names of its
+      contributors may be used to endorse or promote products derived
+      from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/pkgs/io/README.md b/pkgs/io/README.md
new file mode 100644
index 0000000..adbc941
--- /dev/null
+++ b/pkgs/io/README.md
@@ -0,0 +1,104 @@
+[![Build Status](https://github.com/dart-lang/tools/actions/workflows/io.yaml/badge.svg)](https://github.com/dart-lang/tools/actions/workflows/io.yaml)
+[![pub package](https://img.shields.io/pub/v/io.svg)](https://pub.dev/packages/io)
+[![package publisher](https://img.shields.io/pub/publisher/io.svg)](https://pub.dev/packages/io/publisher)
+
+Contains utilities for the Dart VM's `dart:io`.
+
+## Usage - `io.dart`
+
+### Files
+
+#### `isExecutable`
+ 
+Returns whether a provided file path is considered _executable_ on the host
+operating system.
+
+### Processes
+
+#### `ExitCode`
+
+An `enum`-like class that contains known exit codes.
+
+#### `ProcessManager`
+
+A higher-level service for spawning and communicating with processes.
+
+##### Use `spawn` to create a process with std[in|out|err] forwarded by default
+
+```dart
+Future<void> main() async {
+  final manager = ProcessManager();
+
+  // Print `dart` tool version to stdout.
+  print('** Running `dart --version`');
+  var spawn = await manager.spawn('dart', ['--version']);
+  await spawn.exitCode;
+
+  // Check formatting and print the result to stdout.
+  print('** Running `dart format --output=none .`');
+  spawn = await manager.spawn('dart', ['format', '--output=none', '.']);
+  await spawn.exitCode;
+
+  // Check if a package is ready for publishing.
+  // Upon hitting a blocking stdin state, you may directly
+  // output to the processes's stdin via your own, similar to how a bash or
+  // shell script would spawn a process.
+  print('** Running pub publish');
+  spawn = await manager.spawn('dart', ['pub', 'publish', '--dry-run']);
+  await spawn.exitCode;
+
+  // Closes stdin for the entire program.
+  await sharedStdIn.terminate();
+}
+```
+
+#### `sharedStdIn`
+
+A safer version of the default `stdin` stream from `dart:io` that allows a
+subscriber to cancel their subscription, and then allows a _new_ subscriber to
+start listening. This differs from the default behavior where only a single
+listener is ever allowed in the application lifecycle:
+
+```dart
+test('should allow multiple subscribers', () async {
+  final logs = <String>[];
+  final asUtf8 = sharedStdIn.transform(UTF8.decoder);
+  // Wait for input for the user.
+  logs.add(await asUtf8.first);
+  // Wait for more input for the user.
+  logs.add(await asUtf8.first);
+  expect(logs, ['Hello World', 'Goodbye World']);
+});
+```
+
+For testing, an instance of `SharedStdIn` may be created directly.
+
+## Usage - `ansi.dart`
+
+```dart
+import 'dart:io' as io;
+import 'package:io/ansi.dart';
+
+void main() {
+  // To use one style, call the `wrap` method on one of the provided top-level
+  // values.
+  io.stderr.writeln(red.wrap("Bad error!"));
+
+  // To use multiple styles, call `wrapWith`.
+  print(wrapWith('** Important **', [red, styleBold, styleUnderlined]));
+
+  // The wrap functions will simply return the provided value unchanged if
+  // `ansiOutputEnabled` is false.
+  //
+  // You can override the value `ansiOutputEnabled` by wrapping code in
+  // `overrideAnsiOutput`.
+  overrideAnsiOutput(false, () {
+    assert('Normal text' == green.wrap('Normal text'));
+  });
+}
+```
+
+## Publishing automation
+
+For information about our publishing automation and release process, see
+https://github.com/dart-lang/ecosystem/wiki/Publishing-automation.
diff --git a/pkgs/io/analysis_options.yaml b/pkgs/io/analysis_options.yaml
new file mode 100644
index 0000000..6d74ee9
--- /dev/null
+++ b/pkgs/io/analysis_options.yaml
@@ -0,0 +1,32 @@
+# https://dart.dev/guides/language/analysis-options
+include: package:dart_flutter_team_lints/analysis_options.yaml
+
+analyzer:
+  language:
+    strict-casts: true
+    strict-inference: true
+    strict-raw-types: true
+
+linter:
+  rules:
+    - avoid_bool_literals_in_conditional_expressions
+    - avoid_classes_with_only_static_members
+    - avoid_private_typedef_functions
+    - avoid_redundant_argument_values
+    - avoid_returning_this
+    - avoid_unused_constructor_parameters
+    - avoid_void_async
+    - cancel_subscriptions
+    - join_return_with_assignment
+    - literal_only_boolean_expressions
+    - missing_whitespace_between_adjacent_strings
+    - no_adjacent_strings_in_list
+    - no_runtimeType_toString
+    - prefer_const_declarations
+    - prefer_expression_function_bodies
+    - prefer_final_locals
+    - unnecessary_await_in_return
+    - unnecessary_breaks
+    - use_if_null_to_convert_nulls_to_bools
+    - use_raw_strings
+    - use_string_buffers
diff --git a/pkgs/io/example/example.dart b/pkgs/io/example/example.dart
new file mode 100644
index 0000000..8e358fd
--- /dev/null
+++ b/pkgs/io/example/example.dart
@@ -0,0 +1,33 @@
+// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:math';
+
+import 'package:io/ansi.dart';
+
+/// Prints a sample of all of the `AnsiCode` values.
+void main(List<String> args) {
+  final forScript = args.contains('--for-script');
+
+  if (!ansiOutputEnabled) {
+    print('`ansiOutputEnabled` is `false`.');
+    print("Don't expect pretty output.");
+  }
+  _preview('Foreground', foregroundColors, forScript);
+  _preview('Background', backgroundColors, forScript);
+  _preview('Styles', styles, forScript);
+}
+
+void _preview(String name, List<AnsiCode> values, bool forScript) {
+  print('');
+  final longest = values.map((ac) => ac.name.length).reduce(max);
+
+  print(wrapWith('** $name **', [styleBold, styleUnderlined]));
+  for (var code in values) {
+    final header =
+        '${code.name.padRight(longest)} ${code.code.toString().padLeft(3)}';
+
+    print("$header: ${code.wrap('Sample', forScript: forScript)}");
+  }
+}
diff --git a/pkgs/io/example/spawn_process_example.dart b/pkgs/io/example/spawn_process_example.dart
new file mode 100644
index 0000000..b7ba247
--- /dev/null
+++ b/pkgs/io/example/spawn_process_example.dart
@@ -0,0 +1,33 @@
+// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+
+import 'package:io/io.dart';
+
+/// Runs a few subcommands in the `dart` command.
+Future<void> main() async {
+  final manager = ProcessManager();
+
+  // Print `dart` tool version to stdout.
+  print('** Running `dart --version`');
+  var spawn = await manager.spawn('dart', ['--version']);
+  await spawn.exitCode;
+
+  // Check formatting and print the result to stdout.
+  print('** Running `dart format --output=none .`');
+  spawn = await manager.spawn('dart', ['format', '--output=none', '.']);
+  await spawn.exitCode;
+
+  // Check if a package is ready for publishing.
+  // Upon hitting a blocking stdin state, you may directly
+  // output to the processes's stdin via your own, similar to how a bash or
+  // shell script would spawn a process.
+  print('** Running pub publish');
+  spawn = await manager.spawn('dart', ['pub', 'publish', '--dry-run']);
+  await spawn.exitCode;
+
+  // Closes stdin for the entire program.
+  await sharedStdIn.terminate();
+}
diff --git a/pkgs/io/lib/ansi.dart b/pkgs/io/lib/ansi.dart
new file mode 100644
index 0000000..a2adbe7
--- /dev/null
+++ b/pkgs/io/lib/ansi.dart
@@ -0,0 +1,5 @@
+// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+export 'src/ansi_code.dart';
diff --git a/pkgs/io/lib/io.dart b/pkgs/io/lib/io.dart
new file mode 100644
index 0000000..8ee0843
--- /dev/null
+++ b/pkgs/io/lib/io.dart
@@ -0,0 +1,10 @@
+// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+export 'src/copy_path.dart' show copyPath, copyPathSync;
+export 'src/exit_code.dart' show ExitCode;
+export 'src/permissions.dart' show isExecutable;
+export 'src/process_manager.dart' show ProcessManager, Spawn, StartProcess;
+export 'src/shared_stdin.dart' show SharedStdIn, sharedStdIn;
+export 'src/shell_words.dart' show shellSplit;
diff --git a/pkgs/io/lib/src/ansi_code.dart b/pkgs/io/lib/src/ansi_code.dart
new file mode 100644
index 0000000..c9a22c5
--- /dev/null
+++ b/pkgs/io/lib/src/ansi_code.dart
@@ -0,0 +1,316 @@
+// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+import 'dart:io' as io;
+
+const _ansiEscapeLiteral = '\x1B';
+const _ansiEscapeForScript = r'\033';
+
+/// Whether formatted ANSI output is enabled for [wrapWith] and [AnsiCode.wrap].
+///
+/// By default, returns `true` if both `stdout.supportsAnsiEscapes` and
+/// `stderr.supportsAnsiEscapes` from `dart:io` are `true`.
+///
+/// The default can be overridden by setting the [Zone] variable [AnsiCode] to
+/// either `true` or `false`.
+///
+/// [overrideAnsiOutput] is provided to make this easy.
+bool get ansiOutputEnabled =>
+    Zone.current[AnsiCode] as bool? ??
+    (io.stdout.supportsAnsiEscapes && io.stderr.supportsAnsiEscapes);
+
+/// Returns `true` no formatting is required for [input].
+bool _isNoop(bool skip, String? input, bool? forScript) =>
+    skip ||
+    input == null ||
+    input.isEmpty ||
+    !((forScript ?? false) || ansiOutputEnabled);
+
+/// Allows overriding [ansiOutputEnabled] to [enableAnsiOutput] for the code run
+/// within [body].
+T overrideAnsiOutput<T>(bool enableAnsiOutput, T Function() body) =>
+    runZoned(body, zoneValues: <Object, Object>{AnsiCode: enableAnsiOutput});
+
+/// The type of code represented by [AnsiCode].
+class AnsiCodeType {
+  final String _name;
+
+  /// A foreground color.
+  static const AnsiCodeType foreground = AnsiCodeType._('foreground');
+
+  /// A style.
+  static const AnsiCodeType style = AnsiCodeType._('style');
+
+  /// A background color.
+  static const AnsiCodeType background = AnsiCodeType._('background');
+
+  /// A reset value.
+  static const AnsiCodeType reset = AnsiCodeType._('reset');
+
+  const AnsiCodeType._(this._name);
+
+  @override
+  String toString() => 'AnsiType.$_name';
+}
+
+/// Standard ANSI escape code for customizing terminal text output.
+///
+/// [Source](https://en.wikipedia.org/wiki/ANSI_escape_code#Colors)
+class AnsiCode {
+  /// The numeric value associated with this code.
+  final int code;
+
+  /// The [AnsiCode] that resets this value, if one exists.
+  ///
+  /// Otherwise, `null`.
+  final AnsiCode? reset;
+
+  /// A description of this code.
+  final String name;
+
+  /// The type of code that is represented.
+  final AnsiCodeType type;
+
+  const AnsiCode._(this.name, this.type, this.code, this.reset);
+
+  /// Represents the value escaped for use in terminal output.
+  String get escape => '$_ansiEscapeLiteral[${code}m';
+
+  /// Represents the value as an unescaped literal suitable for scripts.
+  String get escapeForScript => '$_ansiEscapeForScript[${code}m';
+
+  String _escapeValue({bool forScript = false}) =>
+      forScript ? escapeForScript : escape;
+
+  /// Wraps [value] with the [escape] value for this code, followed by
+  /// [resetAll].
+  ///
+  /// If [forScript] is `true`, the return value is an unescaped literal. The
+  /// value of [ansiOutputEnabled] is also ignored.
+  ///
+  /// Returns `value` unchanged if
+  ///   * [value] is `null` or empty
+  ///   * both [ansiOutputEnabled] and [forScript] are `false`.
+  ///   * [type] is [AnsiCodeType.reset]
+  String? wrap(String? value, {bool forScript = false}) =>
+      _isNoop(type == AnsiCodeType.reset, value, forScript)
+          ? value
+          : '${_escapeValue(forScript: forScript)}$value'
+              '${reset!._escapeValue(forScript: forScript)}';
+
+  @override
+  String toString() => '$name ${type._name} ($code)';
+}
+
+/// Returns a [String] formatted with [codes].
+///
+/// If [forScript] is `true`, the return value is an unescaped literal. The
+/// value of [ansiOutputEnabled] is also ignored.
+///
+/// Returns `value` unchanged if
+///   * [value] is `null` or empty.
+///   * both [ansiOutputEnabled] and [forScript] are `false`.
+///   * [codes] is empty.
+///
+/// Throws an [ArgumentError] if
+///   * [codes] contains more than one value of type [AnsiCodeType.foreground].
+///   * [codes] contains more than one value of type [AnsiCodeType.background].
+///   * [codes] contains any value of type [AnsiCodeType.reset].
+String? wrapWith(String? value, Iterable<AnsiCode> codes,
+    {bool forScript = false}) {
+  // Eliminate duplicates
+  final myCodes = codes.toSet();
+
+  if (_isNoop(myCodes.isEmpty, value, forScript)) {
+    return value;
+  }
+
+  var foreground = 0, background = 0;
+  for (var code in myCodes) {
+    switch (code.type) {
+      case AnsiCodeType.foreground:
+        foreground++;
+        if (foreground > 1) {
+          throw ArgumentError.value(codes, 'codes',
+              'Cannot contain more than one foreground color code.');
+        }
+      case AnsiCodeType.background:
+        background++;
+        if (background > 1) {
+          throw ArgumentError.value(codes, 'codes',
+              'Cannot contain more than one foreground color code.');
+        }
+      case AnsiCodeType.reset:
+        throw ArgumentError.value(
+            codes, 'codes', 'Cannot contain reset codes.');
+      case AnsiCodeType.style:
+        // Ignore.
+        break;
+    }
+  }
+
+  final sortedCodes = myCodes.map((ac) => ac.code).toList()..sort();
+  final escapeValue = forScript ? _ansiEscapeForScript : _ansiEscapeLiteral;
+
+  return "$escapeValue[${sortedCodes.join(';')}m$value"
+      '${resetAll._escapeValue(forScript: forScript)}';
+}
+
+//
+// Style values
+//
+
+const styleBold = AnsiCode._('bold', AnsiCodeType.style, 1, resetBold);
+const styleDim = AnsiCode._('dim', AnsiCodeType.style, 2, resetDim);
+const styleItalic = AnsiCode._('italic', AnsiCodeType.style, 3, resetItalic);
+const styleUnderlined =
+    AnsiCode._('underlined', AnsiCodeType.style, 4, resetUnderlined);
+const styleBlink = AnsiCode._('blink', AnsiCodeType.style, 5, resetBlink);
+const styleReverse = AnsiCode._('reverse', AnsiCodeType.style, 7, resetReverse);
+
+/// Not widely supported.
+const styleHidden = AnsiCode._('hidden', AnsiCodeType.style, 8, resetHidden);
+
+/// Not widely supported.
+const styleCrossedOut =
+    AnsiCode._('crossed out', AnsiCodeType.style, 9, resetCrossedOut);
+
+//
+// Reset values
+//
+
+const resetAll = AnsiCode._('all', AnsiCodeType.reset, 0, null);
+
+// NOTE: bold is weird. The reset code seems to be 22 sometimes – not 21
+// See https://gitlab.com/gnachman/iterm2/issues/3208
+const resetBold = AnsiCode._('bold', AnsiCodeType.reset, 22, null);
+const resetDim = AnsiCode._('dim', AnsiCodeType.reset, 22, null);
+const resetItalic = AnsiCode._('italic', AnsiCodeType.reset, 23, null);
+const resetUnderlined = AnsiCode._('underlined', AnsiCodeType.reset, 24, null);
+const resetBlink = AnsiCode._('blink', AnsiCodeType.reset, 25, null);
+const resetReverse = AnsiCode._('reverse', AnsiCodeType.reset, 27, null);
+const resetHidden = AnsiCode._('hidden', AnsiCodeType.reset, 28, null);
+const resetCrossedOut = AnsiCode._('crossed out', AnsiCodeType.reset, 29, null);
+
+//
+// Foreground values
+//
+
+const black = AnsiCode._('black', AnsiCodeType.foreground, 30, resetAll);
+const red = AnsiCode._('red', AnsiCodeType.foreground, 31, resetAll);
+const green = AnsiCode._('green', AnsiCodeType.foreground, 32, resetAll);
+const yellow = AnsiCode._('yellow', AnsiCodeType.foreground, 33, resetAll);
+const blue = AnsiCode._('blue', AnsiCodeType.foreground, 34, resetAll);
+const magenta = AnsiCode._('magenta', AnsiCodeType.foreground, 35, resetAll);
+const cyan = AnsiCode._('cyan', AnsiCodeType.foreground, 36, resetAll);
+const lightGray =
+    AnsiCode._('light gray', AnsiCodeType.foreground, 37, resetAll);
+const defaultForeground =
+    AnsiCode._('default', AnsiCodeType.foreground, 39, resetAll);
+const darkGray = AnsiCode._('dark gray', AnsiCodeType.foreground, 90, resetAll);
+const lightRed = AnsiCode._('light red', AnsiCodeType.foreground, 91, resetAll);
+const lightGreen =
+    AnsiCode._('light green', AnsiCodeType.foreground, 92, resetAll);
+const lightYellow =
+    AnsiCode._('light yellow', AnsiCodeType.foreground, 93, resetAll);
+const lightBlue =
+    AnsiCode._('light blue', AnsiCodeType.foreground, 94, resetAll);
+const lightMagenta =
+    AnsiCode._('light magenta', AnsiCodeType.foreground, 95, resetAll);
+const lightCyan =
+    AnsiCode._('light cyan', AnsiCodeType.foreground, 96, resetAll);
+const white = AnsiCode._('white', AnsiCodeType.foreground, 97, resetAll);
+
+//
+// Background values
+//
+
+const backgroundBlack =
+    AnsiCode._('black', AnsiCodeType.background, 40, resetAll);
+const backgroundRed = AnsiCode._('red', AnsiCodeType.background, 41, resetAll);
+const backgroundGreen =
+    AnsiCode._('green', AnsiCodeType.background, 42, resetAll);
+const backgroundYellow =
+    AnsiCode._('yellow', AnsiCodeType.background, 43, resetAll);
+const backgroundBlue =
+    AnsiCode._('blue', AnsiCodeType.background, 44, resetAll);
+const backgroundMagenta =
+    AnsiCode._('magenta', AnsiCodeType.background, 45, resetAll);
+const backgroundCyan =
+    AnsiCode._('cyan', AnsiCodeType.background, 46, resetAll);
+const backgroundLightGray =
+    AnsiCode._('light gray', AnsiCodeType.background, 47, resetAll);
+const backgroundDefault =
+    AnsiCode._('default', AnsiCodeType.background, 49, resetAll);
+const backgroundDarkGray =
+    AnsiCode._('dark gray', AnsiCodeType.background, 100, resetAll);
+const backgroundLightRed =
+    AnsiCode._('light red', AnsiCodeType.background, 101, resetAll);
+const backgroundLightGreen =
+    AnsiCode._('light green', AnsiCodeType.background, 102, resetAll);
+const backgroundLightYellow =
+    AnsiCode._('light yellow', AnsiCodeType.background, 103, resetAll);
+const backgroundLightBlue =
+    AnsiCode._('light blue', AnsiCodeType.background, 104, resetAll);
+const backgroundLightMagenta =
+    AnsiCode._('light magenta', AnsiCodeType.background, 105, resetAll);
+const backgroundLightCyan =
+    AnsiCode._('light cyan', AnsiCodeType.background, 106, resetAll);
+const backgroundWhite =
+    AnsiCode._('white', AnsiCodeType.background, 107, resetAll);
+
+/// All of the [AnsiCode] values that represent [AnsiCodeType.style].
+const List<AnsiCode> styles = [
+  styleBold,
+  styleDim,
+  styleItalic,
+  styleUnderlined,
+  styleBlink,
+  styleReverse,
+  styleHidden,
+  styleCrossedOut
+];
+
+/// All of the [AnsiCode] values that represent [AnsiCodeType.foreground].
+const List<AnsiCode> foregroundColors = [
+  black,
+  red,
+  green,
+  yellow,
+  blue,
+  magenta,
+  cyan,
+  lightGray,
+  defaultForeground,
+  darkGray,
+  lightRed,
+  lightGreen,
+  lightYellow,
+  lightBlue,
+  lightMagenta,
+  lightCyan,
+  white
+];
+
+/// All of the [AnsiCode] values that represent [AnsiCodeType.background].
+const List<AnsiCode> backgroundColors = [
+  backgroundBlack,
+  backgroundRed,
+  backgroundGreen,
+  backgroundYellow,
+  backgroundBlue,
+  backgroundMagenta,
+  backgroundCyan,
+  backgroundLightGray,
+  backgroundDefault,
+  backgroundDarkGray,
+  backgroundLightRed,
+  backgroundLightGreen,
+  backgroundLightYellow,
+  backgroundLightBlue,
+  backgroundLightMagenta,
+  backgroundLightCyan,
+  backgroundWhite
+];
diff --git a/pkgs/io/lib/src/charcodes.dart b/pkgs/io/lib/src/charcodes.dart
new file mode 100644
index 0000000..4acaf0a
--- /dev/null
+++ b/pkgs/io/lib/src/charcodes.dart
@@ -0,0 +1,34 @@
+// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+// Generated using:
+//  pub global run charcode \$=dollar \'=single_quote \"=double_quote \
+//      \' '\\\n"$`# \t'
+
+/// "Horizontal Tab" control character, common name.
+const int $tab = 0x09;
+
+/// "Line feed" control character.
+const int $lf = 0x0a;
+
+/// Space character.
+const int $space = 0x20;
+
+/// Character `"`, short name.
+const int $doubleQuote = 0x22;
+
+/// Character `#`.
+const int $hash = 0x23;
+
+/// Character `$`.
+const int $dollar = 0x24;
+
+/// Character "'".
+const int $singleQuote = 0x27;
+
+/// Character `\`.
+const int $backslash = 0x5c;
+
+/// Character `` ` ``.
+const int $backquote = 0x60;
diff --git a/pkgs/io/lib/src/copy_path.dart b/pkgs/io/lib/src/copy_path.dart
new file mode 100644
index 0000000..3a999b6
--- /dev/null
+++ b/pkgs/io/lib/src/copy_path.dart
@@ -0,0 +1,69 @@
+// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:io';
+
+import 'package:path/path.dart' as p;
+
+bool _doNothing(String from, String to) {
+  if (p.canonicalize(from) == p.canonicalize(to)) {
+    return true;
+  }
+  if (p.isWithin(from, to)) {
+    throw ArgumentError('Cannot copy from $from to $to');
+  }
+  return false;
+}
+
+/// Copies all of the files in the [from] directory to [to].
+///
+/// This is similar to `cp -R <from> <to>`:
+/// * Symlinks are supported.
+/// * Existing files are over-written, if any.
+/// * If [to] is within [from], throws [ArgumentError] (an infinite operation).
+/// * If [from] and [to] are canonically the same, no operation occurs.
+///
+/// Returns a future that completes when complete.
+Future<void> copyPath(String from, String to) async {
+  if (_doNothing(from, to)) {
+    return;
+  }
+  await Directory(to).create(recursive: true);
+  await for (final file in Directory(from).list(recursive: true)) {
+    final copyTo = p.join(to, p.relative(file.path, from: from));
+    if (file is Directory) {
+      await Directory(copyTo).create(recursive: true);
+    } else if (file is File) {
+      await File(file.path).copy(copyTo);
+    } else if (file is Link) {
+      await Link(copyTo).create(await file.target(), recursive: true);
+    }
+  }
+}
+
+/// Copies all of the files in the [from] directory to [to].
+///
+/// This is similar to `cp -R <from> <to>`:
+/// * Symlinks are supported.
+/// * Existing files are over-written, if any.
+/// * If [to] is within [from], throws [ArgumentError] (an infinite operation).
+/// * If [from] and [to] are canonically the same, no operation occurs.
+///
+/// This action is performed synchronously (blocking I/O).
+void copyPathSync(String from, String to) {
+  if (_doNothing(from, to)) {
+    return;
+  }
+  Directory(to).createSync(recursive: true);
+  for (final file in Directory(from).listSync(recursive: true)) {
+    final copyTo = p.join(to, p.relative(file.path, from: from));
+    if (file is Directory) {
+      Directory(copyTo).createSync(recursive: true);
+    } else if (file is File) {
+      File(file.path).copySync(copyTo);
+    } else if (file is Link) {
+      Link(copyTo).createSync(file.targetSync(), recursive: true);
+    }
+  }
+}
diff --git a/pkgs/io/lib/src/exit_code.dart b/pkgs/io/lib/src/exit_code.dart
new file mode 100644
index 0000000..d405558
--- /dev/null
+++ b/pkgs/io/lib/src/exit_code.dart
@@ -0,0 +1,82 @@
+// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+/// Exit code constants.
+///
+/// [Source](https://www.freebsd.org/cgi/man.cgi?query=sysexits).
+class ExitCode {
+  /// Command completed successfully.
+  static const success = ExitCode._(0, 'success');
+
+  /// Command was used incorrectly.
+  ///
+  /// This may occur if the wrong number of arguments was used, a bad flag, or
+  /// bad syntax in a parameter.
+  static const usage = ExitCode._(64, 'usage');
+
+  /// Input data was used incorrectly.
+  ///
+  /// This should occur only for user data (not system files).
+  static const data = ExitCode._(65, 'data');
+
+  /// An input file (not a system file) did not exist or was not readable.
+  static const noInput = ExitCode._(66, 'noInput');
+
+  /// User specified did not exist.
+  static const noUser = ExitCode._(67, 'noUser');
+
+  /// Host specified did not exist.
+  static const noHost = ExitCode._(68, 'noHost');
+
+  /// A service is unavailable.
+  ///
+  /// This may occur if a support program or file does not exist. This may also
+  /// be used as a catch-all error when something you wanted to do does not
+  /// work, but you do not know why.
+  static const unavailable = ExitCode._(69, 'unavailable');
+
+  /// An internal software error has been detected.
+  ///
+  /// This should be limited to non-operating system related errors as possible.
+  static const software = ExitCode._(70, 'software');
+
+  /// An operating system error has been detected.
+  ///
+  /// This intended to be used for such thing as `cannot fork` or `cannot pipe`.
+  static const osError = ExitCode._(71, 'osError');
+
+  /// Some system file (e.g. `/etc/passwd`) does not exist or could not be read.
+  static const osFile = ExitCode._(72, 'osFile');
+
+  /// A (user specified) output file cannot be created.
+  static const cantCreate = ExitCode._(73, 'cantCreate');
+
+  /// An error occurred doing I/O on some file.
+  static const ioError = ExitCode._(74, 'ioError');
+
+  /// Temporary failure, indicating something is not really an error.
+  ///
+  /// In some cases, this can be re-attempted and will succeed later.
+  static const tempFail = ExitCode._(75, 'tempFail');
+
+  /// You did not have sufficient permissions to perform the operation.
+  ///
+  /// This is not intended for file system problems, which should use [noInput]
+  /// or [cantCreate], but rather for higher-level permissions.
+  static const noPerm = ExitCode._(77, 'noPerm');
+
+  /// Something was found in an unconfigured or misconfigured state.
+  static const config = ExitCode._(78, 'config');
+
+  /// Exit code value.
+  final int code;
+
+  /// Name of the exit code.
+  final String _name;
+
+  const ExitCode._(this.code, this._name);
+
+  @override
+  String toString() => '$_name: $code';
+}
diff --git a/pkgs/io/lib/src/permissions.dart b/pkgs/io/lib/src/permissions.dart
new file mode 100644
index 0000000..c516943
--- /dev/null
+++ b/pkgs/io/lib/src/permissions.dart
@@ -0,0 +1,69 @@
+// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+import 'dart:io';
+
+/// What type of permission is granted to a file based on file permission roles.
+enum _FilePermission {
+  execute,
+  // Although these two values are unused, their positions in the enum are
+  // meaningful.
+  write, // ignore: unused_field
+  read, // ignore: unused_field
+  setGid,
+  setUid,
+  sticky,
+}
+
+/// What type of role is assigned to a file.
+enum _FilePermissionRole {
+  world,
+  group,
+  user,
+}
+
+/// Returns whether file [stat] has [permission] for a [role] type.
+bool _hasPermission(
+  FileStat stat,
+  _FilePermission permission, {
+  _FilePermissionRole role = _FilePermissionRole.world,
+}) {
+  final index = _permissionBitIndex(permission, role);
+  return (stat.mode & (1 << index)) != 0;
+}
+
+int _permissionBitIndex(_FilePermission permission, _FilePermissionRole role) =>
+    switch (permission) {
+      _FilePermission.setUid => 11,
+      _FilePermission.setGid => 10,
+      _FilePermission.sticky => 9,
+      _ => (role.index * 3) + permission.index
+    };
+
+/// Returns whether [path] is considered an executable file on this OS.
+///
+/// May optionally define how to implement [getStat] or whether to execute based
+/// on whether this is the windows platform ([isWindows]) - if not set it is
+/// automatically extracted from `dart:io#Platform`.
+///
+/// **NOTE**: On windows this always returns `true`.
+FutureOr<bool> isExecutable(
+  String path, {
+  bool? isWindows,
+  FutureOr<FileStat> Function(String path) getStat = FileStat.stat,
+}) {
+  // Windows has no concept of executable.
+  if (isWindows ?? Platform.isWindows) return true;
+  final stat = getStat(path);
+  if (stat is FileStat) {
+    return _isExecutable(stat);
+  }
+  return stat.then(_isExecutable);
+}
+
+bool _isExecutable(FileStat stat) =>
+    stat.type == FileSystemEntityType.file &&
+    _FilePermissionRole.values.any(
+        (role) => _hasPermission(stat, _FilePermission.execute, role: role));
diff --git a/pkgs/io/lib/src/process_manager.dart b/pkgs/io/lib/src/process_manager.dart
new file mode 100644
index 0000000..84d22ec
--- /dev/null
+++ b/pkgs/io/lib/src/process_manager.dart
@@ -0,0 +1,255 @@
+// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+// ignore_for_file: close_sinks,cancel_subscriptions
+
+import 'dart:async';
+import 'dart:io' as io;
+
+import 'package:meta/meta.dart';
+
+import 'shared_stdin.dart';
+
+/// Type definition for both [io.Process.start] and [ProcessManager.spawn].
+///
+/// Useful for taking different implementations of this base functionality.
+typedef StartProcess = Future<io.Process> Function(
+  String executable,
+  List<String> arguments, {
+  String workingDirectory,
+  Map<String, String> environment,
+  bool includeParentEnvironment,
+  bool runInShell,
+  io.ProcessStartMode mode,
+});
+
+/// A high-level abstraction around using and managing processes on the system.
+abstract class ProcessManager {
+  /// Terminates the global `stdin` listener, making future listens impossible.
+  ///
+  /// This method should be invoked only at the _end_ of a program's execution.
+  static Future<void> terminateStdIn() async {
+    await sharedStdIn.terminate();
+  }
+
+  /// Create a new instance of [ProcessManager] for the current platform.
+  ///
+  /// May manually specify whether the current platform [isWindows], otherwise
+  /// this is derived from the Dart runtime (i.e. [io.Platform.isWindows]).
+  factory ProcessManager({
+    Stream<List<int>>? stdin,
+    io.IOSink? stdout,
+    io.IOSink? stderr,
+    bool? isWindows,
+  }) {
+    stdin ??= sharedStdIn;
+    stdout ??= io.stdout;
+    stderr ??= io.stderr;
+    isWindows ??= io.Platform.isWindows;
+    if (isWindows) {
+      return _WindowsProcessManager(stdin, stdout, stderr);
+    }
+    return _UnixProcessManager(stdin, stdout, stderr);
+  }
+
+  final Stream<List<int>> _stdin;
+  final io.IOSink _stdout;
+  final io.IOSink _stderr;
+
+  const ProcessManager._(this._stdin, this._stdout, this._stderr);
+
+  /// Spawns a process by invoking [executable] with [arguments].
+  ///
+  /// This is _similar_ to [io.Process.start], but all standard input and output
+  /// is forwarded/routed between the process and the host, similar to how a
+  /// shell script works.
+  ///
+  /// Returns a future that completes with a handle to the spawned process.
+  Future<io.Process> spawn(
+    String executable,
+    Iterable<String> arguments, {
+    String? workingDirectory,
+    Map<String, String>? environment,
+    bool includeParentEnvironment = true,
+    bool runInShell = false,
+    io.ProcessStartMode mode = io.ProcessStartMode.normal,
+  }) async {
+    final process = io.Process.start(
+      executable,
+      arguments.toList(),
+      workingDirectory: workingDirectory,
+      environment: environment,
+      includeParentEnvironment: includeParentEnvironment,
+      runInShell: runInShell,
+      mode: mode,
+    );
+    return _ForwardingSpawn(await process, _stdin, _stdout, _stderr);
+  }
+
+  /// Spawns a process by invoking [executable] with [arguments].
+  ///
+  /// This is _similar_ to [io.Process.start], but `stdout` and `stderr` is
+  /// forwarded/routed between the process and host, similar to how a shell
+  /// script works.
+  ///
+  /// Returns a future that completes with a handle to the spawned process.
+  Future<io.Process> spawnBackground(
+    String executable,
+    Iterable<String> arguments, {
+    String? workingDirectory,
+    Map<String, String>? environment,
+    bool includeParentEnvironment = true,
+    bool runInShell = false,
+    io.ProcessStartMode mode = io.ProcessStartMode.normal,
+  }) async {
+    final process = io.Process.start(
+      executable,
+      arguments.toList(),
+      workingDirectory: workingDirectory,
+      environment: environment,
+      includeParentEnvironment: includeParentEnvironment,
+      runInShell: runInShell,
+      mode: mode,
+    );
+    return _ForwardingSpawn(
+      await process,
+      const Stream.empty(),
+      _stdout,
+      _stderr,
+    );
+  }
+
+  /// Spawns a process by invoking [executable] with [arguments].
+  ///
+  /// This is _identical to [io.Process.start] (no forwarding of I/O).
+  ///
+  /// Returns a future that completes with a handle to the spawned process.
+  Future<io.Process> spawnDetached(
+    String executable,
+    Iterable<String> arguments, {
+    String? workingDirectory,
+    Map<String, String>? environment,
+    bool includeParentEnvironment = true,
+    bool runInShell = false,
+    io.ProcessStartMode mode = io.ProcessStartMode.normal,
+  }) async =>
+      io.Process.start(
+        executable,
+        arguments.toList(),
+        workingDirectory: workingDirectory,
+        environment: environment,
+        includeParentEnvironment: includeParentEnvironment,
+        runInShell: runInShell,
+        mode: mode,
+      );
+}
+
+/// A process instance created and managed through [ProcessManager].
+///
+/// Unlike one created directly by [io.Process.start] or [io.Process.run], a
+/// spawned process works more like executing a command in a shell script.
+class Spawn implements io.Process {
+  final io.Process _delegate;
+
+  Spawn._(this._delegate) {
+    _delegate.exitCode.then((_) => _onClosed());
+  }
+
+  @mustCallSuper
+  void _onClosed() {}
+
+  @override
+  bool kill([io.ProcessSignal signal = io.ProcessSignal.sigterm]) =>
+      _delegate.kill(signal);
+
+  @override
+  Future<int> get exitCode => _delegate.exitCode;
+
+  @override
+  int get pid => _delegate.pid;
+
+  @override
+  Stream<List<int>> get stderr => _delegate.stderr;
+
+  @override
+  io.IOSink get stdin => _delegate.stdin;
+
+  @override
+  Stream<List<int>> get stdout => _delegate.stdout;
+}
+
+/// Forwards `stdin`/`stdout`/`stderr` to/from the host.
+class _ForwardingSpawn extends Spawn {
+  final StreamSubscription<List<int>> _stdInSub;
+  final StreamSubscription<List<int>> _stdOutSub;
+  final StreamSubscription<List<int>> _stdErrSub;
+  final StreamController<List<int>> _stdOut;
+  final StreamController<List<int>> _stdErr;
+
+  factory _ForwardingSpawn(
+    io.Process delegate,
+    Stream<List<int>> stdin,
+    io.IOSink stdout,
+    io.IOSink stderr,
+  ) {
+    final stdoutSelf = StreamController<List<int>>();
+    final stderrSelf = StreamController<List<int>>();
+    final stdInSub = stdin.listen(delegate.stdin.add);
+    final stdOutSub = delegate.stdout.listen((event) {
+      stdout.add(event);
+      stdoutSelf.add(event);
+    });
+    final stdErrSub = delegate.stderr.listen((event) {
+      stderr.add(event);
+      stderrSelf.add(event);
+    });
+    return _ForwardingSpawn._delegate(
+      delegate,
+      stdInSub,
+      stdOutSub,
+      stdErrSub,
+      stdoutSelf,
+      stderrSelf,
+    );
+  }
+
+  _ForwardingSpawn._delegate(
+    super.delegate,
+    this._stdInSub,
+    this._stdOutSub,
+    this._stdErrSub,
+    this._stdOut,
+    this._stdErr,
+  ) : super._();
+
+  @override
+  void _onClosed() {
+    _stdInSub.cancel();
+    _stdOutSub.cancel();
+    _stdErrSub.cancel();
+    super._onClosed();
+  }
+
+  @override
+  Stream<List<int>> get stdout => _stdOut.stream;
+
+  @override
+  Stream<List<int>> get stderr => _stdErr.stream;
+}
+
+class _UnixProcessManager extends ProcessManager {
+  const _UnixProcessManager(
+    super.stdin,
+    super.stdout,
+    super.stderr,
+  ) : super._();
+}
+
+class _WindowsProcessManager extends ProcessManager {
+  const _WindowsProcessManager(
+    super.stdin,
+    super.stdout,
+    super.stderr,
+  ) : super._();
+}
diff --git a/pkgs/io/lib/src/shared_stdin.dart b/pkgs/io/lib/src/shared_stdin.dart
new file mode 100644
index 0000000..72bb50c
--- /dev/null
+++ b/pkgs/io/lib/src/shared_stdin.dart
@@ -0,0 +1,99 @@
+// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+import 'dart:convert';
+import 'dart:io';
+
+import 'package:meta/meta.dart';
+
+/// A shared singleton instance of `dart:io`'s [stdin] stream.
+///
+/// _Unlike_ the normal [stdin] stream, [sharedStdIn] may switch subscribers
+/// as long as the previous subscriber cancels before the new subscriber starts
+/// listening.
+///
+/// [SharedStdIn.terminate] *must* be invoked in order to close the underlying
+/// connection to [stdin], allowing your program to close automatically without
+/// hanging.
+final SharedStdIn sharedStdIn = SharedStdIn(stdin);
+
+/// A singleton wrapper around `stdin` that allows new subscribers.
+///
+/// This class is visible in order to be used as a test harness for mock
+/// implementations of `stdin`. In normal programs, [sharedStdIn] should be
+/// used directly.
+@visibleForTesting
+class SharedStdIn extends Stream<List<int>> {
+  StreamController<List<int>>? _current;
+  StreamSubscription<List<int>>? _sub;
+
+  SharedStdIn([Stream<List<int>>? stream]) {
+    _sub = (stream ??= stdin).listen(_onInput);
+  }
+
+  /// Returns a future that completes with the next line.
+  ///
+  /// This is similar to the standard [Stdin.readLineSync], but asynchronous.
+  Future<String> nextLine({Encoding encoding = systemEncoding}) =>
+      lines(encoding: encoding).first;
+
+  /// Returns the stream transformed as UTF8 strings separated by line breaks.
+  ///
+  /// This is similar to synchronous code using [Stdin.readLineSync]:
+  /// ```dart
+  /// while (true) {
+  ///   var line = stdin.readLineSync();
+  ///   // ...
+  /// }
+  /// ```
+  ///
+  /// ... but asynchronous.
+  Stream<String> lines({Encoding encoding = systemEncoding}) =>
+      transform(utf8.decoder).transform(const LineSplitter());
+
+  void _onInput(List<int> event) => _getCurrent().add(event);
+
+  StreamController<List<int>> _getCurrent() =>
+      _current ??= StreamController<List<int>>(
+          onCancel: () {
+            _current = null;
+          },
+          sync: true);
+
+  @override
+  StreamSubscription<List<int>> listen(
+    void Function(List<int> event)? onData, {
+    Function? onError,
+    void Function()? onDone,
+    bool? cancelOnError,
+  }) {
+    if (_sub == null) {
+      throw StateError('Stdin has already been terminated.');
+    }
+    // ignore: close_sinks
+    final controller = _getCurrent();
+    if (controller.hasListener) {
+      throw StateError(''
+          'Subscriber already listening. The existing subscriber must cancel '
+          'before another may be added.');
+    }
+    return controller.stream.listen(
+      onData,
+      onDone: onDone,
+      onError: onError,
+      cancelOnError: cancelOnError,
+    );
+  }
+
+  /// Terminates the connection to `stdin`, closing all subscription.
+  Future<void> terminate() async {
+    if (_sub == null) {
+      throw StateError('Stdin has already been terminated.');
+    }
+    await _sub?.cancel();
+    await _current?.close();
+    _sub = null;
+  }
+}
diff --git a/pkgs/io/lib/src/shell_words.dart b/pkgs/io/lib/src/shell_words.dart
new file mode 100644
index 0000000..5fca6d9
--- /dev/null
+++ b/pkgs/io/lib/src/shell_words.dart
@@ -0,0 +1,142 @@
+// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+// ignore_for_file: comment_references
+
+import 'package:string_scanner/string_scanner.dart';
+
+import 'charcodes.dart';
+
+/// Splits [command] into tokens according to [the POSIX shell
+/// specification][spec].
+///
+/// [spec]: http://pubs.opengroup.org/onlinepubs/9699919799/utilities/contents.html
+///
+/// This returns the unquoted values of quoted tokens. For example,
+/// `shellSplit('foo "bar baz"')` returns `["foo", "bar baz"]`. It does not
+/// currently support here-documents. It does *not* treat dynamic features such
+/// as parameter expansion specially. For example, `shellSplit("foo $(bar
+/// baz)")` returns `["foo", "$(bar", "baz)"]`.
+///
+/// This will discard any comments at the end of [command].
+///
+/// Throws a [FormatException] if [command] isn't a valid shell command.
+List<String> shellSplit(String command) {
+  final scanner = StringScanner(command);
+  final results = <String>[];
+  final token = StringBuffer();
+
+  // Whether a token is being parsed, as opposed to a separator character. This
+  // is different than just [token.isEmpty], because empty quoted tokens can
+  // exist.
+  var hasToken = false;
+
+  while (!scanner.isDone) {
+    final next = scanner.readChar();
+    switch (next) {
+      case $backslash:
+        // Section 2.2.1: A <backslash> that is not quoted shall preserve the
+        // literal value of the following character, with the exception of a
+        // <newline>. If a <newline> follows the <backslash>, the shell shall
+        // interpret this as line continuation. The <backslash> and <newline>
+        // shall be removed before splitting the input into tokens. Since the
+        // escaped <newline> is removed entirely from the input and is not
+        // replaced by any white space, it cannot serve as a token separator.
+        if (scanner.scanChar($lf)) break;
+
+        hasToken = true;
+        token.writeCharCode(scanner.readChar());
+
+      case $singleQuote:
+        hasToken = true;
+        // Section 2.2.2: Enclosing characters in single-quotes ( '' ) shall
+        // preserve the literal value of each character within the
+        // single-quotes. A single-quote cannot occur within single-quotes.
+        final firstQuote = scanner.position - 1;
+        while (!scanner.scanChar($singleQuote)) {
+          _checkUnmatchedQuote(scanner, firstQuote);
+          token.writeCharCode(scanner.readChar());
+        }
+
+      case $doubleQuote:
+        hasToken = true;
+        // Section 2.2.3: Enclosing characters in double-quotes ( "" ) shall
+        // preserve the literal value of all characters within the
+        // double-quotes, with the exception of the characters backquote,
+        // <dollar-sign>, and <backslash>.
+        //
+        // (Note that this code doesn't preserve special behavior of backquote
+        // or dollar sign within double quotes, since those are dynamic
+        // features.)
+        final firstQuote = scanner.position - 1;
+        while (!scanner.scanChar($doubleQuote)) {
+          _checkUnmatchedQuote(scanner, firstQuote);
+
+          if (scanner.scanChar($backslash)) {
+            _checkUnmatchedQuote(scanner, firstQuote);
+
+            // The <backslash> shall retain its special meaning as an escape
+            // character (see Escape Character (Backslash)) only when followed
+            // by one of the following characters when considered special:
+            //
+            //     $ ` " \ <newline>
+            final next = scanner.readChar();
+            if (next == $lf) continue;
+            if (next == $dollar ||
+                next == $backquote ||
+                next == $doubleQuote ||
+                next == $backslash) {
+              token.writeCharCode(next);
+            } else {
+              token
+                ..writeCharCode($backslash)
+                ..writeCharCode(next);
+            }
+          } else {
+            token.writeCharCode(scanner.readChar());
+          }
+        }
+
+      case $hash:
+        // Section 2.3: If the current character is a '#' [and the previous
+        // characters was not part of a word], it and all subsequent characters
+        // up to, but excluding, the next <newline> shall be discarded as a
+        // comment. The <newline> that ends the line is not considered part of
+        // the comment.
+        if (hasToken) {
+          token.writeCharCode($hash);
+          break;
+        }
+
+        while (!scanner.isDone && scanner.peekChar() != $lf) {
+          scanner.readChar();
+        }
+
+      case $space:
+      case $tab:
+      case $lf:
+        // ignore: invariant_booleans
+        if (hasToken) results.add(token.toString());
+        hasToken = false;
+        token.clear();
+
+      default:
+        hasToken = true;
+        token.writeCharCode(next);
+    }
+  }
+
+  if (hasToken) results.add(token.toString());
+  return results;
+}
+
+/// Throws a [FormatException] if [scanner] is done indicating that a closing
+/// quote matching the one at position [openingQuote] is missing.
+void _checkUnmatchedQuote(StringScanner scanner, int openingQuote) {
+  if (!scanner.isDone) return;
+  final type = scanner.substring(openingQuote, openingQuote + 1) == '"'
+      ? 'double'
+      : 'single';
+  scanner.error('Unmatched $type quote.', position: openingQuote, length: 1);
+}
diff --git a/pkgs/io/pubspec.yaml b/pkgs/io/pubspec.yaml
new file mode 100644
index 0000000..7e00d99
--- /dev/null
+++ b/pkgs/io/pubspec.yaml
@@ -0,0 +1,19 @@
+name: io
+description: >-
+  Utilities for the Dart VM Runtime including support for ANSI colors, file
+  copying, and standard exit code values.
+version: 1.0.5
+repository: https://github.com/dart-lang/tools/tree/main/pkgs/io
+
+environment:
+  sdk: ^3.4.0
+
+dependencies:
+  meta: ^1.3.0
+  path: ^1.8.0
+  string_scanner: ^1.1.0
+
+dev_dependencies:
+  dart_flutter_team_lints: ^3.0.0
+  test: ^1.16.6
+  test_descriptor: ^2.0.0
diff --git a/pkgs/io/test/_files/is_executable.sh b/pkgs/io/test/_files/is_executable.sh
new file mode 100755
index 0000000..f1f641a
--- /dev/null
+++ b/pkgs/io/test/_files/is_executable.sh
@@ -0,0 +1 @@
+#!/usr/bin/env bash
diff --git a/pkgs/io/test/_files/is_not_executable.sh b/pkgs/io/test/_files/is_not_executable.sh
new file mode 100644
index 0000000..f1f641a
--- /dev/null
+++ b/pkgs/io/test/_files/is_not_executable.sh
@@ -0,0 +1 @@
+#!/usr/bin/env bash
diff --git a/pkgs/io/test/_files/stderr_hello.dart b/pkgs/io/test/_files/stderr_hello.dart
new file mode 100644
index 0000000..ac7a7d3
--- /dev/null
+++ b/pkgs/io/test/_files/stderr_hello.dart
@@ -0,0 +1,7 @@
+// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:io';
+
+void main() => stderr.write('Hello');
diff --git a/pkgs/io/test/_files/stdin_echo.dart b/pkgs/io/test/_files/stdin_echo.dart
new file mode 100644
index 0000000..256e0ee
--- /dev/null
+++ b/pkgs/io/test/_files/stdin_echo.dart
@@ -0,0 +1,7 @@
+// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:io';
+
+void main() => stdout.writeln('You said: ${stdin.readLineSync()}');
diff --git a/pkgs/io/test/_files/stdout_hello.dart b/pkgs/io/test/_files/stdout_hello.dart
new file mode 100644
index 0000000..af3bf51
--- /dev/null
+++ b/pkgs/io/test/_files/stdout_hello.dart
@@ -0,0 +1,7 @@
+// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:io';
+
+void main() => stdout.write('Hello');
diff --git a/pkgs/io/test/ansi_code_test.dart b/pkgs/io/test/ansi_code_test.dart
new file mode 100644
index 0000000..98ae68b
--- /dev/null
+++ b/pkgs/io/test/ansi_code_test.dart
@@ -0,0 +1,187 @@
+// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+@TestOn('vm')
+library;
+
+import 'dart:io';
+
+import 'package:io/ansi.dart';
+import 'package:test/test.dart';
+
+const _ansiEscapeLiteral = '\x1B';
+const _ansiEscapeForScript = r'\033';
+const sampleInput = 'sample input';
+
+void main() {
+  group('ansiOutputEnabled', () {
+    test('default value matches dart:io', () {
+      expect(ansiOutputEnabled,
+          stdout.supportsAnsiEscapes && stderr.supportsAnsiEscapes);
+    });
+
+    test('override true', () {
+      overrideAnsiOutput(true, () {
+        expect(ansiOutputEnabled, isTrue);
+      });
+    });
+
+    test('override false', () {
+      overrideAnsiOutput(false, () {
+        expect(ansiOutputEnabled, isFalse);
+      });
+    });
+
+    test('forScript variaents ignore `ansiOutputEnabled`', () {
+      const expected =
+          '$_ansiEscapeForScript[34m$sampleInput$_ansiEscapeForScript[0m';
+
+      for (var override in [true, false]) {
+        overrideAnsiOutput(override, () {
+          expect(blue.escapeForScript, '$_ansiEscapeForScript[34m');
+          expect(blue.wrap(sampleInput, forScript: true), expected);
+          expect(wrapWith(sampleInput, [blue], forScript: true), expected);
+        });
+      }
+    });
+  });
+
+  test('foreground and background colors match', () {
+    expect(foregroundColors, hasLength(backgroundColors.length));
+
+    for (var i = 0; i < foregroundColors.length; i++) {
+      final foreground = foregroundColors[i];
+      expect(foreground.type, AnsiCodeType.foreground);
+      expect(foreground.name.toLowerCase(), foreground.name,
+          reason: 'All names should be lower case');
+      final background = backgroundColors[i];
+      expect(background.type, AnsiCodeType.background);
+      expect(background.name.toLowerCase(), background.name,
+          reason: 'All names should be lower case');
+
+      expect(foreground.name, background.name);
+
+      // The last base-10 digit also matches – good to sanity check
+      expect(foreground.code % 10, background.code % 10);
+    }
+  });
+
+  test('all styles are styles', () {
+    for (var style in styles) {
+      expect(style.type, AnsiCodeType.style);
+      expect(style.name.toLowerCase(), style.name,
+          reason: 'All names should be lower case');
+      if (style == styleBold) {
+        expect(style.reset, resetBold);
+      } else {
+        expect(style.reset!.code, equals(style.code + 20));
+      }
+      expect(style.name, equals(style.reset!.name));
+    }
+  });
+
+  for (var forScript in [true, false]) {
+    group(forScript ? 'forScript' : 'escaped', () {
+      final escapeLiteral =
+          forScript ? _ansiEscapeForScript : _ansiEscapeLiteral;
+
+      group('wrap', () {
+        _test('color', () {
+          final expected = '$escapeLiteral[34m$sampleInput$escapeLiteral[0m';
+
+          expect(blue.wrap(sampleInput, forScript: forScript), expected);
+        });
+
+        _test('style', () {
+          final expected = '$escapeLiteral[1m$sampleInput$escapeLiteral[22m';
+
+          expect(styleBold.wrap(sampleInput, forScript: forScript), expected);
+        });
+
+        _test('style', () {
+          final expected = '$escapeLiteral[34m$sampleInput$escapeLiteral[0m';
+
+          expect(blue.wrap(sampleInput, forScript: forScript), expected);
+        });
+
+        test('empty', () {
+          expect(blue.wrap('', forScript: forScript), '');
+        });
+
+        test(null, () {
+          expect(blue.wrap(null, forScript: forScript), isNull);
+        });
+      });
+
+      group('wrapWith', () {
+        _test('foreground', () {
+          final expected = '$escapeLiteral[34m$sampleInput$escapeLiteral[0m';
+
+          expect(wrapWith(sampleInput, [blue], forScript: forScript), expected);
+        });
+
+        _test('background', () {
+          final expected = '$escapeLiteral[44m$sampleInput$escapeLiteral[0m';
+
+          expect(wrapWith(sampleInput, [backgroundBlue], forScript: forScript),
+              expected);
+        });
+
+        _test('style', () {
+          final expected = '$escapeLiteral[1m$sampleInput$escapeLiteral[0m';
+
+          expect(wrapWith(sampleInput, [styleBold], forScript: forScript),
+              expected);
+        });
+
+        _test('2 styles', () {
+          final expected = '$escapeLiteral[1;3m$sampleInput$escapeLiteral[0m';
+
+          expect(
+              wrapWith(sampleInput, [styleBold, styleItalic],
+                  forScript: forScript),
+              expected);
+        });
+
+        _test('2 foregrounds', () {
+          expect(
+              () => wrapWith(sampleInput, [blue, white], forScript: forScript),
+              throwsArgumentError);
+        });
+
+        _test('multi', () {
+          final expected =
+              '$escapeLiteral[1;4;34;107m$sampleInput$escapeLiteral[0m';
+
+          expect(
+              wrapWith(sampleInput,
+                  [blue, backgroundWhite, styleBold, styleUnderlined],
+                  forScript: forScript),
+              expected);
+        });
+
+        test('no codes', () {
+          expect(wrapWith(sampleInput, []), sampleInput);
+        });
+
+        _test('empty', () {
+          expect(
+              wrapWith('', [blue, backgroundWhite, styleBold],
+                  forScript: forScript),
+              '');
+        });
+
+        _test('null', () {
+          expect(
+              wrapWith(null, [blue, backgroundWhite, styleBold],
+                  forScript: forScript),
+              isNull);
+        });
+      });
+    });
+  }
+}
+
+void _test<T>(String name, T Function() body) =>
+    test(name, () => overrideAnsiOutput<T>(true, body));
diff --git a/pkgs/io/test/copy_path_test.dart b/pkgs/io/test/copy_path_test.dart
new file mode 100644
index 0000000..fd1e9ce
--- /dev/null
+++ b/pkgs/io/test/copy_path_test.dart
@@ -0,0 +1,45 @@
+// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+@TestOn('vm')
+library;
+
+import 'package:io/io.dart';
+import 'package:path/path.dart' as p;
+import 'package:test/test.dart';
+import 'package:test_descriptor/test_descriptor.dart' as d;
+
+void main() {
+  test('should copy a directory (async)', () async {
+    await _create();
+    await copyPath(p.join(d.sandbox, 'parent'), p.join(d.sandbox, 'copy'));
+    await _validate();
+  });
+
+  test('should copy a directory (sync)', () async {
+    await _create();
+    copyPathSync(p.join(d.sandbox, 'parent'), p.join(d.sandbox, 'copy'));
+    await _validate();
+  });
+
+  test('should catch an infinite operation', () async {
+    await _create();
+    expect(
+      copyPath(
+        p.join(d.sandbox, 'parent'),
+        p.join(d.sandbox, 'parent', 'child'),
+      ),
+      throwsArgumentError,
+    );
+  });
+}
+
+d.DirectoryDescriptor _struct() => d.dir('parent', [
+      d.dir('child', [
+        d.file('foo.txt'),
+      ]),
+    ]);
+
+Future<void> _create() => _struct().create();
+Future<void> _validate() => _struct().validate();
diff --git a/pkgs/io/test/permissions_test.dart b/pkgs/io/test/permissions_test.dart
new file mode 100644
index 0000000..478e8df
--- /dev/null
+++ b/pkgs/io/test/permissions_test.dart
@@ -0,0 +1,37 @@
+// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+@TestOn('vm')
+library;
+
+import 'package:io/io.dart';
+import 'package:test/test.dart';
+
+void main() {
+  group('isExecutable', () {
+    const files = 'test/_files';
+    const shellIsExec = '$files/is_executable.sh';
+    const shellNotExec = '$files/is_not_executable.sh';
+
+    group('on shell scripts', () {
+      test('should return true for "is_executable.sh"', () async {
+        expect(await isExecutable(shellIsExec), isTrue);
+      });
+
+      test('should return false for "is_not_executable.sh"', () async {
+        expect(await isExecutable(shellNotExec), isFalse);
+      });
+    }, testOn: '!windows');
+
+    group('on shell scripts [windows]', () {
+      test('should return true for "is_executable.sh"', () async {
+        expect(await isExecutable(shellIsExec, isWindows: true), isTrue);
+      });
+
+      test('should return true for "is_not_executable.sh"', () async {
+        expect(await isExecutable(shellNotExec, isWindows: true), isTrue);
+      });
+    });
+  });
+}
diff --git a/pkgs/io/test/process_manager_test.dart b/pkgs/io/test/process_manager_test.dart
new file mode 100644
index 0000000..9871a77
--- /dev/null
+++ b/pkgs/io/test/process_manager_test.dart
@@ -0,0 +1,100 @@
+// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+// ignore_for_file: close_sinks
+
+import 'dart:async';
+import 'dart:convert';
+import 'dart:io';
+
+import 'package:io/io.dart' hide sharedStdIn;
+import 'package:path/path.dart' as p;
+import 'package:test/test.dart';
+
+void main() {
+  StreamController<String> fakeStdIn;
+  late ProcessManager processManager;
+  SharedStdIn sharedStdIn;
+  late List<String> stdoutLog;
+  late List<String> stderrLog;
+
+  test('spawn functions should match the type definition of Process.start', () {
+    const isStartProcess = TypeMatcher<StartProcess>();
+    expect(Process.start, isStartProcess);
+    final manager = ProcessManager();
+    expect(manager.spawn, isStartProcess);
+    expect(manager.spawnBackground, isStartProcess);
+    expect(manager.spawnDetached, isStartProcess);
+  });
+
+  group('spawn', () {
+    setUp(() async {
+      fakeStdIn = StreamController<String>(sync: true);
+      sharedStdIn = SharedStdIn(fakeStdIn.stream.map((s) => s.codeUnits));
+      stdoutLog = <String>[];
+      stderrLog = <String>[];
+
+      final stdoutController = StreamController<List<int>>(sync: true);
+      stdoutController.stream.map(utf8.decode).listen(stdoutLog.add);
+      final stdout = IOSink(stdoutController);
+      final stderrController = StreamController<List<int>>(sync: true);
+      stderrController.stream.map(utf8.decode).listen(stderrLog.add);
+      final stderr = IOSink(stderrController);
+
+      processManager = ProcessManager(
+        stdin: sharedStdIn,
+        stdout: stdout,
+        stderr: stderr,
+      );
+    });
+
+    final dart = Platform.executable;
+
+    test('should output Hello from another process [via stdout]', () async {
+      final spawn = await processManager.spawn(
+        dart,
+        [p.join('test', '_files', 'stdout_hello.dart')],
+      );
+      await spawn.exitCode;
+      expect(stdoutLog, ['Hello']);
+    });
+
+    test('should output Hello from another process [via stderr]', () async {
+      final spawn = await processManager.spawn(
+        dart,
+        [p.join('test', '_files', 'stderr_hello.dart')],
+      );
+      await spawn.exitCode;
+      expect(stderrLog, ['Hello']);
+    });
+
+    test('should forward stdin to another process', () async {
+      final spawn = await processManager.spawn(
+        dart,
+        [p.join('test', '_files', 'stdin_echo.dart')],
+      );
+      spawn.stdin.writeln('Ping');
+      await spawn.exitCode;
+      expect(stdoutLog.join(), contains('You said: Ping'));
+    });
+
+    group('should return a Process where', () {
+      test('.stdout is readable', () async {
+        final spawn = await processManager.spawn(
+          dart,
+          [p.join('test', '_files', 'stdout_hello.dart')],
+        );
+        expect(await spawn.stdout.transform(utf8.decoder).first, 'Hello');
+      });
+
+      test('.stderr is readable', () async {
+        final spawn = await processManager.spawn(
+          dart,
+          [p.join('test', '_files', 'stderr_hello.dart')],
+        );
+        expect(await spawn.stderr.transform(utf8.decoder).first, 'Hello');
+      });
+    });
+  });
+}
diff --git a/pkgs/io/test/shared_stdin_test.dart b/pkgs/io/test/shared_stdin_test.dart
new file mode 100644
index 0000000..71629ec
--- /dev/null
+++ b/pkgs/io/test/shared_stdin_test.dart
@@ -0,0 +1,80 @@
+// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+import 'dart:convert';
+
+import 'package:io/io.dart' hide sharedStdIn;
+import 'package:test/test.dart';
+
+void main() {
+  // ignore: close_sinks
+  late StreamController<String> fakeStdIn;
+  late SharedStdIn sharedStdIn;
+
+  setUp(() {
+    fakeStdIn = StreamController<String>(sync: true);
+    sharedStdIn = SharedStdIn(fakeStdIn.stream.map((s) => s.codeUnits));
+  });
+
+  test('should allow a single subscriber', () async {
+    final logs = <String>[];
+    final sub = sharedStdIn.transform(utf8.decoder).listen(logs.add);
+    fakeStdIn.add('Hello World');
+    await sub.cancel();
+    expect(logs, ['Hello World']);
+  });
+
+  test('should allow multiple subscribers', () async {
+    final logs = <String>[];
+    final asUtf8 = sharedStdIn.transform(utf8.decoder);
+    var sub = asUtf8.listen(logs.add);
+    fakeStdIn.add('Hello World');
+    await sub.cancel();
+    sub = asUtf8.listen(logs.add);
+    fakeStdIn.add('Goodbye World');
+    await sub.cancel();
+    expect(logs, ['Hello World', 'Goodbye World']);
+  });
+
+  test('should throw if a subscriber is still active', () async {
+    final active = sharedStdIn.listen((_) {});
+    expect(() => sharedStdIn.listen((_) {}), throwsStateError);
+    await active.cancel();
+    expect(() => sharedStdIn.listen((_) {}), returnsNormally);
+  });
+
+  test('should return a stream of lines', () async {
+    expect(
+      sharedStdIn.lines(),
+      emitsInOrder(<dynamic>[
+        'I',
+        'Think',
+        'Therefore',
+        'I',
+        'Am',
+      ]),
+    );
+    [
+      'I\nThink\n',
+      'Therefore\n',
+      'I\n',
+      'Am\n',
+    ].forEach(fakeStdIn.add);
+  });
+
+  test('should return the next line', () {
+    expect(sharedStdIn.nextLine(), completion('Hello World'));
+    fakeStdIn.add('Hello World\n');
+  });
+
+  test('should allow listening for new lines multiple times', () async {
+    expect(sharedStdIn.nextLine(), completion('Hello World'));
+    fakeStdIn.add('Hello World\n');
+    await Future<void>.value();
+
+    expect(sharedStdIn.nextLine(), completion('Hello World'));
+    fakeStdIn.add('Hello World\n');
+  });
+}
diff --git a/pkgs/io/test/shell_words_test.dart b/pkgs/io/test/shell_words_test.dart
new file mode 100644
index 0000000..dc4441c
--- /dev/null
+++ b/pkgs/io/test/shell_words_test.dart
@@ -0,0 +1,185 @@
+// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:io/io.dart';
+import 'package:test/test.dart';
+
+void main() {
+  group('shellSplit()', () {
+    group('returns an empty list for', () {
+      test('an empty string', () {
+        expect(shellSplit(''), isEmpty);
+      });
+
+      test('spaces', () {
+        expect(shellSplit('    '), isEmpty);
+      });
+
+      test('tabs', () {
+        expect(shellSplit('\t\t\t'), isEmpty);
+      });
+
+      test('newlines', () {
+        expect(shellSplit('\n\n\n'), isEmpty);
+      });
+
+      test('a comment', () {
+        expect(shellSplit('#foo bar baz'), isEmpty);
+      });
+
+      test('a mix', () {
+        expect(shellSplit(' \t\n# foo'), isEmpty);
+      });
+    });
+
+    group('parses unquoted', () {
+      test('a single token', () {
+        expect(shellSplit('foo'), equals(['foo']));
+      });
+
+      test('multiple tokens', () {
+        expect(shellSplit('foo bar baz'), equals(['foo', 'bar', 'baz']));
+      });
+
+      test('tokens separated by tabs', () {
+        expect(shellSplit('foo\tbar\tbaz'), equals(['foo', 'bar', 'baz']));
+      });
+
+      test('tokens separated by newlines', () {
+        expect(shellSplit('foo\nbar\nbaz'), equals(['foo', 'bar', 'baz']));
+      });
+
+      test('a token after whitespace', () {
+        expect(shellSplit(' \t\nfoo'), equals(['foo']));
+      });
+
+      test('a token before whitespace', () {
+        expect(shellSplit('foo \t\n'), equals(['foo']));
+      });
+
+      test('a token with a hash', () {
+        expect(shellSplit('foo#bar'), equals(['foo#bar']));
+      });
+
+      test('a token before a comment', () {
+        expect(shellSplit('foo #bar'), equals(['foo']));
+      });
+
+      test('dynamic shell features', () {
+        expect(
+            shellSplit(r'foo $(bar baz)'), equals(['foo', r'$(bar', 'baz)']));
+        expect(shellSplit('foo `bar baz`'), equals(['foo', '`bar', 'baz`']));
+        expect(shellSplit(r'foo $bar | baz'),
+            equals(['foo', r'$bar', '|', 'baz']));
+      });
+    });
+
+    group('parses a backslash', () {
+      test('before a normal character', () {
+        expect(shellSplit(r'foo\bar'), equals(['foobar']));
+      });
+
+      test('before a dynamic shell feature', () {
+        expect(shellSplit(r'foo\$bar'), equals([r'foo$bar']));
+      });
+
+      test('before a single quote', () {
+        expect(shellSplit(r"foo\'bar"), equals(["foo'bar"]));
+      });
+
+      test('before a double quote', () {
+        expect(shellSplit(r'foo\"bar'), equals(['foo"bar']));
+      });
+
+      test('before a space', () {
+        expect(shellSplit(r'foo\ bar'), equals(['foo bar']));
+      });
+
+      test('at the beginning of a token', () {
+        expect(shellSplit(r'\ foo'), equals([' foo']));
+      });
+
+      test('before whitespace followed by a hash', () {
+        expect(shellSplit(r'\ #foo'), equals([' #foo']));
+      });
+
+      test('before a newline in a token', () {
+        expect(shellSplit('foo\\\nbar'), equals(['foobar']));
+      });
+
+      test('before a newline outside a token', () {
+        expect(shellSplit('foo \\\n bar'), equals(['foo', 'bar']));
+      });
+
+      test('before a backslash', () {
+        expect(shellSplit(r'foo\\bar'), equals([r'foo\bar']));
+      });
+    });
+
+    group('parses single quotes', () {
+      test('that are empty', () {
+        expect(shellSplit("''"), equals(['']));
+      });
+
+      test('that contain normal characters', () {
+        expect(shellSplit("'foo'"), equals(['foo']));
+      });
+
+      test('that contain active characters', () {
+        expect(shellSplit("'\" \\#'"), equals([r'" \#']));
+      });
+
+      test('before a hash', () {
+        expect(shellSplit("''#foo"), equals([r'#foo']));
+      });
+
+      test('inside a token', () {
+        expect(shellSplit("foo'bar baz'qux"), equals([r'foobar bazqux']));
+      });
+
+      test('without a closing quote', () {
+        expect(() => shellSplit("'foo bar"), throwsFormatException);
+      });
+    });
+
+    group('parses double quotes', () {
+      test('that are empty', () {
+        expect(shellSplit('""'), equals(['']));
+      });
+
+      test('that contain normal characters', () {
+        expect(shellSplit('"foo"'), equals(['foo']));
+      });
+
+      test('that contain otherwise-active characters', () {
+        expect(shellSplit('"\' #"'), equals(["' #"]));
+      });
+
+      test('that contain escaped characters', () {
+        expect(shellSplit(r'"\$\`\"\\"'), equals([r'$`"\']));
+      });
+
+      test('that contain an escaped newline', () {
+        expect(shellSplit('"\\\n"'), equals(['']));
+      });
+
+      test("that contain a backslash that's not an escape", () {
+        expect(shellSplit(r'"f\oo"'), equals([r'f\oo']));
+      });
+
+      test('before a hash', () {
+        expect(shellSplit('""#foo'), equals([r'#foo']));
+      });
+
+      test('inside a token', () {
+        expect(shellSplit('foo"bar baz"qux'), equals([r'foobar bazqux']));
+      });
+
+      test('without a closing quote', () {
+        expect(() => shellSplit('"foo bar'), throwsFormatException);
+        expect(() => shellSplit(r'"foo bar\'), throwsFormatException);
+      });
+    });
+  });
+}