Uh oh!
There was an error while loading.Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork0
streamich/json-type-cli
Folders and files
| Name | Name | Last commit message | Last commit date | |
|---|---|---|---|---|
Repository files navigation
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 amethod with typed input/output schemas
- Input data forms therequest payload (following JSON Rx RPC)
- Output data is theresponse payload
- Multiple input sources can be composed together
- Multiple output formats are supported (JSON, CBOR, MessagePack, etc.)
The library implements theJSON 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 methodsconstrouter=ObjectValue.create().prop('greet',t.Function(t.Object(t.prop('name',t.str)),// Request schemat.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 CLIconstcli=createCli({ router,version:'v1.0.0',cmd:'my-cli'});cli.run();
Save this asmy-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 amethod call in JSON Rx RPC terms
- The method name is the first positional argument:
my-cli <method> ... - Request data is composed from multiple sources (seeInput 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 RPCRequest 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';constrouter=ObjectValue.create();const{ t}=router;// Simple echo methodrouter.prop('echo',t.Function(t.any,t.any).options({title:'Echo input',description:'Returns the input unchanged'}),async(input)=>input);// Math operationsrouter.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 processingrouter.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'})=>{constfs=require('fs');constcontent=fs.readFileSync(filename,encoding);return{size:content.length,content:content};});
For larger applications, organize routes into modules:
// routes/user.tsexportconstdefineUserRoutes=<RoutesextendsObjectType<any>>(r:ObjectValue<Routes>)=>{returnr.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.tsimport{defineUserRoutes}from'./routes/user';constrouter=defineUserRoutes(ObjectValue.create());constcli=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 greetcat user.json| my-cli user.createcurl -s api.example.com/data.json| my-cli process.data
Use typed parameters to build the request object:
# String valuesmy-cli greet --str/name=Alice --str/title="Ms."# Numeric valuesmy-cli math.add --num/a=10 --num/b=20# Boolean valuesmy-cli user.update --bool/active=true --bool/verified=false# JSON valuesmy-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 firstmy-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 outputmy-cli user.get'{"id": "123"}'# Pretty-printed JSONmy-cli user.get'{"id": "123"}' --format=json4# Binary formatsmy-cli data.export --format=cbor> data.cbormy-cli data.export --format=msgpack> data.msgpack# Text visualizationmy-cli config.get --format=treemy-cli config.get --format=text# Different input/output formatscat data.cbor| my-cli process.data --format=cbor:jsonecho'{"test": 123}'| my-cliecho --format=json:tree
Use the--stdout or--out parameter to extract specific parts of the response:
# Extract specific fieldmy-cli user.get'{"id": "123"}' --out=/user/name# Extract nested datamy-cli api.fetch'{"url": "example.com"}' --out=/response/data/items# Combine with format conversionmy-cli data.complex --out=/results/summary --format=json:text
All parameter paths useJSON Pointer syntax as defined inRFC 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 neitherdatabase norhost existed previously.
my-cli greet --str/name=Alicemy-cli config.set --s/database/host=localhost --s/database/name=mydb
my-cli math.add --num/a=10 --num/b=20my-cli server.start --n/port=3000 --n/workers=4
my-cli user.update --bool/active=truemy-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 filemy-cli process.data --file/input=data.json# Read with specific codecmy-cli import.data --f/data=data.cbor:cbor# Extract path from filemy-cli user.create --f/profile=user.json:json:/personalInfo# Chain: file -> codec -> pathmy-cli complex.import --f/config=settings.msgpack:msgpack:/database/credentials
Execute commands and use their output as values:
# Use command output as stringmy-cli log.write --cmd/message='(echo "Current time: $(date)"):text'# Use command output as JSONmy-cli api.send --c/data='(curl -s api.example.com/data):json'# Extract path from command outputmy-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-cliecho --format=cbor# Separate input and output formatsmy-cli convert --format=cbor:jsonmy-cli transform --fmt=json:tree
Explicitly control STDIN data mapping:
# Use all STDIN dataecho'{"name": "Alice"}'| my-cli greet --stdin# Map STDIN to specific pathecho'{"users": [...]}'| my-cli process.users --in/data=/users# Map with path extractionecho'{"response": {"users": [...]}}'| my-cli save.users --in/users=/response/users
Extract specific parts of the response:
# Extract single fieldmy-cli user.get'{"id": "123"}' --out=/name# Extract nested objectmy-cli api.fetch --out=/response/data# Use with format conversionmy-cli complex.data --out=/results --format=json:tree
my-cli --help# General helpmy-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 methodsmy-cli .type# Get method schemamy-cli .type --out=/methodNamemy-cli .type --out=/user.create/req --format=treemy-cli .type --out=/user.create/res --format=json4
The CLI supports binary data through various codecs:
# Process binary datacat image.jpg| my-cli image.process --format=raw:json# Convert between binary formatscat data.cbor| my-cli convert --format=cbor:msgpack> data.msgpack# Encode JSON as binarymy-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.method2>errors.logmy-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 datamy-cli large.dataset --format=msgpack# Use raw format for simple string/binary outputmy-cli get.file.content --format=raw# Stream processing with STDIN/STDOUTcat 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*asfsfrom'fs';import*aspathfrom'path';constrouter=ObjectValue.create();const{ t}=router;// File operationsrouter.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'})=>{constcontent=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})=>{letfiles=fs.readdirSync(directory);if(pattern){constregex=newRegExp(pattern);files=files.filter(file=>regex.test(file));}return{ files,count:files.length};});constcli=createCli({ router,version:'v1.0.0',cmd:'file-cli'});cli.run();
Usage examples:
# Read a filefile-cli file.read --str/path=package.json --format=json4# Write content from STDINecho"Hello World"| file-cli file.write --str/path=output.txt --in/content# List JavaScript filesfile-cli file.list --str/directory=src --str/pattern='\\.js$' --out=/files# Chain operations: read -> transform -> writefile-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.
About
...
Resources
Uh oh!
There was an error while loading.Please reload this page.
Stars
Watchers
Forks
Sponsor this project
Uh oh!
There was an error while loading.Please reload this page.
Packages0
Contributors3
Uh oh!
There was an error while loading.Please reload this page.