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:
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:
# Using the cluster CLI
justscale db migrate up --steps 3Arguments and Flags
JustScale automatically parses arguments based on your Zod schema:
Positional Arguments
Required fields without defaults become positional arguments:
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:
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:
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 promptedThe io Object
The io object provides methods for terminal output and user interaction:
Basic Output
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
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
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
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:
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:
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# In another terminal:
justscale db migrate up --steps 3
justscale db seed --file ./data.jsonProgrammatic Invocation
Invoke commands programmatically using invoke():
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():
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,
});# Run with tsx or node:
tsx cli.ts db migrate up --steps 3Middleware and Guards
CLI routes support middleware and guards just like HTTP routes:
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:
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
);
}),
}),
});