Basics

Metro CLI tool



Introduction

Metro is a CLI tool that works under the hood of the Nylo framework. It provides a lot of helpful tools to speed up development.


Install

Mac guide

Run the below command from the terminal

echo "alias metro='dart run nylo_framework:main'" >>~/.bash_profile && source ~/.bash_profile

If you open a project that uses Nylo, try to run the following in the terminal.

dart run nylo_framework:main
# or with the alias
metro

You should see an output similar to the below.

Usage: 
    command [options] [arguments]
    
Options
    -h
    
All commands:
 
[Widget Commands]
  make:page
  make:stateful_widget
  make:stateless_widget
  make:state_managed_widget
  make:navigation_hub
  make:form

[App Commands]
  make:model
  make:provider
  make:api_service
  make:controller
  make:event
  make:theme
  make:route_guard
  make:config
  make:interceptor
  make:command

[Custom Commands]
  motivation:quote

Windows Guide

  1. Open PowerShell as an administrator.
  2. Create a PowerShell profile if you don't have one:
if (!(Test-Path -Path $PROFILE)) {
    New-Item -ItemType File -Path $PROFILE -Force
}
  1. Open the profile in a text editor:
notepad $PROFILE
  1. Add the following line to the profile:
function metro { dart run nylo_framework:main @args }
  1. Save the file and close the editor.
  2. Reload your PowerShell profile:
. $PROFILE

Make controller


Making a new controller

You can make a new controller by running the below in the terminal.

dart run nylo_framework:main make:controller profile_controller
# or with the alias metro
metro make:controller profile_controller

This will create a new controller if it doesn't exist within the lib/app/controllers/ directory.


Forcefully make a controller

Arguments:

Using the --force or -f flag will overwrite an existing controller if it already exists.

metro make:controller profile_controller --force

Make model


Making a new model

You can make a new model by running the below in the terminal.

dart run nylo_framework:main make:model product
# or with the alias metro
metro make:model product

It will place the newly created model in lib/app/models/.


Make a model from JSON

Arguments:

Using the --json or -j flag will create a new model from a JSON payload.

dart run nylo_framework:main make:model product --json
# or with the alias metro
metro make:model product --json

Then, you can paste your JSON into the terminal and it will generate a model for you.


Forcefully make a model

Arguments:

Using the --force or -f flag will overwrite an existing model if it already exists.

dart run nylo_framework:main make:model product --force
# or with the alias metro
metro make:model product --force

Make page


Making a new page

You can make a new page by running the below in the terminal.

dart run nylo_framework:main make:page product_page
# or with the alias metro
metro make:page product_page

This will create a new page if it doesn't exist within the lib/resources/pages/ directory.


Create a page with a controller

You can make a new page with a controller by running the below in the terminal.

Arguments:

Using the --controller or -c flag will create a new page with a controller.

dart run nylo_framework:main make:page product_page --controller
# or with the alias metro
metro make:page product_page -c

Create an auth page

You can make a new auth page by running the below in the terminal.

Arguments:

Using the --auth or -a flag will create a new auth page.

dart run nylo_framework:main make:page login_page --auth
# or with the alias metro
metro make:page login_page -a

Create an initial page

You can make a new initial page by running the below in the terminal.

Arguments:

Using the --initial or -i flag will create a new initial page.

dart run nylo_framework:main make:page home_page --initial
# or with the alias metro
metro make:page home_page -i

Create a bottom navigation page

You can make a new bottom navigation page by running the below in the terminal.

Arguments:

Using the --bottom-nav or -b flag will create a new bottom navigation page.

dart run nylo_framework:main make:page dashboard --bottom-nav
# or with the alias metro
metro make:page dashboard -b

Forcefully make a page

Arguments:

Using the --force or -f flag will overwrite an existing page if it already exists.

dart run nylo_framework:main make:page product_page --force
# or with the alias metro
metro make:page product_page --force

Make stateless widget


Making a new stateless widget

You can make a new stateless widget by running the below in the terminal.

dart run nylo_framework:main make:stateless_widget product_rating_widget
# or with the alias metro
metro make:stateless_widget product_rating_widget

The above will create a new widget if it doesn't exist within the lib/resources/widgets/ directory.


Forcefully make a stateless widget

Arguments:

