The Anatomy of a sidekick CLI

passsy
4 min readJan 31, 2023

--

Sidekick CLIs allow you to automate repetitive tasks in your Flutter/Dart projects. It is handy for mono-repositories where developers work across multiple packages. For example, calling pub get for all packages or releasing your app to Firebase in a language you’re familiar with: Dart!

Creating a sidekick CLI

The initial setup is done with the global sidekick CLI. Install it on your system with pub global activate.

dart pub global activate sidekick

Then use the sidekick init command to generate the CLI for your Flutter app by following the instructions.

sidekick init

Only the person generating the CLI for the first time requires the global sidekick package. Once generated, use the generated CLI.

What makes a sidekick CLI

The following diagram shows the layout of a sidekick CLI called amanda. (The name is up to you).

entryPoint and sidekickPackage are fixed points

Every sidekick CLI has two key components:

entryPoint is a shell script that is used to execute the CLI. It also marks the root of the project projectRoot. It’s usually the root of your repository.

sidekickPackage contains the complete source code of the Dart CLI, including shell tooling to download dependencies automatically and the embedded Dart runtime.

It doesn’t matter where the sidekickPackage is located

The sidekickPackage can be located anywhere inside projectRoot.

Access the fixed points

The projectRoot and sidekickPackage location together with the cliName is everything required to generate the CLI. Those locations are fixed points and can be accessed in your CLI via SidekickContext. All three are guaranteed to always exist.

// The directory where the entryPoint is located
SidekickContext.projectRoot;

// The location of the entryPoint, the shell script
// that is used to execute the cli.
SidekickContext.entryPoint;

// Source code and tooling of the sidekick CLI
SidekickContext.sidekickPackage;

// The name of the CLI
SidekickContext.cliName;

Inside the sidekick CLI package

The sidekickPackage (amanda_sidekick) is a normal Dart package with a pubspec.yaml for dependencies and the full source code in lib/. The CLI can be executed and debugged like any other CLI with dart bin/main.dart.

sidekickPackage anatomy

Sidekick CLIs are self-executable

What makes a sidekick CLI special is the bash tooling in /tool which allows the CLI to be self-executable when executed with the entryPoint.

When called via entryPoint the bash scripts do the following:

  • Download a Dart runtime with the version defined in pubspec.yaml (environment.sdk (lowerBound)) to build/cache/dart-sdk. This is the embedded Dart runtime.
  • Checks for changes in the package to eventually recompile the .exe
  • Compiles the CLI to a native executable (.exe) for the given system (saved in /build/cli.exe)

The embedded Dart runtime

Sidekick downloads its own Dart runtime where which is used to compile the sidekick CLI. The SDKs of your various sidekick CLIs are cached in ~/.dart/sdk/cache/<dart-version>.

Which Dart runtime is used is read from the lower-bound environment.sdk in pubspec.yaml.

environment:
sdk: ">=2.19.0 <3.0.0"

This configuration will download the Dart SDK 2.19.

For now, the entire Dart SDK is downloaded. But that might change in the future to reduce download sizes. Do not trust

The embedded Dart SDK is never accessible to your CLI. It is not used for the <sidekick-cli> dart command or any other command.

The dart command reads the Dart SDK from flutterSdkPath or dartSdkPath which might, or might not link to your local Dart SDK (systemDartSdkPath())

Add new commands

New Commands can be added with plugins or manually. Check out our favorite sidekick plugins to see what’s possible.

For plugins use amanda sidekick plugins install <plugin>

To create your own Command create a new file echo_text_command.dart in lib/src/commands/.

class EchoTextCommand extends Command {
@override
String get name => 'echo-text';

@override
String get description => 'Echos the text';

EchoTextCommand() {
argParser.addOption('text');
}

@override
Future<void> run() async {
final cliName = argResults!['text'] as String?;
print('echo $cliName');
}
}

Then register the command at the runner in the registry lib/amanda_sidekick.dart.

  final runner = initializeSidekick();

runner
//.. more commands
..addCommand(FlutterCommand())
..addCommand(EchoTextCommand()); // <-- Register your own command

<cli> sidekick update

To make sure that you’re always using the latest and greatest run amanda sidekick update. Sidekick will automatically check for new sidekick_core updates and will automatically migrate your code to match the new version.

This migration mechanism is especially useful for changes in the bash scripts to guarantee compatibility with all operating systems.

--

--