Interactive Shell Feature

Connect to your running application with an interactive REPL

The @justscale/feature-shell package provides an interactive shell for your application. Think SSH, but you're connecting to the Node.js process itself to run commands, inspect state, and interact with your services.

Installation

Bash
pnpm add @justscale/feature-shell
pnpm add @justscale/cli

Basic Setup

src/app.tsTypeScript
import { createCluster } from '@justscale/cluster';
import { ShellFeature } from '@justscale/feature-shell';

const cluster = createCluster({
  features: [
    ShellFeature(),
  ],
});

await cluster.serve({ http: 3000 });

Connect to the shell:

Bash
justscale shell

What You Get

The shell feature automatically provides an interactive REPL with:

Built-in Commands

  • help - Show available commands
  • info - Show application info (Node version, uptime, memory)
  • health - Show health status
  • ping - Check connection
  • clear - Clear the screen
  • exit or quit - Exit the shell

Command Execution

Any CLI command registered in your application can be executed from within the shell. This includes commands from features like AuthFeature.

Using the Shell

Start the shell and interact with your application:

Bash
$ justscale shell
JustScale Interactive Shell
Type 'help' for commands, 'exit' to quit.

justscale> help

Built-in commands:
  help        Show this help message
  info        Show app information
  health      Show health status
  ping        Check connection
  clear       Clear the screen
  exit        Exit the shell

App commands:
  auth create-user
  auth list-users

justscale> info
┌──────────────┬────────────────────┐
 Field Value
├──────────────┼────────────────────┤
 Node Version v20.10.0
 Platform darwin
 Uptime 0h 5m 23s
 Memory (RSS) │ 45MB               │
 Memory (Heap)│ 12MB               │
 PID 12345
└──────────────┴────────────────────┘

justscale> health
┌─────────┬─────────┐
 Check Status
├─────────┼─────────┤
 Process healthy
 Memory healthy
└─────────┴─────────┘

justscale> ping
pong

justscale> exit
Goodbye!

With AuthFeature

When combined with the AuthFeature, you can manage users directly from the shell:

src/app.tsTypeScript
import { createCluster } from '@justscale/cluster';
import { AuthFeature } from '@justscale/auth';
import { ShellFeature } from '@justscale/feature-shell';

const cluster = createCluster({
  features: [
    AuthFeature(),
    ShellFeature(),
  ],
});

await cluster.serve({ http: 3000 });

Now you can create and manage users from the shell:

Bash
justscale> auth create-user --email admin@example.com --password secret123
User created successfully:
ID: abc123
Email: admin@example.com

justscale> auth list-users
┌────────┬─────────────────────┬──────────────────────┐
 ID Email Created At
├────────┼─────────────────────┼──────────────────────┤
 abc123 admin@example.com 2024-12-10T10:30:00Z
└────────┴─────────────────────┴──────────────────────┘

How It Works

The shell connects to your running application via a Unix socket (or named pipe on Windows). This allows you to interact with the live process without restarting or making HTTP requests.

Connection Flow

  • Your app creates a cluster with createCluster()
  • When you call cluster.serve(), it creates a Unix socket
  • Running justscale shell connects to this socket
  • Commands are sent to the server and executed in the same process
  • Output is streamed back to your terminal in real-time
ℹ️

Info

The shell uses the same CLI infrastructure as standalone commands, so any CLI controller you create automatically works in the shell.

Creating Custom Shell Commands

Add your own commands by creating CLI controllers:

Files
src/controllers/admin.tsTypeScript
import { createController } from '@justscale/core';
import { Cli } from '@justscale/cli';
import { MyService } from '../services/my-service';

export const AdminController = createController({
  inject: { myService: MyService },
  routes: (services) => ({
    clearCache: Cli('admin clear-cache').handle(({ io }) => {
      services.myService.clearCache();
      io.success('Cache cleared!');
    }),

    stats: Cli('admin stats').handle(({ io }) => {
      const stats = services.myService.getStats();
      io.table([
        { Metric: 'Total Requests', Value: stats.requests },
        { Metric: 'Cache Hits', Value: stats.cacheHits },
        { Metric: 'Cache Misses', Value: stats.cacheMisses },
      ]);
    }),

    restart: Cli('admin restart').handle(({ io }) => {
      io.warn('Restarting server...');
      process.exit(0); // Let process manager restart
    }),
  }),
});

Use in the shell:

Bash
justscale> help

App commands:
  admin clear-cache
  admin stats
  admin restart

