{"id":13902966,"url":"https://github.com/mmkal/trpc-cli","last_synced_at":"2025-04-12T20:45:45.217Z","repository":{"id":241044816,"uuid":"524186749","full_name":"mmkal/trpc-cli","owner":"mmkal","description":"Turn a tRPC router into a type-safe, fully-functional, documented CLI","archived":false,"fork":false,"pushed_at":"2025-04-07T16:45:03.000Z","size":41269,"stargazers_count":122,"open_issues_count":9,"forks_count":2,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-04-07T17:42:18.959Z","etag":null,"topics":["cli","tprc","typescript"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/mmkal.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2022-08-12T18:23:50.000Z","updated_at":"2025-04-07T07:18:45.000Z","dependencies_parsed_at":"2024-05-22T05:29:12.787Z","dependency_job_id":"77fe4aaf-bad5-4202-804c-935ed6b202e9","html_url":"https://github.com/mmkal/trpc-cli","commit_stats":{"total_commits":100,"total_committers":5,"mean_commits":20.0,"dds":"0.15000000000000002","last_synced_commit":"3654a7145e4a66d95fb78137736ce3da09ff60f4"},"previous_names":["mmkal/trpc-cli"],"tags_count":28,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mmkal%2Ftrpc-cli","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mmkal%2Ftrpc-cli/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mmkal%2Ftrpc-cli/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mmkal%2Ftrpc-cli/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/mmkal","download_url":"https://codeload.github.com/mmkal/trpc-cli/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247703829,"owners_count":20982286,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cli","tprc","typescript"],"created_at":"2024-08-06T22:01:31.495Z","updated_at":"2025-04-12T20:45:45.196Z","avatar_url":"https://github.com/mmkal.png","language":"TypeScript","funding_links":[],"categories":["cli","TypeScript"],"sub_categories":[],"readme":"# trpc-cli [![Build Status](https://github.com/mmkal/trpc-cli/actions/workflows/ci.yml/badge.svg)](https://github.com/mmkal/trpc-cli/actions/workflows/ci.yml/badge.svg) [![npm](https://badgen.net/npm/v/trpc-cli)](https://www.npmjs.com/package/trpc-cli) [![X Follow](https://img.shields.io/twitter/follow/mmkalmmkal)](https://x.com/mmkalmmkal)\n\n🔥 **Build production-quality command-line tools in minutes, not days** 🔥\n\n![Demo](./docs/usage-demo.gif)\n\ntrpc-cli transforms a [tRPC](https://trpc.io) router into a professional-grade CLI with zero boilerplate. Get end-to-end type safety, input validation, auto-generated help documentation, and command completion for free.\n\n- ✅ Get all of trpc's type safety and DX building a CLI\n- ✅ Automatic positional arguments and options via zod input types (or arktype, or valibot)\n- ✅ Easily add subcommands via nested trpc routers\n- ✅ Rich helptext out of the box\n- ✅ Batteries included - no need to install any other libraries (even trpc!)\n- ✅ Use advanced tRPC features like context and middleware in your CLI\n- ✅ Build multimodal applications - use the same router for a CLI and an HTTP server, and more\n\n---\n\n## Contents\n\n\u003c!-- codegen:start {preset: markdownTOC, maxDepth: 3} --\u003e\n- [Contents](#contents)\n- [Motivation](#motivation)\n- [Installation](#installation)\n- [Usage](#usage)\n   - [Quickstart](#quickstart)\n   - [Disclaimer](#disclaimer)\n   - [Parameters and options](#parameters-and-options)\n   - [Default command](#default-command)\n   - [Complex inputs with JSON](#complex-inputs-with-json)\n   - [API docs](#api-docs)\n   - [Calculator example](#calculator-example)\n- [Validators](#validators)\n   - [zod](#zod)\n   - [arktype](#arktype)\n   - [valibot](#valibot)\n   - [effect](#effect)\n- [tRPC v10 vs v11](#trpc-v10-vs-v11)\n- [Output and lifecycle](#output-and-lifecycle)\n- [Testing your CLI](#testing-your-cli)\n- [Features and Limitations](#features-and-limitations)\n- [More Examples](#more-examples)\n   - [Migrator example](#migrator-example)\n- [Programmatic usage](#programmatic-usage)\n- [Completions](#completions)\n- [Out of scope](#out-of-scope)\n- [Contributing](#contributing)\n   - [Implementation and dependencies](#implementation-and-dependencies)\n   - [Testing](#testing)\n\u003c!-- codegen:end --\u003e\n\n## Motivation\n\ntRPC offers best-in-class type-safety and DX for building \"procedures\" that validate their inputs, and abide by their own contracts. This library gives you all those DX benefits, and allows mapping the procedures directly to a CLI. This offers the easiest way to build a CLI while mapping parsed options into strongly-typed inputs, and automatically outputs `--help` documentation that's always up-to-date.\n\nThis isn't just the easiest and safest way to build a CLI, but you also get all the benefits of tRPC (and zod). For inputs, you can use zod regex types, transforms, refinements, and those will map directly into useful help-text for CLI users, and corresponding type correctness when maintaining your CLI program. You can also use tRPC context and middleware functionality just like you could if you were building a server. And as an added bonus, it becomes trivially easy to turn your CLI program into a fully-functional HTTP server. Or, you could add a \"programmatic usage\" to your library, just by wrapping your server with the built-in `createCaller` function from tRPC. This would all, of course, have runtime and compile-time type safety.\n\n## Installation\n\n```\nnpm install trpc-cli\n```\n\n## Usage\n\n### Quickstart\n\nThe fastest way to get going is to write a normal tRPC router, using `trpcServer` and `zod` exports from this library, and turn it into a fully-functional CLI by passing it to `createCli`:\n\n```ts\nimport {createCli, trpcServer, zod as z, type TrpcCliMeta} from 'trpc-cli'\n\nconst t = trpcServer.initTRPC.meta\u003cTrpcCliMeta\u003e().create()\n\nconst router = t.router({\n  add: t.procedure\n    .input(z.object({left: z.number(), right: z.number()}))\n    .query(({input}) =\u003e input.left + input.right),\n})\n\ncreateCli({router}).run()\n```\n\nAnd that's it! Your tRPC router is now a CLI program with help text and input validation. You can run it with `node yourscript add --left 2 --right 3`.\n\n[Docs here](https://trpc.io/docs/server/routers) if you're not familiar with tRPC.\n\nYou can also create a tRPC router in the usual way using imports from `@trpc/server` and `zod` - the builtin exports are purely a convenience for simple use-case:\n\n```ts\nimport {initTRPC} from '@trpc/server'\nimport {createCli} from 'trpc-cli'\nimport {z} from 'zod'\n\nconst t = initTRPC.create()\n\nexport const router = t.router({\n  add: t.procedure\n    .input(z.object({left: z.number(), right: z.number()}))\n    .query(({input}) =\u003e input.left + input.right),\n})\n\nconst cli = createCli({router})\ncli.run()\n```\n\n### Disclaimer\n\n\u003eNote that this library is still v0, so parts of the API may change slightly. The basic usage of `createCli({router}).run()` will remain though, and any breaking changes will be published via release notes.\n\n### Parameters and options\n\nCLI positional arguments and options are derived from each procedure's input type. Inputs use `zod` types for the procedure to be mapped to a CLI command.\n\n#### positional arguments\n\npositional arguments passed to the CLI can be declared with types representing strings, numbers or booleans:\n\n```ts\nt.router({\n  double: t.procedure\n    .input(z.number()) //\n    .query(({input}) =\u003e input * 2),\n})\n```\n\nYou can also use anything that accepts string, number, or boolean inputs, like `z.enum(['up', 'down'])`, `z.number().int()`, `z.literal(123)`, `z.string().regex(/^\\w+$/)` etc.\n\nMultiple positional arguments can use a `z.tuple(...)` input type:\n\n```ts\nt.router({\n  add: t.procedure\n    .input(z.tuple([z.number(), z.number()]))\n    .query(({input}) =\u003e input[0] + input[1]),\n})\n```\n\nWhich is invoked like `path/to/cli add 2 3` (outputting `5`).\n\n\u003eNote: positional arguments can use `.optional()` or `.nullish()`, but not `.nullable()`.\n\n\u003eNote: positional arguments can be named using `.describe('name of parameter')`, but names should not include any special characters.\n\n\u003eNote: positional arguments are parsed based on the expected target type. Booleans must be written as `true` or `false`, spelled out. In most cases, though, you'd be better off using [options](#options) for boolean inputs.\n\nArray/spread parameters can use an array input type:\n\n```ts\nt.router({\n  lint: t.procedure\n    .input(z.array(z.string()).describe('file paths to lint'))\n    .mutation(({input}) =\u003e {\n      lintFiles(input.map(file =\u003e path.join(process.cwd(), file)))\n    }),\n})\n```\n\nWhich is invoked like `path/to/mycli lint file1 file2 file3 file4`.\n\nArray inputs can also be used with [options](#options) by nesting them in a tuple.\n\n```ts\nt.router({\n  lint: t.procedure\n    .input(\n      z.tuple([\n        z.array(z.string()).describe('file paths to lint'),\n        z.object({maxWarnings: z.number().default(10)}),\n      ]),\n    )\n    .mutation(({input}) =\u003e {\n      const result = lintFiles(\n        input.files.map(file =\u003e path.join(process.cwd(), file)),\n      )\n      if (result.warnings.length \u003e input.maxWarnings) {\n        throw new Error(`Too many warnings: ${result.warnings.length}`)\n      }\n    }),\n})\n```\n\nWhich could be invoked with any of:\n\n- `mycli lint file1 file2 file3 file4 --max-warnings 10`\n- `mycli lint file1 file2 file3 file4 --maxWarnings=10`\n- `mycli lint --maxWarnings=10 file1 file2 file3 file4`\n- `mycli lint --maxWarnings 10 file1 file2 file3 file4`\n\n#### Options\n\n`z.object(...)` inputs become options (passed with `--foo bar` or `--foo=bar`) syntax. Values are accepted in `--kebab-case`, and are parsed like in most CLI programs:\n\nStrings:\n\n- `z.object({foo: z.string()})` will map:\n  - `--foo bar` or `--foo=bar` to `{foo: 'bar'}`\n\nBooleans:\n\n- `z.object({foo: z.boolean()})` or `z.object({foo: z.boolean().default(false)})` will map:\n   - no option supplied to `{foo: false}`\n   - `--foo` `{foo: true}`\n\n- `z.object({foo: z.boolean().default(true)})` will map:\n   - no option supplied to `{foo: true}`\n   - `--no-foo` `{foo: false}`\n\n- `z.object({foo: z.boolean().optional()})` will map:\n  - no option supplied to `{}` (foo is undefined)\n  - `--foo` to `{foo: true}`\n  - `--foo false` to `{foo: false}` (note: `--no-foo` doesn't work here, because its existence prevents `{}` from being the default value)\n\nNumbers:\n\n- `z.object({foo: z.number()})` will map:\n   - `--foo 1` or `--foo=1` to `{foo: 1}`\n\nOther types:\n- `z.object({ foo: z.object({ bar: z.number() }) })` will parse inputs as JSON:\n   - `--foo '{\"bar\": 1}'` maps to `{foo: {bar: 1}}`\n\nMulti-word options:\n\n- `z.object({ multiWord: z.string() })` will map:\n  - `--multi-word foo` to `{multiWord: 'foo'}`\n\nUnions and intersections should also work as expected, but make sure to test them thoroughly, especially if they are deeply-nested.\n\n#### Both\n\nTo use positional arguments _and_ options, use a tuple with an object at the end:\n\n```ts\nt.router({\n  copy: t.procedure\n    .input(\n      z.tuple([\n        z.string().describe('source'),\n        z.string().describe('target'),\n        z.object({\n          mkdirp: z\n            .boolean()\n            .optional()\n            .describe(\"Ensure target's parent directory exists before copying\"),\n        }),\n      ]),\n    )\n    .mutation(async ({input: [source, target, opts]}) =\u003e {\n      if (opts.mkdirp) {\n        await fs.mkdir(path.dirname(target, {recursive: true}))\n      }\n      await fs.copyFile(source, target)\n    }),\n})\n```\n\nYou might use the above with a command like:\n\n```\npath/to/cli copy a.txt b.txt --mkdirp\n```\n\n\u003eNote: object types for options must appear _last_ in the `.input(...)` tuple, when being used with positional arguments. So `z.tuple([z.string(), z.object({mkdirp: z.boolean()}), z.string()])` would not be allowed.\n\n\u003eYou can pass an existing tRPC router that's primarily designed to be deployed as a server, in order to invoke your procedures directly in development.\n\n### Default command\n\nYou can define a default command for your CLI - set this to the procedure that should be invoked directly when calling your CLI. Useful for simple CLIs that only do one thing, or when you want to make the most common command very quick to type (e.g. `yarn` being an alias for `yarn install`):\n\n```ts\n#!/usr/bin/env node\n// filename: yarn\nconst router = t.router({\n  install: t.procedure //\n    .meta({default: true})\n    .mutation(() =\u003e console.log('installing...')),\n})\n\nconst cli = createCli({router})\n\ncli.run()\n```\n\nThe above can be invoked with either `yarn` or `yarn install`. You can also set `default: true` on subcommands, which makes them the default for their parent.\n\n### Complex inputs with JSON\n\nProcedures with inputs that cannot be cleanly mapped to positional arguments and CLI options are automatically configured to accept a JSON string via the `--input` option. This ensures that every procedure in your router is accessible via the CLI, even those with complex input types.\n\n```ts\nconst router = t.router({\n  foo: t.procedure\n    // This input type can't be directly mapped to CLI arguments\n    // (object in the middle of a tuple doesn't work for positional args):\n    .input(z.tuple([z.string(), z.object({abc: z.string()}), z.string()]))\n    .query(({input}) =\u003e `Got ${input[0]}, ${input[1].abc}, and ${input[2]}`),\n})\n\nconst cli = createCli({router})\n\n// Even though the input isn't ideal for CLI, you can still use it:\n// mycli foo --input '[\"first\", {\"abc\": \"middle\"}, \"last\"]'\n```\n\nRather than ignoring these procedures, trpc-cli makes them available through JSON input, allowing you to pass complex data structures that wouldn't work well with traditional CLI arguments.\n\nYou can also explicitly opt into this behavior for any procedure by setting `jsonInput: true` in its meta, regardless of whether its input could be mapped to CLI arguments.\n\n### API docs\n\n\u003c!-- codegen:start {preset: markdownFromJsdoc, source: src/index.ts, export: createCli} --\u003e\n#### [createCli](./src/index.ts#L45)\n\nRun a trpc router as a CLI.\n\n##### Params\n\n|name      |description                                                                              |\n|----------|-----------------------------------------------------------------------------------------|\n|router    |A trpc router                                                                            |\n|context   |The context to use when calling the procedures - needed if your router requires a context|\n|trpcServer|The trpc server module to use. Only needed if using trpc v10.                            |\n\n##### Returns\n\nA CLI object with a `run` method that can be called to run the CLI. The `run` method will parse the command line arguments, call the appropriate trpc procedure, log the result and exit the process. On error, it will log the error and exit with a non-zero exit code.\n\u003c!-- codegen:end --\u003e\n\n### Calculator example\n\nHere's a more involved example, along with what it outputs:\n\n\u003c!-- codegen:start {preset: custom, require: tsx/cjs, source: ./readme-codegen.ts, export: dump, file: test/fixtures/calculator.ts} --\u003e\n\u003c!-- hash:e30cac4beb319a42941777d631465ee0 --\u003e\n```ts\nimport {createCli, type TrpcCliMeta, trpcServer} from 'trpc-cli'\nimport {z} from 'zod'\n\nconst trpc = trpcServer.initTRPC.meta\u003cTrpcCliMeta\u003e().create()\n\nconst router = trpc.router({\n  add: trpc.procedure\n    .meta({\n      description:\n        'Add two numbers. Use this if you and your friend both have apples, and you want to know how many apples there are in total.',\n    })\n    .input(z.tuple([z.number(), z.number()]))\n    .query(({input}) =\u003e input[0] + input[1]),\n  subtract: trpc.procedure\n    .meta({\n      description:\n        'Subtract two numbers. Useful if you have a number and you want to make it smaller.',\n    })\n    .input(z.tuple([z.number(), z.number()]))\n    .query(({input}) =\u003e input[0] - input[1]),\n  multiply: trpc.procedure\n    .meta({\n      description:\n        'Multiply two numbers together. Useful if you want to count the number of tiles on your bathroom wall and are short on time.',\n    })\n    .input(z.tuple([z.number(), z.number()]))\n    .query(({input}) =\u003e input[0] * input[1]),\n  divide: trpc.procedure\n    .meta({\n      version: '1.0.0',\n      description:\n        \"Divide two numbers. Useful if you have a number and you want to make it smaller and `subtract` isn't quite powerful enough for you.\",\n      examples: 'divide --left 8 --right 4',\n    })\n    .input(\n      z.tuple([\n        z.number().describe('numerator'),\n        z\n          .number()\n          .refine(n =\u003e n !== 0)\n          .describe('denominator'),\n      ]),\n    )\n    .mutation(({input}) =\u003e input[0] / input[1]),\n  squareRoot: trpc.procedure\n    .meta({\n      description:\n        'Square root of a number. Useful if you have a square, know the area, and want to find the length of the side.',\n    })\n    .input(z.number())\n    .query(({input}) =\u003e {\n      if (input \u003c 0) throw new Error(`Get real`)\n      return Math.sqrt(input)\n    }),\n})\n\nvoid createCli({router}).run()\n```\n\u003c!-- codegen:end --\u003e\n\n\nRun `node path/to/cli --help` for formatted help text for the `sum` and `divide` commands.\n\n\u003c!-- codegen:start {preset: custom, require: tsx/cjs, source: ./readme-codegen.ts, export: command, command: './node_modules/.bin/tsx test/fixtures/calculator --help'} --\u003e\n`node path/to/calculator --help` output:\n\n```\nUsage: calculator [options] [command]\n\nOptions:\n  -h, --help                            display help for command\n\nCommands:\n  add \u003cparameter_1\u003e \u003cparameter_2\u003e       Add two numbers. Use this if you and\n                                        your friend both have apples, and you\n                                        want to know how many apples there are\n                                        in total.\n  subtract \u003cparameter_1\u003e \u003cparameter_2\u003e  Subtract two numbers. Useful if you have\n                                        a number and you want to make it\n                                        smaller.\n  multiply \u003cparameter_1\u003e \u003cparameter_2\u003e  Multiply two numbers together. Useful if\n                                        you want to count the number of tiles on\n                                        your bathroom wall and are short on\n                                        time.\n  divide \u003cnumerator\u003e \u003cdenominator\u003e      Divide two numbers. Useful if you have a\n                                        number and you want to make it smaller\n                                        and `subtract` isn't quite powerful\n                                        enough for you.\n  squareRoot \u003cnumber\u003e                   Square root of a number. Useful if you\n                                        have a square, know the area, and want\n                                        to find the length of the side.\n  help [command]                        display help for command\n```\n\u003c!-- codegen:end --\u003e\n\nYou can also show help text for the corresponding procedures (which become \"commands\" in the CLI):\n\n\u003c!-- codegen:start {preset: custom, require: tsx/cjs, source: ./readme-codegen.ts, export: command, command: './node_modules/.bin/tsx test/fixtures/calculator add --help'} --\u003e\n`node path/to/calculator add --help` output:\n\n```\nUsage: calculator add [options] \u003cparameter_1\u003e \u003cparameter_2\u003e\n\nAdd two numbers. Use this if you and your friend both have apples, and you want\nto know how many apples there are in total.\n\nArguments:\n  parameter_1  number (required)\n  parameter_2  number (required)\n\nOptions:\n  -h, --help   display help for command\n\n```\n\u003c!-- codegen:end --\u003e\n\nWhen passing a command along with its options, the return value will be logged to stdout:\n\n\u003c!-- codegen:start {preset: custom, require: tsx/cjs, source: ./readme-codegen.ts, export: command, command: './node_modules/.bin/tsx test/fixtures/calculator add 2 3'} --\u003e\n`node path/to/calculator add 2 3` output:\n\n```\n5\n```\n\u003c!-- codegen:end --\u003e\n\nInvalid inputs are helpfully displayed, along with help text for the associated command:\n\n\u003c!-- codegen:start {preset: custom, require: tsx/cjs, source: ./readme-codegen.ts, export: command, command: './node_modules/.bin/tsx test/fixtures/calculator add 2 notanumber', reject: false} --\u003e\n`node path/to/calculator add 2 notanumber` output:\n\n```\nValidation error\n  - Expected number, received string at index 1\n\nUsage: calculator add [options] \u003cparameter_1\u003e \u003cparameter_2\u003e\n\nAdd two numbers. Use this if you and your friend both have apples, and you want\nto know how many apples there are in total.\n\nArguments:\n  parameter_1  number (required)\n  parameter_2  number (required)\n\nOptions:\n  -h, --help   display help for command\n\n```\n\u003c!-- codegen:end --\u003e\n\n\nNote that procedures can define [`meta`](https://trpc.io/docs/server/metadata#create-router-with-typed-metadata) value with `description`, `usage` and `help` props. Zod's [`describe`](https://zod.dev/?id=describe) method allows adding descriptions to individual options.\n\n```ts\nimport {type TrpcCliMeta} from 'trpc-cli'\n\nconst trpc = initTRPC.meta\u003cTrpcCliMeta\u003e().create() // `TrpcCliMeta` is a helper interface with description, usage, and examples, but you can use your own `meta` interface, anything with a `description?: string` property will be fine\n\nconst appRouter = trpc.router({\n  divide: trpc.procedure\n    .meta({\n      description:\n        'Divide two numbers. Useful when you have a pizza and you want to share it equally between friends.',\n    })\n    .input(\n      z.object({\n        left: z.number().describe('The numerator of the division operator'),\n        right: z.number().describe('The denominator of the division operator'),\n      }),\n    )\n    .mutation(({input}) =\u003e input.left / input.right),\n})\n```\n\n## Validators\n\nYou can use any validator that [trpc supports](https://trpc.io/docs/server/validators), but for inputs to be converted into CLI arguments/options, they must be JSON schema compatible. The following validators are supported so far. Contributions are welcome for other validators - the requirement is that they must have a helper function that converts them into a JSON schema representation.\n\nNote that JSON schema representations are not in general perfect 1-1 mappings with every validator library's API, so some procedures may default to use the JSON `--input` option instead.\n\n### zod\n\nZod support is built-in, including the `zod-to-json-schema` conversion helper. You can also \"bring your own\" zod module (e.g. if you want to use a newer/older version of zod than the one included in `trpc-cli`).\n\n### arktype\n\n`arktype` includes a `toJsonSchema` method on its types, so no extra dependencies are reuqired if you're using arktype to validate your inputs.\n\n```ts\nimport {type} from 'arktype'\nimport {type TrpcCliMeta} from 'trpc-cli'\n\nconst t = initTRPC.meta\u003cTrpcCliMeta\u003e().create()\n\nconst router = t.router({\n  add: t.procedure\n    .input(type({left: 'number', right: 'number'}))\n    .query(({input}) =\u003e input.left + input.right),\n})\n\nconst cli = createCli({router})\n\ncli.run() // e.g. `mycli add --left 1 --right 2`\n```\n\n- Note: you will need to install `arktype` as a dependency separately\n- Note: some arktype features result in types that can't be converted cleanly to CLI args/options, so for some procedures you may need to use the `--input` option to pass in a JSON string. Check your CLI help text to see if this is the case. See https://github.com/arktypeio/arktype/issues/1379 for more info.\n\n### valibot\n\nValibot support is enabled via the `@valibot/to-json-schema` package. Simply install it as a dependency and it should work. If you don't have it installed, your procedures will be mapped to commands that accept a plain JSON string as input (the help text will include a message explaining how to get richer input options).\n\n```ts\nimport {type TrpcCliMeta} from 'trpc-cli'\nimport * as v from 'valibot'\n\nconst t = initTRPC.meta\u003cTrpcCliMeta\u003e().create()\n\nconst router = t.router({\n  add: t.procedure\n    .input(v.tuple([v.number(), v.number()]))\n    .query(({input}) =\u003e input[0] + input[1]),\n})\n\nconst cli = createCli({router})\n\ncli.run() // e.g. `mycli add 1 2`\n```\n\n### effect\n\nYou can also use `effect` schemas - see [trpc docs on using effect validators](https://trpc.io/docs/server/validators#with-effect) - you'll need to use the `Schema.standardSchemaV1` helper that ships with `effect`:\n\n\u003eNote: `effect` support requires `effect \u003e= 3.14.2` (which in turn depends on `@trpc/server \u003e= 11.0.1` if passing in a custom `trpcServer`).\n\n```ts\nimport {Schema} from 'effect'\nimport {type TrpcCliMeta} from 'trpc-cli'\n\nconst t = initTRPC.meta\u003cTrpcCliMeta\u003e().create()\n\nconst router = t.router({\n  add: t.procedure\n    .input(Schema.standardSchemaV1(Schema.Tuple(Schema.Number, Schema.Number)))\n    .query(({input}) =\u003e input.left + input.right),\n})\n\nconst cli = createCli({router, trpcServer: import('@trpc/server')})\n\ncli.run() // e.g. `mycli add 1 2`\n```\n\n## tRPC v10 vs v11\n\nBoth versions 10 and 11 of `@trpc/server` are both supported. v11 is included in the dependencies of this packages, so that you can use it out of the box, but you can also use your own installation. If using tRPC v10 you must pass in your `@trpc/server` module to `createCli`:\n\n```ts\nconst cli = createCli({router, trpcServer: import('@trpc/server')})\n```\n\nOr you can use top level await or `require` if you prefer:\n\n```ts\nconst cli = createCli({router, trpcServer: await import('@trpc/server')})\nconst cli = createCli({router, trpcServer: require('@trpc/server')})\n```\n\nNote: previously, when trpc v11 was in preview, v10 was included in the dependencies.\n\n## Output and lifecycle\n\nThe output of the command will be logged if it is truthy. The log algorithm aims to be friendly for bash-piping, usage with jq etc.:\n\n- Arrays will be logged line be line\n- For each line logged:\n   - string, numbers and booleans are logged directly\n   - objects are logged with `JSON.stringify(___, null, 2)`\n\nSo if the procedure returns `['one', 'two', 'three]` this will be written to stdout:\n\n```\none\ntwo\nthree\n```\n\nIf the procedure returns `[{name: 'one'}, {name: 'two'}, {name: 'three'}]` this will be written to stdout:\n\n```\n{\n  \"name\": \"one\"\n}\n{\n  \"name\": \"two\"\n}\n{\n  \"name\": \"three\"\n}\n```\n\nThis is to make it as easy as possible to use with other command line tools like `xargs`, `jq` etc. via bash-piping. If you don't want to rely on this logging, you can always log inside your procedures however you like and avoid returning a value.\n\nThe process will exit with code 0 if the command was successful, or 1 otherwise. \n\nYou can also override the `logger` and `process` properties of the `run` method to change the default return-value logging and/or process.exit behaviour:\n\n\u003c!-- eslint-disable unicorn/no-process-exit --\u003e\n```ts\nimport {createCli} from 'trpc-cli'\n\nconst cli = createCli({router: yourRouter})\n\ncli.run({\n  logger: yourLogger, // should define `.info` and `.error` methods\n  process: {\n    exit: code =\u003e {\n      if (code === 0) process.exit(0)\n      else process.exit(123)\n    },\n  },\n})\n```\n\nYou could also override `process.exit` to avoid killing the process at all - see [programmatic usage](#programmatic-usage) for an example.\n\n## Testing your CLI\n\nRather than testing your CLI via a subprocess, which is slow and doesn't provide great DX, it's better to use the router that is passed to it directly with [`createCallerFactory`](https://trpc.io/docs/server/server-side-calls#create-caller):\n\n```ts\nimport {initTRPC} from '@trpc/server'\nimport {test, expect} from 'your-test-library'\nimport {router} from '../src'\n\nconst caller = initTRPC.create().createCallerFactory(router)({})\n\ntest('add', async () =\u003e {\n  expect(await caller.add([2, 3])).toBe(5)\n})\n```\n\nIf you really want to test it as like a CLI and want to avoid a subprocess, you can also call the `run` method programmatically, and override the `process.exit` call and extract the resolve/reject values from `FailedToExitError`:\n\n```ts\nimport {createCli, FailedToExitError} from 'trpc-cli'\n\nconst run = async (argv: string[]) =\u003e {\n  const cli = createCli({router: calculatorRouter})\n  return cli\n    .run({\n      argv,\n      process: {exit: () =\u003e void 0 as never},\n      logger: {info: () =\u003e {}, error: () =\u003e {}},\n    })\n    .catch(err =\u003e {\n      // this will always throw, because our `exit` handler doesn't throw or exit the process\n      while (err instanceof FailedToExitError) {\n        if (err.exitCode === 0) {\n          return err.cause // this is the return value of the procedure that was invoked\n        }\n        err = err.cause // use the underlying error that caused the exit\n      }\n      throw err\n    })\n}\n\ntest('make sure parsing works correctly', async () =\u003e {\n  await expect(run(['add', '2', '3'])).resolves.toBe(5)\n  await expect(run(['squareRoot', '--value=4'])).resolves.toBe(2)\n  await expect(run(['squareRoot', `--value=-1`])).rejects.toMatchInlineSnapshot(\n    `[Error: Get real]`,\n  )\n  await expect(run(['add', '2', 'notanumber'])).rejects.toMatchInlineSnapshot(`\n    [Error: Validation error\n      - Expected number, received string at index 1\n\n    Usage: program add [options] \u003cparameter_1\u003e \u003cparameter_2\u003e\n\n    Arguments:\n      parameter_1   (required)\n      parameter_2   (required)\n\n    Options:\n      -h, --help   display help for command\n    ]\n  `)\n})\n```\n\nThis will give you strong types for inputs and outputs, and is essentially what `trpc-cli` does under the hood after parsing and validating command-line input.\n\nIn general, you should rely on `trpc-cli` to correctly handle the lifecycle and output etc. when it's invoked as a CLI by end-users. If there are any problems there, they should be fixed on this repo - please raise an issue.\n\n## Features and Limitations\n\n- Nested subrouters ([example](./test/fixtures/migrations.ts)) - procedures in nested routers will become subcommands will be dot separated e.g. `mycli search byId --id 123`\n- Middleware, `ctx`, multi-inputs work as normal\n- Return values are logged using `console.info` (can be configured to pass in a custom logger)\n- `process.exit(...)` called with either 0 or 1 depending on successful resolve\n- Help text shown on invalid inputs\n- Support option aliases via `aliases` meta property (see migrations example below)\n- Union types work, but they should ideally be non-overlapping for best results\n- Limitation: Only zod types are supported right now\n- Limitation: Only object types are allowed as input. No positional arguments supported\n   - If there's interest, this could be added in future for inputs of type `z.string()` or `z.tuple([z.string(), ...])`\n- Limitation: Nested-object input props must be passed as json\n   - e.g. `z.object({ foo: z.object({ bar: z.number() }) }))` can be supplied via using `--foo '{\"bar\": 123}'`\n- Limitation: No `subscription` support.\n   - In theory, this might be supportable via `@inquirer/prompts`. Proposals welcome!\n\n## More Examples\n\n### Migrator example\n\nGiven a migrations router looking like this:\n\n\u003c!-- codegen:start {preset: custom, require: tsx/cjs, source: ./readme-codegen.ts, export: dump, file: test/fixtures/migrations.ts} --\u003e\n\u003c!-- hash:db92443db1beeaa3608be050420248a0 --\u003e\n```ts\nimport {createCli, type TrpcCliMeta, trpcServer, z} from 'trpc-cli'\nimport * as trpcCompat from '../../src/trpc-compat'\n\nconst trpc = trpcServer.initTRPC.meta\u003cTrpcCliMeta\u003e().create()\n\nconst migrations = getMigrations()\n\nconst searchProcedure = trpc.procedure\n  .meta({\n    aliases: {\n      options: {status: 's'},\n    },\n  })\n  .input(\n    z.object({\n      status: z\n        .enum(['executed', 'pending'])\n        .optional()\n        .describe('Filter to only show migrations with this status'),\n    }),\n  )\n  .use(async ({next, input}) =\u003e {\n    return next({\n      ctx: {\n        filter: (list: typeof migrations) =\u003e\n          list.filter(m =\u003e !input.status || m.status === input.status),\n      },\n    })\n  })\n\nconst router = trpc.router({\n  up: trpc.procedure\n    .meta({\n      description:\n        'Apply migrations. By default all pending migrations will be applied.',\n    })\n    .input(\n      z.union([\n        z.object({}).strict(), // use strict here to make sure `{step: 1}` doesn't \"match\" this first, just by having an ignore `step` property\n        z.object({\n          to: z.string().describe('Mark migrations up to this one as exectued'),\n        }),\n        z.object({\n          step: z\n            .number()\n            .int()\n            .positive()\n            .describe('Mark this many migrations as executed'),\n        }),\n      ]),\n    )\n    .query(async ({input}) =\u003e {\n      let toBeApplied = migrations\n      if ('to' in input) {\n        const index = migrations.findIndex(m =\u003e m.name === input.to)\n        toBeApplied = migrations.slice(0, index + 1)\n      }\n      if ('step' in input) {\n        const start = migrations.findIndex(m =\u003e m.status === 'pending')\n        toBeApplied = migrations.slice(0, start + input.step)\n      }\n      toBeApplied.forEach(m =\u003e (m.status = 'executed'))\n      return migrations.map(m =\u003e `${m.name}: ${m.status}`)\n    }),\n  create: trpc.procedure\n    .meta({description: 'Create a new migration'})\n    .input(\n      z.object({name: z.string(), content: z.string()}), //\n    )\n    .mutation(async ({input}) =\u003e {\n      migrations.push({...input, status: 'pending'})\n      return migrations\n    }),\n  list: searchProcedure\n    .meta({description: 'List all migrations'})\n    .query(({ctx}) =\u003e ctx.filter(migrations)),\n  search: trpc.router({\n    byName: searchProcedure\n      .meta({description: 'Look for migrations by name'})\n      .input(z.object({name: z.string()}))\n      .query(({ctx, input}) =\u003e {\n        return ctx.filter(migrations.filter(m =\u003e m.name === input.name))\n      }),\n    byContent: searchProcedure\n      .meta({\n        description: 'Look for migrations by their script content',\n        aliases: {\n          options: {searchTerm: 'q'},\n        },\n      })\n      .input(\n        z.object({\n          searchTerm: z\n            .string()\n            .describe(\n              'Only show migrations whose `content` value contains this string',\n            ),\n        }),\n      )\n      .query(({ctx, input}) =\u003e {\n        return ctx.filter(\n          migrations.filter(m =\u003e m.content.includes(input.searchTerm)),\n        )\n      }),\n  }),\n}) satisfies trpcCompat.Trpc11RouterLike\n\nconst cli = createCli({router})\n\nvoid cli.run()\n\nfunction getMigrations() {\n  return [\n    {\n      name: 'one',\n      content: 'create table one(id int, name text)',\n      status: 'executed',\n    },\n    {\n      name: 'two',\n      content: 'create view two as select name from one',\n      status: 'executed',\n    },\n    {\n      name: 'three',\n      content: 'create table three(id int, foo int)',\n      status: 'pending',\n    },\n    {\n      name: 'four',\n      content: 'create view four as select foo from three',\n      status: 'pending',\n    },\n    {name: 'five', content: 'create table five(id int)', status: 'pending'},\n  ]\n}\n```\n\u003c!-- codegen:end --\u003e\n\nHere's how the CLI will work:\n\n\u003c!-- codegen:start {preset: custom, require: tsx/cjs, source: ./readme-codegen.ts, export: command, command: './node_modules/.bin/tsx test/fixtures/migrations --help'} --\u003e\n`node path/to/migrations --help` output:\n\n```\nUsage: migrations [options] [command]\n\nOptions:\n  -h, --help        display help for command\n\nCommands:\n  up [options]      Apply migrations. By default all pending migrations will be\n                    applied.\n  create [options]  Create a new migration\n  list [options]    List all migrations\n  search            Available subcommands: byName, byContent\n  help [command]    display help for command\n```\n\u003c!-- codegen:end --\u003e\n\n\u003c!-- codegen:start {preset: custom, require: tsx/cjs, source: ./readme-codegen.ts, export: command, command: './node_modules/.bin/tsx test/fixtures/migrations apply --help'} --\u003e\n`node path/to/migrations apply --help` output:\n\n```\nUsage: migrations [options] [command]\n\nOptions:\n  -h, --help        display help for command\n\nCommands:\n  up [options]      Apply migrations. By default all pending migrations will be\n                    applied.\n  create [options]  Create a new migration\n  list [options]    List all migrations\n  search            Available subcommands: byName, byContent\n  help [command]    display help for command\n```\n\u003c!-- codegen:end --\u003e\n\n\u003c!-- codegen:start {preset: custom, require: tsx/cjs, source: ./readme-codegen.ts, export: command, command: './node_modules/.bin/tsx test/fixtures/migrations search.byContent --help'} --\u003e\n`node path/to/migrations search.byContent --help` output:\n\n```\nUsage: migrations [options] [command]\n\nOptions:\n  -h, --help        display help for command\n\nCommands:\n  up [options]      Apply migrations. By default all pending migrations will be\n                    applied.\n  create [options]  Create a new migration\n  list [options]    List all migrations\n  search            Available subcommands: byName, byContent\n  help [command]    display help for command\n```\n\u003c!-- codegen:end --\u003e\n\n## Programmatic usage\n\nThis library should probably _not_ be used programmatically - the functionality all comes from a trpc router, which has [many other ways to be invoked](https://trpc.io/docs/community/awesome-trpc) (including the built-in `createCaller` helper bundled with `@trpc/server`).\n\nThe `.run()` function does return a value, but it's typed as `unknown` since the input is just `argv: string[]` . But if you really need to for some reason, you could override the `console.error` and `process.exit` calls:\n\n```ts\nimport {createCli} from 'trpc-cli'\n\nconst cli = createCli({router: yourAppRouter})\n\nconst runCli = async (argv: string[]) =\u003e {\n  return new Promise\u003cvoid\u003e((resolve, reject) =\u003e {\n    cli.run({\n      argv,\n      logger: yourLogger, // needs `info` and `error` methods, at least\n      process: {\n        exit: code =\u003e {\n          if (code === 0) {\n            resolve()\n          } else {\n            reject(`CLI failed with exit code ${code}`)\n          }\n        },\n      },\n    })\n  })\n}\n```\n\n## Completions\n\nCompletions are supported via [omelette](https://npmjs.com/package/omelette), which is an optional peer dependency. How to get them working:\n\n```bash\nnpm install omelette @types/omelette\n```\n\nThen, pass in an `omelette` instance to the `completion` option:\n\n```ts\nimport omelette from 'omelette'\nimport {createCli} from 'trpc-cli'\n\nconst cli = createCli({router: myRouter})\n\ncli.run({\n  completion: async () =\u003e {\n    const completion = omelette('myprogram')\n    if (process.argv.includes('--setupCompletions')) {\n      completion.setupShellInitFile()\n    }\n    if (process.argv.includes('--removeCompletions')) {\n      completion.cleanupShellInitFile()\n    }\n    return completion\n  },\n})\n```\n\nWrite the completions to your shell init file by running:\n\n```bash\nnode path/to/myprogram --setupCompletions\n```\n\nThen add an alias for the program corresponding to your `omelette` instance (in the example above, `omelette('myprogram')`):\n\n```bash\necho 'myprogram() { node path/to/myprogram.js \"$@\" }' \u003e\u003e ~/.zshrc\n```\n\nThen reload your shell:\n\n```bash\nsource ~/.zshrc\n```\n\nYou can then use tab-completion to autocomplete commands and flags.\n\n## Out of scope\n\n- No stdin reading - I'd recommend using [`@inquirer/prompts`](https://npmjs.com/package/@inquirer/prompts) which is type safe and easy to use\n- No stdout prettiness other than help text - use [`tasuku`](https://npmjs.com/package/tasuku) or [`listr2`](https://npmjs.com/package/listr2)\n\n## Contributing\n\n### Implementation and dependencies\n\nAll dependencies have zero dependencies of their own, so the dependency tree is very shallow.\n\n![Dependency tree](./docs/deps.png)\n\n- [@trpc/server](https://npmjs.com/package/@trpc/server) for the trpc router\n- [commander](https://npmjs.com/package/commander) for parsing arguments before passing to trpc\n- [zod](https://npmjs.com/package/zod) for input validation, included for convenience\n- [zod-to-json-schema](https://npmjs.com/package/zod-to-json-schema) to convert zod schemas to make them easier to recurse and format help text from\n- [zod-validation-error](https://npmjs.com/package/zod-validation-error) to make bad inputs have readable error messages\n\n`zod` and `@tprc/server` are included as dependencies for convenience, but you can use your own separate installations if you prefer. Zod 3+ and @trpc/server 10 and 11, have been tested. It should work with most versions of zod.\n\n### Testing\n\n[`vitest`](https://npmjs.com/package/vitest) is used for testing. The tests consists of the example fixtures from this readme, executed as CLIs via a subprocess. Avoiding mocks this way ensures fully realistic outputs (the tradeoff being test-speed, but they're acceptably fast for now).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmmkal%2Ftrpc-cli","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmmkal%2Ftrpc-cli","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmmkal%2Ftrpc-cli/lists"}