Using the --force or -f flag will overwrite an existing widget if it already exists.

dart run nylo_framework:main make:stateless_widget product_rating_widget --force
# or with the alias metro
metro make:stateless_widget product_rating_widget --force

Make stateful widget


Making a new stateful widget

You can make a new stateful widget by running the below in the terminal.

dart run nylo_framework:main make:stateful_widget product_rating_widget
# or with the alias metro
metro make:stateful_widget product_rating_widget

The above will create a new widget if it doesn't exist within the lib/resources/widgets/ directory.


Forcefully make a stateful widget

Arguments:

Using the --force or -f flag will overwrite an existing widget if it already exists.

dart run nylo_framework:main make:stateful_widget product_rating_widget --force
# or with the alias metro
metro make:stateful_widget product_rating_widget --force

Make API Service


Making a new API Service

You can make a new API service by running the below in the terminal.

dart run nylo_framework:main make:api_service user
# or with the alias metro
metro make:api_service user_api_service

It will place the newly created API service in lib/app/networking/.


Making a new API Service with a model

You can make a new API service with a model by running the below in the terminal.

Arguments:

Using the --model or -m option will create a new API service with a model.

dart run nylo_framework:main make:api_service user --model="User"
# or with the alias metro
metro make:api_service user --model="User"

It will place the newly created API service in lib/app/networking/.


Use Postman to Make API Services

You can make new API services using Postman v2.1 collection files.

First, you must export your postman collection into a JSON file (using v2.1).

Copy the exported file into the public/assets/postman/collections directory.

After copying the file into the above directory, go to the terminal and run the following command.

Arguments:

Using the --postman or -p option will create a new API service using a Postman collection.

dart run nylo_framework:main make:api_service my_collection_name --postman
# or with the alias metro
metro make:api_service my_collection_name --postman

This will prompt you to select a collection from the public/assets/postman/collections directory.

Postman Environments

If your postman collection has an environment, export the file and add it to the public/assets/postman/environments directory.

Then, run the below command.

dart run nylo_framework:main make:api_service collection --postman
# or with the alias metro
metro make:api_service collection --postman

It will prompt you to select an environment from the public/assets/postman/environments directory.

What does it do?

It will also add Models to your project if your Postman collections have a saved response. You can check if your collection has saved responses by going to your Postman collection, then selecting a request. You should see a dropdown which will contain any saved responses.

The saved response name will be used for the Model name that Nylo will create.


Forcefully make an API Service

Arguments:

Using the --force or -f flag will overwrite an existing API Service if it already exists.

dart run nylo_framework:main make:api_service user --force
# or with the alias metro
metro make:api_service user --force

Make event


Making a new event

You can make a new event by running the below in the terminal.

dart run nylo_framework:main make:event login_event
# or with the alias metro
metro make:event login_event

This will create a new event in lib/app/events.


Forcefully make an event

Arguments:

Using the --force or -f flag will overwrite an existing event if it already exists.

dart run nylo_framework:main make:event login_event --force
# or with the alias metro
metro make:event login_event --force

Make provider


Making a new provider

Create new providers in your application using the below command.

dart run nylo_framework:main make:provider firebase_provider
# or with the alias metro
metro make:provider firebase_provider

It will place the newly created provider in lib/app/providers/.


Forcefully make a provider

Arguments:

Using the --force or -f flag will overwrite an existing provider if it already exists.

dart run nylo_framework:main make:provider firebase_provider --force
# or with the alias metro
metro make:provider firebase_provider --force

Make theme


Making a new theme

You can make themes by running the below in the terminal.

dart run nylo_framework:main make:theme bright_theme
# or with the alias metro
metro make:theme bright_theme

This will create a new theme in lib/app/themes.


Forcefully make a theme

Arguments:

Using the --force or -f flag will overwrite an existing theme if it already exists.

dart run nylo_framework:main make:theme bright_theme --force
# or with the alias metro
metro make:theme bright_theme --force

Make Forms


Making a new form

You can make a new form by running the below in the terminal.

dart run nylo_framework:main make:form car_advert_form
# or with the alias metro
metro make:form car_advert_form

This will create a new form in lib/app/forms.


Forcefully make a form

Arguments:

Using the --force or -f flag will overwrite an existing form if it already exists.

