Basics

Metro CLI tool

Introduction

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

Install

When you create a new Nylo project using nylo init, the metro command is automatically configured for your terminal. You can start using it immediately in any Nylo project.

Run metro from your project directory to see all available commands:

metro

You should see an output similar to the below.

Metro - Nylo's Companion to Build Flutter apps by Anthony Gordon

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:journey_widget
  make:bottom_sheet_modal
  make:button
  make:form

[Helper Commands]
  make:model
  make:provider
  make:api_service
  make:controller
  make:event
  make:theme
  make:route_guard
  make:config
  make:interceptor
  make:command
  make:env
  make:key

Make controller

Making a new controller

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

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.

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.

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.

metro make:model product --force

Make page

Making a new page

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

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.

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.

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.

metro make:page home_page -i

Forcefully make a page

Arguments:

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

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.

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.

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.

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.

metro make:stateful_widget product_rating_widget --force

Make journey widget

Making a new journey widget

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

metro make:journey_widget product_journey --parent="[NAVIGATION_HUB]"

# Full example if you have a BaseNavigationHub
metro make:journey_widget welcome,user_dob,user_photos --parent="Base"

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

The --parent argument is used to specify the parent widget that the new journey widget will be added to.

Example

metro make:navigation_hub onboarding

Next, add the new journey widgets.

metro make:journey_widget welcome,user_dob,user_photos --parent="onboarding"

Forcefully make a journey widget

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

metro make:journey_widget product_journey --force --parent="[YOUR_NAVIGATION_HUB]"

Make API Service

Making a new API Service

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

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.

metro make:api_service user --model="User"

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

Forcefully make an API Service

Arguments:

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

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.

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.

metro make:event login_event --force

Make provider

Making a new provider

Create new providers in your application using the below command.

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.

metro make:provider firebase_provider --force

Make theme

Making a new theme

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

metro make:theme bright_theme

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

Forcefully make a theme

Arguments:

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

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.

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.

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.

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.

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.

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.

metro make:config app_config --force

Make Command

Making a new command

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

metro make:command my_command

This will create a new command in lib/app/commands.

Forcefully make a command

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

metro make:command my_command --force

Make State Managed Widget

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

metro make:state_managed_widget product_rating_widget

The above will create a new widget in lib/resources/widgets/.

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

metro make:state_managed_widget product_rating_widget --force

Make Navigation Hub

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

metro make:navigation_hub dashboard

This will create a new navigation hub in lib/resources/pages/ and add the route automatically.

Arguments:

Flag Short Description
--auth -a Create as an auth page
--initial -i Create as the initial page
--force -f Overwrite if exists
# Create as the initial page
metro make:navigation_hub dashboard --initial

Make Bottom Sheet Modal

You can make a new bottom sheet modal by running the below in the terminal.

metro make:bottom_sheet_modal payment_options

This will create a new bottom sheet modal in lib/resources/widgets/.

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

metro make:bottom_sheet_modal payment_options --force

Make Button

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

metro make:button checkout_button

This will create a new button widget in lib/resources/widgets/.

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

metro make:button checkout_button --force

Make Interceptor

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

metro make:interceptor auth_interceptor

This will create a new interceptor in lib/app/networking/dio/interceptors/.

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

metro make:interceptor auth_interceptor --force

Make Env File

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

metro make:env .env.staging

This will create a new .env file in your project root.

Make Key

Generate a secure APP_KEY for environment encryption. This is used for encrypted .env files in v7.

metro make:key

Arguments:

Flag / Option Short Description
--force -f Overwrite existing APP_KEY
--file -e Target .env file (default: .env)
# Generate key and overwrite existing
metro make:key --force

# Generate key for a specific env file
metro make:key --file=.env.production

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.

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:

metro 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:
///   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/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:

metro app:current_time

