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 executed
Get 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.