├── .editorconfig ├── .gitattributes ├── .github └── workflows │ └── test.yml ├── .gitignore ├── .npmrc ├── license ├── package.json ├── readme.md ├── source ├── _app.tsx ├── generate-arguments.tsx ├── generate-command.tsx ├── generate-commands.tsx ├── generate-options.ts ├── index.ts ├── internal-types.ts ├── read-commands.ts ├── read-custom-app.ts └── types.ts ├── test ├── arguments.ts ├── commands.ts ├── fixtures │ ├── all-optional-options │ │ ├── cli.ts │ │ └── commands │ │ │ └── index.tsx │ ├── array-option │ │ ├── alias │ │ │ ├── cli.ts │ │ │ └── commands │ │ │ │ └── index.tsx │ │ ├── default-value-description │ │ │ ├── cli.ts │ │ │ └── commands │ │ │ │ └── index.tsx │ │ ├── default-value │ │ │ ├── cli.ts │ │ │ └── commands │ │ │ │ └── index.tsx │ │ ├── description │ │ │ ├── cli.ts │ │ │ └── commands │ │ │ │ └── index.tsx │ │ ├── optional │ │ │ ├── cli.ts │ │ │ └── commands │ │ │ │ └── index.tsx │ │ ├── required │ │ │ ├── cli.ts │ │ │ └── commands │ │ │ │ └── index.tsx │ │ └── value-description │ │ │ ├── cli.ts │ │ │ └── commands │ │ │ └── index.tsx │ ├── boolean-option │ │ ├── alias │ │ │ ├── cli.ts │ │ │ └── commands │ │ │ │ └── index.tsx │ │ ├── default-value │ │ │ ├── cli.ts │ │ │ └── commands │ │ │ │ └── index.tsx │ │ ├── default │ │ │ ├── cli.ts │ │ │ └── commands │ │ │ │ └── index.tsx │ │ ├── description │ │ │ ├── cli.ts │ │ │ └── commands │ │ │ │ └── index.tsx │ │ └── negated │ │ │ ├── cli.ts │ │ │ └── commands │ │ │ └── index.tsx │ ├── camelcase-argument │ │ ├── cli.ts │ │ └── commands │ │ │ └── index.tsx │ ├── camelcase-command │ │ ├── cli.ts │ │ └── commands │ │ │ ├── auth.tsx │ │ │ └── superDeploy.tsx │ ├── camelcase-option │ │ ├── cli.ts │ │ └── commands │ │ │ └── index.tsx │ ├── command-alias │ │ ├── cli.ts │ │ └── commands │ │ │ └── deploy.tsx │ ├── deeply-nested-commands │ │ ├── cli.ts │ │ └── commands │ │ │ ├── auth.tsx │ │ │ ├── deploy.tsx │ │ │ └── projects │ │ │ ├── create.tsx │ │ │ ├── index.ts │ │ │ ├── list.tsx │ │ │ └── servers │ │ │ ├── create.tsx │ │ │ ├── index.ts │ │ │ └── list.tsx │ ├── enum-argument │ │ ├── array-default-value-description │ │ │ ├── cli.ts │ │ │ └── commands │ │ │ │ └── index.tsx │ │ ├── array-default-value │ │ │ ├── cli.ts │ │ │ └── commands │ │ │ │ └── index.tsx │ │ ├── array-description │ │ │ ├── cli.ts │ │ │ └── commands │ │ │ │ └── index.tsx │ │ ├── array-name │ │ │ ├── cli.ts │ │ │ └── commands │ │ │ │ └── index.tsx │ │ ├── array │ │ │ ├── cli.ts │ │ │ └── commands │ │ │ │ └── index.tsx │ │ ├── default-value-description │ │ │ ├── cli.ts │ │ │ └── commands │ │ │ │ └── index.tsx │ │ ├── default-value │ │ │ ├── cli.ts │ │ │ └── commands │ │ │ │ └── index.tsx │ │ ├── description │ │ │ ├── cli.ts │ │ │ └── commands │ │ │ │ └── index.tsx │ │ ├── optional-array │ │ │ ├── cli.ts │ │ │ └── commands │ │ │ │ └── index.tsx │ │ ├── optional │ │ │ ├── cli.ts │ │ │ └── commands │ │ │ │ └── index.tsx │ │ ├── required │ │ │ ├── cli.ts │ │ │ └── commands │ │ │ │ └── index.tsx │ │ └── variadic │ │ │ ├── cli.ts │ │ │ └── commands │ │ │ └── index.tsx │ ├── enum-option │ │ ├── alias │ │ │ ├── cli.ts │ │ │ └── commands │ │ │ │ └── index.tsx │ │ ├── default-value-description │ │ │ ├── cli.ts │ │ │ └── commands │ │ │ │ └── index.tsx │ │ ├── default-value │ │ │ ├── cli.ts │ │ │ └── commands │ │ │ │ └── index.tsx │ │ ├── description │ │ │ ├── cli.ts │ │ │ └── commands │ │ │ │ └── index.tsx │ │ ├── optional │ │ │ ├── cli.ts │ │ │ └── commands │ │ │ │ └── index.tsx │ │ ├── required │ │ │ ├── cli.ts │ │ │ └── commands │ │ │ │ └── index.tsx │ │ └── value-description │ │ │ ├── cli.ts │ │ │ └── commands │ │ │ └── index.tsx │ ├── multiple-commands │ │ ├── cli.ts │ │ └── commands │ │ │ ├── auth.tsx │ │ │ └── deploy.tsx │ ├── nested-commands-custom-app │ │ ├── cli.ts │ │ └── commands │ │ │ ├── _app.tsx │ │ │ ├── auth.tsx │ │ │ ├── deploy.tsx │ │ │ └── servers │ │ │ ├── create.tsx │ │ │ ├── index.ts │ │ │ └── list.tsx │ ├── nested-commands │ │ ├── cli.ts │ │ └── commands │ │ │ ├── auth.tsx │ │ │ ├── deploy.tsx │ │ │ └── servers │ │ │ ├── create.tsx │ │ │ ├── index.ts │ │ │ └── list.tsx │ ├── number-argument │ │ ├── array-default-value-description │ │ │ ├── cli.ts │ │ │ └── commands │ │ │ │ └── index.tsx │ │ ├── array-default-value │ │ │ ├── cli.ts │ │ │ └── commands │ │ │ │ └── index.tsx │ │ ├── array-description │ │ │ ├── cli.ts │ │ │ └── commands │ │ │ │ └── index.tsx │ │ ├── array-name │ │ │ ├── cli.ts │ │ │ └── commands │ │ │ │ └── index.tsx │ │ ├── array │ │ │ ├── cli.ts │ │ │ └── commands │ │ │ │ └── index.tsx │ │ ├── default-value-description │ │ │ ├── cli.ts │ │ │ └── commands │ │ │ │ └── index.tsx │ │ ├── default-value │ │ │ ├── cli.ts │ │ │ └── commands │ │ │ │ └── index.tsx │ │ ├── description │ │ │ ├── cli.ts │ │ │ └── commands │ │ │ │ └── index.tsx │ │ ├── optional-array │ │ │ ├── cli.ts │ │ │ └── commands │ │ │ │ └── index.tsx │ │ ├── optional │ │ │ ├── cli.ts │ │ │ └── commands │ │ │ │ └── index.tsx │ │ ├── required │ │ │ ├── cli.ts │ │ │ └── commands │ │ │ │ └── index.tsx │ │ └── variadic │ │ │ ├── cli.ts │ │ │ └── commands │ │ │ └── index.tsx │ ├── number-option │ │ ├── alias │ │ │ ├── cli.ts │ │ │ └── commands │ │ │ │ └── index.tsx │ │ ├── default-value-description │ │ │ ├── cli.ts │ │ │ └── commands │ │ │ │ └── index.tsx │ │ ├── default-value │ │ │ ├── cli.ts │ │ │ └── commands │ │ │ │ └── index.tsx │ │ ├── description │ │ │ ├── cli.ts │ │ │ └── commands │ │ │ │ └── index.tsx │ │ ├── optional │ │ │ ├── cli.ts │ │ │ └── commands │ │ │ │ └── index.tsx │ │ ├── required │ │ │ ├── cli.ts │ │ │ └── commands │ │ │ │ └── index.tsx │ │ └── value-description │ │ │ ├── cli.ts │ │ │ └── commands │ │ │ └── index.tsx │ ├── set-option │ │ ├── alias │ │ │ ├── cli.ts │ │ │ └── commands │ │ │ │ └── index.tsx │ │ ├── default-value-description │ │ │ ├── cli.ts │ │ │ └── commands │ │ │ │ └── index.tsx │ │ ├── default-value │ │ │ ├── cli.ts │ │ │ └── commands │ │ │ │ └── index.tsx │ │ ├── description │ │ │ ├── cli.ts │ │ │ └── commands │ │ │ │ └── index.tsx │ │ ├── optional │ │ │ ├── cli.ts │ │ │ └── commands │ │ │ │ └── index.tsx │ │ ├── required │ │ │ ├── cli.ts │ │ │ └── commands │ │ │ │ └── index.tsx │ │ └── value-description │ │ │ ├── cli.ts │ │ │ └── commands │ │ │ └── index.tsx │ ├── single-command-custom-app │ │ ├── cli.ts │ │ └── commands │ │ │ ├── _app.tsx │ │ │ └── deploy.tsx │ ├── single-command │ │ ├── cli.ts │ │ └── commands │ │ │ └── index.tsx │ ├── single-default-command │ │ ├── cli.ts │ │ └── commands │ │ │ └── deploy.tsx │ ├── string-argument │ │ ├── array-default-value-description │ │ │ ├── cli.ts │ │ │ └── commands │ │ │ │ └── index.tsx │ │ ├── array-default-value │ │ │ ├── cli.ts │ │ │ └── commands │ │ │ │ └── index.tsx │ │ ├── array-description │ │ │ ├── cli.ts │ │ │ └── commands │ │ │ │ └── index.tsx │ │ ├── array-name │ │ │ ├── cli.ts │ │ │ └── commands │ │ │ │ └── index.tsx │ │ ├── array │ │ │ ├── cli.ts │ │ │ └── commands │ │ │ │ └── index.tsx │ │ ├── default-value-description │ │ │ ├── cli.ts │ │ │ └── commands │ │ │ │ └── index.tsx │ │ ├── default-value │ │ │ ├── cli.ts │ │ │ └── commands │ │ │ │ └── index.tsx │ │ ├── description │ │ │ ├── cli.ts │ │ │ └── commands │ │ │ │ └── index.tsx │ │ ├── optional-array │ │ │ ├── cli.ts │ │ │ └── commands │ │ │ │ └── index.tsx │ │ ├── optional │ │ │ ├── cli.ts │ │ │ └── commands │ │ │ │ └── index.tsx │ │ ├── required │ │ │ ├── cli.ts │ │ │ └── commands │ │ │ │ └── index.tsx │ │ └── variadic │ │ │ ├── cli.ts │ │ │ └── commands │ │ │ └── index.tsx │ └── string-option │ │ ├── alias │ │ ├── cli.ts │ │ └── commands │ │ │ └── index.tsx │ │ ├── default-value-description │ │ ├── cli.ts │ │ └── commands │ │ │ └── index.tsx │ │ ├── default-value │ │ ├── cli.ts │ │ └── commands │ │ │ └── index.tsx │ │ ├── description │ │ ├── cli.ts │ │ └── commands │ │ │ └── index.tsx │ │ ├── optional │ │ ├── cli.ts │ │ └── commands │ │ │ └── index.tsx │ │ ├── required │ │ ├── cli.ts │ │ └── commands │ │ │ └── index.tsx │ │ └── value-description │ │ ├── cli.ts │ │ └── commands │ │ └── index.tsx ├── helpers │ └── run.ts └── options.ts └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.yml] 11 | indent_style = space 12 | indent_size = 2 13 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | test: 6 | name: Test 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v3 10 | - uses: actions/setup-node@v3 11 | with: 12 | node-version: 18 13 | - run: npm install 14 | - run: npm test -- --serial --fail-fast --timeout=1m 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Vadym Demedes (vadimdemedes.com) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pastel", 3 | "version": "3.0.0", 4 | "description": "Framework for effortlessly building Ink apps", 5 | "license": "MIT", 6 | "repository": "vadimdemedes/pastel", 7 | "author": { 8 | "name": "Vadim Demedes", 9 | "email": "vadimdemedes@hey.com", 10 | "url": "https://vadimdemedes.com" 11 | }, 12 | "type": "module", 13 | "exports": { 14 | "types": "./build/index.d.ts", 15 | "default": "./build/index.js" 16 | }, 17 | "engines": { 18 | "node": ">=18" 19 | }, 20 | "scripts": { 21 | "build": "tsc", 22 | "dev": "tsc --watch", 23 | "test": "tsc --noEmit && xo && NODE_OPTIONS='--loader=ts-node/esm --experimental-specifier-resolution=node --no-warnings' ava", 24 | "prepare": "tsc" 25 | }, 26 | "files": [ 27 | "build" 28 | ], 29 | "keywords": [ 30 | "ink", 31 | "cli", 32 | "cli-app", 33 | "cli-framework" 34 | ], 35 | "dependencies": { 36 | "@inkjs/ui": "^2.0.0", 37 | "commander": "^12.1.0", 38 | "decamelize": "^6.0.0", 39 | "plur": "^5.1.0", 40 | "read-package-up": "^11.0.0", 41 | "zod-validation-error": "^3.3.0" 42 | }, 43 | "devDependencies": { 44 | "@sindresorhus/tsconfig": "^5.0.0", 45 | "@types/react": "^18.3.3", 46 | "@types/yargs": "^17.0.32", 47 | "@vdemedes/prettier-config": "^2.0.1", 48 | "ava": "^6.1.3", 49 | "eslint-config-xo-react": "^0.27.0", 50 | "eslint-plugin-react": "^7.34.2", 51 | "eslint-plugin-react-hooks": "^4.6.2", 52 | "execa": "^9.1.0", 53 | "ink": "^5.0.1", 54 | "prettier": "^3.3.1", 55 | "react": "^18.3.1", 56 | "ts-node": "^10.9.2", 57 | "typescript": "^5.4.5", 58 | "xo": "^0.58.0", 59 | "zod": "^3.23.8" 60 | }, 61 | "peerDependencies": { 62 | "ink": ">=5.0.0", 63 | "react": "^18.2.0", 64 | "zod": "^3.21.4" 65 | }, 66 | "ava": { 67 | "concurrency": 1, 68 | "extensions": { 69 | "ts": "module", 70 | "tsx": "module" 71 | }, 72 | "environmentVariables": { 73 | "NODE_NO_WARNINGS": "1" 74 | } 75 | }, 76 | "xo": { 77 | "extends": "xo-react", 78 | "prettier": true, 79 | "rules": { 80 | "react/prop-types": "off", 81 | "no-await-in-loop": "off", 82 | "unicorn/prevent-abbreviations": "off", 83 | "@typescript-eslint/no-unsafe-assignment": "off" 84 | }, 85 | "overrides": [ 86 | { 87 | "files": "test/fixtures/camelcase-command/commands/superDeploy.tsx", 88 | "rules": { 89 | "unicorn/filename-case": "off" 90 | } 91 | } 92 | ] 93 | }, 94 | "prettier": "@vdemedes/prettier-config" 95 | } 96 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | [![](https://raw.githubusercontent.com/vshymanskyy/StandWithUkraine/main/banner2-direct.svg)](https://github.com/vshymanskyy/StandWithUkraine/blob/main/docs/README.md) 2 | 3 | # Pastel [![test](https://github.com/vadimdemedes/pastel/actions/workflows/test.yml/badge.svg)](https://github.com/vadimdemedes/pastel/actions/workflows/test.yml) 4 | 5 | > Next.js-like framework for CLIs made with [Ink](https://github.com/vadimdemedes/ink). 6 | 7 | ## Features 8 | 9 | - Create files in `commands` folder to add commands. 10 | - Create folders in `commands` to add subcommands. 11 | - Define options and arguments via [Zod](https://zod.dev). 12 | - Full type-safety of options and arguments thanks to Zod. 13 | - Auto-generated help message for commands, options and arguments. 14 | - Uses battle-tested [Commander](https://github.com/tj/commander.js) package under the hood. 15 | 16 | ## Install 17 | 18 | ```console 19 | npm install pastel ink react zod 20 | ``` 21 | 22 | ## Getting started 23 | 24 | Use [create-pastel-app](https://github.com/vadimdemedes/create-pastel-app) to quickly scaffold a Pastel app with TypeScript, linter and tests set up. 25 | 26 | ```console 27 | npm create pastel-app hello-world 28 | hello-world 29 | ``` 30 | 31 |
Manual setup 32 |

33 | 34 | 1. Set up a new project. 35 | 36 | ```console 37 | mkdir hello-world 38 | cd hello-world 39 | npm init --yes 40 | ``` 41 | 42 | 2. Install Pastel and TypeScript. 43 | 44 | ```console 45 | npm install pastel 46 | npm install --save-dev typescript @sindresorhus/tsconfig 47 | ``` 48 | 49 | 3. Create a `tsconfig.json` file to set up TypeScript. 50 | 51 | ```json 52 | { 53 | "extends": "@sindresorhus/tsconfig", 54 | "compilerOptions": { 55 | "moduleResolution": "node16", 56 | "module": "node16", 57 | "outDir": "build", 58 | "sourceMap": true, 59 | "tsx": "react" 60 | }, 61 | "include": ["source"] 62 | } 63 | ``` 64 | 65 | 4. Create a `source` folder for the source code. 66 | 67 | ```console 68 | mkdir source 69 | ``` 70 | 71 | 5. Create a `source/cli.ts` file with the following code, which will be CLI's entrypoint: 72 | 73 | ```js 74 | #!/usr/bin/env node 75 | import Pastel from 'pastel'; 76 | 77 | const app = new Pastel({ 78 | importMeta: import.meta, 79 | }); 80 | 81 | await app.run(); 82 | ``` 83 | 84 | 6. Create `source/commands` folder for defining CLI's commands. 85 | 86 | ```console 87 | mkdir source/commands 88 | ``` 89 | 90 | 7. Create an `source/commands/index.tsx` file for a default command, with the following code: 91 | 92 | ```tsx 93 | import React from 'react'; 94 | import {Text} from 'ink'; 95 | import zod from 'zod'; 96 | 97 | export const options = zod.object({ 98 | name: zod.string().describe('Your name'), 99 | }); 100 | 101 | type Props = { 102 | options: zod.infer; 103 | }; 104 | 105 | export default function Index({options}: Props) { 106 | return Hello, {options.name}!; 107 | } 108 | ``` 109 | 110 | 8. Build your CLI. 111 | 112 | ```console 113 | npx tsc 114 | ``` 115 | 116 | 9. Set up an executable file. 117 | 118 | 9.1. Add `bin` field to `package.json`, which points to the compiled version of `source/cli.ts` file. 119 | 120 | ```diff 121 | "bin": "build/cli.js" 122 | ``` 123 | 124 | 9.2. Make your CLI available globally. 125 | 126 | ```console 127 | npm link --global 128 | ``` 129 | 130 | 10. Run your CLI. 131 | 132 | ```console 133 | hello-world --name=Jane 134 | ``` 135 | 136 | ``` 137 | Hello, Jane! 138 | ``` 139 | 140 | ```console 141 | hello-world --help 142 | ``` 143 | 144 | ``` 145 | Usage: hello-world [options] 146 | 147 | Options: 148 | --name Your name 149 | -v, --version Show version number 150 | -h, --help Show help 151 | 152 | ``` 153 | 154 |