When you run metro 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('Building release version');

    // Second task
    await runProcess('flutter build web --release', silent: true);
    spinner.update('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
    spinner.update('Step 1: Cleaning project');
    await runProcess('flutter clean', silent: true);

    spinner.update('Step 2: Fetching dependencies');
    await runProcess('flutter pub get', silent: true);

    spinner.update('Step 3: Building release');
    await runProcess('flutter build web --release', silent: true);

    // Complete the process
    spinner.stop(completionMessage: 'Deployment completed successfully', success: true);

  } catch (e) {
    spinner.stop(completionMessage: 'Deployment failed: $e', success: false);
  }
}

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...');
  await sleep(5);
  info('Awake now!');
}

Output Formatting

Beyond the basic info, error, success, and warning methods, NyCustomCommand provides additional output helpers:

@override
Future<void> handle(CommandResult result) async {
  // Print plain text (no color)
  line('Processing your request...');

  // Print blank lines
  newLine();       // one blank line
  newLine(3);      // three blank lines

  // Print a muted comment (gray text)
  comment('This is a background note');

  // Print a prominent alert box
  alert('Important: Please read carefully');

  // Ask is an alias for prompt
  final name = ask('What is your name?');

  // Hidden input for sensitive data (e.g., passwords, API keys)
  final apiKey = promptSecret('Enter your API key:');

  // Abort the command with an error message and exit code
  if (name.isEmpty) {
    abort('Name is required');  // exits with code 1
  }
}
Method Description
line(String message) Print plain text without color
newLine([int count = 1]) Print blank lines
comment(String message) Print muted/gray text
alert(String message) Print a prominent alert box
ask(String question, {String defaultValue}) Alias for prompt
promptSecret(String question) Hidden input for sensitive data
abort([String? message, int exitCode = 1]) Exit the command with an error

File System Helpers

NyCustomCommand includes built-in file system helpers so you don't need to manually import dart:io for common operations.

Reading and Writing Files

@override
Future<void> handle(CommandResult result) async {
  // Check if a file exists
  if (fileExists('lib/config/app.dart')) {
    info('Config file found');
  }

  // Check if a directory exists
  if (directoryExists('lib/app/models')) {
    info('Models directory found');
  }

  // Read a file (async)
  String content = await readFile('pubspec.yaml');

  // Read a file (sync)
  String contentSync = readFileSync('pubspec.yaml');

  // Write to a file (async)
  await writeFile('lib/generated/output.dart', 'class Output {}');

  // Write to a file (sync)
  writeFileSync('lib/generated/output.dart', 'class Output {}');

  // Append content to a file
  await appendFile('log.txt', 'New log entry\n');

  // Ensure a directory exists (creates it if missing)
  await ensureDirectory('lib/generated');

  // Delete a file
  await deleteFile('lib/generated/output.dart');

  // Copy a file
  await copyFile('lib/config/app.dart', 'lib/config/app.bak.dart');
}
Method Description
fileExists(String path) Returns true if the file exists
directoryExists(String path) Returns true if the directory exists
readFile(String path) Read file as string (async)
readFileSync(String path) Read file as string (sync)
writeFile(String path, String content) Write content to file (async)
writeFileSync(String path, String content) Write content to file (sync)
appendFile(String path, String content) Append content to file
ensureDirectory(String path) Create directory if it doesn't exist
deleteFile(String path) Delete a file
copyFile(String source, String destination) Copy a file

JSON and YAML Helpers

Read and write JSON and YAML files with built-in helpers.