justscale> admin stats
┌────────────────┬────────┐
 Metric Value
├────────────────┼────────┤
 Total Requests 12,543
 Cache Hits 8,932
 Cache Misses 3,611
└────────────────┴────────┘

justscale> admin clear-cache
 Cache cleared!

Command Arguments

Commands can accept arguments via flags and positional parameters:

user-commands.tsTypeScript
import { z } from 'zod';

const QueryArgsSchema = z.object({
  limit: z.number().default(10),
  offset: z.number().default(0),
  search: z.string().optional(),
});

Cli('users search')
  .args(QueryArgsSchema)
  .handle(({ io, args, userService }) => {
    const users = userService.search({
      search: args.search,
      limit: args.limit,
      offset: args.offset,
    });

    io.table(users.map(u => ({
      ID: u.id,
      Email: u.email,
      Created: u.createdAt,
    })));
  });

Use in the shell:

Bash
justscale> users search --limit 5 --search john
┌────────┬──────────────────┬──────────────────────┐
 ID Email Created
├────────┼──────────────────┼──────────────────────┤
 1 john@example.com 2024-12-10T10:00:00Z
 2 johnny@test.com 2024-12-10T11:00:00Z
└────────┴──────────────────┴──────────────────────┘

IO Utilities

The shell provides rich IO utilities for formatting output:

demo-command.tsTypeScript
Cli('demo').handle(({ io }) => {
  // Simple logging
  io.log('Regular message');
  io.success('Success message (green)');
  io.error('Error message (red)');
  io.warn('Warning message (yellow)');
  io.info('Info message (blue)');

  // Tables
  io.table([
    { Name: 'Alice', Age: 30, City: 'NYC' },
    { Name: 'Bob', Age: 25, City: 'SF' },
  ]);

  // Prompts (for interactive input)
  const name = await io.prompt('Enter your name:');
  const confirmed = await io.confirm('Are you sure?');

  // Spinners
  const spinner = io.spinner('Loading...');
  await someAsyncOperation();
  spinner.stop();

  // Progress bars
  for (let i = 0; i <= 100; i++) {
    io.progress(i, 100, 'Processing');
    await sleep(10);
  }
});

Configuration

The ShellFeature accepts optional configuration:

shell-config.tsTypeScript
ShellFeature({
  // Reserved for future options like:
  // - Custom prompt
  // - Additional built-in commands
  // - Auth requirements
});

Currently, no configuration is required. Future versions may add options for customizing the prompt, adding built-in commands, or requiring authentication.

Security Considerations

⚠️

Warning

The shell provides direct access to your application's internals. In production environments, ensure the socket file has appropriate permissions and is not accessible to untrusted users.

Best practices:

  • Run the app as a dedicated user with restricted permissions
  • Ensure the socket file is in a protected directory
  • Consider adding authentication for sensitive commands
  • Audit what commands are available in production
  • Use separate configurations for dev vs production

Use Cases

Development

  • Create test users without writing seed scripts
  • Inspect application state and configuration
  • Trigger background jobs manually
  • Clear caches and reset state

Operations

  • Monitor application health and metrics
  • Run database migrations
  • Manage feature flags
  • Inspect and modify system state

Debugging

  • Check service configurations
  • Inspect dependency injection container
  • Run diagnostic commands
  • Test individual components in isolation

Complete Example

src/app.tsTypeScript
import { createCluster } from '@justscale/cluster';
import { createController, createService } from '@justscale/core';
import { ShellFeature } from '@justscale/feature-shell';
import { Cli } from '@justscale/cli';

// Custom service
const StatsService = createService({
  inject: {},
  factory: () => {
    let requests = 0;
    return {
      increment: () => requests++,
      get: () => requests,
      reset: () => { requests = 0; },
    };
  },
});

// Custom CLI controller
const AdminController = createController({
  inject: { stats: StatsService },
  routes: (services) => ({
    stats: Cli('admin stats').handle(({ io }) => {
      io.table([
        { Metric: 'Total Requests', Value: services.stats.get() },
      ]);
    }),

    reset: Cli('admin reset').handle(({ io }) => {
      services.stats.reset();
      io.success('Stats reset!');
    }),
  }),
});

// Create cluster with shell
const cluster = createCluster({
  services: [StatsService],
  controllers: [AdminController],
  features: [ShellFeature()],
});

await cluster.serve({ http: 3000 });

console.log('Server running with interactive shell');
console.log('Run: justscale shell');