155 | 156 | ## Table of contents 157 | 158 | - [Commands](#commands) 159 | - [Index commands](#index-commands) 160 | - [Default commands](#default-commands) 161 | - [Subcommands](#subcommands) 162 | - [Aliases](#aliases) 163 | - [Options](#options) 164 | - [Types](#types) 165 | - [String](#string) 166 | - [Number](#number) 167 | - [Boolean](#boolean) 168 | - [Enum](#enum) 169 | - [Array](#array) 170 | - [Set](#set) 171 | - [Optional or required options](#optional-or-required-options) 172 | - [Default value](#default-value) 173 | - [Alias](#alias) 174 | - [Arguments](#arguments) 175 | - [Types](#types-1) 176 | - [String](#string-1) 177 | - [Number](#number-1) 178 | - [Enum](#enum-1) 179 | - [Custom app](#custom-app) 180 | - [Custom program name](#custom-program-name) 181 | - [Custom description](#custom-description) 182 | - [Custom version](#custom-version) 183 | - [API](#api) 184 | 185 | ## Commands 186 | 187 | Pastel treats every file in the `commands` folder as a command, where filename is a command's name (excluding the extension). Files are expected to export a React component, which will be rendered when command is executed. 188 | 189 | You can also nest files in folders to create subcommands. 190 | 191 | Here's an example, which defines `login` and `logout` commands: 192 | 193 | ``` 194 | commands/ 195 | login.tsx 196 | logout.tsx 197 | ``` 198 | 199 | **login.tsx** 200 | 201 | ```tsx 202 | import React from 'react'; 203 | import {Text} from 'ink'; 204 | 205 | export default function Login() { 206 | return Logging in; 207 | } 208 | ``` 209 | 210 | **logout.tsx** 211 | 212 | ```tsx 213 | import React from 'react'; 214 | import {Text} from 'ink'; 215 | 216 | export default function Logout() { 217 | return Logging out; 218 | } 219 | ``` 220 | 221 | Given that your executable is named `my-cli`, you can execute these commands like so: 222 | 223 | ``` 224 | $ my-cli login 225 | $ my-cli logout 226 | ``` 227 | 228 | ### Index commands 229 | 230 | Files named `index.tsx` are index commands. They will be executed by default, when no other command isn't specified. 231 | 232 | ``` 233 | commands/ 234 | index.tsx 235 | login.tsx 236 | logout.tsx 237 | ``` 238 | 239 | Running `my-cli` without a command name will execute `index.tsx` command. 240 | 241 | ``` 242 | $ my-cli 243 | ``` 244 | 245 | Index command is useful when you're building a single-purpose CLI, which has only one command. For example, [`np`](https://github.com/sindresorhus/np) or [fast-cli](https://github.com/sindresorhus/fast-cli). 246 | 247 | ### Default commands 248 | 249 | Default commands are similar to index commands, because they too will be executed when an explicit command isn't specified. The difference is default commands still have a name, just like any other command, and they'll show up in the help message. 250 | 251 | Default commands are useful for creating shortcuts to commands that are used most often. 252 | 253 | Let's say there are 3 commands available: `deploy`, `login` and `logout`. 254 | 255 | ``` 256 | commands/ 257 | deploy.tsx 258 | login.tsx 259 | logout.tsx 260 | ``` 261 | 262 | Each of them can be executed by typing their name. 263 | 264 | ``` 265 | $ my-cli deploy 266 | $ my-cli login 267 | $ my-cli logout 268 | ``` 269 | 270 | Chances are, `deploy` command is going to be used a lot more frequently than `login` and `logout`, so it makes sense to make `deploy` a default command in this CLI. 271 | 272 | Export a variable named `isDefault` from the command file and set it to `true` to mark that command as a default one. 273 | 274 | ```diff 275 | import React from 'react'; 276 | import {Text} from 'ink'; 277 | 278 | + export const isDefault = true; 279 | 280 | export default function Deploy() { 281 | return Deploying...; 282 | } 283 | ``` 284 | 285 | Now, running `my-cli` or `my-cli deploy` will execute a `deploy` command. 286 | 287 | ``` 288 | $ my-cli 289 | ``` 290 | 291 | [Vercel's CLI](https://github.com/vercel/vercel/tree/main/packages/cli) is a real-world example of this approach, where both `vercel` and `vercel deploy` trigger a new deploy of your project. 292 | 293 | ### Subcommands 294 | 295 | As your CLI grows and more commands are added, it makes sense to group the related commands together. 296 | 297 | To do that, create nested folders in `commands` folder and put the relevant commands inside to create subcommands. Here's an example for a CLI that triggers deploys and manages domains for your project: 298 | 299 | ``` 300 | commands/ 301 | deploy.tsx 302 | login.tsx 303 | logout.tsx 304 | domains/ 305 | list.tsx 306 | add.tsx 307 | remove.tsx 308 | ``` 309 | 310 | Commands for managing domains would be executed like so: 311 | 312 | ``` 313 | $ my-cli domains list 314 | $ my-cli domains add 315 | $ my-cli domains remove 316 | ``` 317 | 318 | Subcommands can even be deeply nested within many folders. 319 | 320 | ### Aliases 321 | 322 | Commands can have an alias, which is usually a shorter alternative name for the same command. Power users prefer aliases instead of full names for commands they use often. For example, most users type `npm i` instead of `npm install`. 323 | 324 | Any command in Pastel can assign an alias by exporting a variable named `alias`: 325 | 326 | ```diff 327 | import React from 'react'; 328 | import {Text} from 'ink'; 329 | 330 | + export const alias = 'i'; 331 | 332 | export default function Install() { 333 | return Installing something...; 334 | } 335 | ``` 336 | 337 | Now the same `install` command can be executed by only typing `i`: 338 | 339 | ``` 340 | $ my-cli i 341 | ``` 342 | 343 | ## Options 344 | 345 | Commands can define options to customize their default behavior or ask for some additional data to run properly. 346 | For example, a command that creates a new server might specify options for choosing a server's name, an operating system, memory size or a region where that server should be spin up. 347 | 348 | Pastel uses [Zod](https://zod.dev) to define, parse and validate command options. Export a variable named `options` and set a Zod [object schema](https://zod.dev/?id=objects). Pastel will parse that schema and automatically set these options up. When command is executed, option values are passed via `options` prop to your component. 349 | 350 | ```tsx 351 | import React from 'react'; 352 | import {Text} from 'ink'; 353 | import zod from 'zod'; 354 | 355 | export const options = zod.object({ 356 | name: zod.string().describe('Server name'), 357 | os: zod.enum(['Ubuntu', 'Debian']).describe('Operating system'), 358 | memory: zod.number().describe('Memory size'), 359 | region: zod.enum(['waw', 'lhr', 'nyc']).describe('Region'), 360 | }); 361 | 362 | type Props = { 363 | options: zod.infer; 364 | }; 365 | 366 | export default function Deploy({options}: Props) { 367 | return ( 368 | 369 | Deploying a server named "{options.name}" running {options.os} with memory 370 | size of {options.memory} MB in {options.region} region 371 | 372 | ); 373 | } 374 | ``` 375 | 376 | With options set up, here's an example `deploy` command: 377 | 378 | ``` 379 | $ my-cli deploy --name=Test --os=Ubuntu --memory=1024 --region=waw 380 | Deploying a server named "Test" running Ubuntu with memory size of 1024 MB in waw region. 381 | ``` 382 | 383 | Help message is auto-generated for you as well. 384 | 385 | ``` 386 | $ my-cli deploy --help 387 | Usage: my-cli deploy [options] 388 | 389 | Options: 390 | --name Server name 391 | --os Operating system (choices: "Ubuntu", "Debian") 392 | --memory Memory size 393 | --region Region 394 | -v, --version Show version number 395 | -h, --help Show help 396 | ``` 397 | 398 | ### Types 399 | 400 | Pastel only supports [string](https://zod.dev/?id=strings), [number](https://zod.dev/?id=numbers), [boolean](https://zod.dev/?id=booleans), [enum](https://zod.dev/?id=zod-enums), [array](https://zod.dev/?id=arrays) and [set](https://zod.dev/?id=sets) types for defining options. 401 | 402 | #### String 403 | 404 | Example that defines a `--name` string option: 405 | 406 | ```tsx 407 | import React from 'react'; 408 | import {Text} from 'ink'; 409 | import zod from 'zod'; 410 | 411 | export const options = zod.object({ 412 | name: zod.string().describe('Your name'), 413 | }); 414 | 415 | type Props = { 416 | options: zod.infer; 417 | }; 418 | 419 | export default function Example({options}: Props) { 420 | return Name = {options.name}; 421 | } 422 | ``` 423 | 424 | ``` 425 | $ my-cli --name=Jane 426 | Name = Jane 427 | ``` 428 | 429 | #### Number 430 | 431 | Example that defines a `--age` number option: 432 | 433 | ```tsx 434 | import React from 'react'; 435 | import {Text} from 'ink'; 436 | import zod from 'zod'; 437 | 438 | export const options = zod.object({ 439 | age: zod.number().describe('Your age'), 440 | }); 441 | 442 | type Props = { 443 | options: zod.infer; 444 | }; 445 | 446 | export default function Example({options}: Props) { 447 | return Age = {options.age}; 448 | } 449 | ``` 450 | 451 | ``` 452 | $ my-cli --age=28 453 | Age = 28 454 | ``` 455 | 456 | #### Boolean 457 | 458 | Example that defines a `--compress` number option: 459 | 460 | ```tsx 461 | import React from 'react'; 462 | import {Text} from 'ink'; 463 | import zod from 'zod'; 464 | 465 | export const options = zod.object({ 466 | compress: zod.boolean().describe('Compress output'), 467 | }); 468 | 469 | type Props = { 470 | options: zod.infer; 471 | }; 472 | 473 | export default function Example({options}: Props) { 474 | return Compress = {String(options.compress)}; 475 | } 476 | ``` 477 | 478 | ``` 479 | $ my-cli --compress 480 | Compress = true 481 | ``` 482 | 483 | Boolean options are special, because they can't be required and default to `false`, even if Zod schema doesn't use a `default(false)` function. 484 | 485 | When boolean option defaults to `true`, it's treated as a negated option, which adds a `no-` prefix to its name. 486 | 487 | ```tsx 488 | import React from 'react'; 489 | import {Text} from 'ink'; 490 | import zod from 'zod'; 491 | 492 | export const options = zod.object({ 493 | compress: zod.boolean().default(true).describe("Don't compress output"), 494 | }); 495 | 496 | type Props = { 497 | options: zod.infer; 498 | }; 499 | 500 | export default function Example({options}: Props) { 501 | return Compress = {String(options.compress)}; 502 | } 503 | ``` 504 | 505 | ``` 506 | $ my-cli --no-compress 507 | Compress = false 508 | ``` 509 | 510 | #### Enum 511 | 512 | Example that defines an `--os` enum option with a set of allowed values. 513 | 514 | ```tsx 515 | import React from 'react'; 516 | import {Text} from 'ink'; 517 | import zod from 'zod'; 518 | 519 | export const options = zod.object({ 520 | os: zod.enum(['Ubuntu', 'Debian']).describe('Operating system'), 521 | }); 522 | 523 | type Props = { 524 | options: zod.infer; 525 | }; 526 | 527 | export default function Example({options}: Props) { 528 | return Operating system = {options.os}; 529 | } 530 | ``` 531 | 532 | ``` 533 | $ my-cli --os=Ubuntu 534 | Operating system = Ubuntu 535 | 536 | $ my-cli --os=Debian 537 | Operating system = Debian 538 | 539 | $ my-cli --os=Windows 540 | error: option '--os ' argument 'Windows' is invalid. Allowed choices are Ubuntu, Debian. 541 | ``` 542 | 543 | #### Array 544 | 545 | Example that defines a `--tag` array option, which can be specified multiple times. 546 | 547 | ```tsx 548 | import React from 'react'; 549 | import {Text} from 'ink'; 550 | import zod from 'zod'; 551 | 552 | export const options = zod.object({ 553 | tag: zod.array(zod.string()).describe('Tags'), 554 | }); 555 | 556 | type Props = { 557 | options: zod.infer; 558 | }; 559 | 560 | export default function Example({options}: Props) { 561 | return Tags = {options.tags.join(', ')}; 562 | } 563 | ``` 564 | 565 | ``` 566 | $ my-cli --tag=App --tag=Production 567 | Tags = App, Production 568 | ``` 569 | 570 | Array options can only include strings (`zod.string`), numbers (`zod.number`) or enums (`zod.enum`). 571 | 572 | #### Set 573 | 574 | Example that defines a `--tag` set option, which can be specified multiple times. It's similar to an array option, except duplicate values are removed, since the option's value is a [`Set`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set) instance. 575 | 576 | ```tsx 577 | import React from 'react'; 578 | import {Text} from 'ink'; 579 | import zod from 'zod'; 580 | 581 | export const options = zod.object({ 582 | tag: zod.set(zod.string()).describe('Tags'), 583 | }); 584 | 585 | type Props = { 586 | options: zod.infer; 587 | }; 588 | 589 | export default function Example({options}: Props) { 590 | return Tags = {[...options.tags].join(', ')}; 591 | } 592 | ``` 593 | 594 | ``` 595 | $ my-cli --tag=App --tag=Production --tag=Production 596 | Tags = App, Production 597 | ``` 598 | 599 | Set options can only include strings (`zod.string`), numbers (`zod.number`) or enums (`zod.enum`). 600 | 601 | ### Optional or required options 602 | 603 | Pastel determines whether option is optional or required by parsing its Zod schema. Since Zod schemas are required by default, so are options in Pastel. 604 | 605 | If an option isn't be required for a command to function properly, mark it as optional. 606 | 607 | ```tsx 608 | import React from 'react'; 609 | import {Text} from 'ink'; 610 | import zod from 'zod'; 611 | 612 | export const options = zod.object({ 613 | os: zod.enum(['Ubuntu', 'Debian']).optional().describe('Operating system'), 614 | }); 615 | 616 | type Props = { 617 | options: zod.infer; 618 | }; 619 | 620 | export default function Example({options}: Props) { 621 | return Operating system = {options.os ?? 'unspecified'}; 622 | } 623 | ``` 624 | 625 | ``` 626 | $ my-cli --os=Ubuntu 627 | Operating system = Ubuntu 628 | 629 | $ my-cli 630 | Operating system = unspecified 631 | ``` 632 | 633 | ### Default value 634 | 635 | Default value for an option can be set via a `default` function in Zod schema. 636 | 637 | ```tsx 638 | import React from 'react'; 639 | import {Text} from 'ink'; 640 | import zod from 'zod'; 641 | 642 | export const options = zod.object({ 643 | size: zod.number().default(1024).describe('Memory size'), 644 | }); 645 | 646 | type Props = { 647 | options: zod.infer; 648 | }; 649 | 650 | export default function Example({options}: Props) { 651 | return Memory = {options.size} MB; 652 | } 653 | ``` 654 | 655 | ``` 656 | $ my-cli 657 | Memory size = 1024 MB 658 | ``` 659 | 660 | JSON representation of default value will be displayed in the help message. 661 | 662 | ``` 663 | $ my-cli --help 664 | Usage: my-cli [options] 665 | 666 | Options: 667 | --size Memory size (default: 1024) 668 | ``` 669 | 670 | You can also customize it via `defaultValueDescription` option in `option` helper function. 671 | 672 | ```tsx 673 | import React from 'react'; 674 | import {Text} from 'ink'; 675 | import zod from 'zod'; 676 | import {option} from 'pastel'; 677 | 678 | export const options = zod.object({ 679 | size: zod 680 | .number() 681 | .default(1024) 682 | .describe( 683 | option({ 684 | description: 'Memory size', 685 | defaultValueDescription: '1 GB', 686 | }), 687 | ), 688 | }); 689 | 690 | type Props = { 691 | options: zod.infer; 692 | }; 693 | 694 | export default function Example({options}: Props) { 695 | return Memory = {options.size} MB; 696 | } 697 | ``` 698 | 699 | ``` 700 | $ my-cli --help 701 | Usage: my-cli [options] 702 | 703 | Options: 704 | --size Memory size (default: 1 GB) 705 | ``` 706 | 707 | ### Alias 708 | 709 | Options can specify an alias, which is usually the first letter of an original option name. 710 | 711 | ```tsx 712 | import React from 'react'; 713 | import {Text} from 'ink'; 714 | import zod from 'zod'; 715 | import {option} from 'pastel'; 716 | 717 | export const options = zod.object({ 718 | force: zod.boolean().describe( 719 | option({ 720 | description: 'Force', 721 | alias: 'f', 722 | }), 723 | ), 724 | }); 725 | 726 | type Props = { 727 | options: zod.infer; 728 | }; 729 | 730 | export default function Example({options}: Props) { 731 | return Force = {String(options.force)}; 732 | } 733 | ``` 734 | 735 | ``` 736 | $ my-cli --force 737 | Force = true 738 | 739 | $ my-cli -f 740 | Force = true 741 | ``` 742 | 743 | ## Arguments 744 | 745 | Arguments are similar to options, except they don't require a flag to specify them (e.g. `--name`) and they're always specified after command name and options. For example, [`mv`](https://linux.die.net/man/1/mv) requires 2 arguments, where first argument is a source path and second argument is a target path. 746 | 747 | ``` 748 | $ mv source.txt target.txt 749 | ``` 750 | 751 | A theoretical `mv` command in Pastel can define similar arguments like so: 752 | 753 | ```tsx 754 | import React from 'react'; 755 | import {Text} from 'ink'; 756 | import zod from 'zod'; 757 | 758 | export const args = zod.tuple([zod.string(), zod.string()]); 759 | 760 | type Props = { 761 | args: zod.infer; 762 | }; 763 | 764 | export default function Move({args}: Props) { 765 | return ( 766 | 767 | Moving from {args[0]} to {args[1]} 768 | 769 | ); 770 | } 771 | ``` 772 | 773 | ``` 774 | $ my-cli source.txt target.txt 775 | Moving from source.txt to target.txt 776 | ``` 777 | 778 | This command defines two positional arguments, which means that argument's position matters for command's execution. This is why positional arguments are defined via [zod.tuple](https://zod.dev/?id=tuples) in Zod, where a specific number of values is expected. 779 | 780 | However, there are commands like [`rm`](https://linux.die.net/man/1/rm), which can accept any number of arguments. To accomplish that in Pastel, use [`zod.array`](https://zod.dev/?id=arrays) instead. 781 | 782 | ```tsx 783 | import React from 'react'; 784 | import {Text} from 'ink'; 785 | import zod from 'zod'; 786 | 787 | export const args = zod.array(zod.string()); 788 | 789 | type Props = { 790 | args: zod.infer; 791 | }; 792 | 793 | export default function Remove({args}: Props) { 794 | return Removing {args.join(', ')}; 795 | } 796 | ``` 797 | 798 | ``` 799 | $ my-cli a.txt b.txt c.txt 800 | Removing a.txt, b.txt, c.txt 801 | ``` 802 | 803 | ### Types 804 | 805 | Pastel only supports [string](https://zod.dev/?id=strings), [number](https://zod.dev/?id=numbers) and [enum](https://zod.dev/?id=zod-enums) types for defining arguments inside [tuple](https://zod.dev/?id=tuples) or [array](https://zod.dev/?id=arrays). 806 | 807 | #### String 808 | 809 | Example that defines a string argument. 810 | 811 | ```tsx 812 | import React from 'react'; 813 | import {Text} from 'ink'; 814 | import zod from 'zod'; 815 | import {argument} from 'pastel'; 816 | 817 | export const args = zod.tuple([ 818 | zod.string().describe( 819 | argument({ 820 | name: 'name', 821 | description: 'Your name', 822 | }), 823 | ), 824 | ]); 825 | 826 | type Props = { 827 | args: zod.infer; 828 | }; 829 | 830 | export default function Hello({args}: Props) { 831 | return Hello, {args[0]}; 832 | } 833 | ``` 834 | 835 | ``` 836 | $ my-cli Jane 837 | Hello, Jane 838 | ``` 839 | 840 | #### Number 841 | 842 | Example that defines a number argument. 843 | 844 | ```tsx 845 | import React from 'react'; 846 | import {Text} from 'ink'; 847 | import zod from 'zod'; 848 | import {argument} from 'pastel'; 849 | 850 | export const args = zod.tuple([ 851 | zod.number().describe( 852 | argument({ 853 | name: 'age', 854 | description: 'Age', 855 | }), 856 | ), 857 | ]); 858 | 859 | type Props = { 860 | args: zod.infer; 861 | }; 862 | 863 | export default function Hello({args}: Props) { 864 | return Your age is {args[0]}; 865 | } 866 | ``` 867 | 868 | ``` 869 | $ my-cli 28 870 | Your age is 28 871 | ``` 872 | 873 | #### Enum 874 | 875 | Example that defines an enum argument. 876 | 877 | ```tsx 878 | import React from 'react'; 879 | import {Text} from 'ink'; 880 | import zod from 'zod'; 881 | import {argument} from 'pastel'; 882 | 883 | export const args = zod.tuple([ 884 | zod.enum(['Ubuntu', 'Debian']).describe( 885 | argument({ 886 | name: 'os', 887 | description: 'Operating system', 888 | }), 889 | ), 890 | ]); 891 | 892 | type Props = { 893 | args: zod.infer; 894 | }; 895 | 896 | export default function Example({args}: Props) { 897 | return Operating system = {args[0]}; 898 | } 899 | ``` 900 | 901 | ``` 902 | $ my-cli Ubuntu 903 | Operating system = Ubuntu 904 | ``` 905 | 906 | ### Default value 907 | 908 | Default value for an argument can be via a `default` function in Zod schema. 909 | 910 | ```tsx 911 | import React from 'react'; 912 | import {Text} from 'ink'; 913 | import zod from 'zod'; 914 | import {argument} from 'pastel'; 915 | 916 | export const args = zod.tuple([ 917 | zod 918 | .number() 919 | .default(1024) 920 | .describe( 921 | argument({ 922 | name: 'number', 923 | description: 'Some number', 924 | }), 925 | ), 926 | ]); 927 | 928 | type Props = { 929 | args: zod.infer; 930 | }; 931 | 932 | export default function Example({args}: Props) { 933 | return Some number = {args[0]}; 934 | } 935 | ``` 936 | 937 | ``` 938 | $ my-cli 939 | Some number = 1024 940 | ``` 941 | 942 | JSON representation of default value will be displayed in the help message. 943 | 944 | ``` 945 | $ my-cli --help 946 | Usage: my-cli [options] [number] 947 | 948 | Arguments: 949 | number Some number (default: 1024) 950 | ``` 951 | 952 | You can also customize it via `defaultValueDescription` option in `option` helper function. 953 | 954 | ```tsx 955 | import React from 'react'; 956 | import {Text} from 'ink'; 957 | import zod from 'zod'; 958 | import {argument} from 'pastel'; 959 | 960 | export const args = zod.tuple([ 961 | zod 962 | .number() 963 | .default(1024) 964 | .describe( 965 | argument({ 966 | name: 'number', 967 | description: 'Some number', 968 | defaultValueDescription: '1,204', 969 | }), 970 | ), 971 | ]); 972 | 973 | type Props = { 974 | args: zod.infer; 975 | }; 976 | 977 | export default function Example({args}: Props) { 978 | return Some number = {args[0]}; 979 | } 980 | ``` 981 | 982 | ``` 983 | $ my-cli --help 984 | Usage: my-cli [options] [number] 985 | 986 | Arguments: 987 | number Some number (default: 1,024) 988 | ``` 989 | 990 | ## Custom app 991 | 992 | Similar to Next.js, Pastel wraps every command component with a component exported from `commands/_app.tsx`. If this file doesn't exist, Pastel uses a default app component, which does nothing but render your component with `options` and `args` props. 993 | 994 | ```tsx 995 | import React from 'react'; 996 | import type {AppProps} from 'pastel'; 997 | 998 | export default function App({Component, commandProps}: AppProps) { 999 | return ; 1000 | } 1001 | ``` 1002 | 1003 | You can copy paste that code into `commands/_app.tsx` and add some logic that will be shared across all commands. 1004 | 1005 | ## Custom program name 1006 | 1007 | Pastel extracts a program name from the `name` field in the nearest `package.json` file. If it doesn't exist, a first argument in `process.argv` is used. 1008 | 1009 | When the name of an executable doesn't match the `name` in `package.json`, it can be customized via a `name` option during app initialization. 1010 | 1011 | ```tsx 1012 | import Pastel from 'pastel'; 1013 | 1014 | const app = new Pastel({ 1015 | name: 'custom-cli-name', 1016 | }); 1017 | 1018 | await app.run(); 1019 | ``` 1020 | 1021 | ## Custom description 1022 | 1023 | Similar to program name, Pastel looks for a description in `description` field in the nearest `package.json` file. To customize it, use a `description` option when initializating Pastel. 1024 | 1025 | ```tsx 1026 | import Pastel from 'pastel'; 1027 | 1028 | const app = new Pastel({ 1029 | description: 'Custom description', 1030 | }); 1031 | 1032 | await app.run(); 1033 | ``` 1034 | 1035 | ## Custom version 1036 | 1037 | Similar to program name and description, Pastel looks for a version in `version` field in the nearest `package.json` file. If Pastel can't find it, version will be hidden in the help message and `-v, --version` options won't be available. 1038 | 1039 | To customize it, use a `version` option during app initialization. 1040 | 1041 | ```tsx 1042 | import Pastel from 'pastel'; 1043 | 1044 | const app = new Pastel({ 1045 | version: '1.0.0 1046 | }); 1047 | 1048 | await app.run() 1049 | ``` 1050 | 1051 | ## API 1052 | 1053 | ### Pastel(options) 1054 | 1055 | Initializes a Pastel app. 1056 | 1057 | #### options 1058 | 1059 | Type: `object` 1060 | 1061 | ##### name 1062 | 1063 | Type: `string` 1064 | 1065 | Program name. Defaults to `name` in the nearest package.json or the name of the executable. 1066 | 1067 | ##### version 1068 | 1069 | Type: `string` 1070 | 1071 | Version. Defaults to `version` in the nearest package.json. 1072 | 1073 | ##### description 1074 | 1075 | Type: `string` 1076 | 1077 | Description. Defaults to `description` in the nearest package.json. 1078 | 1079 | ##### importMeta 1080 | 1081 | Type: [`ImportMeta`](https://nodejs.org/dist/latest/docs/api/esm.html#esm_import_meta) 1082 | 1083 | Pass in [`import.meta`](https://nodejs.org/dist/latest/docs/api/esm.html#esm_import_meta). This is used to find the `commands` directory. 1084 | 1085 | #### run(argv) 1086 | 1087 | Parses the arguments and runs the app. 1088 | 1089 | ##### argv 1090 | 1091 | Type: `Array`\ 1092 | Default: `process.argv` 1093 | 1094 | Program arguments. 1095 | 1096 | ### option(config) 1097 | 1098 | Set additional metadata for an option. Must be used as an argument to `describe` function from Zod. 1099 | 1100 | #### config 1101 | 1102 | Type: `object` 1103 | 1104 | ##### description 1105 | 1106 | Type: `string` 1107 | 1108 | Description. If description is missing, option won't appear in the "Options" section of the help message. 1109 | 1110 | ##### defaultValueDescription 1111 | 1112 | Type: `string` 1113 | 1114 | Description of a default value. 1115 | 1116 | ##### valueDescription 1117 | 1118 | Type: `string` 1119 | 1120 | Description of a value. Replaces "value" in `--flag ` in the help message. 1121 | 1122 | ##### alias 1123 | 1124 | Type: `string` 1125 | 1126 | Alias. Usually a first letter of the full option name. 1127 | 1128 | ### argument(config) 1129 | 1130 | Set additional metadata for an argument. Must be used as an argument to `describe` function from Zod. 1131 | 1132 | #### config 1133 | 1134 | Type: `object` 1135 | 1136 | ##### name 1137 | 1138 | Type: `string`\ 1139 | Default: `'arg'` 1140 | 1141 | Argument's name. Displayed in "Usage" part of the help message. Doesn't affect how argument is parsed. 1142 | 1143 | ##### description 1144 | 1145 | Type: `string` 1146 | 1147 | Description of an argument. If description is missing, argument won't appear in the "Arguments" section of the help message. 1148 | 1149 | ##### defaultValueDescription 1150 | 1151 | Type: `string` 1152 | 1153 | Description of a default value. 1154 | -------------------------------------------------------------------------------- /source/_app.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import type {AppProps} from './types.js'; 3 | 4 | export default function App({Component, commandProps}: AppProps) { 5 | return ; 6 | } 7 | -------------------------------------------------------------------------------- /source/generate-arguments.tsx: -------------------------------------------------------------------------------- 1 | import {isDeepStrictEqual} from 'node:util'; 2 | import {Argument} from 'commander'; 3 | import { 4 | ZodArray, 5 | ZodDefault, 6 | ZodEnum, 7 | ZodNumber, 8 | ZodOptional, 9 | ZodTuple, 10 | } from 'zod'; 11 | import decamelize from 'decamelize'; 12 | import {type CommandArguments} from './internal-types.js'; 13 | import {type CommandArgumentConfig} from './types.js'; 14 | 15 | const getConfig = ( 16 | value: string | undefined, 17 | ): CommandArgumentConfig | undefined => { 18 | return value?.startsWith('__pastel_argument_config__') 19 | ? (JSON.parse( 20 | value.replace('__pastel_argument_config__', ''), 21 | ) as CommandArgumentConfig) 22 | : undefined; 23 | }; 24 | 25 | const getName = (value: string | undefined): string | undefined => { 26 | return getConfig(value)?.name ?? value; 27 | }; 28 | 29 | const getDescription = (value: string | undefined): string | undefined => { 30 | return getConfig(value)?.description; 31 | }; 32 | 33 | const getDefaultValueDescription = ( 34 | value: string | undefined, 35 | ): string | undefined => { 36 | return getConfig(value)?.defaultValueDescription; 37 | }; 38 | 39 | export default function generateArguments( 40 | argumentsSchema: CommandArguments, 41 | ): Argument[] { 42 | let isOptionalByDefault = false; 43 | let arrayDefaultValue: unknown; 44 | let arrayName = getName(argumentsSchema.description); 45 | const arrayDescription = getDescription(argumentsSchema.description); 46 | const arrayDefaultValueDescription = getDefaultValueDescription( 47 | argumentsSchema.description, 48 | ); 49 | 50 | if (argumentsSchema instanceof ZodOptional) { 51 | isOptionalByDefault = true; 52 | argumentsSchema = argumentsSchema._def.innerType; 53 | arrayName = argumentsSchema.description ?? arrayName; 54 | } 55 | 56 | if (argumentsSchema instanceof ZodDefault) { 57 | isOptionalByDefault = true; 58 | arrayDefaultValue = argumentsSchema._def.defaultValue(); 59 | argumentsSchema = argumentsSchema._def.innerType; 60 | arrayName = argumentsSchema.description ?? arrayName; 61 | } 62 | 63 | if (argumentsSchema instanceof ZodOptional) { 64 | isOptionalByDefault = true; 65 | argumentsSchema = argumentsSchema._def.innerType; 66 | arrayName = argumentsSchema.description ?? arrayName; 67 | } 68 | 69 | const arguments_: Argument[] = []; 70 | 71 | if (argumentsSchema instanceof ZodTuple) { 72 | for (let argumentSchema of argumentsSchema._def.items) { 73 | let isOptional = isOptionalByDefault; 74 | let defaultValue: unknown; 75 | const defaultValueDescription = getDefaultValueDescription( 76 | argumentSchema.description, 77 | ); 78 | let name = getName(argumentSchema.description); 79 | const description = getDescription(argumentSchema.description); 80 | 81 | if (argumentSchema instanceof ZodOptional) { 82 | isOptional = true; 83 | argumentSchema = argumentSchema._def.innerType; 84 | name = getName(argumentSchema.description) ?? name; 85 | } 86 | 87 | if (argumentSchema instanceof ZodDefault) { 88 | isOptional = true; 89 | defaultValue = argumentSchema._def.defaultValue(); 90 | argumentSchema = argumentSchema._def.innerType; 91 | name = getName(argumentSchema.description) ?? name; 92 | } 93 | 94 | if (argumentSchema instanceof ZodOptional) { 95 | isOptional = true; 96 | argumentSchema = argumentSchema._def.innerType; 97 | name = getName(argumentSchema.description) ?? name; 98 | } 99 | 100 | name = decamelize(name ?? 'arg', {separator: '-'}); 101 | 102 | const argument = new Argument( 103 | isOptional ? `[${name}]` : `<${name}>`, 104 | description, 105 | ); 106 | 107 | if (argumentSchema instanceof ZodNumber) { 108 | argument.argParser(value => Number.parseFloat(value)); 109 | } 110 | 111 | if (argumentSchema instanceof ZodEnum) { 112 | argument.choices(argumentSchema._def.values); 113 | } 114 | 115 | if (defaultValue !== undefined) { 116 | argument.default(defaultValue, defaultValueDescription); 117 | } 118 | 119 | arguments_.push(argument); 120 | } 121 | 122 | const restSchema = argumentsSchema._def.rest; 123 | 124 | if (restSchema) { 125 | const name = getName(restSchema.description) ?? 'arg'; 126 | const argument = new Argument(`[${name}...]`); 127 | 128 | if (restSchema instanceof ZodNumber) { 129 | argument.argParser((value, previousValue) => { 130 | return [...(previousValue ?? []), Number.parseFloat(value)]; 131 | }); 132 | } 133 | 134 | if (restSchema instanceof ZodEnum) { 135 | argument.choices(restSchema._def.values); 136 | } 137 | 138 | arguments_.push(argument); 139 | } 140 | } 141 | 142 | if (argumentsSchema instanceof ZodArray) { 143 | const name = arrayName ?? 'arg'; 144 | 145 | const argument = new Argument( 146 | isOptionalByDefault ? `[${name}...]` : `<${name}...>`, 147 | arrayDescription, 148 | ); 149 | 150 | if (arrayDefaultValue !== undefined) { 151 | argument.default(arrayDefaultValue, arrayDefaultValueDescription); 152 | } 153 | 154 | if (argumentsSchema.element instanceof ZodNumber) { 155 | argument.argParser((value, previousValue) => { 156 | const joinPreviousValue = !isDeepStrictEqual( 157 | previousValue, 158 | arrayDefaultValue, 159 | ); 160 | 161 | return joinPreviousValue 162 | ? [...(previousValue ?? []), Number.parseFloat(value)] 163 | : [Number.parseFloat(value)]; 164 | }); 165 | } 166 | 167 | arguments_.push(argument); 168 | } 169 | 170 | return arguments_; 171 | } 172 | -------------------------------------------------------------------------------- /source/generate-command.tsx: -------------------------------------------------------------------------------- 1 | import process from 'node:process'; 2 | import {type Command as CommanderCommand} from 'commander'; 3 | import {render} from 'ink'; 4 | import React, {type ComponentType} from 'react'; 5 | import {StatusMessage} from '@inkjs/ui'; 6 | import {fromZodError} from 'zod-validation-error'; 7 | import {type Command} from './internal-types.js'; 8 | import generateOptions from './generate-options.js'; 9 | import generateArguments from './generate-arguments.js'; 10 | import {type AppProps} from './types.js'; 11 | 12 | const generateCommand = ( 13 | commanderCommand: CommanderCommand, 14 | pastelCommand: Command, 15 | {appComponent}: {appComponent: ComponentType}, 16 | ) => { 17 | commanderCommand.helpOption('-h, --help', 'Show help'); 18 | 19 | if (pastelCommand.description) { 20 | commanderCommand.description(pastelCommand.description); 21 | } 22 | 23 | if (pastelCommand.alias) { 24 | commanderCommand.alias(pastelCommand.alias); 25 | } 26 | 27 | const optionsSchema = pastelCommand.options; 28 | 29 | if (optionsSchema) { 30 | const options = generateOptions(optionsSchema); 31 | 32 | for (const option of options) { 33 | commanderCommand.addOption(option); 34 | } 35 | } 36 | 37 | let hasVariadicArgument = false; 38 | 39 | const argumentsSchema = pastelCommand.args; 40 | 41 | if (argumentsSchema) { 42 | const arguments_ = generateArguments(argumentsSchema); 43 | 44 | for (const argument of arguments_) { 45 | if (argument.variadic) { 46 | hasVariadicArgument = true; 47 | } 48 | 49 | commanderCommand.addArgument(argument); 50 | } 51 | } 52 | 53 | const {component} = pastelCommand; 54 | 55 | if (component) { 56 | commanderCommand.action((...input) => { 57 | // Remove the last argument, which is an instance of Commander command 58 | input.pop(); 59 | 60 | const options = input.pop() as Record; 61 | let parsedOptions: Record = {}; 62 | 63 | if (pastelCommand.options) { 64 | const result = pastelCommand.options.safeParse(options); 65 | 66 | if (result.success) { 67 | parsedOptions = result.data ?? {}; 68 | } else { 69 | render( 70 | 71 | { 72 | fromZodError(result.error, { 73 | maxIssuesInMessage: 1, 74 | prefix: '', 75 | prefixSeparator: '', 76 | }).message 77 | } 78 | , 79 | ); 80 | 81 | // eslint-disable-next-line unicorn/no-process-exit 82 | process.exit(1); 83 | } 84 | } 85 | 86 | let arguments_: unknown[] = []; 87 | 88 | if (pastelCommand.args) { 89 | const result = pastelCommand.args.safeParse( 90 | hasVariadicArgument ? input.flat() : input, 91 | ); 92 | 93 | if (result.success) { 94 | arguments_ = result.data ?? []; 95 | } else { 96 | render( 97 | 98 | { 99 | fromZodError(result.error, { 100 | maxIssuesInMessage: 1, 101 | prefix: '', 102 | prefixSeparator: '', 103 | }).message 104 | } 105 | , 106 | ); 107 | 108 | // eslint-disable-next-line unicorn/no-process-exit 109 | process.exit(1); 110 | } 111 | } 112 | 113 | render( 114 | React.createElement(appComponent, { 115 | Component: component, 116 | commandProps: { 117 | options: parsedOptions, 118 | args: arguments_, 119 | }, 120 | }), 121 | ); 122 | }); 123 | } 124 | }; 125 | 126 | export default generateCommand; 127 | -------------------------------------------------------------------------------- /source/generate-commands.tsx: -------------------------------------------------------------------------------- 1 | import {Command as CommanderCommand} from 'commander'; 2 | import {type ComponentType} from 'react'; 3 | import type {Command} from './internal-types.js'; 4 | import generateCommand from './generate-command.js'; 5 | import {type AppProps} from './types.js'; 6 | 7 | const generateCommands = ( 8 | parentCommanderCommand: CommanderCommand, 9 | pastelCommands: Map, 10 | {appComponent}: {appComponent: ComponentType}, 11 | ) => { 12 | if (pastelCommands.size > 0) { 13 | parentCommanderCommand.addHelpCommand( 14 | 'help [command]', 15 | 'Show help for command', 16 | ); 17 | } 18 | 19 | for (const [name, pastelCommand] of pastelCommands) { 20 | const commanderCommand = new CommanderCommand(name); 21 | generateCommand(commanderCommand, pastelCommand, {appComponent}); 22 | 23 | if (pastelCommand.commands) { 24 | generateCommands(commanderCommand, pastelCommand.commands, { 25 | appComponent, 26 | }); 27 | } 28 | 29 | parentCommanderCommand.addCommand(commanderCommand, { 30 | isDefault: pastelCommand.isDefault, 31 | }); 32 | } 33 | }; 34 | 35 | export default generateCommands; 36 | -------------------------------------------------------------------------------- /source/generate-options.ts: -------------------------------------------------------------------------------- 1 | import {isDeepStrictEqual} from 'node:util'; 2 | import {Option} from 'commander'; 3 | import decamelize from 'decamelize'; 4 | import { 5 | ZodArray, 6 | ZodBoolean, 7 | ZodDefault, 8 | ZodEnum, 9 | ZodNumber, 10 | ZodOptional, 11 | ZodSet, 12 | } from 'zod'; 13 | import plur from 'plur'; 14 | import {type CommandOptions} from './internal-types.js'; 15 | import {type CommandOptionConfig} from './types.js'; 16 | 17 | const getConfig = ( 18 | value: string | undefined, 19 | ): CommandOptionConfig | undefined => { 20 | return value?.startsWith('__pastel_option_config__') 21 | ? (JSON.parse( 22 | value.replace('__pastel_option_config__', ''), 23 | ) as CommandOptionConfig) 24 | : undefined; 25 | }; 26 | 27 | const getDescription = (value: string | undefined): string | undefined => { 28 | return getConfig(value)?.description ?? value; 29 | }; 30 | 31 | const getDefaultValueDescription = ( 32 | value: string | undefined, 33 | ): string | undefined => { 34 | return getConfig(value)?.defaultValueDescription; 35 | }; 36 | 37 | const getValueDescription = (value: string | undefined): string | undefined => { 38 | return getConfig(value)?.valueDescription; 39 | }; 40 | 41 | const getAlias = (value: string | undefined): string | undefined => { 42 | return getConfig(value)?.alias; 43 | }; 44 | 45 | export default function generateOptions( 46 | optionsSchema: CommandOptions, 47 | ): Option[] { 48 | let isOptionalByDefault = false; 49 | 50 | if (optionsSchema instanceof ZodOptional) { 51 | isOptionalByDefault = true; 52 | optionsSchema = optionsSchema._def.innerType; 53 | } 54 | 55 | const options: Option[] = []; 56 | 57 | for (let [name, optionSchema] of Object.entries(optionsSchema._def.shape())) { 58 | name = decamelize(name, {separator: '-'}); 59 | 60 | let defaultValue: unknown; 61 | 62 | let defaultValueDescription = getDefaultValueDescription( 63 | optionSchema.description, 64 | ); 65 | 66 | const description = getDescription(optionSchema.description); 67 | let valueDescription = getValueDescription(optionSchema.description); 68 | let isOptional = isOptionalByDefault; 69 | 70 | // Unwrap zod.string().optional() 71 | if (optionSchema instanceof ZodOptional) { 72 | isOptional = true; 73 | optionSchema = optionSchema._def.innerType; 74 | } 75 | 76 | // Unwrap zod.string().optional().default() 77 | if (optionSchema instanceof ZodDefault) { 78 | isOptional = true; 79 | defaultValue = optionSchema._def.defaultValue(); 80 | optionSchema = optionSchema._def.innerType; 81 | } 82 | 83 | // Unwrap zod.string().default().optional() 84 | if (optionSchema instanceof ZodOptional) { 85 | isOptional = true; 86 | optionSchema = optionSchema._def.innerType; 87 | } 88 | 89 | const alias = getAlias(optionSchema.description); 90 | let flag = `--${name}`; 91 | 92 | if (optionSchema instanceof ZodBoolean && defaultValue === true) { 93 | flag = `--no-${name}`; 94 | } else if (alias) { 95 | flag = `-${alias}, --${name}`; 96 | } 97 | 98 | const expectsValue = !(optionSchema instanceof ZodBoolean); 99 | 100 | if (expectsValue) { 101 | const isVariadic = 102 | optionSchema instanceof ZodArray || optionSchema instanceof ZodSet; 103 | 104 | valueDescription ||= isVariadic ? plur(name) : name; 105 | 106 | const rest = isVariadic ? '...' : ''; 107 | 108 | flag += 109 | isOptional || defaultValue 110 | ? ` [${valueDescription}${rest}]` 111 | : ` <${valueDescription}${rest}>`; 112 | } 113 | 114 | const option = new Option(flag, description); 115 | 116 | if (optionSchema instanceof ZodNumber) { 117 | option.argParser(value => Number.parseFloat(value)); 118 | } 119 | 120 | if (optionSchema instanceof ZodEnum) { 121 | option.choices(optionSchema._def.values); 122 | } 123 | 124 | if (optionSchema instanceof ZodBoolean && defaultValue === undefined) { 125 | defaultValue = false; 126 | } 127 | 128 | if (optionSchema instanceof ZodSet) { 129 | option.argParser((value, previousValue) => { 130 | const joinPreviousValue = 131 | previousValue instanceof Set && 132 | !isDeepStrictEqual(previousValue, defaultValue); 133 | 134 | return joinPreviousValue 135 | ? new Set([...previousValue, value]) 136 | : new Set([value]); 137 | }); 138 | } 139 | 140 | if (defaultValue !== undefined) { 141 | if (!defaultValueDescription && defaultValue instanceof Set) { 142 | defaultValueDescription = JSON.stringify([...defaultValue]); 143 | } 144 | 145 | option.default(defaultValue, defaultValueDescription); 146 | } 147 | 148 | options.push(option); 149 | } 150 | 151 | return options; 152 | } 153 | -------------------------------------------------------------------------------- /source/index.ts: -------------------------------------------------------------------------------- 1 | import {fileURLToPath} from 'node:url'; 2 | import process from 'node:process'; 3 | import {Command} from 'commander'; 4 | import {readPackageUp} from 'read-package-up'; 5 | import generateCommand from './generate-command.js'; 6 | import readCommands from './read-commands.js'; 7 | import generateCommands from './generate-commands.js'; 8 | import App from './_app.js'; 9 | import readCustomApp from './read-custom-app.js'; 10 | import type {CommandArgumentConfig, CommandOptionConfig} from './types.js'; 11 | 12 | export type Options = { 13 | /** 14 | * Program name. Defaults to `name` in the nearest package.json or the name of the executable. 15 | */ 16 | name?: string; 17 | 18 | /** 19 | * Version. Defaults to `version` in the nearest package.json. 20 | */ 21 | version?: string; 22 | 23 | /** 24 | * Description. Defaults to `description` in the nearest package.json. 25 | */ 26 | description?: string; 27 | 28 | /** 29 | * Pass in [`import.meta`](https://nodejs.org/dist/latest/docs/api/esm.html#esm_import_meta). This is used to find the `commands` directory. 30 | */ 31 | importMeta: ImportMeta; 32 | }; 33 | 34 | export default class Pastel { 35 | constructor(private readonly options: Options) {} 36 | 37 | /** 38 | * Run the app. 39 | */ 40 | async run(argv: string[] = process.argv) { 41 | const commandsDirectory = fileURLToPath( 42 | new URL('commands', this.options.importMeta.url), 43 | ); 44 | 45 | const appComponent = (await readCustomApp(commandsDirectory)) ?? App; 46 | const program = new Command(); 47 | 48 | const commands = await readCommands(commandsDirectory); 49 | const indexCommand = commands.get('index'); 50 | 51 | if (indexCommand) { 52 | generateCommand(program, indexCommand, {appComponent}); 53 | commands.delete('index'); 54 | } 55 | 56 | generateCommands(program, commands, {appComponent}); 57 | 58 | if (this.options.name) { 59 | program.name(this.options.name); 60 | } 61 | 62 | const package_ = await readPackageUp(); 63 | 64 | const version = this.options.version ?? package_?.packageJson.version; 65 | 66 | if (version) { 67 | program.version(version, '-v, --version', 'Show version number'); 68 | } 69 | 70 | const description = 71 | indexCommand?.description ?? 72 | this.options.description ?? 73 | package_?.packageJson.description ?? 74 | ''; 75 | 76 | program.description(description); 77 | program.helpOption('-h, --help', 'Show help'); 78 | program.parse(argv); 79 | } 80 | } 81 | 82 | /** 83 | * Set additional metadata for an option. Must be used as an argument to `describe` function from Zod. 84 | */ 85 | export function option(config: CommandOptionConfig) { 86 | return `__pastel_option_config__${JSON.stringify(config)}`; 87 | } 88 | 89 | /** 90 | * Set additional metadata for an argument. Must be used as an argument to `describe` function from Zod. 91 | */ 92 | export function argument(config: CommandArgumentConfig) { 93 | return `__pastel_argument_config__${JSON.stringify(config)}`; 94 | } 95 | 96 | export * from './types.js'; 97 | -------------------------------------------------------------------------------- /source/internal-types.ts: -------------------------------------------------------------------------------- 1 | import type {ComponentType} from 'react'; 2 | import type { 3 | ZodObject, 4 | ZodOptional, 5 | ZodDefault, 6 | ZodTuple, 7 | ZodArray, 8 | ZodSet, 9 | ZodString, 10 | ZodNumber, 11 | ZodEnum, 12 | ZodBoolean, 13 | ZodTypeAny, 14 | } from 'zod'; 15 | import {type AppProps} from './types.js'; 16 | 17 | export type Command = { 18 | name: string; 19 | description?: string; 20 | isDefault: boolean; 21 | alias?: string; 22 | options?: CommandOptions; 23 | args?: CommandArguments; 24 | component?: ComponentType; 25 | commands?: Map; 26 | parentCommand?: Command; 27 | }; 28 | 29 | export type CommandExports = { 30 | description?: string; 31 | isDefault?: boolean; 32 | alias?: string; 33 | options?: CommandOptions; 34 | args?: CommandArguments; 35 | default?: ComponentType; 36 | }; 37 | 38 | export type CommandOptions = ZodMaybeOptional< 39 | ZodObject> 40 | >; 41 | 42 | export type ZodMaybeOptional = T | ZodOptional; 43 | 44 | export type ZodMaybeOptionalOrDefault = 45 | | T 46 | | ZodOptional 47 | | ZodDefault 48 | | ZodOptional> 49 | | ZodDefault>; 50 | 51 | export type CommandOption = 52 | | ZodMaybeOptionalOrDefault 53 | | ZodMaybeOptionalOrDefault 54 | | ZodMaybeOptionalOrDefault> 55 | | ZodMaybeOptionalOrDefault 56 | | ZodMaybeOptionalOrDefault> 57 | | ZodMaybeOptionalOrDefault> 58 | | ZodMaybeOptionalOrDefault>> 59 | | ZodMaybeOptionalOrDefault> 60 | | ZodMaybeOptionalOrDefault> 61 | | ZodMaybeOptionalOrDefault>>; 62 | 63 | export type CommandArguments = ZodMaybeOptionalOrDefault< 64 | CommandArgumentsTuple | CommandArgumentsArray 65 | >; 66 | 67 | export type CommandArgumentsTuple = ZodTuple< 68 | [ 69 | ZodMaybeOptionalOrDefault, 70 | ...Array>, 71 | ], 72 | // eslint-disable-next-line @typescript-eslint/ban-types 73 | CommandArgument | null 74 | >; 75 | 76 | export type CommandArgumentsArray = 77 | | ZodArray 78 | | ZodArray 79 | | ZodArray>; 80 | 81 | export type CommandArgument = 82 | | ZodString 83 | | ZodNumber 84 | | ZodEnum<[string, ...string[]]>; 85 | -------------------------------------------------------------------------------- /source/read-commands.ts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs/promises'; 2 | import path from 'node:path'; 3 | import {pathToFileURL} from 'node:url'; 4 | import decamelize from 'decamelize'; 5 | import type {CommandExports, Command} from './internal-types.js'; 6 | 7 | const readCommands = async ( 8 | directory: string, 9 | ): Promise> => { 10 | const commands = new Map(); 11 | const files = await fs.readdir(directory); 12 | 13 | for (const file of files) { 14 | if (file.startsWith('_app')) { 15 | continue; 16 | } 17 | 18 | const filePath = path.join(directory, file); 19 | const stat = await fs.stat(filePath); 20 | 21 | if (stat.isDirectory()) { 22 | const subCommands = await readCommands(filePath); 23 | const indexCommand = subCommands.get('index'); 24 | 25 | if (indexCommand) { 26 | indexCommand.name = file; 27 | indexCommand.commands = subCommands; 28 | subCommands.delete('index'); 29 | commands.set(file, indexCommand); 30 | continue; 31 | } 32 | 33 | const command: Command = indexCommand ?? { 34 | name: file, 35 | isDefault: false, 36 | commands: subCommands, 37 | }; 38 | 39 | commands.set(file, command); 40 | continue; 41 | } 42 | 43 | if (!/\.(js|ts)x?$/.test(file) || file.endsWith('.d.ts')) { 44 | continue; 45 | } 46 | 47 | const fileUrl = pathToFileURL(filePath); 48 | const m = (await import(fileUrl.href)) as CommandExports; 49 | const name = decamelize(file.replace(/\.(js|ts)x?$/, ''), {separator: '-'}); 50 | 51 | commands.set(name, { 52 | name, 53 | description: m.description, 54 | isDefault: m.isDefault ?? false, 55 | alias: m.alias, 56 | options: m.options, 57 | args: m.args, 58 | component: m.default, 59 | }); 60 | } 61 | 62 | return commands; 63 | }; 64 | 65 | export default readCommands; 66 | -------------------------------------------------------------------------------- /source/read-custom-app.ts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs/promises'; 2 | import path from 'node:path'; 3 | import {type ComponentType} from 'react'; 4 | import {type AppProps} from './types.js'; 5 | 6 | type AppExports = { 7 | default: ComponentType; 8 | }; 9 | 10 | export default async function readCustomApp( 11 | directory: string, 12 | ): Promise | undefined> { 13 | const files = await fs.readdir(directory); 14 | let customApp: ComponentType | undefined; 15 | 16 | for (const file of files) { 17 | if (!/^_app\.(js|ts)x?$/.test(file)) { 18 | continue; 19 | } 20 | 21 | const filePath = path.join(directory, file); 22 | const m = (await import(filePath)) as AppExports; 23 | 24 | customApp = m.default; 25 | break; 26 | } 27 | 28 | return customApp; 29 | } 30 | -------------------------------------------------------------------------------- /source/types.ts: -------------------------------------------------------------------------------- 1 | import type {ComponentType} from 'react'; 2 | 3 | export type AppProps = { 4 | /** 5 | * Command's component. 6 | */ 7 | Component: ComponentType<{ 8 | options: Record; 9 | args: unknown[]; 10 | }>; 11 | 12 | /** 13 | * Props to pass to command's component. 14 | */ 15 | commandProps: { 16 | /** 17 | * Options. 18 | */ 19 | options: Record; 20 | 21 | /** 22 | * Arguments. 23 | */ 24 | args: unknown[]; 25 | }; 26 | }; 27 | 28 | /** 29 | * Additional metadata for an option. 30 | */ 31 | export type CommandOptionConfig = { 32 | /** 33 | * Description. If description is missing, option won't appear in the "Options" section of the help message. 34 | */ 35 | description?: string; 36 | 37 | /** 38 | * Description of a default value. 39 | * 40 | * @default JSON.stringify(defaultValue) 41 | */ 42 | defaultValueDescription?: string; 43 | 44 | /** 45 | * Description of a value. Replaces "value" in `--flag ` in the help message. 46 | * 47 | * @default "value" 48 | */ 49 | valueDescription?: string; 50 | 51 | /** 52 | * Alias. Usually a first letter of the full option name. 53 | */ 54 | alias?: string; 55 | }; 56 | 57 | /** 58 | * Additional metadata for an argument. 59 | */ 60 | export type CommandArgumentConfig = { 61 | /** 62 | * Argument's name. Displayed in "Usage" part of the help message. Doesn't affect how argument is parsed. 63 | * 64 | * @default "arg" 65 | */ 66 | name?: string; 67 | 68 | /** 69 | * Description of an argument. If description is missing, argument won't appear in the "Arguments" section of the help message. 70 | */ 71 | description?: string; 72 | 73 | /** 74 | * Description of a default value. 75 | * 76 | * @default JSON.stringify(defaultValue) 77 | */ 78 | defaultValueDescription?: string; 79 | }; 80 | -------------------------------------------------------------------------------- /test/arguments.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import run from './helpers/run.js'; 3 | 4 | test('string argument', async t => { 5 | const fixture = 'string-argument/required'; 6 | 7 | const valid = await run(fixture, ['Jane', 'Hopper']); 8 | t.is(valid.stdout, 'Arguments = Jane, Hopper'); 9 | 10 | await t.throwsAsync(async () => run(fixture), { 11 | message: /error: missing required argument 'name'/, 12 | }); 13 | 14 | await t.throwsAsync(async () => run(fixture, ['Jane']), { 15 | message: /error: missing required argument 'surname'/, 16 | }); 17 | 18 | const help = await run(fixture, ['--help']); 19 | 20 | t.is( 21 | help.stdout, 22 | [ 23 | 'Usage: test [options] ', 24 | '', 25 | 'Description', 26 | '', 27 | 'Options:', 28 | ` -v, --version Show version number`, 29 | ` -h, --help Show help`, 30 | ].join('\n'), 31 | ); 32 | }); 33 | 34 | test('optional string argument', async t => { 35 | const fixture = 'string-argument/optional'; 36 | 37 | const empty = await run(fixture); 38 | t.is(empty.stdout, 'Arguments = ,'); 39 | 40 | const onlyOne = await run(fixture, ['Jane']); 41 | t.is(onlyOne.stdout, 'Arguments = Jane,'); 42 | 43 | const both = await run(fixture, ['Jane', 'Hopper']); 44 | t.is(both.stdout, 'Arguments = Jane, Hopper'); 45 | 46 | const help = await run(fixture, ['--help']); 47 | 48 | t.is( 49 | help.stdout, 50 | [ 51 | 'Usage: test [options] [name] [surname]', 52 | '', 53 | 'Description', 54 | '', 55 | 'Options:', 56 | ` -v, --version Show version number`, 57 | ` -h, --help Show help`, 58 | ].join('\n'), 59 | ); 60 | }); 61 | 62 | test('string argument with default value', async t => { 63 | const fixture = 'string-argument/default-value'; 64 | 65 | const empty = await run(fixture); 66 | t.is(empty.stdout, 'Arguments = , Hopper'); 67 | 68 | const onlyOne = await run(fixture, ['Jane']); 69 | t.is(onlyOne.stdout, 'Arguments = Jane, Hopper'); 70 | 71 | const both = await run(fixture, ['Jane', 'Hopper']); 72 | t.is(both.stdout, 'Arguments = Jane, Hopper'); 73 | 74 | const help = await run(fixture, ['--help']); 75 | 76 | t.is( 77 | help.stdout, 78 | [ 79 | 'Usage: test [options] [name] [surname]', 80 | '', 81 | 'Description', 82 | '', 83 | 'Options:', 84 | ` -v, --version Show version number`, 85 | ` -h, --help Show help`, 86 | ].join('\n'), 87 | ); 88 | }); 89 | 90 | test('string argument with default value and custom description', async t => { 91 | const fixture = 'string-argument/default-value-description'; 92 | 93 | const empty = await run(fixture); 94 | t.is(empty.stdout, 'Arguments = , Hopper'); 95 | 96 | const onlyOne = await run(fixture, ['Jane']); 97 | t.is(onlyOne.stdout, 'Arguments = Jane, Hopper'); 98 | 99 | const both = await run(fixture, ['Jane', 'Hopper']); 100 | t.is(both.stdout, 'Arguments = Jane, Hopper'); 101 | 102 | const help = await run(fixture, ['--help']); 103 | 104 | t.is( 105 | help.stdout, 106 | [ 107 | 'Usage: test [options] [name] [surname]', 108 | '', 109 | 'Description', 110 | '', 111 | 'Arguments:', 112 | ' name Name', 113 | ' surname Surname (default: Hopper)', 114 | '', 115 | 'Options:', 116 | ` -v, --version Show version number`, 117 | ` -h, --help Show help`, 118 | ].join('\n'), 119 | ); 120 | }); 121 | 122 | test('variadic string argument', async t => { 123 | const fixture = 'string-argument/variadic'; 124 | 125 | const withoutVariadic = await run(fixture, ['Jane', 'Hopper']); 126 | t.is(withoutVariadic.stdout, 'Arguments = Jane, Hopper'); 127 | 128 | const withVariadic = await run(fixture, [ 129 | 'Jane', 130 | 'Hopper', 131 | 'Eleven', 132 | 'Hawkins', 133 | ]); 134 | 135 | t.is(withVariadic.stdout, 'Arguments = Jane, Hopper, Eleven, Hawkins'); 136 | 137 | await t.throwsAsync(async () => run(fixture), { 138 | message: /error: missing required argument 'name'/, 139 | }); 140 | 141 | await t.throwsAsync(async () => run(fixture, ['Jane']), { 142 | message: /error: missing required argument 'surname'/, 143 | }); 144 | 145 | const help = await run(fixture, ['--help']); 146 | 147 | t.is( 148 | help.stdout, 149 | [ 150 | 'Usage: test [options] [traits...]', 151 | '', 152 | 'Description', 153 | '', 154 | 'Options:', 155 | ` -v, --version Show version number`, 156 | ` -h, --help Show help`, 157 | ].join('\n'), 158 | ); 159 | }); 160 | 161 | test('string array argument', async t => { 162 | const fixture = 'string-argument/array'; 163 | 164 | const valid = await run(fixture, ['Jane', 'Hopper']); 165 | t.is(valid.stdout, 'Arguments = Jane, Hopper'); 166 | 167 | await t.throwsAsync(async () => run(fixture), { 168 | message: /error: missing required argument 'traits'/, 169 | }); 170 | 171 | const help = await run(fixture, ['--help']); 172 | 173 | t.is( 174 | help.stdout, 175 | [ 176 | 'Usage: test [options] ', 177 | '', 178 | 'Description', 179 | '', 180 | 'Options:', 181 | ` -v, --version Show version number`, 182 | ` -h, --help Show help`, 183 | ].join('\n'), 184 | ); 185 | }); 186 | 187 | test('string array argument with description', async t => { 188 | const fixture = 'string-argument/array-description'; 189 | 190 | const valid = await run(fixture, ['Jane', 'Hopper']); 191 | t.is(valid.stdout, 'Arguments = Jane, Hopper'); 192 | 193 | await t.throwsAsync(async () => run(fixture), { 194 | message: /error: missing required argument 'traits'/, 195 | }); 196 | 197 | const help = await run(fixture, ['--help']); 198 | 199 | t.is( 200 | help.stdout, 201 | [ 202 | 'Usage: test [options] ', 203 | '', 204 | 'Description', 205 | '', 206 | 'Arguments:', 207 | ' traits Traits', 208 | '', 209 | 'Options:', 210 | ` -v, --version Show version number`, 211 | ` -h, --help Show help`, 212 | ].join('\n'), 213 | ); 214 | }); 215 | 216 | test('optional string array argument', async t => { 217 | const fixture = 'string-argument/optional-array'; 218 | 219 | const empty = await run(fixture); 220 | t.is(empty.stdout, 'Arguments ='); 221 | 222 | const valid = await run(fixture, ['Jane', 'Hopper']); 223 | t.is(valid.stdout, 'Arguments = Jane, Hopper'); 224 | 225 | const help = await run(fixture, ['--help']); 226 | 227 | t.is( 228 | help.stdout, 229 | [ 230 | 'Usage: test [options] [traits...]', 231 | '', 232 | 'Description', 233 | '', 234 | 'Options:', 235 | ` -v, --version Show version number`, 236 | ` -h, --help Show help`, 237 | ].join('\n'), 238 | ); 239 | }); 240 | 241 | test('string array argument with default value', async t => { 242 | const fixture = 'string-argument/array-default-value'; 243 | 244 | const empty = await run(fixture); 245 | t.is(empty.stdout, 'Arguments = Jane, Hopper'); 246 | 247 | const valid = await run(fixture, ['Jim', 'Hopper']); 248 | t.is(valid.stdout, 'Arguments = Jim, Hopper'); 249 | 250 | const help = await run(fixture, ['--help']); 251 | 252 | t.is( 253 | help.stdout, 254 | [ 255 | 'Usage: test [options] [traits...]', 256 | '', 257 | 'Description', 258 | '', 259 | 'Options:', 260 | ` -v, --version Show version number`, 261 | ` -h, --help Show help`, 262 | ].join('\n'), 263 | ); 264 | }); 265 | 266 | test('string array argument with default value and description', async t => { 267 | const fixture = 'string-argument/array-default-value-description'; 268 | 269 | const empty = await run(fixture); 270 | t.is(empty.stdout, 'Arguments = Jane, Hopper'); 271 | 272 | const valid = await run(fixture, ['Jim', 'Hopper']); 273 | t.is(valid.stdout, 'Arguments = Jim, Hopper'); 274 | 275 | const help = await run(fixture, ['--help']); 276 | 277 | t.is( 278 | help.stdout, 279 | [ 280 | 'Usage: test [options] [traits...]', 281 | '', 282 | 'Description', 283 | '', 284 | 'Arguments:', 285 | ' traits Traits (default: Jane, Hopper)', 286 | '', 287 | 'Options:', 288 | ` -v, --version Show version number`, 289 | ` -h, --help Show help`, 290 | ].join('\n'), 291 | ); 292 | }); 293 | 294 | test('string argument with description', async t => { 295 | const fixture = 'string-argument/description'; 296 | 297 | const valid = await run(fixture, ['Jane', 'Hopper']); 298 | t.is(valid.stdout, 'Arguments = Jane, Hopper'); 299 | 300 | await t.throwsAsync(async () => run(fixture), { 301 | message: /error: missing required argument 'name'/, 302 | }); 303 | 304 | await t.throwsAsync(async () => run(fixture, ['Jane']), { 305 | message: /error: missing required argument 'surname'/, 306 | }); 307 | 308 | const help = await run(fixture, ['--help']); 309 | 310 | t.is( 311 | help.stdout, 312 | [ 313 | 'Usage: test [options] ', 314 | '', 315 | 'Description', 316 | '', 317 | 'Arguments:', 318 | ' name Name', 319 | ' surname Surname', 320 | '', 321 | 'Options:', 322 | ` -v, --version Show version number`, 323 | ` -h, --help Show help`, 324 | ].join('\n'), 325 | ); 326 | }); 327 | 328 | test('string array argument with name from `argument`', async t => { 329 | const fixture = 'string-argument/array-name'; 330 | 331 | const valid = await run(fixture, ['Jane', 'Hopper']); 332 | t.is(valid.stdout, 'Arguments = Jane, Hopper'); 333 | 334 | await t.throwsAsync(async () => run(fixture), { 335 | message: /error: missing required argument 'traits'/, 336 | }); 337 | 338 | const help = await run(fixture, ['--help']); 339 | 340 | t.is( 341 | help.stdout, 342 | [ 343 | 'Usage: test [options] ', 344 | '', 345 | 'Description', 346 | '', 347 | 'Options:', 348 | ` -v, --version Show version number`, 349 | ` -h, --help Show help`, 350 | ].join('\n'), 351 | ); 352 | }); 353 | 354 | test('number argument', async t => { 355 | const fixture = 'number-argument/required'; 356 | 357 | const valid = await run(fixture, ['128', '256']); 358 | t.is(valid.stdout, 'Arguments = 128, 256'); 359 | 360 | await t.throwsAsync(async () => run(fixture), { 361 | message: /error: missing required argument 'first'/, 362 | }); 363 | 364 | await t.throwsAsync(async () => run(fixture, ['128']), { 365 | message: /error: missing required argument 'second'/, 366 | }); 367 | 368 | await t.throwsAsync(async () => run(fixture, ['Jane', 'Hopper']), { 369 | message: /Expected number, received nan at index 0/, 370 | }); 371 | 372 | const help = await run(fixture, ['--help']); 373 | 374 | t.is( 375 | help.stdout, 376 | [ 377 | 'Usage: test [options] ', 378 | '', 379 | 'Description', 380 | '', 381 | 'Options:', 382 | ` -v, --version Show version number`, 383 | ` -h, --help Show help`, 384 | ].join('\n'), 385 | ); 386 | }); 387 | 388 | test('optional number argument', async t => { 389 | const fixture = 'number-argument/optional'; 390 | 391 | const empty = await run(fixture); 392 | t.is(empty.stdout, 'Arguments = ,'); 393 | 394 | const onlyOne = await run(fixture, ['128']); 395 | t.is(onlyOne.stdout, 'Arguments = 128,'); 396 | 397 | const both = await run(fixture, ['128', '256']); 398 | t.is(both.stdout, 'Arguments = 128, 256'); 399 | 400 | const help = await run(fixture, ['--help']); 401 | 402 | t.is( 403 | help.stdout, 404 | [ 405 | 'Usage: test [options] [first] [second]', 406 | '', 407 | 'Description', 408 | '', 409 | 'Options:', 410 | ` -v, --version Show version number`, 411 | ` -h, --help Show help`, 412 | ].join('\n'), 413 | ); 414 | }); 415 | 416 | test('number argument with default value', async t => { 417 | const fixture = 'number-argument/default-value'; 418 | 419 | const empty = await run(fixture); 420 | t.is(empty.stdout, 'Arguments = , 256'); 421 | 422 | const onlyOne = await run(fixture, ['128']); 423 | t.is(onlyOne.stdout, 'Arguments = 128, 256'); 424 | 425 | const both = await run(fixture, ['128', '512']); 426 | t.is(both.stdout, 'Arguments = 128, 512'); 427 | 428 | const help = await run(fixture, ['--help']); 429 | 430 | t.is( 431 | help.stdout, 432 | [ 433 | 'Usage: test [options] [first] [second]', 434 | '', 435 | 'Description', 436 | '', 437 | 'Options:', 438 | ` -v, --version Show version number`, 439 | ` -h, --help Show help`, 440 | ].join('\n'), 441 | ); 442 | }); 443 | 444 | test('number argument with default value and custom description', async t => { 445 | const fixture = 'number-argument/default-value-description'; 446 | 447 | const empty = await run(fixture); 448 | t.is(empty.stdout, 'Arguments = , 256'); 449 | 450 | const onlyOne = await run(fixture, ['128']); 451 | t.is(onlyOne.stdout, 'Arguments = 128, 256'); 452 | 453 | const both = await run(fixture, ['128', '512']); 454 | t.is(both.stdout, 'Arguments = 128, 512'); 455 | 456 | const help = await run(fixture, ['--help']); 457 | 458 | t.is( 459 | help.stdout, 460 | [ 461 | 'Usage: test [options] [first] [second]', 462 | '', 463 | 'Description', 464 | '', 465 | 'Arguments:', 466 | ' first First', 467 | ' second Second (default: 256 MB)', 468 | '', 469 | 'Options:', 470 | ` -v, --version Show version number`, 471 | ` -h, --help Show help`, 472 | ].join('\n'), 473 | ); 474 | }); 475 | 476 | test('variadic number argument', async t => { 477 | const fixture = 'number-argument/variadic'; 478 | 479 | const withoutVariadic = await run(fixture, ['128', '256']); 480 | t.is(withoutVariadic.stdout, 'Arguments = 128, 256'); 481 | 482 | const withVariadic = await run(fixture, ['128', '256', '512', '1024']); 483 | t.is(withVariadic.stdout, 'Arguments = 128, 256, 512, 1024'); 484 | 485 | await t.throwsAsync(async () => run(fixture), { 486 | message: /error: missing required argument 'first'/, 487 | }); 488 | 489 | await t.throwsAsync(async () => run(fixture, ['128']), { 490 | message: /error: missing required argument 'second'/, 491 | }); 492 | 493 | const help = await run(fixture, ['--help']); 494 | 495 | t.is( 496 | help.stdout, 497 | [ 498 | 'Usage: test [options] [rest...]', 499 | '', 500 | 'Description', 501 | '', 502 | 'Options:', 503 | ` -v, --version Show version number`, 504 | ` -h, --help Show help`, 505 | ].join('\n'), 506 | ); 507 | }); 508 | 509 | test('number array argument', async t => { 510 | const fixture = 'number-argument/array'; 511 | 512 | const valid = await run(fixture, ['128', '256']); 513 | t.is(valid.stdout, 'Arguments = 128, 256'); 514 | 515 | await t.throwsAsync(async () => run(fixture), { 516 | message: /error: missing required argument 'number'/, 517 | }); 518 | 519 | const help = await run(fixture, ['--help']); 520 | 521 | t.is( 522 | help.stdout, 523 | [ 524 | 'Usage: test [options] ', 525 | '', 526 | 'Description', 527 | '', 528 | 'Options:', 529 | ` -v, --version Show version number`, 530 | ` -h, --help Show help`, 531 | ].join('\n'), 532 | ); 533 | }); 534 | 535 | test('number array argument with description', async t => { 536 | const fixture = 'number-argument/array-description'; 537 | 538 | const valid = await run(fixture, ['128', '256']); 539 | t.is(valid.stdout, 'Arguments = 128, 256'); 540 | 541 | await t.throwsAsync(async () => run(fixture), { 542 | message: /error: missing required argument 'number'/, 543 | }); 544 | 545 | const help = await run(fixture, ['--help']); 546 | 547 | t.is( 548 | help.stdout, 549 | [ 550 | 'Usage: test [options] ', 551 | '', 552 | 'Description', 553 | '', 554 | 'Arguments:', 555 | ' number Numbers', 556 | '', 557 | 'Options:', 558 | ` -v, --version Show version number`, 559 | ` -h, --help Show help`, 560 | ].join('\n'), 561 | ); 562 | }); 563 | 564 | test('optional number array argument', async t => { 565 | const fixture = 'number-argument/optional-array'; 566 | 567 | const empty = await run(fixture); 568 | t.is(empty.stdout, 'Arguments ='); 569 | 570 | const valid = await run(fixture, ['128', '256']); 571 | t.is(valid.stdout, 'Arguments = 128, 256'); 572 | 573 | const help = await run(fixture, ['--help']); 574 | 575 | t.is( 576 | help.stdout, 577 | [ 578 | 'Usage: test [options] [number...]', 579 | '', 580 | 'Description', 581 | '', 582 | 'Options:', 583 | ` -v, --version Show version number`, 584 | ` -h, --help Show help`, 585 | ].join('\n'), 586 | ); 587 | }); 588 | 589 | test('number array argument with default value', async t => { 590 | const fixture = 'number-argument/array-default-value'; 591 | 592 | const empty = await run(fixture); 593 | t.is(empty.stdout, 'Arguments = 128, 256'); 594 | 595 | const valid = await run(fixture, ['512', '1024']); 596 | t.is(valid.stdout, 'Arguments = 512, 1024'); 597 | 598 | const help = await run(fixture, ['--help']); 599 | 600 | t.is( 601 | help.stdout, 602 | [ 603 | 'Usage: test [options] [number...]', 604 | '', 605 | 'Description', 606 | '', 607 | 'Options:', 608 | ` -v, --version Show version number`, 609 | ` -h, --help Show help`, 610 | ].join('\n'), 611 | ); 612 | }); 613 | 614 | test('number array argument with default value and description', async t => { 615 | const fixture = 'number-argument/array-default-value-description'; 616 | 617 | const empty = await run(fixture); 618 | t.is(empty.stdout, 'Arguments = 128, 256'); 619 | 620 | const valid = await run(fixture, ['512', '1024']); 621 | t.is(valid.stdout, 'Arguments = 512, 1024'); 622 | 623 | const help = await run(fixture, ['--help']); 624 | 625 | t.is( 626 | help.stdout, 627 | [ 628 | 'Usage: test [options] [number...]', 629 | '', 630 | 'Description', 631 | '', 632 | 'Arguments:', 633 | ' number Numbers (default: 128, 256)', 634 | '', 635 | 'Options:', 636 | ` -v, --version Show version number`, 637 | ` -h, --help Show help`, 638 | ].join('\n'), 639 | ); 640 | }); 641 | 642 | test('number argument with description', async t => { 643 | const fixture = 'number-argument/description'; 644 | 645 | const valid = await run(fixture, ['128', '256']); 646 | t.is(valid.stdout, 'Arguments = 128, 256'); 647 | 648 | await t.throwsAsync(async () => run(fixture), { 649 | message: /error: missing required argument 'first'/, 650 | }); 651 | 652 | await t.throwsAsync(async () => run(fixture, ['128']), { 653 | message: /error: missing required argument 'second'/, 654 | }); 655 | 656 | await t.throwsAsync(async () => run(fixture, ['Jane', 'Hopper']), { 657 | message: /Expected number, received nan at index 0/, 658 | }); 659 | 660 | const help = await run(fixture, ['--help']); 661 | 662 | t.is( 663 | help.stdout, 664 | [ 665 | 'Usage: test [options] ', 666 | '', 667 | 'Description', 668 | '', 669 | 'Arguments:', 670 | ' first First', 671 | ' second Second', 672 | '', 673 | 'Options:', 674 | ` -v, --version Show version number`, 675 | ` -h, --help Show help`, 676 | ].join('\n'), 677 | ); 678 | }); 679 | 680 | test('number array argument with name from `argument`', async t => { 681 | const fixture = 'number-argument/array-name'; 682 | 683 | const valid = await run(fixture, ['128', '256']); 684 | t.is(valid.stdout, 'Arguments = 128, 256'); 685 | 686 | await t.throwsAsync(async () => run(fixture), { 687 | message: /error: missing required argument 'number'/, 688 | }); 689 | 690 | const help = await run(fixture, ['--help']); 691 | 692 | t.is( 693 | help.stdout, 694 | [ 695 | 'Usage: test [options] ', 696 | '', 697 | 'Description', 698 | '', 699 | 'Options:', 700 | ` -v, --version Show version number`, 701 | ` -h, --help Show help`, 702 | ].join('\n'), 703 | ); 704 | }); 705 | 706 | test('enum argument', async t => { 707 | const fixture = 'enum-argument/required'; 708 | 709 | const valid = await run(fixture, ['Ubuntu', 'Debian']); 710 | t.is(valid.stdout, 'Arguments = Ubuntu, Debian'); 711 | 712 | await t.throwsAsync(async () => run(fixture), { 713 | message: /error: missing required argument 'first'/, 714 | }); 715 | 716 | await t.throwsAsync(async () => run(fixture, ['Ubuntu']), { 717 | message: /error: missing required argument 'second'/, 718 | }); 719 | 720 | const help = await run(fixture, ['--help']); 721 | 722 | t.is( 723 | help.stdout, 724 | [ 725 | 'Usage: test [options] ', 726 | '', 727 | 'Description', 728 | '', 729 | 'Options:', 730 | ` -v, --version Show version number`, 731 | ` -h, --help Show help`, 732 | ].join('\n'), 733 | ); 734 | }); 735 | 736 | test('optional enum argument', async t => { 737 | const fixture = 'enum-argument/optional'; 738 | 739 | const empty = await run(fixture); 740 | t.is(empty.stdout, 'Arguments = ,'); 741 | 742 | const onlyOne = await run(fixture, ['Ubuntu']); 743 | t.is(onlyOne.stdout, 'Arguments = Ubuntu,'); 744 | 745 | const both = await run(fixture, ['Ubuntu', 'Debian']); 746 | t.is(both.stdout, 'Arguments = Ubuntu, Debian'); 747 | 748 | const help = await run(fixture, ['--help']); 749 | 750 | t.is( 751 | help.stdout, 752 | [ 753 | 'Usage: test [options] [first] [second]', 754 | '', 755 | 'Description', 756 | '', 757 | 'Options:', 758 | ` -v, --version Show version number`, 759 | ` -h, --help Show help`, 760 | ].join('\n'), 761 | ); 762 | }); 763 | 764 | test('enum argument with default value', async t => { 765 | const fixture = 'enum-argument/default-value'; 766 | 767 | const empty = await run(fixture); 768 | t.is(empty.stdout, 'Arguments = , Debian'); 769 | 770 | const onlyOne = await run(fixture, ['Ubuntu']); 771 | t.is(onlyOne.stdout, 'Arguments = Ubuntu, Debian'); 772 | 773 | const both = await run(fixture, ['Ubuntu', 'Debian']); 774 | t.is(both.stdout, 'Arguments = Ubuntu, Debian'); 775 | 776 | const help = await run(fixture, ['--help']); 777 | 778 | t.is( 779 | help.stdout, 780 | [ 781 | 'Usage: test [options] [first] [second]', 782 | '', 783 | 'Description', 784 | '', 785 | 'Options:', 786 | ` -v, --version Show version number`, 787 | ` -h, --help Show help`, 788 | ].join('\n'), 789 | ); 790 | }); 791 | 792 | test('enum argument with default value and custom description', async t => { 793 | const fixture = 'enum-argument/default-value-description'; 794 | 795 | const empty = await run(fixture); 796 | t.is(empty.stdout, 'Arguments = , Debian'); 797 | 798 | const onlyOne = await run(fixture, ['Ubuntu']); 799 | t.is(onlyOne.stdout, 'Arguments = Ubuntu, Debian'); 800 | 801 | const both = await run(fixture, ['Ubuntu', 'Debian']); 802 | t.is(both.stdout, 'Arguments = Ubuntu, Debian'); 803 | 804 | const help = await run(fixture, ['--help']); 805 | 806 | t.is( 807 | help.stdout, 808 | [ 809 | 'Usage: test [options] [first] [second]', 810 | '', 811 | 'Description', 812 | '', 813 | 'Arguments:', 814 | ' first First (choices: "Ubuntu", "Debian")', 815 | ' second Second (choices: "Ubuntu", "Debian", default: Debian)', 816 | '', 817 | 'Options:', 818 | ` -v, --version Show version number`, 819 | ` -h, --help Show help`, 820 | ].join('\n'), 821 | ); 822 | }); 823 | 824 | test('variadic enum argument', async t => { 825 | const fixture = 'enum-argument/variadic'; 826 | 827 | const withoutVariadic = await run(fixture, ['Ubuntu', 'Debian']); 828 | t.is(withoutVariadic.stdout, 'Arguments = Ubuntu, Debian'); 829 | 830 | const withVariadic = await run(fixture, [ 831 | 'Ubuntu', 832 | 'Debian', 833 | 'macOS', 834 | 'Windows', 835 | ]); 836 | 837 | t.is(withVariadic.stdout, 'Arguments = Ubuntu, Debian, macOS, Windows'); 838 | 839 | await t.throwsAsync(async () => run(fixture), { 840 | message: /error: missing required argument 'first'/, 841 | }); 842 | 843 | await t.throwsAsync(async () => run(fixture, ['Ubuntu']), { 844 | message: /error: missing required argument 'second'/, 845 | }); 846 | 847 | const help = await run(fixture, ['--help']); 848 | 849 | t.is( 850 | help.stdout, 851 | [ 852 | 'Usage: test [options] [rest...]', 853 | '', 854 | 'Description', 855 | '', 856 | 'Options:', 857 | ` -v, --version Show version number`, 858 | ` -h, --help Show help`, 859 | ].join('\n'), 860 | ); 861 | }); 862 | 863 | test('enum array argument', async t => { 864 | const fixture = 'enum-argument/array'; 865 | 866 | const valid = await run(fixture, ['Ubuntu', 'Debian']); 867 | t.is(valid.stdout, 'Arguments = Ubuntu, Debian'); 868 | 869 | await t.throwsAsync(async () => run(fixture), { 870 | message: /error: missing required argument 'os'/, 871 | }); 872 | 873 | const help = await run(fixture, ['--help']); 874 | 875 | t.is( 876 | help.stdout, 877 | [ 878 | 'Usage: test [options] ', 879 | '', 880 | 'Description', 881 | '', 882 | 'Options:', 883 | ` -v, --version Show version number`, 884 | ` -h, --help Show help`, 885 | ].join('\n'), 886 | ); 887 | }); 888 | 889 | test('enum array argument with description', async t => { 890 | const fixture = 'enum-argument/array-description'; 891 | 892 | const valid = await run(fixture, ['Ubuntu', 'Debian']); 893 | t.is(valid.stdout, 'Arguments = Ubuntu, Debian'); 894 | 895 | await t.throwsAsync(async () => run(fixture), { 896 | message: /error: missing required argument 'os'/, 897 | }); 898 | 899 | const help = await run(fixture, ['--help']); 900 | 901 | t.is( 902 | help.stdout, 903 | [ 904 | 'Usage: test [options] ', 905 | '', 906 | 'Description', 907 | '', 908 | 'Arguments:', 909 | ' os Operating systems', 910 | '', 911 | 'Options:', 912 | ` -v, --version Show version number`, 913 | ` -h, --help Show help`, 914 | ].join('\n'), 915 | ); 916 | }); 917 | 918 | test('optional enum array argument', async t => { 919 | const fixture = 'enum-argument/optional-array'; 920 | 921 | const empty = await run(fixture); 922 | t.is(empty.stdout, 'Arguments ='); 923 | 924 | const valid = await run(fixture, ['Ubuntu', 'Debian']); 925 | t.is(valid.stdout, 'Arguments = Ubuntu, Debian'); 926 | 927 | const help = await run(fixture, ['--help']); 928 | 929 | t.is( 930 | help.stdout, 931 | [ 932 | 'Usage: test [options] [os...]', 933 | '', 934 | 'Description', 935 | '', 936 | 'Options:', 937 | ` -v, --version Show version number`, 938 | ` -h, --help Show help`, 939 | ].join('\n'), 940 | ); 941 | }); 942 | 943 | test('enum array argument with default value', async t => { 944 | const fixture = 'enum-argument/array-default-value'; 945 | 946 | const empty = await run(fixture); 947 | t.is(empty.stdout, 'Arguments = macOS, Windows'); 948 | 949 | const valid = await run(fixture, ['Ubuntu', 'Debian']); 950 | t.is(valid.stdout, 'Arguments = Ubuntu, Debian'); 951 | 952 | const help = await run(fixture, ['--help']); 953 | 954 | t.is( 955 | help.stdout, 956 | [ 957 | 'Usage: test [options] [os...]', 958 | '', 959 | 'Description', 960 | '', 961 | 'Options:', 962 | ` -v, --version Show version number`, 963 | ` -h, --help Show help`, 964 | ].join('\n'), 965 | ); 966 | }); 967 | 968 | test('enum array argument with default value and description', async t => { 969 | const fixture = 'enum-argument/array-default-value-description'; 970 | 971 | const empty = await run(fixture); 972 | t.is(empty.stdout, 'Arguments = macOS, Windows'); 973 | 974 | const valid = await run(fixture, ['Ubuntu', 'Debian']); 975 | t.is(valid.stdout, 'Arguments = Ubuntu, Debian'); 976 | 977 | const help = await run(fixture, ['--help']); 978 | 979 | t.is( 980 | help.stdout, 981 | [ 982 | 'Usage: test [options] [os...]', 983 | '', 984 | 'Description', 985 | '', 986 | 'Arguments:', 987 | ' os Operating systems (default: macOS, Windows)', 988 | '', 989 | 'Options:', 990 | ` -v, --version Show version number`, 991 | ` -h, --help Show help`, 992 | ].join('\n'), 993 | ); 994 | }); 995 | 996 | test('enum argument with description', async t => { 997 | const fixture = 'enum-argument/description'; 998 | 999 | const valid = await run(fixture, ['Ubuntu', 'Debian']); 1000 | t.is(valid.stdout, 'Arguments = Ubuntu, Debian'); 1001 | 1002 | await t.throwsAsync(async () => run(fixture), { 1003 | message: /error: missing required argument 'first'/, 1004 | }); 1005 | 1006 | await t.throwsAsync(async () => run(fixture, ['Ubuntu']), { 1007 | message: /error: missing required argument 'second'/, 1008 | }); 1009 | 1010 | const help = await run(fixture, ['--help']); 1011 | 1012 | t.is( 1013 | help.stdout, 1014 | [ 1015 | 'Usage: test [options] ', 1016 | '', 1017 | 'Description', 1018 | '', 1019 | 'Arguments:', 1020 | ' first First (choices: "Ubuntu", "Debian")', 1021 | ' second Second (choices: "Ubuntu", "Debian")', 1022 | '', 1023 | 'Options:', 1024 | ` -v, --version Show version number`, 1025 | ` -h, --help Show help`, 1026 | ].join('\n'), 1027 | ); 1028 | }); 1029 | 1030 | test('enum array argument with name from `argument`', async t => { 1031 | const fixture = 'enum-argument/array-name'; 1032 | 1033 | const valid = await run(fixture, ['Ubuntu', 'Debian']); 1034 | t.is(valid.stdout, 'Arguments = Ubuntu, Debian'); 1035 | 1036 | await t.throwsAsync(async () => run(fixture), { 1037 | message: /error: missing required argument 'os'/, 1038 | }); 1039 | 1040 | const help = await run(fixture, ['--help']); 1041 | 1042 | t.is( 1043 | help.stdout, 1044 | [ 1045 | 'Usage: test [options] ', 1046 | '', 1047 | 'Description', 1048 | '', 1049 | 'Options:', 1050 | ` -v, --version Show version number`, 1051 | ` -h, --help Show help`, 1052 | ].join('\n'), 1053 | ); 1054 | }); 1055 | 1056 | test('snake case argument name', async t => { 1057 | const fixture = 'camelcase-argument'; 1058 | 1059 | const valid = await run(fixture, ['Hello']); 1060 | t.is(valid.stdout, 'Arguments = Hello'); 1061 | 1062 | const help = await run(fixture, ['--help']); 1063 | 1064 | t.is( 1065 | help.stdout, 1066 | [ 1067 | 'Usage: test [options] ', 1068 | '', 1069 | 'Description', 1070 | '', 1071 | 'Options:', 1072 | ` -v, --version Show version number`, 1073 | ` -h, --help Show help`, 1074 | ].join('\n'), 1075 | ); 1076 | }); 1077 | -------------------------------------------------------------------------------- /test/commands.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import run from './helpers/run.js'; 3 | 4 | test('single command', async t => { 5 | const fixture = 'single-command'; 6 | 7 | const index = await run(fixture); 8 | t.is(index.stdout, 'Index'); 9 | 10 | const help = await run(fixture, ['--help']); 11 | 12 | t.is( 13 | help.stdout, 14 | [ 15 | 'Usage: test [options]', 16 | '', 17 | 'Index command', 18 | '', 19 | 'Options:', 20 | ' -v, --version Show version number', 21 | ' -h, --help Show help', 22 | ].join('\n'), 23 | ); 24 | }); 25 | 26 | test('single default command', async t => { 27 | const fixture = 'single-default-command'; 28 | 29 | const deploy = await run(fixture); 30 | t.is(deploy.stdout, 'Deploy'); 31 | 32 | const help = await run(fixture, ['--help']); 33 | 34 | t.is( 35 | help.stdout, 36 | [ 37 | 'Usage: test [options] [command]', 38 | '', 39 | 'Description', 40 | '', 41 | 'Options:', 42 | ' -v, --version Show version number', 43 | ' -h, --help Show help', 44 | '', 45 | 'Commands:', 46 | ' deploy Deploy command', 47 | ' help [command] Show help for command', 48 | ].join('\n'), 49 | ); 50 | }); 51 | 52 | test('multiple commands', async t => { 53 | const fixture = 'multiple-commands'; 54 | 55 | const index = await run(fixture); 56 | t.is(index.stdout, 'Deploy'); 57 | 58 | const indexHelp = await run(fixture, ['--help']); 59 | 60 | t.is( 61 | indexHelp.stdout, 62 | [ 63 | 'Usage: test [options] [command]', 64 | '', 65 | 'Description', 66 | '', 67 | 'Options:', 68 | ' -v, --version Show version number', 69 | ' -h, --help Show help', 70 | '', 71 | 'Commands:', 72 | ' auth Auth command', 73 | ' deploy Deploy command', 74 | ' help [command] Show help for command', 75 | ].join('\n'), 76 | ); 77 | 78 | const deploy = await run(fixture, ['deploy']); 79 | t.is(deploy.stdout, 'Deploy'); 80 | 81 | const deployHelp = await run(fixture, ['deploy', '--help']); 82 | 83 | t.is( 84 | deployHelp.stdout, 85 | [ 86 | 'Usage: test deploy [options]', 87 | '', 88 | 'Deploy command', 89 | '', 90 | 'Options:', 91 | ' -h, --help Show help', 92 | ].join('\n'), 93 | ); 94 | 95 | const auth = await run(fixture, ['auth']); 96 | t.is(auth.stdout, 'Auth'); 97 | 98 | const authHelp = await run(fixture, ['auth', '--help']); 99 | 100 | t.is( 101 | authHelp.stdout, 102 | [ 103 | 'Usage: test auth [options]', 104 | '', 105 | 'Auth command', 106 | '', 107 | 'Options:', 108 | ' -h, --help Show help', 109 | ].join('\n'), 110 | ); 111 | }); 112 | 113 | test('nested commands', async t => { 114 | const fixture = 'nested-commands'; 115 | 116 | const index = await run(fixture); 117 | t.is(index.stdout, 'Deploy'); 118 | 119 | const indexHelp = await run(fixture, ['--help']); 120 | 121 | t.is( 122 | indexHelp.stdout, 123 | [ 124 | 'Usage: test [options] [command]', 125 | '', 126 | 'Description', 127 | '', 128 | 'Options:', 129 | ' -v, --version Show version number', 130 | ' -h, --help Show help', 131 | '', 132 | 'Commands:', 133 | ' auth Auth command', 134 | ' deploy Deploy command', 135 | ' servers Manage servers', 136 | ' help [command] Show help for command', 137 | ].join('\n'), 138 | ); 139 | 140 | const auth = await run(fixture, ['auth']); 141 | t.is(auth.stdout, 'Auth'); 142 | 143 | const authHelp = await run(fixture, ['auth', '--help']); 144 | 145 | t.is( 146 | authHelp.stdout, 147 | [ 148 | 'Usage: test auth [options]', 149 | '', 150 | 'Auth command', 151 | '', 152 | 'Options:', 153 | ' -h, --help Show help', 154 | ].join('\n'), 155 | ); 156 | 157 | const servers = await run(fixture, ['servers'], { 158 | reject: false, 159 | }); 160 | 161 | t.is( 162 | servers.stderr, 163 | [ 164 | 'Usage: test servers [options] [command]', 165 | '', 166 | 'Manage servers', 167 | '', 168 | 'Options:', 169 | ' -h, --help Show help', 170 | '', 171 | 'Commands:', 172 | ' create Create server', 173 | ' list List servers', 174 | ' help [command] Show help for command', 175 | ].join('\n'), 176 | ); 177 | 178 | const createServer = await run(fixture, ['servers', 'create']); 179 | t.is(createServer.stdout, 'Create server'); 180 | 181 | const createServerHelp = await run(fixture, ['servers', 'create', '--help']); 182 | 183 | t.is( 184 | createServerHelp.stdout, 185 | [ 186 | 'Usage: test servers create [options]', 187 | '', 188 | 'Create server', 189 | '', 190 | 'Options:', 191 | ' -h, --help Show help', 192 | ].join('\n'), 193 | ); 194 | 195 | const listServers = await run(fixture, ['servers', 'list']); 196 | t.is(listServers.stdout, 'List servers'); 197 | 198 | const listServersHelp = await run(fixture, ['servers', 'list', '--help']); 199 | 200 | t.is( 201 | listServersHelp.stdout, 202 | [ 203 | 'Usage: test servers list [options]', 204 | '', 205 | 'List servers', 206 | '', 207 | 'Options:', 208 | ' -h, --help Show help', 209 | ].join('\n'), 210 | ); 211 | }); 212 | 213 | test('deeply nested commands', async t => { 214 | const fixture = 'deeply-nested-commands'; 215 | 216 | const index = await run(fixture); 217 | t.is(index.stdout, 'Deploy'); 218 | 219 | const indexHelp = await run(fixture, ['--help']); 220 | 221 | t.is( 222 | indexHelp.stdout, 223 | [ 224 | 'Usage: test [options] [command]', 225 | '', 226 | 'Description', 227 | '', 228 | 'Options:', 229 | ' -v, --version Show version number', 230 | ' -h, --help Show help', 231 | '', 232 | 'Commands:', 233 | ' auth Auth command', 234 | ' deploy Deploy command', 235 | ' projects Manage projects', 236 | ' help [command] Show help for command', 237 | ].join('\n'), 238 | ); 239 | 240 | const auth = await run(fixture, ['auth']); 241 | t.is(auth.stdout, 'Auth'); 242 | 243 | const authHelp = await run(fixture, ['auth', '--help']); 244 | 245 | t.is( 246 | authHelp.stdout, 247 | [ 248 | 'Usage: test auth [options]', 249 | '', 250 | 'Auth command', 251 | '', 252 | 'Options:', 253 | ' -h, --help Show help', 254 | ].join('\n'), 255 | ); 256 | 257 | const projects = await run(fixture, ['projects'], { 258 | reject: false, 259 | }); 260 | 261 | t.is( 262 | projects.stderr, 263 | [ 264 | 'Usage: test projects [options] [command]', 265 | '', 266 | 'Manage projects', 267 | '', 268 | 'Options:', 269 | ' -h, --help Show help', 270 | '', 271 | 'Commands:', 272 | ' create Create project', 273 | ' list List projects', 274 | ' servers Manage servers', 275 | ' help [command] Show help for command', 276 | ].join('\n'), 277 | ); 278 | 279 | const createProject = await run(fixture, ['projects', 'create']); 280 | t.is(createProject.stdout, 'Create project'); 281 | 282 | const createProjectHelp = await run(fixture, [ 283 | 'projects', 284 | 'create', 285 | '--help', 286 | ]); 287 | 288 | t.is( 289 | createProjectHelp.stdout, 290 | [ 291 | 'Usage: test projects create [options]', 292 | '', 293 | 'Create project', 294 | '', 295 | 'Options:', 296 | ' -h, --help Show help', 297 | ].join('\n'), 298 | ); 299 | 300 | const listProjects = await run(fixture, ['projects', 'list']); 301 | t.is(listProjects.stdout, 'List projects'); 302 | 303 | const listProjectsHelp = await run(fixture, ['projects', 'list', '--help']); 304 | 305 | t.is( 306 | listProjectsHelp.stdout, 307 | [ 308 | 'Usage: test projects list [options]', 309 | '', 310 | 'List projects', 311 | '', 312 | 'Options:', 313 | ' -h, --help Show help', 314 | ].join('\n'), 315 | ); 316 | 317 | const servers = await run(fixture, ['projects', 'servers'], { 318 | reject: false, 319 | }); 320 | 321 | t.is( 322 | servers.stderr, 323 | [ 324 | 'Usage: test projects servers [options] [command]', 325 | '', 326 | 'Manage servers', 327 | '', 328 | 'Options:', 329 | ' -h, --help Show help', 330 | '', 331 | 'Commands:', 332 | ' create Create server', 333 | ' list List servers', 334 | ' help [command] Show help for command', 335 | ].join('\n'), 336 | ); 337 | 338 | const createServer = await run(fixture, ['projects', 'servers', 'create']); 339 | t.is(createServer.stdout, 'Create server'); 340 | 341 | const createServerHelp = await run(fixture, [ 342 | 'projects', 343 | 'servers', 344 | 'create', 345 | '--help', 346 | ]); 347 | 348 | t.is( 349 | createServerHelp.stdout, 350 | [ 351 | 'Usage: test projects servers create [options]', 352 | '', 353 | 'Create server', 354 | '', 355 | 'Options:', 356 | ' -h, --help Show help', 357 | ].join('\n'), 358 | ); 359 | 360 | const listServers = await run(fixture, ['projects', 'servers', 'list']); 361 | t.is(listServers.stdout, 'List servers'); 362 | 363 | const listServersHelp = await run(fixture, [ 364 | 'projects', 365 | 'servers', 366 | 'list', 367 | '--help', 368 | ]); 369 | 370 | t.is( 371 | listServersHelp.stdout, 372 | [ 373 | 'Usage: test projects servers list [options]', 374 | '', 375 | 'List servers', 376 | '', 377 | 'Options:', 378 | ' -h, --help Show help', 379 | ].join('\n'), 380 | ); 381 | }); 382 | 383 | test('snake case command name', async t => { 384 | const fixture = 'camelcase-command'; 385 | 386 | const index = await run(fixture); 387 | t.is(index.stdout, 'Deploy'); 388 | 389 | const indexHelp = await run(fixture, ['--help']); 390 | 391 | t.is( 392 | indexHelp.stdout, 393 | [ 394 | 'Usage: test [options] [command]', 395 | '', 396 | 'Description', 397 | '', 398 | 'Options:', 399 | ' -v, --version Show version number', 400 | ' -h, --help Show help', 401 | '', 402 | 'Commands:', 403 | ' auth Auth command', 404 | ' super-deploy Deploy command', 405 | ' help [command] Show help for command', 406 | ].join('\n'), 407 | ); 408 | 409 | const deploy = await run(fixture, ['super-deploy']); 410 | t.is(deploy.stdout, 'Deploy'); 411 | 412 | const deployHelp = await run(fixture, ['super-deploy', '--help']); 413 | 414 | t.is( 415 | deployHelp.stdout, 416 | [ 417 | 'Usage: test super-deploy [options]', 418 | '', 419 | 'Deploy command', 420 | '', 421 | 'Options:', 422 | ' -h, --help Show help', 423 | ].join('\n'), 424 | ); 425 | 426 | const auth = await run(fixture, ['auth']); 427 | t.is(auth.stdout, 'Auth'); 428 | 429 | const authHelp = await run(fixture, ['auth', '--help']); 430 | 431 | t.is( 432 | authHelp.stdout, 433 | [ 434 | 'Usage: test auth [options]', 435 | '', 436 | 'Auth command', 437 | '', 438 | 'Options:', 439 | ' -h, --help Show help', 440 | ].join('\n'), 441 | ); 442 | }); 443 | 444 | test('single command with custom app', async t => { 445 | const fixture = 'single-command-custom-app'; 446 | 447 | const deploy = await run(fixture, ['--name', 'test']); 448 | t.is(deploy.stdout, '\n\n Deploy test\n\n'); 449 | 450 | const help = await run(fixture, ['--help']); 451 | 452 | t.is( 453 | help.stdout, 454 | [ 455 | 'Usage: test [options] [command]', 456 | '', 457 | 'Description', 458 | '', 459 | 'Options:', 460 | ' -v, --version Show version number', 461 | ' -h, --help Show help', 462 | '', 463 | 'Commands:', 464 | ' deploy [options] Deploy command', 465 | ' help [command] Show help for command', 466 | ].join('\n'), 467 | ); 468 | }); 469 | 470 | test('nested commands with custom app', async t => { 471 | const fixture = 'nested-commands-custom-app'; 472 | 473 | const index = await run(fixture); 474 | t.is(index.stdout, '\n\n Deploy\n\n'); 475 | 476 | const indexHelp = await run(fixture, ['--help']); 477 | 478 | t.is( 479 | indexHelp.stdout, 480 | [ 481 | 'Usage: test [options] [command]', 482 | '', 483 | 'Description', 484 | '', 485 | 'Options:', 486 | ' -v, --version Show version number', 487 | ' -h, --help Show help', 488 | '', 489 | 'Commands:', 490 | ' auth Auth command', 491 | ' deploy Deploy command', 492 | ' servers Manage servers', 493 | ' help [command] Show help for command', 494 | ].join('\n'), 495 | ); 496 | 497 | const auth = await run(fixture, ['auth']); 498 | t.is(auth.stdout, '\n\n Auth\n\n'); 499 | 500 | const authHelp = await run(fixture, ['auth', '--help']); 501 | 502 | t.is( 503 | authHelp.stdout, 504 | [ 505 | 'Usage: test auth [options]', 506 | '', 507 | 'Auth command', 508 | '', 509 | 'Options:', 510 | ' -h, --help Show help', 511 | ].join('\n'), 512 | ); 513 | 514 | const servers = await run(fixture, ['servers'], { 515 | reject: false, 516 | }); 517 | 518 | t.is( 519 | servers.stderr, 520 | [ 521 | 'Usage: test servers [options] [command]', 522 | '', 523 | 'Manage servers', 524 | '', 525 | 'Options:', 526 | ' -h, --help Show help', 527 | '', 528 | 'Commands:', 529 | ' create Create server', 530 | ' list List servers', 531 | ' help [command] Show help for command', 532 | ].join('\n'), 533 | ); 534 | 535 | const createServer = await run(fixture, ['servers', 'create']); 536 | t.is(createServer.stdout, '\n\n Create server\n\n'); 537 | 538 | const createServerHelp = await run(fixture, ['servers', 'create', '--help']); 539 | 540 | t.is( 541 | createServerHelp.stdout, 542 | [ 543 | 'Usage: test servers create [options]', 544 | '', 545 | 'Create server', 546 | '', 547 | 'Options:', 548 | ' -h, --help Show help', 549 | ].join('\n'), 550 | ); 551 | 552 | const listServers = await run(fixture, ['servers', 'list']); 553 | t.is(listServers.stdout, '\n\n List servers\n\n'); 554 | 555 | const listServersHelp = await run(fixture, ['servers', 'list', '--help']); 556 | 557 | t.is( 558 | listServersHelp.stdout, 559 | [ 560 | 'Usage: test servers list [options]', 561 | '', 562 | 'List servers', 563 | '', 564 | 'Options:', 565 | ' -h, --help Show help', 566 | ].join('\n'), 567 | ); 568 | }); 569 | 570 | test('command with an alias', async t => { 571 | const fixture = 'command-alias'; 572 | 573 | const deploy = await run(fixture); 574 | t.is(deploy.stdout, 'Deploy'); 575 | 576 | const push = await run(fixture, ['push']); 577 | t.is(push.stdout, 'Deploy'); 578 | 579 | const help = await run(fixture, ['--help']); 580 | 581 | t.is( 582 | help.stdout, 583 | [ 584 | 'Usage: test [options] [command]', 585 | '', 586 | 'Description', 587 | '', 588 | 'Options:', 589 | ' -v, --version Show version number', 590 | ' -h, --help Show help', 591 | '', 592 | 'Commands:', 593 | ' deploy|push Deploy command', 594 | ' help [command] Show help for command', 595 | ].join('\n'), 596 | ); 597 | }); 598 | -------------------------------------------------------------------------------- /test/fixtures/all-optional-options/cli.ts: -------------------------------------------------------------------------------- 1 | import Pastel from '../../../source/index.js'; 2 | 3 | const app = new Pastel({ 4 | name: 'test', 5 | version: '0.0.0', 6 | description: 'Description', 7 | importMeta: import.meta, 8 | }); 9 | 10 | await app.run(); 11 | -------------------------------------------------------------------------------- /test/fixtures/all-optional-options/commands/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Text} from 'ink'; 3 | import zod from 'zod'; 4 | 5 | export const options = zod 6 | .object({ 7 | name: zod.string().describe('Name'), 8 | }) 9 | .partial(); 10 | 11 | type Properties = { 12 | readonly options: zod.infer; 13 | }; 14 | 15 | export default function Index({options}: Properties) { 16 | return Name = {options.name ?? 'empty'}; 17 | } 18 | -------------------------------------------------------------------------------- /test/fixtures/array-option/alias/cli.ts: -------------------------------------------------------------------------------- 1 | import Pastel from '../../../../source/index.js'; 2 | 3 | const app = new Pastel({ 4 | name: 'test', 5 | version: '0.0.0', 6 | description: 'Description', 7 | importMeta: import.meta, 8 | }); 9 | 10 | await app.run(); 11 | -------------------------------------------------------------------------------- /test/fixtures/array-option/alias/commands/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Text} from 'ink'; 3 | import zod from 'zod'; 4 | import {option} from '../../../../../source/index.js'; 5 | 6 | export const options = zod.object({ 7 | tag: zod.array(zod.string()).describe( 8 | option({ 9 | description: 'Tags', 10 | alias: 't', 11 | }), 12 | ), 13 | }); 14 | 15 | type Properties = { 16 | readonly options: zod.infer; 17 | }; 18 | 19 | export default function Index({options}: Properties) { 20 | return Tags = {options.tag.join(', ')}; 21 | } 22 | -------------------------------------------------------------------------------- /test/fixtures/array-option/default-value-description/cli.ts: -------------------------------------------------------------------------------- 1 | import Pastel from '../../../../source/index.js'; 2 | 3 | const app = new Pastel({ 4 | name: 'test', 5 | version: '0.0.0', 6 | description: 'Description', 7 | importMeta: import.meta, 8 | }); 9 | 10 | await app.run(); 11 | -------------------------------------------------------------------------------- /test/fixtures/array-option/default-value-description/commands/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Text} from 'ink'; 3 | import zod from 'zod'; 4 | import {option} from '../../../../../source/index.js'; 5 | 6 | export const options = zod.object({ 7 | tag: zod 8 | .array(zod.string()) 9 | .default(['A', 'B']) 10 | .describe( 11 | option({ 12 | description: 'Tags', 13 | defaultValueDescription: 'A, B', 14 | }), 15 | ), 16 | }); 17 | 18 | type Properties = { 19 | readonly options: zod.infer; 20 | }; 21 | 22 | export default function Index({options}: Properties) { 23 | return Tags = {options.tag.join(', ')}; 24 | } 25 | -------------------------------------------------------------------------------- /test/fixtures/array-option/default-value/cli.ts: -------------------------------------------------------------------------------- 1 | import Pastel from '../../../../source/index.js'; 2 | 3 | const app = new Pastel({ 4 | name: 'test', 5 | version: '0.0.0', 6 | description: 'Description', 7 | importMeta: import.meta, 8 | }); 9 | 10 | await app.run(); 11 | -------------------------------------------------------------------------------- /test/fixtures/array-option/default-value/commands/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Text} from 'ink'; 3 | import zod from 'zod'; 4 | 5 | export const options = zod.object({ 6 | tag: zod.array(zod.string()).default(['A', 'B']).describe('Tags'), 7 | }); 8 | 9 | type Properties = { 10 | readonly options: zod.infer; 11 | }; 12 | 13 | export default function Index({options}: Properties) { 14 | return Tags = {options.tag.join(', ')}; 15 | } 16 | -------------------------------------------------------------------------------- /test/fixtures/array-option/description/cli.ts: -------------------------------------------------------------------------------- 1 | import Pastel from '../../../../source/index.js'; 2 | 3 | const app = new Pastel({ 4 | name: 'test', 5 | version: '0.0.0', 6 | description: 'Description', 7 | importMeta: import.meta, 8 | }); 9 | 10 | await app.run(); 11 | -------------------------------------------------------------------------------- /test/fixtures/array-option/description/commands/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Text} from 'ink'; 3 | import zod from 'zod'; 4 | import {option} from '../../../../../source/index.js'; 5 | 6 | export const options = zod.object({ 7 | tag: zod.array(zod.string()).describe( 8 | option({ 9 | description: 'Tags', 10 | }), 11 | ), 12 | }); 13 | 14 | type Properties = { 15 | readonly options: zod.infer; 16 | }; 17 | 18 | export default function Index({options}: Properties) { 19 | return Tags = {options.tag.join(', ')}; 20 | } 21 | -------------------------------------------------------------------------------- /test/fixtures/array-option/optional/cli.ts: -------------------------------------------------------------------------------- 1 | import Pastel from '../../../../source/index.js'; 2 | 3 | const app = new Pastel({ 4 | name: 'test', 5 | version: '0.0.0', 6 | description: 'Description', 7 | importMeta: import.meta, 8 | }); 9 | 10 | await app.run(); 11 | -------------------------------------------------------------------------------- /test/fixtures/array-option/optional/commands/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Text} from 'ink'; 3 | import zod from 'zod'; 4 | 5 | export const options = zod.object({ 6 | tag: zod.array(zod.string()).optional().describe('Tags'), 7 | }); 8 | 9 | type Properties = { 10 | readonly options: zod.infer; 11 | }; 12 | 13 | export default function Index({options}: Properties) { 14 | return Tags = {options.tag?.join(', ')}; 15 | } 16 | -------------------------------------------------------------------------------- /test/fixtures/array-option/required/cli.ts: -------------------------------------------------------------------------------- 1 | import Pastel from '../../../../source/index.js'; 2 | 3 | const app = new Pastel({ 4 | name: 'test', 5 | version: '0.0.0', 6 | description: 'Description', 7 | importMeta: import.meta, 8 | }); 9 | 10 | await app.run(); 11 | -------------------------------------------------------------------------------- /test/fixtures/array-option/required/commands/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Text} from 'ink'; 3 | import zod from 'zod'; 4 | 5 | export const options = zod.object({ 6 | tag: zod.array(zod.string()).describe('Tags'), 7 | }); 8 | 9 | type Properties = { 10 | readonly options: zod.infer; 11 | }; 12 | 13 | export default function Index({options}: Properties) { 14 | return Tags = {options.tag.join(', ')}; 15 | } 16 | -------------------------------------------------------------------------------- /test/fixtures/array-option/value-description/cli.ts: -------------------------------------------------------------------------------- 1 | import Pastel from '../../../../source/index.js'; 2 | 3 | const app = new Pastel({ 4 | name: 'test', 5 | version: '0.0.0', 6 | description: 'Description', 7 | importMeta: import.meta, 8 | }); 9 | 10 | await app.run(); 11 | -------------------------------------------------------------------------------- /test/fixtures/array-option/value-description/commands/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Text} from 'ink'; 3 | import zod from 'zod'; 4 | import {option} from '../../../../../source/index.js'; 5 | 6 | export const options = zod.object({ 7 | tag: zod.array(zod.string()).describe( 8 | option({ 9 | description: 'Tags', 10 | valueDescription: 'some-tags', 11 | }), 12 | ), 13 | }); 14 | 15 | type Properties = { 16 | readonly options: zod.infer; 17 | }; 18 | 19 | export default function Index({options}: Properties) { 20 | return Tags = {options.tag.join(', ')}; 21 | } 22 | -------------------------------------------------------------------------------- /test/fixtures/boolean-option/alias/cli.ts: -------------------------------------------------------------------------------- 1 | import Pastel from '../../../../source/index.js'; 2 | 3 | const app = new Pastel({ 4 | name: 'test', 5 | version: '0.0.0', 6 | description: 'Description', 7 | importMeta: import.meta, 8 | }); 9 | 10 | await app.run(); 11 | -------------------------------------------------------------------------------- /test/fixtures/boolean-option/alias/commands/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Text} from 'ink'; 3 | import zod from 'zod'; 4 | import {option} from '../../../../../source/index.js'; 5 | 6 | export const options = zod.object({ 7 | force: zod.boolean().describe( 8 | option({ 9 | description: 'Force', 10 | alias: 'f', 11 | }), 12 | ), 13 | }); 14 | 15 | type Properties = { 16 | readonly options: zod.infer; 17 | }; 18 | 19 | export default function Index({options}: Properties) { 20 | return Force = {String(options.force)}; 21 | } 22 | -------------------------------------------------------------------------------- /test/fixtures/boolean-option/default-value/cli.ts: -------------------------------------------------------------------------------- 1 | import Pastel from '../../../../source/index.js'; 2 | 3 | const app = new Pastel({ 4 | name: 'test', 5 | version: '0.0.0', 6 | description: 'Description', 7 | importMeta: import.meta, 8 | }); 9 | 10 | await app.run(); 11 | -------------------------------------------------------------------------------- /test/fixtures/boolean-option/default-value/commands/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Text} from 'ink'; 3 | import zod from 'zod'; 4 | 5 | export const options = zod.object({ 6 | force: zod.boolean().default(true).describe('Force'), 7 | }); 8 | 9 | type Properties = { 10 | readonly options: zod.infer; 11 | }; 12 | 13 | export default function Index({options}: Properties) { 14 | return Force = {String(options.force)}; 15 | } 16 | -------------------------------------------------------------------------------- /test/fixtures/boolean-option/default/cli.ts: -------------------------------------------------------------------------------- 1 | import Pastel from '../../../../source/index.js'; 2 | 3 | const app = new Pastel({ 4 | name: 'test', 5 | version: '0.0.0', 6 | description: 'Description', 7 | importMeta: import.meta, 8 | }); 9 | 10 | await app.run(); 11 | -------------------------------------------------------------------------------- /test/fixtures/boolean-option/default/commands/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Text} from 'ink'; 3 | import zod from 'zod'; 4 | 5 | export const options = zod.object({ 6 | force: zod.boolean().describe('Force'), 7 | }); 8 | 9 | type Properties = { 10 | readonly options: zod.infer; 11 | }; 12 | 13 | export default function Index({options}: Properties) { 14 | return Force = {String(options.force)}; 15 | } 16 | -------------------------------------------------------------------------------- /test/fixtures/boolean-option/description/cli.ts: -------------------------------------------------------------------------------- 1 | import Pastel from '../../../../source/index.js'; 2 | 3 | const app = new Pastel({ 4 | name: 'test', 5 | version: '0.0.0', 6 | description: 'Description', 7 | importMeta: import.meta, 8 | }); 9 | 10 | await app.run(); 11 | -------------------------------------------------------------------------------- /test/fixtures/boolean-option/description/commands/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Text} from 'ink'; 3 | import zod from 'zod'; 4 | import {option} from '../../../../../source/index.js'; 5 | 6 | export const options = zod.object({ 7 | force: zod.boolean().describe( 8 | option({ 9 | description: 'Force', 10 | }), 11 | ), 12 | }); 13 | 14 | type Properties = { 15 | readonly options: zod.infer; 16 | }; 17 | 18 | export default function Index({options}: Properties) { 19 | return Force = {String(options.force)}; 20 | } 21 | -------------------------------------------------------------------------------- /test/fixtures/boolean-option/negated/cli.ts: -------------------------------------------------------------------------------- 1 | import Pastel from '../../../../source/index.js'; 2 | 3 | const app = new Pastel({ 4 | name: 'test', 5 | version: '0.0.0', 6 | description: 'Description', 7 | importMeta: import.meta, 8 | }); 9 | 10 | await app.run(); 11 | -------------------------------------------------------------------------------- /test/fixtures/boolean-option/negated/commands/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Text} from 'ink'; 3 | import zod from 'zod'; 4 | 5 | export const options = zod.object({ 6 | force: zod.boolean().default(true).describe("Don't force"), 7 | }); 8 | 9 | type Properties = { 10 | readonly options: zod.infer; 11 | }; 12 | 13 | export default function Index({options}: Properties) { 14 | return Force = {String(options.force)}; 15 | } 16 | -------------------------------------------------------------------------------- /test/fixtures/camelcase-argument/cli.ts: -------------------------------------------------------------------------------- 1 | import Pastel from '../../../source/index.js'; 2 | 3 | const app = new Pastel({ 4 | name: 'test', 5 | version: '0.0.0', 6 | description: 'Description', 7 | importMeta: import.meta, 8 | }); 9 | 10 | await app.run(); 11 | -------------------------------------------------------------------------------- /test/fixtures/camelcase-argument/commands/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Text} from 'ink'; 3 | import zod from 'zod'; 4 | 5 | export const args = zod.tuple([zod.string().describe('firstName')]); 6 | 7 | type Properties = { 8 | readonly args: zod.infer; 9 | }; 10 | 11 | export default function Index({args}: Properties) { 12 | return Arguments = {args.join(', ')}; 13 | } 14 | -------------------------------------------------------------------------------- /test/fixtures/camelcase-command/cli.ts: -------------------------------------------------------------------------------- 1 | import Pastel from '../../../source/index.js'; 2 | 3 | const app = new Pastel({ 4 | name: 'test', 5 | version: '0.0.0', 6 | description: 'Description', 7 | importMeta: import.meta, 8 | }); 9 | 10 | await app.run(); 11 | -------------------------------------------------------------------------------- /test/fixtures/camelcase-command/commands/auth.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Text} from 'ink'; 3 | 4 | export const description = 'Auth command'; 5 | 6 | export default function Auth() { 7 | return Auth; 8 | } 9 | -------------------------------------------------------------------------------- /test/fixtures/camelcase-command/commands/superDeploy.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Text} from 'ink'; 3 | 4 | export const isDefault = true; 5 | export const description = 'Deploy command'; 6 | 7 | export default function Deploy() { 8 | return Deploy; 9 | } 10 | -------------------------------------------------------------------------------- /test/fixtures/camelcase-option/cli.ts: -------------------------------------------------------------------------------- 1 | import Pastel from '../../../source/index.js'; 2 | 3 | const app = new Pastel({ 4 | name: 'test', 5 | version: '0.0.0', 6 | description: 'Description', 7 | importMeta: import.meta, 8 | }); 9 | 10 | await app.run(); 11 | -------------------------------------------------------------------------------- /test/fixtures/camelcase-option/commands/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Text} from 'ink'; 3 | import zod from 'zod'; 4 | 5 | export const options = zod.object({ 6 | firstName: zod.string().describe('Name'), 7 | }); 8 | 9 | type Properties = { 10 | readonly options: zod.infer; 11 | }; 12 | 13 | export default function Index({options}: Properties) { 14 | return Name = {options.firstName}; 15 | } 16 | -------------------------------------------------------------------------------- /test/fixtures/command-alias/cli.ts: -------------------------------------------------------------------------------- 1 | import Pastel from '../../../source/index.js'; 2 | 3 | const app = new Pastel({ 4 | name: 'test', 5 | version: '0.0.0', 6 | description: 'Description', 7 | importMeta: import.meta, 8 | }); 9 | 10 | await app.run(); 11 | -------------------------------------------------------------------------------- /test/fixtures/command-alias/commands/deploy.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Text} from 'ink'; 3 | 4 | export const isDefault = true; 5 | export const description = 'Deploy command'; 6 | export const alias = 'push'; 7 | 8 | export default function Deploy() { 9 | return Deploy; 10 | } 11 | -------------------------------------------------------------------------------- /test/fixtures/deeply-nested-commands/cli.ts: -------------------------------------------------------------------------------- 1 | import Pastel from '../../../source/index.js'; 2 | 3 | const app = new Pastel({ 4 | name: 'test', 5 | version: '0.0.0', 6 | description: 'Description', 7 | importMeta: import.meta, 8 | }); 9 | 10 | await app.run(); 11 | -------------------------------------------------------------------------------- /test/fixtures/deeply-nested-commands/commands/auth.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Text} from 'ink'; 3 | 4 | export const description = 'Auth command'; 5 | 6 | export default function Auth() { 7 | return Auth; 8 | } 9 | -------------------------------------------------------------------------------- /test/fixtures/deeply-nested-commands/commands/deploy.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Text} from 'ink'; 3 | 4 | export const isDefault = true; 5 | export const description = 'Deploy command'; 6 | 7 | export default function Deploy() { 8 | return Deploy; 9 | } 10 | -------------------------------------------------------------------------------- /test/fixtures/deeply-nested-commands/commands/projects/create.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Text} from 'ink'; 3 | 4 | export const description = 'Create project'; 5 | 6 | export default function CreateProject() { 7 | return Create project; 8 | } 9 | -------------------------------------------------------------------------------- /test/fixtures/deeply-nested-commands/commands/projects/index.ts: -------------------------------------------------------------------------------- 1 | export const description = 'Manage projects'; 2 | -------------------------------------------------------------------------------- /test/fixtures/deeply-nested-commands/commands/projects/list.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Text} from 'ink'; 3 | 4 | export const description = 'List projects'; 5 | 6 | export default function ListProjects() { 7 | return List projects; 8 | } 9 | -------------------------------------------------------------------------------- /test/fixtures/deeply-nested-commands/commands/projects/servers/create.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Text} from 'ink'; 3 | 4 | export const description = 'Create server'; 5 | 6 | export default function CreateServer() { 7 | return Create server; 8 | } 9 | -------------------------------------------------------------------------------- /test/fixtures/deeply-nested-commands/commands/projects/servers/index.ts: -------------------------------------------------------------------------------- 1 | export const description = 'Manage servers'; 2 | -------------------------------------------------------------------------------- /test/fixtures/deeply-nested-commands/commands/projects/servers/list.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Text} from 'ink'; 3 | 4 | export const description = 'List servers'; 5 | 6 | export default function ListServers() { 7 | return List servers; 8 | } 9 | -------------------------------------------------------------------------------- /test/fixtures/enum-argument/array-default-value-description/cli.ts: -------------------------------------------------------------------------------- 1 | import Pastel from '../../../../source/index.js'; 2 | 3 | const app = new Pastel({ 4 | name: 'test', 5 | version: '0.0.0', 6 | description: 'Description', 7 | importMeta: import.meta, 8 | }); 9 | 10 | await app.run(); 11 | -------------------------------------------------------------------------------- /test/fixtures/enum-argument/array-default-value-description/commands/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Text} from 'ink'; 3 | import zod from 'zod'; 4 | import {argument} from '../../../../../source/index.js'; 5 | 6 | export const args = zod 7 | .array(zod.enum(['macOS', 'Ubuntu', 'Debian', 'Windows'])) 8 | .default(['macOS', 'Windows']) 9 | .describe( 10 | argument({ 11 | name: 'os', 12 | description: 'Operating systems', 13 | defaultValueDescription: 'macOS, Windows', 14 | }), 15 | ); 16 | 17 | type Properties = { 18 | readonly args: zod.infer; 19 | }; 20 | 21 | export default function Index({args}: Properties) { 22 | return Arguments = {args.join(', ')}; 23 | } 24 | -------------------------------------------------------------------------------- /test/fixtures/enum-argument/array-default-value/cli.ts: -------------------------------------------------------------------------------- 1 | import Pastel from '../../../../source/index.js'; 2 | 3 | const app = new Pastel({ 4 | name: 'test', 5 | version: '0.0.0', 6 | description: 'Description', 7 | importMeta: import.meta, 8 | }); 9 | 10 | await app.run(); 11 | -------------------------------------------------------------------------------- /test/fixtures/enum-argument/array-default-value/commands/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Text} from 'ink'; 3 | import zod from 'zod'; 4 | 5 | export const args = zod 6 | .array(zod.enum(['macOS', 'Ubuntu', 'Debian', 'Windows'])) 7 | .default(['macOS', 'Windows']) 8 | .describe('os'); 9 | 10 | type Properties = { 11 | readonly args: zod.infer; 12 | }; 13 | 14 | export default function Index({args}: Properties) { 15 | return Arguments = {args.join(', ')}; 16 | } 17 | -------------------------------------------------------------------------------- /test/fixtures/enum-argument/array-description/cli.ts: -------------------------------------------------------------------------------- 1 | import Pastel from '../../../../source/index.js'; 2 | 3 | const app = new Pastel({ 4 | name: 'test', 5 | version: '0.0.0', 6 | description: 'Description', 7 | importMeta: import.meta, 8 | }); 9 | 10 | await app.run(); 11 | -------------------------------------------------------------------------------- /test/fixtures/enum-argument/array-description/commands/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Text} from 'ink'; 3 | import zod from 'zod'; 4 | import {argument} from '../../../../../source/index.js'; 5 | 6 | export const args = zod 7 | .array(zod.enum(['Ubuntu', 'Debian'])) 8 | .describe(argument({name: 'os', description: 'Operating systems'})); 9 | 10 | type Properties = { 11 | readonly args: zod.infer; 12 | }; 13 | 14 | export default function Index({args}: Properties) { 15 | return Arguments = {args.join(', ')}; 16 | } 17 | -------------------------------------------------------------------------------- /test/fixtures/enum-argument/array-name/cli.ts: -------------------------------------------------------------------------------- 1 | import Pastel from '../../../../source/index.js'; 2 | 3 | const app = new Pastel({ 4 | name: 'test', 5 | version: '0.0.0', 6 | description: 'Description', 7 | importMeta: import.meta, 8 | }); 9 | 10 | await app.run(); 11 | -------------------------------------------------------------------------------- /test/fixtures/enum-argument/array-name/commands/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Text} from 'ink'; 3 | import zod from 'zod'; 4 | import {argument} from '../../../../../source/index.js'; 5 | 6 | export const args = zod 7 | .array(zod.enum(['Ubuntu', 'Debian'])) 8 | .describe(argument({name: 'os'})); 9 | 10 | type Properties = { 11 | readonly args: zod.infer; 12 | }; 13 | 14 | export default function Index({args}: Properties) { 15 | return Arguments = {args.join(', ')}; 16 | } 17 | -------------------------------------------------------------------------------- /test/fixtures/enum-argument/array/cli.ts: -------------------------------------------------------------------------------- 1 | import Pastel from '../../../../source/index.js'; 2 | 3 | const app = new Pastel({ 4 | name: 'test', 5 | version: '0.0.0', 6 | description: 'Description', 7 | importMeta: import.meta, 8 | }); 9 | 10 | await app.run(); 11 | -------------------------------------------------------------------------------- /test/fixtures/enum-argument/array/commands/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Text} from 'ink'; 3 | import zod from 'zod'; 4 | 5 | export const args = zod.array(zod.enum(['Ubuntu', 'Debian'])).describe('os'); 6 | 7 | type Properties = { 8 | readonly args: zod.infer; 9 | }; 10 | 11 | export default function Index({args}: Properties) { 12 | return Arguments = {args.join(', ')}; 13 | } 14 | -------------------------------------------------------------------------------- /test/fixtures/enum-argument/default-value-description/cli.ts: -------------------------------------------------------------------------------- 1 | import Pastel from '../../../../source/index.js'; 2 | 3 | const app = new Pastel({ 4 | name: 'test', 5 | version: '0.0.0', 6 | description: 'Description', 7 | importMeta: import.meta, 8 | }); 9 | 10 | await app.run(); 11 | -------------------------------------------------------------------------------- /test/fixtures/enum-argument/default-value-description/commands/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Text} from 'ink'; 3 | import zod from 'zod'; 4 | import {argument} from '../../../../../source/index.js'; 5 | 6 | const os = zod.enum(['Ubuntu', 'Debian']); 7 | 8 | export const args = zod.tuple([ 9 | os.optional().describe( 10 | argument({ 11 | name: 'first', 12 | description: 'First', 13 | }), 14 | ), 15 | os.default('Debian').describe( 16 | argument({ 17 | name: 'second', 18 | description: 'Second', 19 | defaultValueDescription: 'Debian', 20 | }), 21 | ), 22 | ]); 23 | 24 | type Properties = { 25 | readonly args: zod.infer; 26 | }; 27 | 28 | export default function Index({args}: Properties) { 29 | return Arguments = {args.join(', ')}; 30 | } 31 | -------------------------------------------------------------------------------- /test/fixtures/enum-argument/default-value/cli.ts: -------------------------------------------------------------------------------- 1 | import Pastel from '../../../../source/index.js'; 2 | 3 | const app = new Pastel({ 4 | name: 'test', 5 | version: '0.0.0', 6 | description: 'Description', 7 | importMeta: import.meta, 8 | }); 9 | 10 | await app.run(); 11 | -------------------------------------------------------------------------------- /test/fixtures/enum-argument/default-value/commands/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Text} from 'ink'; 3 | import zod from 'zod'; 4 | 5 | const os = zod.enum(['Ubuntu', 'Debian']); 6 | 7 | export const args = zod.tuple([ 8 | os.optional().describe('first'), 9 | os.default('Debian').describe('second'), 10 | ]); 11 | 12 | type Properties = { 13 | readonly args: zod.infer; 14 | }; 15 | 16 | export default function Index({args}: Properties) { 17 | return Arguments = {args.join(', ')}; 18 | } 19 | -------------------------------------------------------------------------------- /test/fixtures/enum-argument/description/cli.ts: -------------------------------------------------------------------------------- 1 | import Pastel from '../../../../source/index.js'; 2 | 3 | const app = new Pastel({ 4 | name: 'test', 5 | version: '0.0.0', 6 | description: 'Description', 7 | importMeta: import.meta, 8 | }); 9 | 10 | await app.run(); 11 | -------------------------------------------------------------------------------- /test/fixtures/enum-argument/description/commands/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Text} from 'ink'; 3 | import zod from 'zod'; 4 | import {argument} from '../../../../../source/index.js'; 5 | 6 | const os = zod.enum(['Ubuntu', 'Debian']); 7 | 8 | export const args = zod.tuple([ 9 | os.describe( 10 | argument({ 11 | name: 'first', 12 | description: 'First', 13 | }), 14 | ), 15 | os.describe( 16 | argument({ 17 | name: 'second', 18 | description: 'Second', 19 | }), 20 | ), 21 | ]); 22 | 23 | type Properties = { 24 | readonly args: zod.infer; 25 | }; 26 | 27 | export default function Index({args}: Properties) { 28 | return Arguments = {args.join(', ')}; 29 | } 30 | -------------------------------------------------------------------------------- /test/fixtures/enum-argument/optional-array/cli.ts: -------------------------------------------------------------------------------- 1 | import Pastel from '../../../../source/index.js'; 2 | 3 | const app = new Pastel({ 4 | name: 'test', 5 | version: '0.0.0', 6 | description: 'Description', 7 | importMeta: import.meta, 8 | }); 9 | 10 | await app.run(); 11 | -------------------------------------------------------------------------------- /test/fixtures/enum-argument/optional-array/commands/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Text} from 'ink'; 3 | import zod from 'zod'; 4 | 5 | export const args = zod 6 | .array(zod.enum(['Ubuntu', 'Debian'])) 7 | .optional() 8 | .describe('os'); 9 | 10 | type Properties = { 11 | readonly args: zod.infer; 12 | }; 13 | 14 | export default function Index({args}: Properties) { 15 | return Arguments = {args?.join(', ') ?? ''}; 16 | } 17 | -------------------------------------------------------------------------------- /test/fixtures/enum-argument/optional/cli.ts: -------------------------------------------------------------------------------- 1 | import Pastel from '../../../../source/index.js'; 2 | 3 | const app = new Pastel({ 4 | name: 'test', 5 | version: '0.0.0', 6 | description: 'Description', 7 | importMeta: import.meta, 8 | }); 9 | 10 | await app.run(); 11 | -------------------------------------------------------------------------------- /test/fixtures/enum-argument/optional/commands/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Text} from 'ink'; 3 | import zod from 'zod'; 4 | 5 | const os = zod.enum(['Ubuntu', 'Debian']).optional(); 6 | 7 | export const args = zod.tuple([os.describe('first'), os.describe('second')]); 8 | 9 | type Properties = { 10 | readonly args: zod.infer; 11 | }; 12 | 13 | export default function Index({args}: Properties) { 14 | return Arguments = {args.join(', ')}; 15 | } 16 | -------------------------------------------------------------------------------- /test/fixtures/enum-argument/required/cli.ts: -------------------------------------------------------------------------------- 1 | import Pastel from '../../../../source/index.js'; 2 | 3 | const app = new Pastel({ 4 | name: 'test', 5 | version: '0.0.0', 6 | description: 'Description', 7 | importMeta: import.meta, 8 | }); 9 | 10 | await app.run(); 11 | -------------------------------------------------------------------------------- /test/fixtures/enum-argument/required/commands/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Text} from 'ink'; 3 | import zod from 'zod'; 4 | import {argument} from '../../../../../source/index.js'; 5 | 6 | const os = zod.enum(['Ubuntu', 'Debian']); 7 | 8 | export const args = zod.tuple([ 9 | os.describe('first'), 10 | os.describe( 11 | argument({ 12 | name: 'second', 13 | }), 14 | ), 15 | ]); 16 | 17 | type Properties = { 18 | readonly args: zod.infer; 19 | }; 20 | 21 | export default function Index({args}: Properties) { 22 | return Arguments = {args.join(', ')}; 23 | } 24 | -------------------------------------------------------------------------------- /test/fixtures/enum-argument/variadic/cli.ts: -------------------------------------------------------------------------------- 1 | import Pastel from '../../../../source/index.js'; 2 | 3 | const app = new Pastel({ 4 | name: 'test', 5 | version: '0.0.0', 6 | description: 'Description', 7 | importMeta: import.meta, 8 | }); 9 | 10 | await app.run(); 11 | -------------------------------------------------------------------------------- /test/fixtures/enum-argument/variadic/commands/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Text} from 'ink'; 3 | import zod from 'zod'; 4 | 5 | const os = zod.enum(['macOS', 'Ubuntu', 'Debian', 'Windows']); 6 | 7 | export const args = zod 8 | .tuple([os.describe('first'), os.describe('second')]) 9 | .rest(os.describe('rest')); 10 | 11 | type Properties = { 12 | readonly args: zod.infer; 13 | }; 14 | 15 | export default function Index({args}: Properties) { 16 | return Arguments = {args.join(', ')}; 17 | } 18 | -------------------------------------------------------------------------------- /test/fixtures/enum-option/alias/cli.ts: -------------------------------------------------------------------------------- 1 | import Pastel from '../../../../source/index.js'; 2 | 3 | const app = new Pastel({ 4 | name: 'test', 5 | version: '0.0.0', 6 | description: 'Description', 7 | importMeta: import.meta, 8 | }); 9 | 10 | await app.run(); 11 | -------------------------------------------------------------------------------- /test/fixtures/enum-option/alias/commands/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Text} from 'ink'; 3 | import zod from 'zod'; 4 | import {option} from '../../../../../source/index.js'; 5 | 6 | export const options = zod.object({ 7 | os: zod.enum(['Ubuntu', 'Debian']).describe( 8 | option({ 9 | description: 'Operating system', 10 | alias: 's', 11 | }), 12 | ), 13 | }); 14 | 15 | type Properties = { 16 | readonly options: zod.infer; 17 | }; 18 | 19 | export default function Index({options}: Properties) { 20 | return OS = {options.os}; 21 | } 22 | -------------------------------------------------------------------------------- /test/fixtures/enum-option/default-value-description/cli.ts: -------------------------------------------------------------------------------- 1 | import Pastel from '../../../../source/index.js'; 2 | 3 | const app = new Pastel({ 4 | name: 'test', 5 | version: '0.0.0', 6 | description: 'Description', 7 | importMeta: import.meta, 8 | }); 9 | 10 | await app.run(); 11 | -------------------------------------------------------------------------------- /test/fixtures/enum-option/default-value-description/commands/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Text} from 'ink'; 3 | import zod from 'zod'; 4 | import {option} from '../../../../../source/index.js'; 5 | 6 | export const options = zod.object({ 7 | os: zod 8 | .enum(['Ubuntu', 'Debian']) 9 | .default('Ubuntu') 10 | .describe( 11 | option({ 12 | description: 'Operating system', 13 | defaultValueDescription: 'Canonical', 14 | }), 15 | ), 16 | }); 17 | 18 | type Properties = { 19 | readonly options: zod.infer; 20 | }; 21 | 22 | export default function Index({options}: Properties) { 23 | return OS = {options.os}; 24 | } 25 | -------------------------------------------------------------------------------- /test/fixtures/enum-option/default-value/cli.ts: -------------------------------------------------------------------------------- 1 | import Pastel from '../../../../source/index.js'; 2 | 3 | const app = new Pastel({ 4 | name: 'test', 5 | version: '0.0.0', 6 | description: 'Description', 7 | importMeta: import.meta, 8 | }); 9 | 10 | await app.run(); 11 | -------------------------------------------------------------------------------- /test/fixtures/enum-option/default-value/commands/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Text} from 'ink'; 3 | import zod from 'zod'; 4 | 5 | export const options = zod.object({ 6 | os: zod 7 | .enum(['Ubuntu', 'Debian']) 8 | .default('Ubuntu') 9 | .describe('Operating system'), 10 | }); 11 | 12 | type Properties = { 13 | readonly options: zod.infer; 14 | }; 15 | 16 | export default function Index({options}: Properties) { 17 | return OS = {options.os}; 18 | } 19 | -------------------------------------------------------------------------------- /test/fixtures/enum-option/description/cli.ts: -------------------------------------------------------------------------------- 1 | import Pastel from '../../../../source/index.js'; 2 | 3 | const app = new Pastel({ 4 | name: 'test', 5 | version: '0.0.0', 6 | description: 'Description', 7 | importMeta: import.meta, 8 | }); 9 | 10 | await app.run(); 11 | -------------------------------------------------------------------------------- /test/fixtures/enum-option/description/commands/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Text} from 'ink'; 3 | import zod from 'zod'; 4 | import {option} from '../../../../../source/index.js'; 5 | 6 | export const options = zod.object({ 7 | os: zod.enum(['Ubuntu', 'Debian']).describe( 8 | option({ 9 | description: 'Operating system', 10 | }), 11 | ), 12 | }); 13 | 14 | type Properties = { 15 | readonly options: zod.infer; 16 | }; 17 | 18 | export default function Index({options}: Properties) { 19 | return OS = {options.os}; 20 | } 21 | -------------------------------------------------------------------------------- /test/fixtures/enum-option/optional/cli.ts: -------------------------------------------------------------------------------- 1 | import Pastel from '../../../../source/index.js'; 2 | 3 | const app = new Pastel({ 4 | name: 'test', 5 | version: '0.0.0', 6 | description: 'Description', 7 | importMeta: import.meta, 8 | }); 9 | 10 | await app.run(); 11 | -------------------------------------------------------------------------------- /test/fixtures/enum-option/optional/commands/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Text} from 'ink'; 3 | import zod from 'zod'; 4 | 5 | export const options = zod.object({ 6 | os: zod.enum(['Ubuntu', 'Debian']).optional().describe('Operating system'), 7 | }); 8 | 9 | type Properties = { 10 | readonly options: zod.infer; 11 | }; 12 | 13 | export default function Index({options}: Properties) { 14 | return OS = {options.os ?? 'empty'}; 15 | } 16 | -------------------------------------------------------------------------------- /test/fixtures/enum-option/required/cli.ts: -------------------------------------------------------------------------------- 1 | import Pastel from '../../../../source/index.js'; 2 | 3 | const app = new Pastel({ 4 | name: 'test', 5 | version: '0.0.0', 6 | description: 'Description', 7 | importMeta: import.meta, 8 | }); 9 | 10 | await app.run(); 11 | -------------------------------------------------------------------------------- /test/fixtures/enum-option/required/commands/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Text} from 'ink'; 3 | import zod from 'zod'; 4 | 5 | export const options = zod.object({ 6 | os: zod.enum(['Ubuntu', 'Debian']).describe('Operating system'), 7 | }); 8 | 9 | type Properties = { 10 | readonly options: zod.infer; 11 | }; 12 | 13 | export default function Index({options}: Properties) { 14 | return OS = {options.os}; 15 | } 16 | -------------------------------------------------------------------------------- /test/fixtures/enum-option/value-description/cli.ts: -------------------------------------------------------------------------------- 1 | import Pastel from '../../../../source/index.js'; 2 | 3 | const app = new Pastel({ 4 | name: 'test', 5 | version: '0.0.0', 6 | description: 'Description', 7 | importMeta: import.meta, 8 | }); 9 | 10 | await app.run(); 11 | -------------------------------------------------------------------------------- /test/fixtures/enum-option/value-description/commands/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Text} from 'ink'; 3 | import zod from 'zod'; 4 | import {option} from '../../../../../source/index.js'; 5 | 6 | export const options = zod.object({ 7 | os: zod.enum(['Ubuntu', 'Debian']).describe( 8 | option({ 9 | description: 'Operating system', 10 | valueDescription: 'OS', 11 | }), 12 | ), 13 | }); 14 | 15 | type Properties = { 16 | readonly options: zod.infer; 17 | }; 18 | 19 | export default function Index({options}: Properties) { 20 | return OS = {options.os}; 21 | } 22 | -------------------------------------------------------------------------------- /test/fixtures/multiple-commands/cli.ts: -------------------------------------------------------------------------------- 1 | import Pastel from '../../../source/index.js'; 2 | 3 | const app = new Pastel({ 4 | name: 'test', 5 | version: '0.0.0', 6 | description: 'Description', 7 | importMeta: import.meta, 8 | }); 9 | 10 | await app.run(); 11 | -------------------------------------------------------------------------------- /test/fixtures/multiple-commands/commands/auth.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Text} from 'ink'; 3 | 4 | export const description = 'Auth command'; 5 | 6 | export default function Auth() { 7 | return Auth; 8 | } 9 | -------------------------------------------------------------------------------- /test/fixtures/multiple-commands/commands/deploy.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Text} from 'ink'; 3 | 4 | export const isDefault = true; 5 | export const description = 'Deploy command'; 6 | 7 | export default function Deploy() { 8 | return Deploy; 9 | } 10 | -------------------------------------------------------------------------------- /test/fixtures/nested-commands-custom-app/cli.ts: -------------------------------------------------------------------------------- 1 | import Pastel from '../../../source/index.js'; 2 | 3 | const app = new Pastel({ 4 | name: 'test', 5 | version: '0.0.0', 6 | description: 'Description', 7 | importMeta: import.meta, 8 | }); 9 | 10 | await app.run(); 11 | -------------------------------------------------------------------------------- /test/fixtures/nested-commands-custom-app/commands/_app.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Box} from 'ink'; 3 | import {type AppProps} from '../../../../source/index.js'; 4 | 5 | export default function CustomApp({Component, commandProps}: AppProps) { 6 | return ( 7 | 8 | 9 | 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /test/fixtures/nested-commands-custom-app/commands/auth.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Text} from 'ink'; 3 | 4 | export const description = 'Auth command'; 5 | 6 | export default function Auth() { 7 | return Auth; 8 | } 9 | -------------------------------------------------------------------------------- /test/fixtures/nested-commands-custom-app/commands/deploy.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Text} from 'ink'; 3 | 4 | export const isDefault = true; 5 | export const description = 'Deploy command'; 6 | 7 | export default function Deploy() { 8 | return Deploy; 9 | } 10 | -------------------------------------------------------------------------------- /test/fixtures/nested-commands-custom-app/commands/servers/create.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Text} from 'ink'; 3 | 4 | export const description = 'Create server'; 5 | 6 | export default function CreateServer() { 7 | return Create server; 8 | } 9 | -------------------------------------------------------------------------------- /test/fixtures/nested-commands-custom-app/commands/servers/index.ts: -------------------------------------------------------------------------------- 1 | export const description = 'Manage servers'; 2 | -------------------------------------------------------------------------------- /test/fixtures/nested-commands-custom-app/commands/servers/list.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Text} from 'ink'; 3 | 4 | export const description = 'List servers'; 5 | 6 | export default function ListServers() { 7 | return List servers; 8 | } 9 | -------------------------------------------------------------------------------- /test/fixtures/nested-commands/cli.ts: -------------------------------------------------------------------------------- 1 | import Pastel from '../../../source/index.js'; 2 | 3 | const app = new Pastel({ 4 | name: 'test', 5 | version: '0.0.0', 6 | description: 'Description', 7 | importMeta: import.meta, 8 | }); 9 | 10 | await app.run(); 11 | -------------------------------------------------------------------------------- /test/fixtures/nested-commands/commands/auth.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Text} from 'ink'; 3 | 4 | export const description = 'Auth command'; 5 | 6 | export default function Auth() { 7 | return Auth; 8 | } 9 | -------------------------------------------------------------------------------- /test/fixtures/nested-commands/commands/deploy.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Text} from 'ink'; 3 | 4 | export const isDefault = true; 5 | export const description = 'Deploy command'; 6 | 7 | export default function Deploy() { 8 | return Deploy; 9 | } 10 | -------------------------------------------------------------------------------- /test/fixtures/nested-commands/commands/servers/create.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Text} from 'ink'; 3 | 4 | export const description = 'Create server'; 5 | 6 | export default function CreateServer() { 7 | return Create server; 8 | } 9 | -------------------------------------------------------------------------------- /test/fixtures/nested-commands/commands/servers/index.ts: -------------------------------------------------------------------------------- 1 | export const description = 'Manage servers'; 2 | -------------------------------------------------------------------------------- /test/fixtures/nested-commands/commands/servers/list.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Text} from 'ink'; 3 | 4 | export const description = 'List servers'; 5 | 6 | export default function ListServers() { 7 | return List servers; 8 | } 9 | -------------------------------------------------------------------------------- /test/fixtures/number-argument/array-default-value-description/cli.ts: -------------------------------------------------------------------------------- 1 | import Pastel from '../../../../source/index.js'; 2 | 3 | const app = new Pastel({ 4 | name: 'test', 5 | version: '0.0.0', 6 | description: 'Description', 7 | importMeta: import.meta, 8 | }); 9 | 10 | await app.run(); 11 | -------------------------------------------------------------------------------- /test/fixtures/number-argument/array-default-value-description/commands/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Text} from 'ink'; 3 | import zod from 'zod'; 4 | import {argument} from '../../../../../source/index.js'; 5 | 6 | export const args = zod 7 | .array(zod.number()) 8 | .default([128, 256]) 9 | .describe( 10 | argument({ 11 | name: 'number', 12 | description: 'Numbers', 13 | defaultValueDescription: '128, 256', 14 | }), 15 | ); 16 | 17 | type Properties = { 18 | readonly args: zod.infer; 19 | }; 20 | 21 | export default function Index({args}: Properties) { 22 | return Arguments = {args.join(', ')}; 23 | } 24 | -------------------------------------------------------------------------------- /test/fixtures/number-argument/array-default-value/cli.ts: -------------------------------------------------------------------------------- 1 | import Pastel from '../../../../source/index.js'; 2 | 3 | const app = new Pastel({ 4 | name: 'test', 5 | version: '0.0.0', 6 | description: 'Description', 7 | importMeta: import.meta, 8 | }); 9 | 10 | await app.run(); 11 | -------------------------------------------------------------------------------- /test/fixtures/number-argument/array-default-value/commands/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Text} from 'ink'; 3 | import zod from 'zod'; 4 | 5 | export const args = zod 6 | .array(zod.number()) 7 | .default([128, 256]) 8 | .describe('number'); 9 | 10 | type Properties = { 11 | readonly args: zod.infer; 12 | }; 13 | 14 | export default function Index({args}: Properties) { 15 | return Arguments = {args.join(', ')}; 16 | } 17 | -------------------------------------------------------------------------------- /test/fixtures/number-argument/array-description/cli.ts: -------------------------------------------------------------------------------- 1 | import Pastel from '../../../../source/index.js'; 2 | 3 | const app = new Pastel({ 4 | name: 'test', 5 | version: '0.0.0', 6 | description: 'Description', 7 | importMeta: import.meta, 8 | }); 9 | 10 | await app.run(); 11 | -------------------------------------------------------------------------------- /test/fixtures/number-argument/array-description/commands/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Text} from 'ink'; 3 | import zod from 'zod'; 4 | import {argument} from '../../../../../source/index.js'; 5 | 6 | export const args = zod 7 | .array(zod.number()) 8 | .describe(argument({name: 'number', description: 'Numbers'})); 9 | 10 | type Properties = { 11 | readonly args: zod.infer; 12 | }; 13 | 14 | export default function Index({args}: Properties) { 15 | return Arguments = {args.join(', ')}; 16 | } 17 | -------------------------------------------------------------------------------- /test/fixtures/number-argument/array-name/cli.ts: -------------------------------------------------------------------------------- 1 | import Pastel from '../../../../source/index.js'; 2 | 3 | const app = new Pastel({ 4 | name: 'test', 5 | version: '0.0.0', 6 | description: 'Description', 7 | importMeta: import.meta, 8 | }); 9 | 10 | await app.run(); 11 | -------------------------------------------------------------------------------- /test/fixtures/number-argument/array-name/commands/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Text} from 'ink'; 3 | import zod from 'zod'; 4 | import {argument} from '../../../../../source/index.js'; 5 | 6 | export const args = zod 7 | .array(zod.number()) 8 | .describe(argument({name: 'number'})); 9 | 10 | type Properties = { 11 | readonly args: zod.infer; 12 | }; 13 | 14 | export default function Index({args}: Properties) { 15 | return Arguments = {args.join(', ')}; 16 | } 17 | -------------------------------------------------------------------------------- /test/fixtures/number-argument/array/cli.ts: -------------------------------------------------------------------------------- 1 | import Pastel from '../../../../source/index.js'; 2 | 3 | const app = new Pastel({ 4 | name: 'test', 5 | version: '0.0.0', 6 | description: 'Description', 7 | importMeta: import.meta, 8 | }); 9 | 10 | await app.run(); 11 | -------------------------------------------------------------------------------- /test/fixtures/number-argument/array/commands/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Text} from 'ink'; 3 | import zod from 'zod'; 4 | 5 | export const args = zod.array(zod.number()).describe('number'); 6 | 7 | type Properties = { 8 | readonly args: zod.infer; 9 | }; 10 | 11 | export default function Index({args}: Properties) { 12 | return Arguments = {args.join(', ')}; 13 | } 14 | -------------------------------------------------------------------------------- /test/fixtures/number-argument/default-value-description/cli.ts: -------------------------------------------------------------------------------- 1 | import Pastel from '../../../../source/index.js'; 2 | 3 | const app = new Pastel({ 4 | name: 'test', 5 | version: '0.0.0', 6 | description: 'Description', 7 | importMeta: import.meta, 8 | }); 9 | 10 | await app.run(); 11 | -------------------------------------------------------------------------------- /test/fixtures/number-argument/default-value-description/commands/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Text} from 'ink'; 3 | import zod from 'zod'; 4 | import {argument} from '../../../../../source/index.js'; 5 | 6 | export const args = zod.tuple([ 7 | zod 8 | .number() 9 | .optional() 10 | .describe( 11 | argument({ 12 | name: 'first', 13 | description: 'First', 14 | }), 15 | ), 16 | zod 17 | .number() 18 | .default(256) 19 | .describe( 20 | argument({ 21 | name: 'second', 22 | description: 'Second', 23 | defaultValueDescription: '256 MB', 24 | }), 25 | ), 26 | ]); 27 | 28 | type Properties = { 29 | readonly args: zod.infer; 30 | }; 31 | 32 | export default function Index({args}: Properties) { 33 | return Arguments = {args.join(', ')}; 34 | } 35 | -------------------------------------------------------------------------------- /test/fixtures/number-argument/default-value/cli.ts: -------------------------------------------------------------------------------- 1 | import Pastel from '../../../../source/index.js'; 2 | 3 | const app = new Pastel({ 4 | name: 'test', 5 | version: '0.0.0', 6 | description: 'Description', 7 | importMeta: import.meta, 8 | }); 9 | 10 | await app.run(); 11 | -------------------------------------------------------------------------------- /test/fixtures/number-argument/default-value/commands/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Text} from 'ink'; 3 | import zod from 'zod'; 4 | 5 | export const args = zod.tuple([ 6 | zod.number().optional().describe('first'), 7 | zod.number().default(256).describe('second'), 8 | ]); 9 | 10 | type Properties = { 11 | readonly args: zod.infer; 12 | }; 13 | 14 | export default function Index({args}: Properties) { 15 | return Arguments = {args.join(', ')}; 16 | } 17 | -------------------------------------------------------------------------------- /test/fixtures/number-argument/description/cli.ts: -------------------------------------------------------------------------------- 1 | import Pastel from '../../../../source/index.js'; 2 | 3 | const app = new Pastel({ 4 | name: 'test', 5 | version: '0.0.0', 6 | description: 'Description', 7 | importMeta: import.meta, 8 | }); 9 | 10 | await app.run(); 11 | -------------------------------------------------------------------------------- /test/fixtures/number-argument/description/commands/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Text} from 'ink'; 3 | import zod from 'zod'; 4 | import {argument} from '../../../../../source/index.js'; 5 | 6 | export const args = zod.tuple([ 7 | zod.number().describe( 8 | argument({ 9 | name: 'first', 10 | description: 'First', 11 | }), 12 | ), 13 | zod.number().describe( 14 | argument({ 15 | name: 'second', 16 | description: 'Second', 17 | }), 18 | ), 19 | ]); 20 | 21 | type Properties = { 22 | readonly args: zod.infer; 23 | }; 24 | 25 | export default function Index({args}: Properties) { 26 | return Arguments = {args.join(', ')}; 27 | } 28 | -------------------------------------------------------------------------------- /test/fixtures/number-argument/optional-array/cli.ts: -------------------------------------------------------------------------------- 1 | import Pastel from '../../../../source/index.js'; 2 | 3 | const app = new Pastel({ 4 | name: 'test', 5 | version: '0.0.0', 6 | description: 'Description', 7 | importMeta: import.meta, 8 | }); 9 | 10 | await app.run(); 11 | -------------------------------------------------------------------------------- /test/fixtures/number-argument/optional-array/commands/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Text} from 'ink'; 3 | import zod from 'zod'; 4 | 5 | export const args = zod.array(zod.number()).optional().describe('number'); 6 | 7 | type Properties = { 8 | readonly args: zod.infer; 9 | }; 10 | 11 | export default function Index({args}: Properties) { 12 | return Arguments = {args?.join(', ') ?? ''}; 13 | } 14 | -------------------------------------------------------------------------------- /test/fixtures/number-argument/optional/cli.ts: -------------------------------------------------------------------------------- 1 | import Pastel from '../../../../source/index.js'; 2 | 3 | const app = new Pastel({ 4 | name: 'test', 5 | version: '0.0.0', 6 | description: 'Description', 7 | importMeta: import.meta, 8 | }); 9 | 10 | await app.run(); 11 | -------------------------------------------------------------------------------- /test/fixtures/number-argument/optional/commands/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Text} from 'ink'; 3 | import zod from 'zod'; 4 | 5 | export const args = zod.tuple([ 6 | zod.number().optional().describe('first'), 7 | zod.number().optional().describe('second'), 8 | ]); 9 | 10 | type Properties = { 11 | readonly args: zod.infer; 12 | }; 13 | 14 | export default function Index({args}: Properties) { 15 | return Arguments = {args.join(', ')}; 16 | } 17 | -------------------------------------------------------------------------------- /test/fixtures/number-argument/required/cli.ts: -------------------------------------------------------------------------------- 1 | import Pastel from '../../../../source/index.js'; 2 | 3 | const app = new Pastel({ 4 | name: 'test', 5 | version: '0.0.0', 6 | description: 'Description', 7 | importMeta: import.meta, 8 | }); 9 | 10 | await app.run(); 11 | -------------------------------------------------------------------------------- /test/fixtures/number-argument/required/commands/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Text} from 'ink'; 3 | import zod from 'zod'; 4 | import {argument} from '../../../../../source/index.js'; 5 | 6 | export const args = zod.tuple([ 7 | zod.number().describe('first'), 8 | zod.number().describe( 9 | argument({ 10 | name: 'second', 11 | }), 12 | ), 13 | ]); 14 | 15 | type Properties = { 16 | readonly args: zod.infer; 17 | }; 18 | 19 | export default function Index({args}: Properties) { 20 | return Arguments = {args.join(', ')}; 21 | } 22 | -------------------------------------------------------------------------------- /test/fixtures/number-argument/variadic/cli.ts: -------------------------------------------------------------------------------- 1 | import Pastel from '../../../../source/index.js'; 2 | 3 | const app = new Pastel({ 4 | name: 'test', 5 | version: '0.0.0', 6 | description: 'Description', 7 | importMeta: import.meta, 8 | }); 9 | 10 | await app.run(); 11 | -------------------------------------------------------------------------------- /test/fixtures/number-argument/variadic/commands/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Text} from 'ink'; 3 | import zod from 'zod'; 4 | 5 | export const args = zod 6 | .tuple([zod.number().describe('first'), zod.number().describe('second')]) 7 | .rest(zod.number().describe('rest')); 8 | 9 | type Properties = { 10 | readonly args: zod.infer; 11 | }; 12 | 13 | export default function Index({args}: Properties) { 14 | return Arguments = {args.join(', ')}; 15 | } 16 | -------------------------------------------------------------------------------- /test/fixtures/number-option/alias/cli.ts: -------------------------------------------------------------------------------- 1 | import Pastel from '../../../../source/index.js'; 2 | 3 | const app = new Pastel({ 4 | name: 'test', 5 | version: '0.0.0', 6 | description: 'Description', 7 | importMeta: import.meta, 8 | }); 9 | 10 | await app.run(); 11 | -------------------------------------------------------------------------------- /test/fixtures/number-option/alias/commands/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Text} from 'ink'; 3 | import zod from 'zod'; 4 | import {option} from '../../../../../source/index.js'; 5 | 6 | export const options = zod.object({ 7 | size: zod.number().describe( 8 | option({ 9 | description: 'Size', 10 | alias: 's', 11 | }), 12 | ), 13 | }); 14 | 15 | type Properties = { 16 | readonly options: zod.infer; 17 | }; 18 | 19 | export default function Index({options}: Properties) { 20 | return Size = {options.size}; 21 | } 22 | -------------------------------------------------------------------------------- /test/fixtures/number-option/default-value-description/cli.ts: -------------------------------------------------------------------------------- 1 | import Pastel from '../../../../source/index.js'; 2 | 3 | const app = new Pastel({ 4 | name: 'test', 5 | version: '0.0.0', 6 | description: 'Description', 7 | importMeta: import.meta, 8 | }); 9 | 10 | await app.run(); 11 | -------------------------------------------------------------------------------- /test/fixtures/number-option/default-value-description/commands/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Text} from 'ink'; 3 | import zod from 'zod'; 4 | import {option} from '../../../../../source/index.js'; 5 | 6 | export const options = zod.object({ 7 | size: zod 8 | .number() 9 | .default(128) 10 | .describe( 11 | option({ 12 | description: 'Size', 13 | defaultValueDescription: '128 MB', 14 | }), 15 | ), 16 | }); 17 | 18 | type Properties = { 19 | readonly options: zod.infer; 20 | }; 21 | 22 | export default function Index({options}: Properties) { 23 | return Size = {options.size}; 24 | } 25 | -------------------------------------------------------------------------------- /test/fixtures/number-option/default-value/cli.ts: -------------------------------------------------------------------------------- 1 | import Pastel from '../../../../source/index.js'; 2 | 3 | const app = new Pastel({ 4 | name: 'test', 5 | version: '0.0.0', 6 | description: 'Description', 7 | importMeta: import.meta, 8 | }); 9 | 10 | await app.run(); 11 | -------------------------------------------------------------------------------- /test/fixtures/number-option/default-value/commands/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Text} from 'ink'; 3 | import zod from 'zod'; 4 | 5 | export const options = zod.object({ 6 | size: zod.number().default(128).describe('Size'), 7 | }); 8 | 9 | type Properties = { 10 | readonly options: zod.infer; 11 | }; 12 | 13 | export default function Index({options}: Properties) { 14 | return Size = {options.size}; 15 | } 16 | -------------------------------------------------------------------------------- /test/fixtures/number-option/description/cli.ts: -------------------------------------------------------------------------------- 1 | import Pastel from '../../../../source/index.js'; 2 | 3 | const app = new Pastel({ 4 | name: 'test', 5 | version: '0.0.0', 6 | description: 'Description', 7 | importMeta: import.meta, 8 | }); 9 | 10 | await app.run(); 11 | -------------------------------------------------------------------------------- /test/fixtures/number-option/description/commands/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Text} from 'ink'; 3 | import zod from 'zod'; 4 | import {option} from '../../../../../source/index.js'; 5 | 6 | export const options = zod.object({ 7 | size: zod.number().describe( 8 | option({ 9 | description: 'Size', 10 | }), 11 | ), 12 | }); 13 | 14 | type Properties = { 15 | readonly options: zod.infer; 16 | }; 17 | 18 | export default function Index({options}: Properties) { 19 | return Size = {options.size}; 20 | } 21 | -------------------------------------------------------------------------------- /test/fixtures/number-option/optional/cli.ts: -------------------------------------------------------------------------------- 1 | import Pastel from '../../../../source/index.js'; 2 | 3 | const app = new Pastel({ 4 | name: 'test', 5 | version: '0.0.0', 6 | description: 'Description', 7 | importMeta: import.meta, 8 | }); 9 | 10 | await app.run(); 11 | -------------------------------------------------------------------------------- /test/fixtures/number-option/optional/commands/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Text} from 'ink'; 3 | import zod from 'zod'; 4 | 5 | export const options = zod.object({ 6 | size: zod.number().optional().describe('Size'), 7 | }); 8 | 9 | type Properties = { 10 | readonly options: zod.infer; 11 | }; 12 | 13 | export default function Index({options}: Properties) { 14 | return Size = {options.size ?? -1}; 15 | } 16 | -------------------------------------------------------------------------------- /test/fixtures/number-option/required/cli.ts: -------------------------------------------------------------------------------- 1 | import Pastel from '../../../../source/index.js'; 2 | 3 | const app = new Pastel({ 4 | name: 'test', 5 | version: '0.0.0', 6 | description: 'Description', 7 | importMeta: import.meta, 8 | }); 9 | 10 | await app.run(); 11 | -------------------------------------------------------------------------------- /test/fixtures/number-option/required/commands/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Text} from 'ink'; 3 | import zod from 'zod'; 4 | 5 | export const options = zod.object({ 6 | size: zod.number().describe('Size'), 7 | }); 8 | 9 | type Properties = { 10 | readonly options: zod.infer; 11 | }; 12 | 13 | export default function Index({options}: Properties) { 14 | return Size = {options.size}; 15 | } 16 | -------------------------------------------------------------------------------- /test/fixtures/number-option/value-description/cli.ts: -------------------------------------------------------------------------------- 1 | import Pastel from '../../../../source/index.js'; 2 | 3 | const app = new Pastel({ 4 | name: 'test', 5 | version: '0.0.0', 6 | description: 'Description', 7 | importMeta: import.meta, 8 | }); 9 | 10 | await app.run(); 11 | -------------------------------------------------------------------------------- /test/fixtures/number-option/value-description/commands/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Text} from 'ink'; 3 | import zod from 'zod'; 4 | import {option} from '../../../../../source/index.js'; 5 | 6 | export const options = zod.object({ 7 | size: zod.number().describe( 8 | option({ 9 | description: 'Size', 10 | valueDescription: 'some-size', 11 | }), 12 | ), 13 | }); 14 | 15 | type Properties = { 16 | readonly options: zod.infer; 17 | }; 18 | 19 | export default function Index({options}: Properties) { 20 | return Size = {options.size}; 21 | } 22 | -------------------------------------------------------------------------------- /test/fixtures/set-option/alias/cli.ts: -------------------------------------------------------------------------------- 1 | import Pastel from '../../../../source/index.js'; 2 | 3 | const app = new Pastel({ 4 | name: 'test', 5 | version: '0.0.0', 6 | description: 'Description', 7 | importMeta: import.meta, 8 | }); 9 | 10 | await app.run(); 11 | -------------------------------------------------------------------------------- /test/fixtures/set-option/alias/commands/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Text} from 'ink'; 3 | import zod from 'zod'; 4 | import {option} from '../../../../../source/index.js'; 5 | 6 | export const options = zod.object({ 7 | tag: zod.set(zod.string()).describe( 8 | option({ 9 | description: 'Tags', 10 | alias: 't', 11 | }), 12 | ), 13 | }); 14 | 15 | type Properties = { 16 | readonly options: zod.infer; 17 | }; 18 | 19 | export default function Index({options}: Properties) { 20 | return Tags = {[...options.tag].join(', ')}; 21 | } 22 | -------------------------------------------------------------------------------- /test/fixtures/set-option/default-value-description/cli.ts: -------------------------------------------------------------------------------- 1 | import Pastel from '../../../../source/index.js'; 2 | 3 | const app = new Pastel({ 4 | name: 'test', 5 | version: '0.0.0', 6 | description: 'Description', 7 | importMeta: import.meta, 8 | }); 9 | 10 | await app.run(); 11 | -------------------------------------------------------------------------------- /test/fixtures/set-option/default-value-description/commands/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Text} from 'ink'; 3 | import zod from 'zod'; 4 | import {option} from '../../../../../source/index.js'; 5 | 6 | export const options = zod.object({ 7 | tag: zod 8 | .set(zod.string()) 9 | .default(new Set(['A', 'B'])) 10 | .describe( 11 | option({ 12 | description: 'Tags', 13 | defaultValueDescription: 'A, B', 14 | }), 15 | ), 16 | }); 17 | 18 | type Properties = { 19 | readonly options: zod.infer; 20 | }; 21 | 22 | export default function Index({options}: Properties) { 23 | return Tags = {[...options.tag].join(', ')}; 24 | } 25 | -------------------------------------------------------------------------------- /test/fixtures/set-option/default-value/cli.ts: -------------------------------------------------------------------------------- 1 | import Pastel from '../../../../source/index.js'; 2 | 3 | const app = new Pastel({ 4 | name: 'test', 5 | version: '0.0.0', 6 | description: 'Description', 7 | importMeta: import.meta, 8 | }); 9 | 10 | await app.run(); 11 | -------------------------------------------------------------------------------- /test/fixtures/set-option/default-value/commands/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Text} from 'ink'; 3 | import zod from 'zod'; 4 | 5 | export const options = zod.object({ 6 | tag: zod 7 | .set(zod.string()) 8 | .default(new Set(['A', 'B'])) 9 | .describe('Tags'), 10 | }); 11 | 12 | type Properties = { 13 | readonly options: zod.infer; 14 | }; 15 | 16 | export default function Index({options}: Properties) { 17 | return Tags = {[...options.tag].join(', ')}; 18 | } 19 | -------------------------------------------------------------------------------- /test/fixtures/set-option/description/cli.ts: -------------------------------------------------------------------------------- 1 | import Pastel from '../../../../source/index.js'; 2 | 3 | const app = new Pastel({ 4 | name: 'test', 5 | version: '0.0.0', 6 | description: 'Description', 7 | importMeta: import.meta, 8 | }); 9 | 10 | await app.run(); 11 | -------------------------------------------------------------------------------- /test/fixtures/set-option/description/commands/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Text} from 'ink'; 3 | import zod from 'zod'; 4 | import {option} from '../../../../../source/index.js'; 5 | 6 | export const options = zod.object({ 7 | tag: zod.set(zod.string()).describe( 8 | option({ 9 | description: 'Tags', 10 | }), 11 | ), 12 | }); 13 | 14 | type Properties = { 15 | readonly options: zod.infer; 16 | }; 17 | 18 | export default function Index({options}: Properties) { 19 | return Tags = {[...options.tag].join(', ')}; 20 | } 21 | -------------------------------------------------------------------------------- /test/fixtures/set-option/optional/cli.ts: -------------------------------------------------------------------------------- 1 | import Pastel from '../../../../source/index.js'; 2 | 3 | const app = new Pastel({ 4 | name: 'test', 5 | version: '0.0.0', 6 | description: 'Description', 7 | importMeta: import.meta, 8 | }); 9 | 10 | await app.run(); 11 | -------------------------------------------------------------------------------- /test/fixtures/set-option/optional/commands/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Text} from 'ink'; 3 | import zod from 'zod'; 4 | 5 | export const options = zod.object({ 6 | tag: zod.set(zod.string()).optional().describe('Tags'), 7 | }); 8 | 9 | type Properties = { 10 | readonly options: zod.infer; 11 | }; 12 | 13 | export default function Index({options}: Properties) { 14 | return Tags = {[...(options.tag ?? [])].join(', ')}; 15 | } 16 | -------------------------------------------------------------------------------- /test/fixtures/set-option/required/cli.ts: -------------------------------------------------------------------------------- 1 | import Pastel from '../../../../source/index.js'; 2 | 3 | const app = new Pastel({ 4 | name: 'test', 5 | version: '0.0.0', 6 | description: 'Description', 7 | importMeta: import.meta, 8 | }); 9 | 10 | await app.run(); 11 | -------------------------------------------------------------------------------- /test/fixtures/set-option/required/commands/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Text} from 'ink'; 3 | import zod from 'zod'; 4 | 5 | export const options = zod.object({ 6 | tag: zod.set(zod.string()).describe('Tags'), 7 | }); 8 | 9 | type Properties = { 10 | readonly options: zod.infer; 11 | }; 12 | 13 | export default function Index({options}: Properties) { 14 | return Tags = {[...options.tag].join(', ')}; 15 | } 16 | -------------------------------------------------------------------------------- /test/fixtures/set-option/value-description/cli.ts: -------------------------------------------------------------------------------- 1 | import Pastel from '../../../../source/index.js'; 2 | 3 | const app = new Pastel({ 4 | name: 'test', 5 | version: '0.0.0', 6 | description: 'Description', 7 | importMeta: import.meta, 8 | }); 9 | 10 | await app.run(); 11 | -------------------------------------------------------------------------------- /test/fixtures/set-option/value-description/commands/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Text} from 'ink'; 3 | import zod from 'zod'; 4 | import {option} from '../../../../../source/index.js'; 5 | 6 | export const options = zod.object({ 7 | tag: zod.set(zod.string()).describe( 8 | option({ 9 | description: 'Tags', 10 | valueDescription: 'some-tags', 11 | }), 12 | ), 13 | }); 14 | 15 | type Properties = { 16 | readonly options: zod.infer; 17 | }; 18 | 19 | export default function Index({options}: Properties) { 20 | return Tags = {[...options.tag].join(', ')}; 21 | } 22 | -------------------------------------------------------------------------------- /test/fixtures/single-command-custom-app/cli.ts: -------------------------------------------------------------------------------- 1 | import Pastel from '../../../source/index.js'; 2 | 3 | const app = new Pastel({ 4 | name: 'test', 5 | version: '0.0.0', 6 | description: 'Description', 7 | importMeta: import.meta, 8 | }); 9 | 10 | await app.run(); 11 | -------------------------------------------------------------------------------- /test/fixtures/single-command-custom-app/commands/_app.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Box} from 'ink'; 3 | import {type AppProps} from '../../../../source/index.js'; 4 | 5 | export default function CustomApp({Component, commandProps}: AppProps) { 6 | return ( 7 | 8 | 9 | 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /test/fixtures/single-command-custom-app/commands/deploy.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Text} from 'ink'; 3 | import zod from 'zod'; 4 | 5 | export const isDefault = true; 6 | export const description = 'Deploy command'; 7 | 8 | export const options = zod.object({ 9 | name: zod.string().describe('Name'), 10 | }); 11 | 12 | type Properties = { 13 | readonly options: zod.infer; 14 | }; 15 | 16 | export default function Deploy({options}: Properties) { 17 | return Deploy {options.name}; 18 | } 19 | -------------------------------------------------------------------------------- /test/fixtures/single-command/cli.ts: -------------------------------------------------------------------------------- 1 | import Pastel from '../../../source/index.js'; 2 | 3 | const app = new Pastel({ 4 | name: 'test', 5 | version: '0.0.0', 6 | description: 'Description', 7 | importMeta: import.meta, 8 | }); 9 | 10 | await app.run(); 11 | -------------------------------------------------------------------------------- /test/fixtures/single-command/commands/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Text} from 'ink'; 3 | 4 | export const description = 'Index command'; 5 | 6 | export default function Index() { 7 | return Index; 8 | } 9 | -------------------------------------------------------------------------------- /test/fixtures/single-default-command/cli.ts: -------------------------------------------------------------------------------- 1 | import Pastel from '../../../source/index.js'; 2 | 3 | const app = new Pastel({ 4 | name: 'test', 5 | version: '0.0.0', 6 | description: 'Description', 7 | importMeta: import.meta, 8 | }); 9 | 10 | await app.run(); 11 | -------------------------------------------------------------------------------- /test/fixtures/single-default-command/commands/deploy.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Text} from 'ink'; 3 | 4 | export const isDefault = true; 5 | export const description = 'Deploy command'; 6 | 7 | export default function Deploy() { 8 | return Deploy; 9 | } 10 | -------------------------------------------------------------------------------- /test/fixtures/string-argument/array-default-value-description/cli.ts: -------------------------------------------------------------------------------- 1 | import Pastel from '../../../../source/index.js'; 2 | 3 | const app = new Pastel({ 4 | name: 'test', 5 | version: '0.0.0', 6 | description: 'Description', 7 | importMeta: import.meta, 8 | }); 9 | 10 | await app.run(); 11 | -------------------------------------------------------------------------------- /test/fixtures/string-argument/array-default-value-description/commands/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Text} from 'ink'; 3 | import zod from 'zod'; 4 | import {argument} from '../../../../../source/index.js'; 5 | 6 | export const args = zod 7 | .array(zod.string()) 8 | .default(['Jane', 'Hopper']) 9 | .describe( 10 | argument({ 11 | name: 'traits', 12 | description: 'Traits', 13 | defaultValueDescription: 'Jane, Hopper', 14 | }), 15 | ); 16 | 17 | type Properties = { 18 | readonly args: zod.infer; 19 | }; 20 | 21 | export default function Index({args}: Properties) { 22 | return Arguments = {args.join(', ')}; 23 | } 24 | -------------------------------------------------------------------------------- /test/fixtures/string-argument/array-default-value/cli.ts: -------------------------------------------------------------------------------- 1 | import Pastel from '../../../../source/index.js'; 2 | 3 | const app = new Pastel({ 4 | name: 'test', 5 | version: '0.0.0', 6 | description: 'Description', 7 | importMeta: import.meta, 8 | }); 9 | 10 | await app.run(); 11 | -------------------------------------------------------------------------------- /test/fixtures/string-argument/array-default-value/commands/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Text} from 'ink'; 3 | import zod from 'zod'; 4 | 5 | export const args = zod 6 | .array(zod.string()) 7 | .default(['Jane', 'Hopper']) 8 | .describe('traits'); 9 | 10 | type Properties = { 11 | readonly args: zod.infer; 12 | }; 13 | 14 | export default function Index({args}: Properties) { 15 | return Arguments = {args.join(', ')}; 16 | } 17 | -------------------------------------------------------------------------------- /test/fixtures/string-argument/array-description/cli.ts: -------------------------------------------------------------------------------- 1 | import Pastel from '../../../../source/index.js'; 2 | 3 | const app = new Pastel({ 4 | name: 'test', 5 | version: '0.0.0', 6 | description: 'Description', 7 | importMeta: import.meta, 8 | }); 9 | 10 | await app.run(); 11 | -------------------------------------------------------------------------------- /test/fixtures/string-argument/array-description/commands/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Text} from 'ink'; 3 | import zod from 'zod'; 4 | import {argument} from '../../../../../source/index.js'; 5 | 6 | export const args = zod 7 | .array(zod.string()) 8 | .describe(argument({name: 'traits', description: 'Traits'})); 9 | 10 | type Properties = { 11 | readonly args: zod.infer; 12 | }; 13 | 14 | export default function Index({args}: Properties) { 15 | return Arguments = {args.join(', ')}; 16 | } 17 | -------------------------------------------------------------------------------- /test/fixtures/string-argument/array-name/cli.ts: -------------------------------------------------------------------------------- 1 | import Pastel from '../../../../source/index.js'; 2 | 3 | const app = new Pastel({ 4 | name: 'test', 5 | version: '0.0.0', 6 | description: 'Description', 7 | importMeta: import.meta, 8 | }); 9 | 10 | await app.run(); 11 | -------------------------------------------------------------------------------- /test/fixtures/string-argument/array-name/commands/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Text} from 'ink'; 3 | import zod from 'zod'; 4 | import {argument} from '../../../../../source/index.js'; 5 | 6 | export const args = zod 7 | .array(zod.string()) 8 | .describe(argument({name: 'traits'})); 9 | 10 | type Properties = { 11 | readonly args: zod.infer; 12 | }; 13 | 14 | export default function Index({args}: Properties) { 15 | return Arguments = {args.join(', ')}; 16 | } 17 | -------------------------------------------------------------------------------- /test/fixtures/string-argument/array/cli.ts: -------------------------------------------------------------------------------- 1 | import Pastel from '../../../../source/index.js'; 2 | 3 | const app = new Pastel({ 4 | name: 'test', 5 | version: '0.0.0', 6 | description: 'Description', 7 | importMeta: import.meta, 8 | }); 9 | 10 | await app.run(); 11 | -------------------------------------------------------------------------------- /test/fixtures/string-argument/array/commands/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Text} from 'ink'; 3 | import zod from 'zod'; 4 | 5 | export const args = zod.array(zod.string()).describe('traits'); 6 | 7 | type Properties = { 8 | readonly args: zod.infer; 9 | }; 10 | 11 | export default function Index({args}: Properties) { 12 | return Arguments = {args.join(', ')}; 13 | } 14 | -------------------------------------------------------------------------------- /test/fixtures/string-argument/default-value-description/cli.ts: -------------------------------------------------------------------------------- 1 | import Pastel from '../../../../source/index.js'; 2 | 3 | const app = new Pastel({ 4 | name: 'test', 5 | version: '0.0.0', 6 | description: 'Description', 7 | importMeta: import.meta, 8 | }); 9 | 10 | await app.run(); 11 | -------------------------------------------------------------------------------- /test/fixtures/string-argument/default-value-description/commands/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Text} from 'ink'; 3 | import zod from 'zod'; 4 | import {argument} from '../../../../../source/index.js'; 5 | 6 | export const args = zod.tuple([ 7 | zod 8 | .string() 9 | .optional() 10 | .describe( 11 | argument({ 12 | name: 'name', 13 | description: 'Name', 14 | }), 15 | ), 16 | zod 17 | .string() 18 | .default('Hopper') 19 | .describe( 20 | argument({ 21 | name: 'surname', 22 | description: 'Surname', 23 | defaultValueDescription: 'Hopper', 24 | }), 25 | ), 26 | ]); 27 | 28 | type Properties = { 29 | readonly args: zod.infer; 30 | }; 31 | 32 | export default function Index({args}: Properties) { 33 | return Arguments = {args.join(', ')}; 34 | } 35 | -------------------------------------------------------------------------------- /test/fixtures/string-argument/default-value/cli.ts: -------------------------------------------------------------------------------- 1 | import Pastel from '../../../../source/index.js'; 2 | 3 | const app = new Pastel({ 4 | name: 'test', 5 | version: '0.0.0', 6 | description: 'Description', 7 | importMeta: import.meta, 8 | }); 9 | 10 | await app.run(); 11 | -------------------------------------------------------------------------------- /test/fixtures/string-argument/default-value/commands/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Text} from 'ink'; 3 | import zod from 'zod'; 4 | 5 | export const args = zod.tuple([ 6 | zod.string().optional().describe('name'), 7 | zod.string().default('Hopper').describe('surname'), 8 | ]); 9 | 10 | type Properties = { 11 | readonly args: zod.infer; 12 | }; 13 | 14 | export default function Index({args}: Properties) { 15 | return Arguments = {args.join(', ')}; 16 | } 17 | -------------------------------------------------------------------------------- /test/fixtures/string-argument/description/cli.ts: -------------------------------------------------------------------------------- 1 | import Pastel from '../../../../source/index.js'; 2 | 3 | const app = new Pastel({ 4 | name: 'test', 5 | version: '0.0.0', 6 | description: 'Description', 7 | importMeta: import.meta, 8 | }); 9 | 10 | await app.run(); 11 | -------------------------------------------------------------------------------- /test/fixtures/string-argument/description/commands/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Text} from 'ink'; 3 | import zod from 'zod'; 4 | import {argument} from '../../../../../source/index.js'; 5 | 6 | export const args = zod.tuple([ 7 | zod.string().describe( 8 | argument({ 9 | name: 'name', 10 | description: 'Name', 11 | }), 12 | ), 13 | zod.string().describe( 14 | argument({ 15 | name: 'surname', 16 | description: 'Surname', 17 | }), 18 | ), 19 | ]); 20 | 21 | type Properties = { 22 | readonly args: zod.infer; 23 | }; 24 | 25 | export default function Index({args}: Properties) { 26 | return Arguments = {args.join(', ')}; 27 | } 28 | -------------------------------------------------------------------------------- /test/fixtures/string-argument/optional-array/cli.ts: -------------------------------------------------------------------------------- 1 | import Pastel from '../../../../source/index.js'; 2 | 3 | const app = new Pastel({ 4 | name: 'test', 5 | version: '0.0.0', 6 | description: 'Description', 7 | importMeta: import.meta, 8 | }); 9 | 10 | await app.run(); 11 | -------------------------------------------------------------------------------- /test/fixtures/string-argument/optional-array/commands/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Text} from 'ink'; 3 | import zod from 'zod'; 4 | 5 | export const args = zod.array(zod.string()).optional().describe('traits'); 6 | 7 | type Properties = { 8 | readonly args: zod.infer; 9 | }; 10 | 11 | export default function Index({args}: Properties) { 12 | return Arguments = {args?.join(', ') ?? ''}; 13 | } 14 | -------------------------------------------------------------------------------- /test/fixtures/string-argument/optional/cli.ts: -------------------------------------------------------------------------------- 1 | import Pastel from '../../../../source/index.js'; 2 | 3 | const app = new Pastel({ 4 | name: 'test', 5 | version: '0.0.0', 6 | description: 'Description', 7 | importMeta: import.meta, 8 | }); 9 | 10 | await app.run(); 11 | -------------------------------------------------------------------------------- /test/fixtures/string-argument/optional/commands/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Text} from 'ink'; 3 | import zod from 'zod'; 4 | 5 | export const args = zod.tuple([ 6 | zod.string().optional().describe('name'), 7 | zod.string().optional().describe('surname'), 8 | ]); 9 | 10 | type Properties = { 11 | readonly args: zod.infer; 12 | }; 13 | 14 | export default function Index({args}: Properties) { 15 | return Arguments = {args.join(', ')}; 16 | } 17 | -------------------------------------------------------------------------------- /test/fixtures/string-argument/required/cli.ts: -------------------------------------------------------------------------------- 1 | import Pastel from '../../../../source/index.js'; 2 | 3 | const app = new Pastel({ 4 | name: 'test', 5 | version: '0.0.0', 6 | description: 'Description', 7 | importMeta: import.meta, 8 | }); 9 | 10 | await app.run(); 11 | -------------------------------------------------------------------------------- /test/fixtures/string-argument/required/commands/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Text} from 'ink'; 3 | import zod from 'zod'; 4 | import {argument} from '../../../../../source/index.js'; 5 | 6 | export const args = zod.tuple([ 7 | zod.string().describe('name'), 8 | zod.string().describe( 9 | argument({ 10 | name: 'surname', 11 | }), 12 | ), 13 | ]); 14 | 15 | type Properties = { 16 | readonly args: zod.infer; 17 | }; 18 | 19 | export default function Index({args}: Properties) { 20 | return Arguments = {args.join(', ')}; 21 | } 22 | -------------------------------------------------------------------------------- /test/fixtures/string-argument/variadic/cli.ts: -------------------------------------------------------------------------------- 1 | import Pastel from '../../../../source/index.js'; 2 | 3 | const app = new Pastel({ 4 | name: 'test', 5 | version: '0.0.0', 6 | description: 'Description', 7 | importMeta: import.meta, 8 | }); 9 | 10 | await app.run(); 11 | -------------------------------------------------------------------------------- /test/fixtures/string-argument/variadic/commands/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Text} from 'ink'; 3 | import zod from 'zod'; 4 | 5 | export const args = zod 6 | .tuple([zod.string().describe('name'), zod.string().describe('surname')]) 7 | .rest(zod.string().describe('traits')); 8 | 9 | type Properties = { 10 | readonly args: zod.infer; 11 | }; 12 | 13 | export default function Index({args}: Properties) { 14 | return Arguments = {args.join(', ')}; 15 | } 16 | -------------------------------------------------------------------------------- /test/fixtures/string-option/alias/cli.ts: -------------------------------------------------------------------------------- 1 | import Pastel from '../../../../source/index.js'; 2 | 3 | const app = new Pastel({ 4 | name: 'test', 5 | version: '0.0.0', 6 | description: 'Description', 7 | importMeta: import.meta, 8 | }); 9 | 10 | await app.run(); 11 | -------------------------------------------------------------------------------- /test/fixtures/string-option/alias/commands/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Text} from 'ink'; 3 | import zod from 'zod'; 4 | import {option} from '../../../../../source/index.js'; 5 | 6 | export const options = zod.object({ 7 | name: zod 8 | .string() 9 | .regex(/[a-z]+/i, { 10 | message: 'Invalid value', 11 | }) 12 | .describe( 13 | option({ 14 | description: 'Name', 15 | alias: 'n', 16 | }), 17 | ), 18 | }); 19 | 20 | type Properties = { 21 | readonly options: zod.infer; 22 | }; 23 | 24 | export default function Index({options}: Properties) { 25 | return Name = {options.name}; 26 | } 27 | -------------------------------------------------------------------------------- /test/fixtures/string-option/default-value-description/cli.ts: -------------------------------------------------------------------------------- 1 | import Pastel from '../../../../source/index.js'; 2 | 3 | const app = new Pastel({ 4 | name: 'test', 5 | version: '0.0.0', 6 | description: 'Description', 7 | importMeta: import.meta, 8 | }); 9 | 10 | await app.run(); 11 | -------------------------------------------------------------------------------- /test/fixtures/string-option/default-value-description/commands/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Text} from 'ink'; 3 | import zod from 'zod'; 4 | import {option} from '../../../../../source/index.js'; 5 | 6 | export const options = zod.object({ 7 | name: zod 8 | .string() 9 | .regex(/[a-z]+/i, { 10 | message: 'Invalid value', 11 | }) 12 | .optional() 13 | .default('Mike') 14 | .describe( 15 | option({ 16 | description: 'Name', 17 | defaultValueDescription: 'Mike', 18 | }), 19 | ), 20 | }); 21 | 22 | type Properties = { 23 | readonly options: zod.infer; 24 | }; 25 | 26 | export default function Index({options}: Properties) { 27 | return Name = {options.name}; 28 | } 29 | -------------------------------------------------------------------------------- /test/fixtures/string-option/default-value/cli.ts: -------------------------------------------------------------------------------- 1 | import Pastel from '../../../../source/index.js'; 2 | 3 | const app = new Pastel({ 4 | name: 'test', 5 | version: '0.0.0', 6 | description: 'Description', 7 | importMeta: import.meta, 8 | }); 9 | 10 | await app.run(); 11 | -------------------------------------------------------------------------------- /test/fixtures/string-option/default-value/commands/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Text} from 'ink'; 3 | import zod from 'zod'; 4 | 5 | export const options = zod.object({ 6 | name: zod 7 | .string() 8 | .regex(/[a-z]+/i, { 9 | message: 'Invalid value', 10 | }) 11 | .describe('Name') 12 | .optional() 13 | .default('Mike'), 14 | }); 15 | 16 | type Properties = { 17 | readonly options: zod.infer; 18 | }; 19 | 20 | export default function Index({options}: Properties) { 21 | return Name = {options.name}; 22 | } 23 | -------------------------------------------------------------------------------- /test/fixtures/string-option/description/cli.ts: -------------------------------------------------------------------------------- 1 | import Pastel from '../../../../source/index.js'; 2 | 3 | const app = new Pastel({ 4 | name: 'test', 5 | version: '0.0.0', 6 | description: 'Description', 7 | importMeta: import.meta, 8 | }); 9 | 10 | await app.run(); 11 | -------------------------------------------------------------------------------- /test/fixtures/string-option/description/commands/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Text} from 'ink'; 3 | import zod from 'zod'; 4 | import {option} from '../../../../../source/index.js'; 5 | 6 | export const options = zod.object({ 7 | name: zod 8 | .string() 9 | .regex(/[a-z]+/i, { 10 | message: 'Invalid value', 11 | }) 12 | .describe( 13 | option({ 14 | description: 'Name', 15 | }), 16 | ), 17 | }); 18 | 19 | type Properties = { 20 | readonly options: zod.infer; 21 | }; 22 | 23 | export default function Index({options}: Properties) { 24 | return Name = {options.name}; 25 | } 26 | -------------------------------------------------------------------------------- /test/fixtures/string-option/optional/cli.ts: -------------------------------------------------------------------------------- 1 | import Pastel from '../../../../source/index.js'; 2 | 3 | const app = new Pastel({ 4 | name: 'test', 5 | version: '0.0.0', 6 | description: 'Description', 7 | importMeta: import.meta, 8 | }); 9 | 10 | await app.run(); 11 | -------------------------------------------------------------------------------- /test/fixtures/string-option/optional/commands/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Text} from 'ink'; 3 | import zod from 'zod'; 4 | 5 | export const options = zod.object({ 6 | name: zod 7 | .string() 8 | .regex(/[a-z]+/i, { 9 | message: 'Invalid value', 10 | }) 11 | .describe('Name') 12 | .optional(), 13 | }); 14 | 15 | type Properties = { 16 | readonly options: zod.infer; 17 | }; 18 | 19 | export default function Index({options}: Properties) { 20 | return Name = {options.name ?? 'empty'}; 21 | } 22 | -------------------------------------------------------------------------------- /test/fixtures/string-option/required/cli.ts: -------------------------------------------------------------------------------- 1 | import Pastel from '../../../../source/index.js'; 2 | 3 | const app = new Pastel({ 4 | name: 'test', 5 | version: '0.0.0', 6 | description: 'Description', 7 | importMeta: import.meta, 8 | }); 9 | 10 | await app.run(); 11 | -------------------------------------------------------------------------------- /test/fixtures/string-option/required/commands/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Text} from 'ink'; 3 | import zod from 'zod'; 4 | 5 | export const options = zod.object({ 6 | name: zod 7 | .string() 8 | .regex(/[a-z]+/i, { 9 | message: 'Invalid value', 10 | }) 11 | .describe('Name'), 12 | }); 13 | 14 | type Properties = { 15 | readonly options: zod.infer; 16 | }; 17 | 18 | export default function Index({options}: Properties) { 19 | return Name = {options.name}; 20 | } 21 | -------------------------------------------------------------------------------- /test/fixtures/string-option/value-description/cli.ts: -------------------------------------------------------------------------------- 1 | import Pastel from '../../../../source/index.js'; 2 | 3 | const app = new Pastel({ 4 | name: 'test', 5 | version: '0.0.0', 6 | description: 'Description', 7 | importMeta: import.meta, 8 | }); 9 | 10 | await app.run(); 11 | -------------------------------------------------------------------------------- /test/fixtures/string-option/value-description/commands/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Text} from 'ink'; 3 | import zod from 'zod'; 4 | import {option} from '../../../../../source/index.js'; 5 | 6 | export const options = zod.object({ 7 | name: zod 8 | .string() 9 | .regex(/[a-z]+/i, { 10 | message: 'Invalid value', 11 | }) 12 | .describe( 13 | option({ 14 | description: 'Name', 15 | valueDescription: 'first-name', 16 | }), 17 | ), 18 | }); 19 | 20 | type Properties = { 21 | readonly options: zod.infer; 22 | }; 23 | 24 | export default function Index({options}: Properties) { 25 | return Name = {options.name}; 26 | } 27 | -------------------------------------------------------------------------------- /test/helpers/run.ts: -------------------------------------------------------------------------------- 1 | import {fileURLToPath} from 'node:url'; 2 | import {execaNode, type ExecaChildProcess, type Options} from 'execa'; 3 | 4 | export default async function run( 5 | fixture: string, 6 | arguments_: string[] = [], 7 | options?: Options, 8 | ): Promise { 9 | const cliPath = fileURLToPath( 10 | new URL(`../fixtures/${fixture}/cli.ts`, import.meta.url), 11 | ); 12 | 13 | return execaNode(cliPath, arguments_, { 14 | env: { 15 | // eslint-disable-next-line @typescript-eslint/naming-convention 16 | NODE_OPTIONS: 17 | '--loader=ts-node/esm --experimental-specifier-resolution=node --no-warnings', 18 | }, 19 | ...options, 20 | }); 21 | } 22 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@sindresorhus/tsconfig", 3 | "compilerOptions": { 4 | "outDir": "build", 5 | "sourceMap": true, 6 | "jsx": "react" 7 | }, 8 | "include": ["source"], 9 | "ts-node": { 10 | "transpileOnly": true, 11 | "files": true, 12 | "experimentalResolver": true, 13 | "experimentalSpecifierResolution": "node" 14 | } 15 | } 16 | --------------------------------------------------------------------------------