@override
Future<void> handle(CommandResult result) async {
  // Read a JSON file as a Map
  Map<String, dynamic> config = await readJson('config.json');

  // Read a JSON file as a List
  List<dynamic> items = await readJsonArray('lib/app/commands/commands.json');

  // Write data to a JSON file (pretty printed by default)
  await writeJson('output.json', {'name': 'MyApp', 'version': '1.0.0'});

  // Write compact JSON
  await writeJson('output.json', data, pretty: false);

  // Append an item to a JSON array file
  // If the file contains [{"name": "a"}], this adds to that array
  await appendToJsonArray(
    'lib/app/commands/commands.json',
    {'name': 'my_command', 'category': 'app', 'script': 'my_command.dart'},
    uniqueKey: 'name',  // prevents duplicates by this key
  );

  // Read a YAML file as a Map
  Map<String, dynamic> pubspec = await readYaml('pubspec.yaml');
  info('Project: ${pubspec['name']}');
}
Method Description
readJson(String path) Read JSON file as Map<String, dynamic>
readJsonArray(String path) Read JSON file as List<dynamic>
writeJson(String path, dynamic data, {bool pretty = true}) Write data as JSON
appendToJsonArray(String path, Map item, {String? uniqueKey}) Append to a JSON array file
readYaml(String path) Read YAML file as Map<String, dynamic>

Case Conversion Helpers

Convert strings between naming conventions without importing the recase package.

@override
Future<void> handle(CommandResult result) async {
  String input = 'user profile page';

  info(snakeCase(input));    // user_profile_page
  info(camelCase(input));    // userProfilePage
  info(pascalCase(input));   // UserProfilePage
  info(titleCase(input));    // User Profile Page
  info(kebabCase(input));    // user-profile-page
  info(constantCase(input)); // USER_PROFILE_PAGE
}
Method Output Format Example
snakeCase(String input) snake_case user_profile
camelCase(String input) camelCase userProfile
pascalCase(String input) PascalCase UserProfile
titleCase(String input) Title Case User Profile
kebabCase(String input) kebab-case user-profile
constantCase(String input) CONSTANT_CASE USER_PROFILE

Project Path Helpers

Getters for standard Nylo Website project directories. These return paths relative to the project root.

@override
Future<void> handle(CommandResult result) async {
  info(modelsPath);       // lib/app/models
  info(controllersPath);  // lib/app/controllers
  info(widgetsPath);      // lib/resources/widgets
  info(pagesPath);        // lib/resources/pages
  info(commandsPath);     // lib/app/commands
  info(configPath);       // lib/config
  info(providersPath);    // lib/app/providers
  info(eventsPath);       // lib/app/events
  info(networkingPath);   // lib/app/networking
  info(themesPath);       // lib/resources/themes

  // Build a custom path relative to the project root
  String customPath = projectPath('lib/app/services/auth_service.dart');
}
Property Path
modelsPath lib/app/models
controllersPath lib/app/controllers
widgetsPath lib/resources/widgets
pagesPath lib/resources/pages
commandsPath lib/app/commands
configPath lib/config
providersPath lib/app/providers
eventsPath lib/app/events
networkingPath lib/app/networking
themesPath lib/resources/themes
projectPath(String relativePath) Resolves a relative path within the project

Platform Helpers

Check the platform and access environment variables.

@override
Future<void> handle(CommandResult result) async {
  // Platform checks
  if (isWindows) {
    info('Running on Windows');
  } else if (isMacOS) {
    info('Running on macOS');
  } else if (isLinux) {
    info('Running on Linux');
  }

  // Current working directory
  info('Working in: $workingDirectory');

  // Read system environment variables
  String home = env('HOME', '/default/path');
}
Property / Method Description
isWindows true if running on Windows
isMacOS true if running on macOS
isLinux true if running on Linux
workingDirectory Current working directory path
env(String key, [String defaultValue = '']) Read system environment variable

Dart and Flutter Commands

Run common Dart and Flutter CLI commands as helper methods. Each returns the process exit code.

@override
Future<void> handle(CommandResult result) async {
  // Format a Dart file or directory
  await dartFormat('lib/app/models/user.dart');

  // Run dart analyze
  int analyzeResult = await dartAnalyze('lib/');

  // Run flutter pub get
  await flutterPubGet();

  // Run flutter clean
  await flutterClean();

  // Build for a target with additional args
  await flutterBuild('apk', args: ['--release', '--split-per-abi']);
  await flutterBuild('web', args: ['--release']);

  // Run flutter test
  await flutterTest();
  await flutterTest('test/unit/');  // specific directory
}
Method Description
dartFormat(String path) Run dart format on a file or directory
dartAnalyze([String? path]) Run dart analyze
flutterPubGet() Run flutter pub get
flutterClean() Run flutter clean
flutterBuild(String target, {List<String> args}) Run flutter build <target>
flutterTest([String? path]) Run flutter test

