json-type-cli
v1.1.0
Published
High-performance JSON Pointer implementation
Maintainers
Readme
JSON Type CLI
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.
Overview
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.
Quick Start
Installation
npm install json-type-cliBasic Usage
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:
Request/Response Pattern
- 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
JSON Rx RPC Message Flow
- 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.
Building CLI Tools
Defining Methods
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
};
}
);Organizing Routes
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 Parameter
Provide JSON directly as the second argument:
my-cli greet '{"name": "Alice", "age": 30}'
my-cli math.add '{"a": 5, "b": 3}'2. STDIN Input
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.data3. Command Line Options
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=5432Combining Input Sources
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)
Supported Codecs
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 |
Using Output Formats
# 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:treeExtracting Response Data
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:textCommand Line Parameters
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.
Data Type Parameters
String Values (--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]' Undefined Values (--und)
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:treeUtility Parameters
Help (--help or --h)
my-cli --help # General help
my-cli method.name --help # Method-specific help Version (--version or --v)
my-cli --versionExecution Plan (--plan)
my-cli complex.operation --plan # Show what would be executedAdvanced Features
Type Introspection
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=json4Binary Data Handling
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.cborError Handling
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.jsonError 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.)
Performance Optimization
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=cborComplete Example
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:rawThis 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.
