CLI Usage

Build command-line interfaces with JustScale

Overview

The CLI transport allows you to expose controller methods as command-line commands. Routes defined with Cli() become executable commands with automatic argument parsing, validation, and rich terminal output.

Basic CLI Route

Create a CLI route using the Cli() factory in your controller:

controllers/db.tsTypeScript
import { createController } from '@justscale/core';
import { Cli } from '@justscale/cli';
import { z } from 'zod';

export const DbController = createController('db', {
  routes: () => ({
    migrate: Cli('migrate')
      .input(z.object({
        direction: z.enum(['up', 'down']),
        steps: z.number().default(1),
      }))
      .handle(({ args, io }) => {
        io.log(`Running ${args.steps} migrations ${args.direction}...`);
        io.result({ applied: args.steps });
      }),
  }),
});

This creates a command that can be run as:

Bash
# Using the cluster CLI
justscale db migrate up --steps 3

Arguments and Flags

JustScale automatically parses arguments based on your Zod schema:

Positional Arguments

Required fields without defaults become positional arguments:

create-user-command.tsTypeScript
import { Cli } from '@justscale/cli';
import { z } from 'zod';

const CreateUserArgs = z.object({
  email: z.string().email(),
  name: z.string(),
  admin: z.boolean().default(false),
});

Cli('create-user')
  .input(CreateUserArgs)
  .handle(({ args, io }) => {
    // Email and name are positional: create-user john@example.com "John Doe"
    // Admin is a flag: --admin
    io.log(`Creating user: ${args.name} (${args.email})`);
  });

Named Flags

Optional fields and fields with defaults become named flags:

build-command.tsTypeScript
import { Cli } from '@justscale/cli';
import { z } from 'zod';

const BuildArgs = z.object({
  src: z.string().default('./src'),
  out: z.string().default('./dist'),
  verbose: z.boolean().default(false),
});

Cli('build')
  .input(BuildArgs)
  .handle(({ args, io }) => {
    // All are flags: build --src ./app --out ./build --verbose
  });

Interactive Prompts

Use the arg() decorator for interactive prompts:

interactive-command.tsTypeScript
import { Cli, arg } from '@justscale/cli';
import { z } from 'zod';

const CreateUserArgs = z.object({
  email: z.string().email(),
  password: arg(z.string(), {
    prompt: 'Password',
    secret: true,
    confirm: true,
  }),
});

// If password is not provided as a flag, user will be prompted

The io Object

The io object provides methods for terminal output and user interaction:

Basic Output

status-command.tsTypeScript
import { Cli } from '@justscale/cli';

Cli('status').handle(({ io }) => {
  io.log('Everything is working');     // Normal output
  io.warn('This might be a problem');  // Yellow warning
  io.error('Something went wrong');    // Red error to stderr
  io.debug('Verbose information');     // Only shown with --verbose
});

Progress Indicators

deploy-command.tsTypeScript
import { Cli } from '@justscale/cli';

Cli('deploy').handle(async ({ io }) => {
  // Spinner for indeterminate operations
  const spinner = io.spinner('Deploying application...');
  await performDeploy();
  spinner.success('Deployment complete');

  // Progress bar for tasks with known total
  const progress = io.progress('Uploading files', 100);
  for (let i = 0; i <= 100; i += 10) {
    progress.update(i, `Uploading file ${i/10}`);
    await uploadFile(i);
  }
  progress.complete();
});

Interactive Input

setup-command.tsTypeScript
import { Cli } from '@justscale/cli';

Cli('setup').handle(async ({ io }) => {
  // Text prompt
  const name = await io.prompt('What is your name?', 'Default Name');

  // Confirmation
  const proceed = await io.confirm('Continue with installation?');

  // Single selection
  const env = await io.select('Select environment', [
    'development',
    'staging',
    'production'
  ]);

  // Selection with labels
  const region = await io.select('Select region', [
    { label: 'US East', value: 'us-east-1' },
    { label: 'EU West', value: 'eu-west-1' },
  ]);

  // Password (hidden input)
  const password = await io.password('Enter password');
});