Dart File Manipulation

Helpers for programmatically editing Dart files, useful when building scaffolding tools.

@override
Future<void> handle(CommandResult result) async {
  // Add an import statement to a Dart file (avoids duplicates)
  await addImport(
    'lib/bootstrap/providers.dart',
    "import '/app/providers/firebase_provider.dart';",
  );

  // Insert code before the last closing brace in a file
  // Useful for adding entries to registration maps
  await insertBeforeClosingBrace(
    'lib/bootstrap/providers.dart',
    '  FirebaseProvider(),',
  );

  // Check if a file contains a specific string
  bool hasImport = await fileContains(
    'lib/bootstrap/providers.dart',
    'firebase_provider',
  );

  // Check if a file matches a regex pattern
  bool hasClass = await fileContainsPattern(
    'lib/app/models/user.dart',
    RegExp(r'class User'),
  );
}
Method Description
addImport(String filePath, String importStatement) Add import to Dart file (skips if already present)
insertBeforeClosingBrace(String filePath, String code) Insert code before last } in file
fileContains(String filePath, String identifier) Check if file contains a string
fileContainsPattern(String filePath, Pattern pattern) Check if file matches a pattern

Directory Helpers

Helpers for working with directories and finding files.

@override
Future<void> handle(CommandResult result) async {
  // List directory contents
  var entities = listDirectory('lib/app/models');
  for (var entity in entities) {
    info(entity.path);
  }

  // List recursively
  var allEntities = listDirectory('lib/', recursive: true);

  // Find files matching criteria
  List<File> dartFiles = findFiles(
    'lib/app/models',
    extension: '.dart',
    recursive: true,
  );

  // Find files by name pattern
  List<File> testFiles = findFiles(
    'test/',
    namePattern: RegExp(r'_test\.dart$'),
  );

  // Delete a directory recursively
  await deleteDirectory('build/');

  // Copy a directory (recursive)
  await copyDirectory('lib/templates', 'lib/generated');
}
Method Description
listDirectory(String path, {bool recursive = false}) List directory contents
findFiles(String directory, {String? extension, Pattern? namePattern, bool recursive = true}) Find files matching criteria
deleteDirectory(String path) Delete directory recursively
copyDirectory(String source, String destination) Copy directory recursively

Validation Helpers

Helpers for validating and cleaning user input for code generation.

@override
Future<void> handle(CommandResult result) async {
  // Validate a Dart identifier
  if (!isValidDartIdentifier('MyClass')) {
    error('Invalid Dart identifier');
  }

  // Require a non-empty first argument
  String name = requireArgument(result, message: 'Please provide a name');

  // Clean a class name (PascalCase, remove suffixes)
  String className = cleanClassName('user_model', removeSuffixes: ['_model']);
  // Returns: 'User'

  // Clean a file name (snake_case with extension)
  String fileName = cleanFileName('UserModel', extension: '.dart');
  // Returns: 'user_model.dart'
}
Method Description
isValidDartIdentifier(String name) Validate a Dart identifier name
requireArgument(CommandResult result, {String? message}) Require non-empty first argument or abort
cleanClassName(String name, {List<String> removeSuffixes}) Clean and PascalCase a class name
cleanFileName(String name, {String extension = '.dart'}) Clean and snake_case a file name

File Scaffolding

Create one or many files with content using the scaffolding system.

Single File

@override
Future<void> handle(CommandResult result) async {
  await scaffold(
    path: 'lib/app/services/auth_service.dart',
    content: '''
class AuthService {
  Future<bool> login(String email, String password) async {
    // TODO: implement login
    return false;
  }
}
''',
    force: false,  // don't overwrite if exists
    successMessage: 'AuthService created',
  );
}