dart run nylo_framework:main make:form login_form --force
# or with the alias metro
metro make:form login_form --force

Make Route Guard


Making a new route guard

You can make a route guard by running the below in the terminal.

dart run nylo_framework:main make:route_guard premium_content
# or with the alias metro
metro make:route_guard premium_content

This will create a new route guard in lib/app/route_guards.


Forcefully make a route guard

Arguments:

Using the --force or -f flag will overwrite an existing route guard if it already exists.

dart run nylo_framework:main make:route_guard premium_content --force
# or with the alias metro
metro make:route_guard premium_content --force

Make Config File


Making a new config file

You can make a new config file by running the below in the terminal.

dart run nylo_framework:main make:config shopping_settings
# or with the alias metro
metro make:config shopping_settings

This will create a new config file in lib/app/config.


Forcefully make a config file

Arguments:

Using the --force or -f flag will overwrite an existing config file if it already exists.

dart run nylo_framework:main make:config app_config --force
# or with the alias metro
metro make:config app_config --force

Build App Icons

You can generate all the app icons for IOS and Android by running the below command.

dart run flutter_launcher_icons:main

This uses the flutter_icons configuration in your pubspec.yaml file.


Publishing files with Slate

If you are new to Slate packages, you can read more about them here.


Publishing files with Slate

You can publish files with Slate by running the below in the terminal.

dart run nylo_framework:main slate:publish my_slate_package
# or with the alias metro
metro slate:publish my_slate_package

This will publish all the files in your project.


Forcefully publish files with Slate

Arguments:

Using the --force or -f flag will overwrite any existing files in your project.

dart run nylo_framework:main slate:publish --force
# or with the alias metro
metro slate:publish --force

Install and publish with Slate

You can install and publish files with Slate by running the below in the terminal.

dart run nylo_framework:main slate:install my_slate_package
# or with the alias metro
metro slate:install my_slate_package

This will install the Slate package and publish all the files in your project.


Custom Commands

Custom commands allow you to extend Nylo's CLI with your own project-specific commands. This feature enables you to automate repetitive tasks, implement deployment workflows, or add any custom functionality directly into your project's command-line tools.

Note: You currently cannot import nylo_framework.dart in your custom commands, please use ny_cli.dart instead.


Creating Custom Commands

To create a new custom command, you can use the make:command feature:

# Using Metro
metro make:command current_time

# Using Dart directly
dart run nylo_framework:main make:command current_time

You can specify a category for your command using the --category option:

# Specify a category
metro make:command current_time --category="project"

This will create a new command file at lib/app/commands/current_time.dart with the following structure:

import 'package:nylo_framework/metro/ny_cli.dart';

void main(arguments) => _CurrentTimeCommand(arguments).run();

/// Current Time Command
///
/// Usage:
///   [From Terminal] dart run nylo_framework:main app:current_time
///   [With Metro]    metro app:current_time
class _CurrentTimeCommand extends NyCustomCommand {
  _CurrentTimeCommand(super.arguments);

  @override
  CommandBuilder builder(CommandBuilder command) {
    command.addOption('format', defaultValue: 'HH:mm:ss');
    return command;
  }

  @override
  Future<void> handle(CommandResult result) async {
      final format = result.getString("format");

      // Get the current time
      final now = DateTime.now();
      final DateFormat dateFormat = DateFormat(format);

      // Format the current time
      final formattedTime = dateFormat.format(now);
      info("The current time is " + formattedTime);
  }
}

The command will automatically be registered in the lib/app/commands/custom_commands.json file, which contains a list of all registered commands:

[
  {
    "name": "install_firebase",
    "category": "project",
    "script": "install_firebase.dart"
  },
  {
    "name": "current_time",
    "category": "app",
    "script": "current_time.dart"
  }
]

Running Custom Commands

Once created, you can run your custom command using either the Metro shorthand or the full Dart command:

# Using Metro
metro app:current_time

# Using Dart directly
dart run nylo_framework:main app:current_time

When you run metro or dart run nylo_framework:main without arguments, you'll see your custom commands listed in the menu under the "Custom Commands" section:

[Custom Commands]
  app:app_icon
  app:clear_pub
  project:install_firebase
  project:deploy

