JSON Type CLI is a powerful Node.js package for building command-line interface (CLI) utilities that implement the JSON Rx RPC protocol. It uses JSON as the primary data format and provides request/response communication pattern where each CLI interaction is a request that produces a response.
This package enables you to build type-safe CLI tools where:
- Each CLI command is a method with typed input/output schemas
- Input data forms the request payload (following JSON Rx RPC)
- Output data is the response payload
- Multiple input sources can be composed together
- Multiple output formats are supported (JSON, CBOR, MessagePack, etc.)
The library implements the JSON Rx RPC protocol, where each CLI interaction follows the request/response pattern. You define methods with typed schemas using the JSON Type system, and the CLI handles parsing, validation, method calling, and response formatting automatically.
npm install json-type-cli
import { createCli } from 'json-type-cli'; import { ObjectValue } from '@jsonjoy.com/json-type/lib/value/ObjectValue'; // Create a router with your methods const router = ObjectValue.create() .prop('greet', t.Function( t.Object(t.prop('name', t.str)), // Request schema t.Object(t.prop('message', t.str)) // Response schema ).options({ title: 'Greet a person', description: 'Returns a greeting message for the given name' }), async ({ name }) => ({ message: `Hello, ${name}!` }) ); // Create and run CLI const cli = createCli({ router, version: 'v1.0.0', cmd: 'my-cli' }); cli.run();
Save this as my-cli.js and run:
node my-cli.js greet '{"name": "World"}' # Output: {"message": "Hello, World!"} node my-cli.js greet --str/name=Alice # Output: {"message": "Hello, Alice!"}
This CLI tool implements the JSON Rx RPC protocol with the following characteristics:
- Each CLI command is a method call in JSON Rx RPC terms
- The method name is the first positional argument:
my-cli <method> ... - Request data is composed from multiple sources (see Input Sources)
- Response data is returned to STDOUT in the specified format
- Request Composition: CLI parses arguments and builds request payload
- Method Invocation: Calls the specified method with the request data
- Response Generation: Method returns response payload
- Response Encoding: Encodes response in the specified format (JSON, CBOR, etc.)
This follows the JSON Rx RPC Request Complete message pattern where the client (CLI) sends a complete request and receives a complete response.
Methods are defined using the JSON Type system and added to a router:
import { createCli } from 'json-type-cli'; import { ObjectValue } from '@jsonjoy.com/json-type/lib/value/ObjectValue'; const router = ObjectValue.create(); const { t } = router; // Simple echo method router.prop('echo', t.Function(t.any, t.any).options({ title: 'Echo input', description: 'Returns the input unchanged' }), async (input) => input ); // Math operations router.prop('math.add', t.Function( t.Object( t.prop('a', t.num), t.prop('b', t.num) ), t.Object(t.prop('result', t.num)) ).options({ title: 'Add two numbers', description: 'Adds two numbers and returns the result' }), async ({ a, b }) => ({ result: a + b }) ); // File processing router.prop('file.process', t.Function( t.Object( t.prop('filename', t.str), t.propOpt('encoding', t.str) ), t.Object( t.prop('size', t.num), t.prop('content', t.str) ) ).options({ title: 'Process a file', description: 'Reads and processes a file' }), async ({ filename, encoding = 'utf8' }) => { const fs = require('fs'); const content = fs.readFileSync(filename, encoding); return { size: content.length, content: content }; } );
For larger applications, organize routes into modules:
// routes/user.ts export const defineUserRoutes = <Routes extends ObjectType<any>>(r: ObjectValue<Routes>) => { return r.extend((t, r) => [ r('user.create', t.Function( t.Object( t.prop('name', t.str), t.prop('email', t.str) ), t.Object( t.prop('id', t.str), t.prop('name', t.str), t.prop('email', t.str) ) ).options({ title: 'Create a user', description: 'Creates a new user account' }), async ({ name, email }) => ({ id: generateId(), name, email }) ), r('user.get', t.Function( t.Object(t.prop('id', t.str)), t.Object( t.prop('id', t.str), t.prop('name', t.str), t.prop('email', t.str) ) ).options({ title: 'Get user by ID', description: 'Retrieves user information by ID' }), async ({ id }) => getUserById(id) ) ]); }; // main.ts import { defineUserRoutes } from './routes/user'; const router = defineUserRoutes(ObjectValue.create()); const cli = createCli({ router });
The CLI composes request data from three sources in this priority order:
Provide JSON directly as the second argument:
my-cli greet '{"name": "Alice", "age": 30}' my-cli math.add '{"a": 5, "b": 3}'
Pipe JSON data to the CLI:
echo '{"name": "Bob"}' | my-cli greet cat user.json | my-cli user.create curl -s api.example.com/data.json | my-cli process.data
Use typed parameters to build the request object:
# String values my-cli greet --str/name=Alice --str/title="Ms." # Numeric values my-cli math.add --num/a=10 --num/b=20 # Boolean values my-cli user.update --bool/active=true --bool/verified=false # JSON values my-cli config.set --json/settings='{"theme": "dark", "lang": "en"}' # Nested paths using JSON Pointer (requires parent structure to exist) my-cli user.update '{"profile": {}}' --str/profile/name="Alice" --num/profile/age=25 # To create nested structures, provide the base structure first my-cli config.set '{"database": {}}' --str/database/host=localhost --num/database/port=5432
All sources can be combined. Command line options override STDIN data, which overrides the JSON parameter:
echo '{"name": "Default", "age": 0}' | my-cli greet --str/name=Alice --num/age=30 # Result: {"name": "Alice", "age": 30}
The CLI supports multiple output formats through codecs:
| Codec | Description | Use Case |
|---|---|---|
json |
Standard JSON (default) | Human-readable, web APIs |
json2 |
Pretty JSON (2 spaces) | Development, debugging |
json4 |
Pretty JSON (4 spaces) | Documentation, config files |
cbor |
CBOR binary format | Compact binary, IoT |
msgpack |
MessagePack binary | High performance, caching |
ubjson |
Universal Binary JSON | Cross-platform binary |
text |
Formatted text output | Human-readable reports |
tree |
Tree visualization | Debugging, data exploration |
raw |
Raw data output | Binary data, strings |
# Default JSON output my-cli user.get '{"id": "123"}' # Pretty-printed JSON my-cli user.get '{"id": "123"}' --format=json4 # Binary formats my-cli data.export --format=cbor > data.cbor my-cli data.export --format=msgpack > data.msgpack # Text visualization my-cli config.get --format=tree my-cli config.get --format=text # Different input/output formats cat data.cbor | my-cli process.data --format=cbor:json echo '{"test": 123}' | my-cli echo --format=json:tree
Use the --stdout or --out parameter to extract specific parts of the response:
# Extract specific field my-cli user.get '{"id": "123"}' --out=/user/name # Extract nested data my-cli api.fetch '{"url": "example.com"}' --out=/response/data/items # Combine with format conversion my-cli data.complex --out=/results/summary --format=json:text
All parameter paths use JSON Pointer syntax as defined in RFC 6901. JSON Pointers provide a standardized way to reference specific values within JSON documents using slash-separated paths.
When a path doesn't exist in the target object, the CLI automatically creates the necessary nested structure. For example, --str/database/host=localhost will create the object {"database": {"host": "localhost"}} even if neither database nor host existed previously.
my-cli greet --str/name=Alice my-cli config.set --s/database/host=localhost --s/database/name=mydb
my-cli math.add --num/a=10 --num/b=20 my-cli server.start --n/port=3000 --n/workers=4
my-cli user.update --bool/active=true my-cli feature.toggle --b/enabled=false
my-cli config.merge --json/settings='{"theme": "dark"}' my-cli api.call --j/payload='[1,2,3]'
my-cli optional.field --und/optionalParam
Read values from files with optional format and path extraction:
# Read JSON file my-cli process.data --file/input=data.json # Read with specific codec my-cli import.data --f/data=data.cbor:cbor # Extract path from file my-cli user.create --f/profile=user.json:json:/personalInfo # Chain: file -> codec -> path my-cli complex.import --f/config=settings.msgpack:msgpack:/database/credentials
Execute commands and use their output as values:
# Use command output as string my-cli log.write --cmd/message='(echo "Current time: $(date)"):text' # Use command output as JSON my-cli api.send --c/data='(curl -s api.example.com/data):json' # Extract path from command output my-cli process.status --c/info='(ps aux | grep node):json:/0/pid'
Control input and output encoding:
# Single format (for both input and output) my-cli echo --format=cbor # Separate input and output formats my-cli convert --format=cbor:json my-cli transform --fmt=json:tree
Explicitly control STDIN data mapping:
# Use all STDIN data echo '{"name": "Alice"}' | my-cli greet --stdin # Map STDIN to specific path echo '{"users": [...]}' | my-cli process.users --in/data=/users # Map with path extraction echo '{"response": {"users": [...]}}' | my-cli save.users --in/users=/response/users
Extract specific parts of the response:
# Extract single field my-cli user.get '{"id": "123"}' --out=/name # Extract nested object my-cli api.fetch --out=/response/data # Use with format conversion my-cli complex.data --out=/results --format=json:tree
my-cli --help # General help my-cli method.name --help # Method-specific help
my-cli --version
my-cli complex.operation --plan # Show what would be executedGet information about available methods and their schemas:
# List all methods my-cli .type # Get method schema my-cli .type --out=/methodName my-cli .type --out=/user.create/req --format=tree my-cli .type --out=/user.create/res --format=json4
The CLI supports binary data through various codecs:
# Process binary data cat image.jpg | my-cli image.process --format=raw:json # Convert between binary formats cat data.cbor | my-cli convert --format=cbor:msgpack > data.msgpack # Encode JSON as binary my-cli data.export '{"large": "dataset"}' --format=json:cbor > export.cbor
Errors follow JSON Rx RPC error format and are sent to STDERR:
my-cli invalid.method 2>errors.log my-cli user.get '{"invalid": "data"}' 2>validation-errors.json
Error objects include:
message: Human-readable error descriptioncode: Stable error code for programmatic handlingerrno: Numeric error codeerrorId: Unique error identifier for loggingmeta: Additional error metadata (stack traces, etc.)
For high-performance scenarios:
# Use binary formats for large data my-cli large.dataset --format=msgpack # Use raw format for simple string/binary output my-cli get.file.content --format=raw # Stream processing with STDIN/STDOUT cat large-file.json | my-cli process.stream --format=json:cbor | my-cli save.processed --format=cbor
Here's a complete example building a file processing CLI:
import { createCli } from '@jsonjoy.com/json-type-cli'; import { ObjectValue } from '@jsonjoy.com/json-type/lib/value/ObjectValue'; import * as fs from 'fs'; import * as path from 'path'; const router = ObjectValue.create(); const { t } = router; // File operations router .prop('file.read', t.Function( t.Object( t.prop('path', t.str), t.propOpt('encoding', t.str) ), t.Object( t.prop('content', t.str), t.prop('size', t.num) ) ).options({ title: 'Read file content', description: 'Reads a file and returns its content and size' }), async ({ path: filePath, encoding = 'utf8' }) => { const content = fs.readFileSync(filePath, encoding); return { content, size: content.length }; } ) .prop('file.write', t.Function( t.Object( t.prop('path', t.str), t.prop('content', t.str), t.propOpt('encoding', t.str) ), t.Object( t.prop('success', t.bool), t.prop('bytesWritten', t.num) ) ).options({ title: 'Write file content', description: 'Writes content to a file' }), async ({ path: filePath, content, encoding = 'utf8' }) => { fs.writeFileSync(filePath, content, encoding); return { success: true, bytesWritten: Buffer.from(content, encoding).length }; } ) .prop('file.list', t.Function( t.Object( t.prop('directory', t.str), t.propOpt('pattern', t.str) ), t.Object( t.prop('files', t.Array(t.str)), t.prop('count', t.num) ) ).options({ title: 'List directory files', description: 'Lists files in a directory, optionally filtered by pattern' }), async ({ directory, pattern }) => { let files = fs.readdirSync(directory); if (pattern) { const regex = new RegExp(pattern); files = files.filter(file => regex.test(file)); } return { files, count: files.length }; } ); const cli = createCli({ router, version: 'v1.0.0', cmd: 'file-cli' }); cli.run();
Usage examples:
# Read a file file-cli file.read --str/path=package.json --format=json4 # Write content from STDIN echo "Hello World" | file-cli file.write --str/path=output.txt --in/content # List JavaScript files file-cli file.list --str/directory=src --str/pattern='\\.js$' --out=/files # Chain operations: read -> transform -> write file-cli file.read --str/path=input.json | file-cli transform.data | file-cli file.write --str/path=output.json --in/content --format=json:raw
This example demonstrates the full power of JSON Type CLI for building robust, type-safe command-line tools that implement the JSON Rx RPC protocol with rich input/output capabilities.