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:
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!"}JSON Rx RPC Protocol Implementation
This CLI tool implements the JSON Rx RPC protocol with the following characteristics:
my-cli <method> ...
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 });Input Sources (Request Composition)
The CLI composes request data from three sources in this priority order:
1. Command Line JSON ParameterProvide 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}Output Formats (Response Encoding)
The CLI supports multiple output formats through codecs:
Codec Description Use Casejson
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.
--str
or --s
)
my-cli greet --str/name=Alice my-cli config.set --s/database/host=localhost --s/database/name=mydbNumeric Values (
--num
or --n
)
my-cli math.add --num/a=10 --num/b=20 my-cli server.start --n/port=3000 --n/workers=4Boolean Values (
--bool
or --b
)
my-cli user.update --bool/active=true my-cli feature.toggle --b/enabled=falseJSON Values (
--json
or --j
)
my-cli config.merge --json/settings='{"theme": "dark"}' my-cli api.call --j/payload='[1,2,3]'
my-cli optional.field --und/optionalParamFile Input (
--file
or --f
)
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/credentialsCommand Execution (
--cmd
or --c
)
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'Format Control (
--format
or --fmt
)
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:treeSTDIN Control (
--stdin
or --in
)
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/usersOutput Control (
--stdout
or --out
)
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 helpVersion (
--version
or --v
)
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.
RetroSearch is an open source project built by @garambo | Open a GitHub Issue
Search and Browse the WWW like it's 1997 | Search results from DuckDuckGo
HTML:
3.2
| Encoding:
UTF-8
| Version:
0.7.4