To display help information for your command, use the --help or -h flag:

metro project:install_firebase --help

Adding Options to Commands

Options allow your command to accept additional input from users. You can add options to your command in the builder method:

@override
CommandBuilder builder(CommandBuilder command) {
  
  // Add an option with a default value
  command.addOption(
    'environment',     // option name 
    abbr: 'e',         // short form abbreviation
    help: 'Target deployment environment', // help text
    defaultValue: 'development',  // default value
    allowed: ['development', 'staging', 'production'] // allowed values
  );
  
  return command;
}

Then access the option value in your command's handle method:

@override
Future<void> handle(CommandResult result) async {
  final environment = result.getString('environment');
  info('Deploying to $environment environment...');
  
  // Command implementation...
}

Example usage:

metro project:deploy --environment=production
# or using abbreviation
metro project:deploy -e production

Adding Flags to Commands

Flags are boolean options that can be toggled on or off. Add flags to your command using the addFlag method:

@override
CommandBuilder builder(CommandBuilder command) {
  
  command.addFlag(
    'verbose',       // flag name
    abbr: 'v',       // short form abbreviation
    help: 'Enable verbose output', // help text 
    defaultValue: false  // default to off
  );
  
  return command;
}

Then check the flag state in your command's handle method:

@override
Future<void> handle(CommandResult result) async {
  final verbose = result.getBool('verbose');
  
  if (verbose) {
    info('Verbose mode enabled');
    // Additional logging...
  }
  
  // Command implementation...
}

Example usage:

metro project:deploy --verbose
# or using abbreviation
metro project:deploy -v

Helper Methods

The NyCustomCommand base class provides several helper methods to assist with common tasks:

Printing Messages

Here are some methods for printing messages in different colors:

info Print an info message in blue text
error Print an error message in red text
success Print a success message in green text
warning Print a warning message in yellow text

Running Processes

Run processes and display their output in the console:

addPackage Add a package to pubspec.yaml
addPackages Add multiple packages to pubspec.yaml
runProcess Run an external process and display output in the console
prompt Collect user input as text
confirm Ask a yes/no question and return a boolean result
select Present a list of options and let the user select one
multiSelect Allow the user to select multiple options from a list

Network Requests

Making network requests via the console:

api Make an API call using the Nylo API client

Loading Spinner

Display a loading spinner while executing a function:

withSpinner Show a loading spinner while executing a function
createSpinner Create a spinner instance for manual control

Custom Command Helpers

You can also use the following helper methods to manage command arguments:

getString Get a string value from command arguments
getBool Get a boolean value from command arguments
getInt Get an integer value from command arguments
sleep Pause execution for a specified duration

Running External Processes

// Run a process with output displayed in the console
await runProcess('flutter build web --release');

// Run a process silently
await runProcess('flutter pub get', silent: true);

// Run a process in a specific directory
await runProcess('git pull', workingDirectory: './my-project');

Package Management


// Add a package to pubspec.yaml
addPackage('firebase_core', version: '^2.4.0');

// Add a dev package to pubspec.yaml
addPackage('build_runner', dev: true);

// Add multiple packages at once
addPackages(['firebase_auth', 'firebase_storage', 'quickalert']);

Output Formatting

// Print status messages with color coding
info('Processing files...');    // Blue text
error('Operation failed');      // Red text
success('Deployment complete'); // Green text
warning('Outdated package');    // Yellow text

Interactive Input Methods

The NyCustomCommand base class provides several methods for collecting user input in the terminal. These methods make it easy to create interactive command-line interfaces for your custom commands.


Text Input

String prompt(String question, {String defaultValue = ''})

Displays a question to the user and collects their text response.

Parameters:

  • question: The question or prompt to display
  • defaultValue: Optional default value if the user just presses Enter

Returns: The user's input as a string, or the default value if no input was provided

Example:

final name = prompt('What is your project name?', defaultValue: 'my_app');
final description = prompt('Enter a project description:');

Confirmation

bool confirm(String question, {bool defaultValue = false})

Asks the user a yes/no question and returns a boolean result.

Parameters:

  • question: The yes/no question to ask
  • defaultValue: The default answer (true for yes, false for no)

Returns: true if the user answered yes, false if they answered no

Example:

if (confirm('Would you like to continue?', defaultValue: true)) {
  // User confirmed or pressed Enter (accepting the default)
  await runProcess('flutter pub get');
} else {
  // User declined
  info('Operation canceled');
}

Single Selection

String select(String question, List<String> options, {String? defaultOption})

Presents a list of options and lets the user select one.

Parameters:

  • question: The selection prompt
  • options: List of available options
  • defaultOption: Optional default selection

Returns: The selected option as a string

Example:

final environment = select(
  'Select deployment environment:',
  ['development', 'staging', 'production'],
  defaultOption: 'development'
);

info('Deploying to $environment environment...');

Multiple Selection

List<String> multiSelect(String question, List<String> options)

Allows the user to select multiple options from a list.

Parameters:

  • question: The selection prompt
  • options: List of available options

Returns: A list of the selected options

Example:

final packages = multiSelect(
  'Select packages to install:',
  ['firebase_auth', 'dio', 'provider', 'shared_preferences', 'path_provider']
);

if (packages.isNotEmpty) {
  info('Installing ${packages.length} packages...');
  addPackages(packages);
  await runProcess('flutter pub get');
}

API Helper Method

The api helper method simplifies making network requests from your custom commands.

Future<T?> api<T>(Future<T?> Function(ApiService) request) async

Basic Usage Examples

GET Request

// Fetch data
final userData = await api((request) => 
  request.get('https://api.example.com/users/1')
);

POST Request

// Create a resource
final result = await api((request) => 
  request.post(
    'https://api.example.com/items',
    data: {'name': 'New Item', 'price': 19.99}
  )
);

PUT Request

// Update a resource
final updateResult = await api((request) => 
  request.put(
    'https://api.example.com/items/42',
    data: {'name': 'Updated Item', 'price': 29.99}
  )
);

DELETE Request

// Delete a resource
final deleteResult = await api((request) => request.delete('https://api.example.com/items/42'));

PATCH Request

// Partially update a resource
final patchResult = await api((request) => request.patch(
    'https://api.example.com/items/42',
    data: {'price': 24.99}
  )
);

With Query Parameters

// Add query parameters
final searchResults = await api((request) => request.get(
    'https://api.example.com/search',
    queryParameters: {'q': 'keyword', 'limit': 10}
  )
);

With Spinner

// Using with spinner for better UI
final data = await withSpinner(
  task: () async {
    final data = await api((request) => request.get('https://api.example.com/config'));
    // Process the data
  },
  message: 'Loading configuration',
);

Spinner Functionality

Spinners provide visual feedback during long-running operations in your custom commands. They display an animated indicator along with a message while your command executes asynchronous tasks, enhancing the user experience by showing progress and status.


Using with spinner

The withSpinner method lets you wrap an asynchronous task with a spinner animation that automatically starts when the task begins and stops when it completes or fails:

Future<T> withSpinner<T>({
  required Future<T> Function() task,
  required String message,
  String? successMessage,
  String? errorMessage,
}) async

Parameters:

  • task: The asynchronous function to execute
  • message: Text to display while the spinner is running
  • successMessage: Optional message to display upon successful completion
  • errorMessage: Optional message to display if the task fails

Returns: The result of the task function

Example:

@override
Future<void> handle(CommandResult result) async {
  // Run a task with a spinner
  final projectFiles = await withSpinner(
    task: () async {
      // Long-running task (e.g., analyzing project files)
      await sleep(2);
      return ['pubspec.yaml', 'lib/main.dart', 'README.md'];
    },
    message: 'Analyzing project structure',
    successMessage: 'Project analysis complete',
    errorMessage: 'Failed to analyze project',
  );
  
  // Continue with the results
  info('Found ${projectFiles.length} key files');
}

Manual Spinner Control

For more complex scenarios where you need to control the spinner state manually, you can create a spinner instance:

ConsoleSpinner createSpinner(String message)

Parameters:

  • message: Text to display while the spinner is running

Returns: A ConsoleSpinner instance that you can manually control

Example with manual control:

@override
Future<void> handle(CommandResult result) async {
  // Create a spinner instance
  final spinner = createSpinner('Deploying to production');
  spinner.start();
  
  try {
    // First task
    await runProcess('flutter clean', silent: true);
    spinner.update(message: 'Building release version');
    
    // Second task
    await runProcess('flutter build web --release', silent: true);
    spinner.update(message: 'Uploading to server');
    
    // Third task
    await runProcess('./deploy.sh', silent: true);
    
    // Complete successfully
    spinner.stop(completionMessage: 'Deployment completed successfully', success: true);
  } catch (e) {
    // Handle failure
    spinner.stop(completionMessage: 'Deployment failed: $e', success: false);
    rethrow;
  }
}

Examples

Simple Task with Spinner

@override
Future<void> handle(CommandResult result) async {
  await withSpinner(
    task: () async {
      // Install dependencies
      await runProcess('flutter pub get', silent: true);
      return true;
    },
    message: 'Installing dependencies',
    successMessage: 'Dependencies installed successfully',
  );
}

Multiple Consecutive Operations

@override
Future<void> handle(CommandResult result) async {
  // First operation with spinner
  await withSpinner(
    task: () => runProcess('flutter clean', silent: true),
    message: 'Cleaning project',
  );
  
  // Second operation with spinner
  await withSpinner(
    task: () => runProcess('flutter pub get', silent: true),
    message: 'Updating dependencies',
  );
  
  // Third operation with spinner
  final buildSuccess = await withSpinner(
    task: () async {
      await runProcess('flutter build apk --release', silent: true);
      return true;
    },
    message: 'Building release APK',
    successMessage: 'Release APK built successfully',
  );
  
  if (buildSuccess) {
    success('Build process completed');
  }
}

Complex Workflow with Manual Control

@override
Future<void> handle(CommandResult result) async {
  final spinner = createSpinner('Starting deployment process');
  spinner.start();
  
  try {
    // Run multiple steps with status updates
    for (var i = 1; i <= 5; i++) {
      spinner.update(message: 'Deployment step $i of 5');
      
      // Simulate work for each step
      await sleep(1);
      
      // Pretend step 3 has a warning we want to show
      if (i == 3) {
        // Temporarily pause the spinner to show a warning
        spinner.pause();
        warning('Configuration file missing, using defaults');
        // Resume the spinner
        spinner.resume();
      }
    }
    
    // Complete the process
    spinner.stop(completionMessage: 'Deployment completed successfully', success: true);
    
  } catch (e) {
    spinner.stop(completionMessage: 'Deployment failed', success: false);
    error('Error details: $e');
  }
}

Using spinners in your custom commands provides clear visual feedback to users during long-running operations, creating a more polished and professional command-line experience.


Get a string value from options

String getString(String name, {String defaultValue = ''})

Parameters:

  • name: The name of the option to retrieve
  • defaultValue: Optional default value if the option is not provided

Returns: The value of the option as a string

Example:

@override
CommandBuilder builder(CommandBuilder command) {
  command.addOption("name", defaultValue: "Anthony");
  return command;
}

Future<void> handle(CommandResult result) async {
  final name = result.getString('name');
  info('Hello, $name!');
}

Get a bool value from options

bool getBool(String name, {bool defaultValue = false})

Parameters:

  • name: The name of the option to retrieve
  • defaultValue: Optional default value if the option is not provided

Returns: The value of the option as a boolean

Example:

@override
CommandBuilder builder(CommandBuilder command) {
  command.addFlag("verbose", defaultValue: false);
  return command;
}

Future<void> handle(CommandResult result) async {
  final verbose = result.getBool('verbose');
  if (verbose) {
    info('Verbose mode enabled');
  } else {
    info('Verbose mode disabled');
  }
}

Get an int value from options

int getInt(String name, {int defaultValue = 0})

Parameters:

  • name: The name of the option to retrieve
  • defaultValue: Optional default value if the option is not provided

Returns: The value of the option as an integer

Example:

@override
CommandBuilder builder(CommandBuilder command) {
  command.addOption("count", defaultValue: 5);
  return command;
}

Future<void> handle(CommandResult result) async {
  final count = result.getInt('count');
  info('Count is set to $count');
}

Sleep for a specified duration

void sleep(int seconds)

Parameters:

  • seconds: The number of seconds to sleep

Returns: None

Example:

@override
Future<void> handle(CommandResult result) async {
  info('Sleeping for 5 seconds...');
  sleep(5);
  info('Awake now!');
}