Tables

list-users-command.tsTypeScript
import { Cli } from '@justscale/cli';

Cli('list-users').handle(async ({ io, users }) => {
  const allUsers = await users.findAll();

  io.table(allUsers, ['id', 'email', 'createdAt']);

  // Or with custom columns
  io.table(allUsers, [
    { key: 'id', header: 'ID', width: 8 },
    { key: 'email', header: 'Email Address', width: 30 },
    { key: 'createdAt', header: 'Created', align: 'right' },
  ]);
});

Structured Results

Use io.result() to return structured data for programmatic use:

status-result-command.tsTypeScript
import { Cli } from '@justscale/cli';
import { z } from 'zod';

const StatusResult = z.object({
  branch: z.string(),
  clean: z.boolean(),
  ahead: z.number(),
});

Cli('status')
  .returns(StatusResult)
  .handle(({ io }) => {
    io.log('Checking git status...');

    // Return structured data (validated against schema)
    io.result({
      branch: 'main',
      clean: true,
      ahead: 0,
    });
  });

Running CLI Commands

There are several ways to execute CLI commands:

Using the Cluster CLI

When you start a cluster with cluster.serve(), a Unix socket is created that allows the justscale binary to invoke commands:

index.tsTypeScript
import { createCluster } from '@justscale/cluster';
import { DbController } from './controllers/db';

const cluster = createCluster({
  controllers: [DbController],
});

await cluster.serve({ http: 3000 });
// Socket created - CLI commands now work
Bash
# In another terminal:
justscale db migrate up --steps 3
justscale db seed --file ./data.json

Programmatic Invocation

Invoke commands programmatically using invoke():

invoke-example.tsTypeScript
import { createCluster } from '@justscale/cluster';
import { invoke } from '@justscale/cli';
import { DbController } from './controllers/db';

const cluster = createCluster({
  controllers: [DbController],
});

const result = await invoke(cluster.app, 'db migrate', {
  direction: 'up',
  steps: 3,
});

Direct Execution

Run commands directly from argv using run():

cli.tsTypeScript
import { createCluster } from '@justscale/cluster';
import { run } from '@justscale/cli';
import { DbController } from './controllers/db';

const cluster = createCluster({
  controllers: [DbController],
});

// This parses process.argv and executes the command
await run(cluster.app, {
  name: 'myapp',
  exitOnError: true,
});
Bash
# Run with tsx or node:
tsx cli.ts db migrate up --steps 3

Middleware and Guards

CLI routes support middleware and guards just like HTTP routes:

delete-user-command.tsTypeScript
import { createMiddleware } from '@justscale/core';
import { Cli } from '@justscale/cli';
import { z } from 'zod';
import { AuthService } from './auth-service';

const requireAuth = createMiddleware({
  inject: { auth: AuthService },
  handler: ({ auth }) => async (ctx: { args: { token?: string } }) => {
    const token = ctx.args.token;
    if (!token || !auth.verify(token)) {
      throw new Error('Authentication required');
    }
    return { user: await auth.getUser(token) };
  },
});

Cli('delete-user')
  .input(z.object({
    userId: z.string(),
    token: z.string(),
  }))
  .use(requireAuth)
  .handle(({ userId, user, io }) => {
    // Only runs if authenticated
    io.log(`Deleting user ${userId} (authorized by ${user.email})`);
  });

Calling Other Commands

Use CliService to invoke other commands from within a CLI handler:

shell-controller.tsTypeScript
import { createController } from '@justscale/core';
import { Cli, CliService } from '@justscale/cli';

const ShellController = createController({
  inject: { cli: CliService },
  routes: (services) => ({
    shell: Cli('shell').handle(async ({ io }) => {
      const { cli } = services;
      // List all available commands
      const commands = cli.listCommands();
      io.table(commands.map(cmd => ({ command: cmd })));

      // Execute another command
      const result = await cli.execute(
        'db migrate',
        { direction: 'up', steps: 1 },
        io
      );
    }),
  }),
});