Multiple Files

@override
Future<void> handle(CommandResult result) async {
  await scaffoldMany([
    ScaffoldFile(
      path: 'lib/app/models/product.dart',
      content: 'class Product {}',
      successMessage: 'Product model created',
    ),
    ScaffoldFile(
      path: 'lib/app/networking/product_api_service.dart',
      content: 'class ProductApiService {}',
      successMessage: 'Product API service created',
    ),
  ], force: false);
}

The ScaffoldFile class accepts:

Property Type Description
path String File path to create
content String File content
successMessage String? Message shown on success

Task Runner

Run a series of named tasks with automatic status output.

Basic Task Runner

@override
Future<void> handle(CommandResult result) async {
  await runTasks([
    CommandTask(
      'Clean project',
      () => runProcess('flutter clean', silent: true),
    ),
    CommandTask(
      'Fetch dependencies',
      () => runProcess('flutter pub get', silent: true),
    ),
    CommandTask(
      'Run tests',
      () => runProcess('flutter test', silent: true),
      stopOnError: true,  // stop pipeline if this fails (default)
    ),
  ]);
}

Task Runner with Spinner

@override
Future<void> handle(CommandResult result) async {
  await runTasksWithSpinner([
    CommandTask(
      name: 'Preparing release',
      action: () async {
        await flutterClean();
        await flutterPubGet();
      },
    ),
    CommandTask(
      name: 'Building APK',
      action: () => flutterBuild('apk', args: ['--release']),
    ),
  ]);
}

The CommandTask class accepts:

Property Type Default Description
name String required Task name shown in output
action Future<void> Function() required Async function to execute
stopOnError bool true Whether to stop remaining tasks if this one fails

Table Output

Display formatted ASCII tables in the console.

@override
Future<void> handle(CommandResult result) async {
  table(
    ['Name', 'Version', 'Status'],
    [
      ['nylo_framework', '7.0.0', 'installed'],
      ['nylo_support', '7.0.0', 'installed'],
      ['dio', '5.4.0', 'installed'],
    ],
  );
}

Output:

┌─────────────────┬─────────┬───────────┐
│ Name            │ Version │ Status    │
├─────────────────┼─────────┼───────────┤
│ nylo_framework  │ 7.0.0   │ installed │
│ nylo_support    │ 7.0.0   │ installed │
│ dio             │ 5.4.0   │ installed │
└─────────────────┴─────────┴───────────┘

Progress Bar

Display a progress bar for operations with known item counts.

Manual Progress Bar

@override
Future<void> handle(CommandResult result) async {
  // Create a progress bar for 100 items
  final progress = progressBar(100, message: 'Processing files');
  progress.start();

  for (int i = 0; i < 100; i++) {
    await Future.delayed(Duration(milliseconds: 50));
    progress.tick();  // increment by 1
  }

  progress.complete('All files processed');
}

Process Items with Progress

@override
Future<void> handle(CommandResult result) async {
  final files = findFiles('lib/', extension: '.dart');

  // Process items with automatic progress tracking
  final results = await withProgress<File, String>(
    items: files,
    process: (file, index) async {
      // process each file
      return file.path;
    },
    message: 'Analyzing Dart files',
    completionMessage: 'Analysis complete',
  );

  info('Processed ${results.length} files');
}

Synchronous Progress

@override
Future<void> handle(CommandResult result) async {
  final items = ['a', 'b', 'c', 'd', 'e'];

  final results = withProgressSync<String, String>(
    items: items,
    process: (item, index) {
      // synchronous processing
      return item.toUpperCase();
    },
    message: 'Converting items',
  );

  info('Results: $results');
}

The ConsoleProgressBar class provides:

Method Description
start() Start the progress bar
tick([int amount = 1]) Increment progress
update(int value) Set progress to a specific value
updateMessage(String newMessage) Change the displayed message
complete([String? completionMessage]) Complete with optional message
stop() Stop without completing
current Current progress value (getter)
percentage Progress as a percentage (getter)