schniz / cmd-ts Goto Github PK
View Code? Open in Web Editor NEW๐ป A type-driven command line argument parser
Home Page: https://cmd-ts.now.sh
License: MIT License
๐ป A type-driven command line argument parser
Home Page: https://cmd-ts.now.sh
License: MIT License
I noticed that the docs for flags are still wrong -- this was fixed by f6f0ce9, but the website hasn't been rebuilt since then, or is being rebuilt on an even older commit, so the fix doesn't show up. This is a point of friction for someone new trying to use this library :)
The Url type does not allow for file, mongo or NATS URLs, it requires http/https just like the HttpUrl.
Line 16 in e675907
Unless I'm missing something or using things incorrectly, it is not possible to specify aliases for "parent" commands that use subcommands
. Only "leaf" commands can be aliased. It would be nice to be able to alias "parent" commands.
When there are "with highlight" and "with no highlight" errors, the text that precedes the "with no highlight" error(s) is one of the following, depending on plurality:
Along the following error:
Along the following errors:
The messages should include the word "with":
Along with the following error:
Along with the following errors:
I've only found this in docs:
- Some arguments may be an integer; so providing a float should result in an error
however when I pass a float ex. 0.22
it parses to 0
.
There doesn't seem to be any support for requiring that arguments are present? I skimmed the implementation briefly, but I'm not seeing anything about requiring positionals.
This is also a bigger issue since the implementation uses a handler
instead of returning optional values; since the handler
will never get called, I have to do my own validation outside of cmd-ts
, which โฆ largely defeats the purpose of it. :P
There is an error with this repository's Renovate configuration that needs to be fixed. As a precaution, Renovate will stop PRs until it is resolved.
Error type: undefined. Note: this is a nested preset so please contact the preset author if you are unable to fix it yourself.
I tried doing this:
import { command, optional, positional, run, string } from "cmd-ts"
import { execa } from "execa"
const cmd = command({
name: "gitty",
description: "automate git",
version: "0.0.1",
args: {
query: positional({ type: optional(string), displayName: "query" }),
},
handler: async (args) => {
const { stdout } = await execa("echo", ["unicorns"])
console.log(stdout)
},
})
run(cmd, process.argv.slice(2))
But then if you try run it, you won't get echo unicorns
as output.
If you run below code outside the handler:
const { stdout } = await execa("echo", ["unicorns"])
console.log(stdout)
It works as expected. Not sure why that happens or how to fix.
From what I can see, the README.md file doesn't mention that any docs exist.
But they do exist, they are just hidden inside of the "docs" subdirectory. That's a big problem.
Additionally, right now, there is a bunch of info in the README.
Should this information be merged into the docs?
I think a good end state might be for the README file to be empty with a link to the docs.
Hello everyone, thanks to @Schniz for this neat library!
I've run into a problem with negative numbers and strings starting with "-". Given this configuration:
command({
name: 'show-my-number',
args: {
myNumber: positional({
type: number,
displayName: 'my-number',
}),
},
handler: (args) => {
console.log(`This is my number: ${args.myNumber}`)
}
})
When I run show-my-number -10
, I get:
error: found 2 errors
-10
^ Unknown argumentsAlong the following error:
2. No value provided for my-number
This also occurs when setting the type of myNumber
to string
, as well as when using optional
instead of positional
.
v0.9.0 is published to npm:
The repo here doesn't seem to reflect all those changes, and there's no tag for that version or any release notes.
Line 3 in e1b65d2
import * as cli from "cmd-ts";
cli.flag({
long: "skip",
type: cli.boolean, // First of all it's possible to specify type here. Unessesery I think.
defaultValue: () => true, // Default value gets printed in help but is not actually a default in handler.
defaultValueIsSerializable: true,
})
Thanks for the great library, I was looking for something resembling cmdliner (OCaml's library) and was happy to find cmd-ts!
A question I have which I wasn't able to figure out by myself: is it possible to implement optional positional arguments?
I know this is kinda possible with restPositional combinator but this outputs a list and not a single value.
It'd be pretty neat if there were some way I could customize the various bits of output. Like one thing, padding all the subcommand names so they align properly. Introducing chalk colorization here and there. I would just like to be able to pass in things into run
to allow customizing the "rendering" of various bits.
Snippet from docs:
import { command, boolean, flag } from 'cmd-ts';
const myFlag = option({
type: boolean,
long: 'my-flag',
short: 'f',
});
const cmd = command({
name: 'my flag',
args: { myFlag },
});
flag
is unused. Also, the example is not type-correct:
Argument of type '{ name: string; args: { myFlag: any; }; }' is not assignable to parameter of type 'CommandConfig<{ myFlag: any; }, HandlerFunc<{ myFlag: any; }>>'.
Property 'handler' is missing in type '{ name: string; args: { myFlag: any; }; }' but required in type 'CommandConfig<{ myFlag: any; }, HandlerFunc<{ myFlag: any; }>>'.ts(2345)
I would like to abort a CLI handler by throw
ing something, but control the exit code, and not see a stack trace.
I can think of two ways this might work:
a) ability to throw new CliError('message to log', exitCode)
b) optional errorHandler
callback receives a thrown error and decides how to handle it. For example, any errors from an underlying http client can be logged in a specific format and exit with a specific exit code
I tried to use Exit
for this, but it's not exported, and comments say it's an internal implementation detail of cmd-ts.
Line 27 in 0d29e91
I'm using runSafely
, but I had to copy the implementation of run
into my code, and it's undocumented. Could this be as simple as explaining runSafely
in the README?
Something like:
dynamic command implementations will allow to lazily load the dependency tree instead of forcing cmd-ts apps to be imported sync
subcommands({
cmds: { hello: async () => (await import('./my-command')).cmd }
})
async functions for the different commands can allow to lazily bootstrap the CLI
subcommands({
cmds: () => Promise.resolve({ hello: ... })
})
mixing them both can allow extremely dynamic applications ๐ฎ
Needs design though. I thought about something like
conditional({ long: string, short?: string }, ArgParser<T>) // ArgParser<T | undefined>
So you can describe something like
command({
// ...,
args: {
// ...,
deploy: conditional(
{
long: 'deploy'
},
object({
tag: ... // can be required if `--deploy` was provided!
})),
},
// then it'll be more declarative
handler({ deploy }) {
if (deploy) {
deploy.tag // ๐
}
}
}
The basic should be the same as command
, and maybe command
should use it internally:
object({
someName: ArgParser<T>,
otherName: ArgParser<R>,
}) => ArgParser<{ someName: T, otherName: R }>
Positional arguments and options' values should be quoted when reporting errors to avoid confusion whenever the values include spaces.
The current lack of quoting makes it hard to parse what exactly was part of what argument/option. There is highlighting in the output, but if it were to be copied over (like when asking for help online) that does not carry over.
Consider the following CLI:
mycat --prefix <text> --transform <case> <file>
Where:
<text>
is any text (no particular parsing/validation)<file>
is an existing file path<case>
is one of 'upper', 'lower', or 'asis' (default)Then the following scenarios in the current version:
$> mycat --prefix 'My beautiful text:' --transform "messing around" nonexistent\ file\ with\ spaces
error: found 2 errors
mycat --prefix My beautiful text: --transform messing around nonexistent file with spaces
^ File does not exist
mycat --prefix My beautiful text: --transform messing around nonexistent file with spaces
^ Invalid value 'messing around'. Expected one of: 'upper', 'lower', or 'asis'
It would be much better if it instead outputed:
$> mycat --prefix 'My beautiful text:' --transform "messing around" nonexistent\ file\ with\ spaces
error: found 2 errors
mycat --prefix 'My beautiful text:' --transform 'messing around' 'nonexistent file with spaces'
^ File does not exist
mycat --prefix 'My beautiful text:' --transform 'messing around' 'nonexistent file with spaces'
^ Invalid value 'messing around'. Expected one of: 'upper', 'lower', or 'asis'
Extra care need to be put if the input value features quotes as well, which might need to not break the quoting in the output.
It would be nice to be able to provide non-positional options and flags at any level of the subcommand hierarchy.
The most obvious use for this is providing flags/options at the root, such as verbosity and other flags.
Having to define a -v
flag on every command seems bit wasteful. It seems that all flags/options from parents could be folded into the handler params.
It would be nice if users could only specify env
for flags/options and not long
This would probably be better as a Discussion, sorry; but they don't seem enabled on this repo.
The documentation uses:
import { ExistingPath } from 'cmd-ts/batteries/fs';
โฆ I haven't been around the JS ecosystem much recently, but I cannot, for the life of me, get this to work out-of-the-box in 2023 JavaScript? (Node 18 or Node 20; TypeScript 5.1.6.)
Using it directly in an .mts
file leads to Node complaining โฆ
$ node bin.mjs
node:internal/process/esm_loader:46
internalBinding('errors').triggerUncaughtException(
^
Error [ERR_UNSUPPORTED_DIR_IMPORT]: Directory import '/Users/ec/Sync/Code/jsonc-merge/node_modules/cmd-ts/batteries/fs' is not supported resolving ES modules imported from /Users/ec/Sync/Code/jsonc-merge/bin.mjs
Did you mean to import cmd-ts/dist/cjs/batteries/fs.js?
at new NodeError (node:internal/errors:405:5)
at finalizeResolution (node:internal/modules/esm/resolve:218:17)
... {
code: 'ERR_UNSUPPORTED_DIR_IMPORT',
url: 'file:///Users/ec/Sync/Code/jsonc-merge/node_modules/cmd-ts/batteries/fs'
}
Node.js v20.5.0
(I also tried --experimental-specifier-resolution=node
, although I don't think that's relevant in 2023 โฆ)
I tried to manually resolve the path, following the package.json
in the otherwise-empty cmd-ts/batteries/fs
folder, but that just leads to a different error about named exports:
$ cat bin.mts
import { command, run, string, restPositionals } from "cmd-ts"
import { ExistingPath } from "cmd-ts/dist/esm/batteries/fs.js"
// ...
$ node bin.mjs
file:///Users/ec/Sync/Code/jsonc-merge/bin.mjs:2
import { ExistingPath } from "cmd-ts/dist/esm/batteries/fs.js";
^^^^^^^^^^^^
SyntaxError: Named export 'ExistingPath' not found. The requested module 'cmd-ts/dist/esm/batteries/fs.js' is a CommonJS module, which may not support all module.exports as named exports.
CommonJS modules can always be imported via the default export, for example using:
import pkg from 'cmd-ts/dist/esm/batteries/fs.js';
const { ExistingPath } = pkg;
at ModuleJob._instantiate (node:internal/modules/esm/module_job:122:21)
at async ModuleJob.run (node:internal/modules/esm/module_job:188:5)
at async DefaultModuleLoader.import (node:internal/modules/esm/loader:228:24)
at async loadESM (node:internal/process/esm_loader:40:7)
...
Node.js v20.5.0
Okay, fine; I follow the instructions in that error-message to manually destructure the package-root โฆ and get another error, because now, having manually resolved that path, Node no longer sees "type": "module"
for that file, I guess?
$ cat bin.mts
import { command, run, string, restPositionals } from "cmd-ts"
import CmdBatteriesFs from "cmd-ts/dist/esm/batteries/fs.js"
const { ExistingPath } = CmdBatteriesFs
// ...
$ node bin.mjs
(node:46165) Warning: To load an ES module, set "type": "module" in the package.json or use the .mjs extension.
(Use `node --trace-warnings ...` to show where the warning was created)
/Users/ec/Sync/Code/jsonc-merge/node_modules/cmd-ts/dist/esm/batteries/fs.js:1
import { extendType, string } from '..';
^^^^^^
SyntaxError: Cannot use import statement outside a module
at internalCompileFunction (node:internal/vm:73:18)
at wrapSafe (node:internal/modules/cjs/loader:1153:20)
at Module._compile (node:internal/modules/cjs/loader:1197:27)
at Module._extensions..js (node:internal/modules/cjs/loader:1287:10)
at Module.load (node:internal/modules/cjs/loader:1091:32)
at Module._load (node:internal/modules/cjs/loader:938:12)
at ModuleWrap.<anonymous> (node:internal/modules/esm/translators:165:29)
at ModuleJob.run (node:internal/modules/esm/module_job:192:25)
at async DefaultModuleLoader.import (node:internal/modules/esm/loader:228:24)
at async loadESM (node:internal/process/esm_loader:40:7)
Node.js v20.5.0
So: How the heck do I actually use cmd-ts/batteries/fs
from an ESM in a TypeScript project? (And, probably, the documentation around this could be improved โฆ) :x
tsconfig.json
{
"$schema": "https://json.schemastore.org/tsconfig",
"compilerOptions": {
"strict": true,
"target": "es2019",
"module": "Node16",
"declaration": true,
"declarationMap": true,
"inlineSourceMap": true,
"isolatedModules": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true
}
}
I would like to be able to exit with status 0 when specifying -h
or --help
, but it appears that 1
is hard-coded as the exit status. Perhaps providing a means for specifying a different exit status when help is invoked would be nice.
for example, I need to pass to my CLI -h
for headless
mode.
I can't do that right now because it will show me the help menu instead.
Was wondering if anyone created some sort of shell completion generator which works with cmd-ts
import {File} from 'cmd-ts/batteries/fs';
Can be imported when using tsconfig "module": "CommonJS"
, but in "NodeNext"
it breaks with the error: Cannot find module 'cmd-ts/batteries/fs' or its corresponding type declarations.
I'm not sure if this is an issue with how cmd-ts uses a package.json to redirect to dist
, or if it's a bug in TypeScript's resolver. I'm using TS 4.9.5
Awesome library. I'm currently using cmd-ts
within a Deno application. Everything behaves beautifully with one exception: at the end of every run is an error related to (what appears to be) a NodeJS cleanup hook (?).
error: Uncaught TypeError: __Process$.exit is not a function
at N.run (https://cdn.esm.sh/v66/[email protected]/es2021/cmd-ts.js:2:4443)
at Module.ve (https://cdn.esm.sh/v66/[email protected]/es2021/cmd-ts.js:9:66)
at async file:///Users/harrysolovay/Desktop/capi/cli/bin.ts:8:1
Using dryRun
suppresses this error... but then there's no console output. Any tips on smoothing this out?
This is a suggestion to add an optional summary
configuration field in command
and subcommands
which would be referenced when a short description is needed/preferable, like when listings choices of subcommands.
This would allow having both a concise description when a command or subcommand appears in a list in another command's help, and a long-form description with all the necessary details to keep in mind when using the CLI when viewing the command's help.
consider the following example:
#!/bin/env node
// machine.ts
import { command, subcommands, binary } from "cmd-ts";
const brewCoffeeCmd = command({
name: "brew coffee",
description: // long description, maybe featuring notes or example usage
`Brew a cup of coffee.
Lorem ipsum dolor sit amet...
`,
summary: "Brew a cup of coffee",
args: { /* args */ },
handler: () => {
console.log("Brewing coffee...");
},
});
const brewCocoaCmd = command({
name: "brew cocoa",
description: // long description, maybe featuring notes or example usage
`Brew a cup of hot cocoa.
Lorem ipsum dolor sit amet...
`,
summary: "Brew a cup of hot cocoa",
args: { /* args */ },
handler: () => {
console.log("Brewing hot cocoa...");
},
});
const brewCmd = subcommands({
name: "brew",
description: // long description, maybe featuring notes or example usage
`Brew a cup of a selection of hot drinks.
Lorem ipsum dolor sit amet...
`,
summary: "Brew a hot drink",
cmds: {
coffee: brewCoffeeCmd,
cocoa: brewCocoaCmd,
},
});
const payCmd = command({
name: "pay",
description: // long description, maybe featuring notes or example usage
`Pay for a drink.
Lorem ipsum dolor sit amet...
`,
summary: "Pay your drink",
args: { /* args */ },
handler: () => {
console.log("Payment complete, enjoy your drink!");
},
});
const machineCmd = subcommands({
name: "machine",
description: "coffee machine CLI utility",
summary: "available but unused?",
cmds: {
brew: brewCmd,
pay: payCmd,
},
});
void run(binary(machineCmd), process.argv);
Which would generate the following help pages:
$> machine --help
machine <subcommand>
> coffee machine CLI utility
Where <subcommand> can be one of:
- brew - Brew a hot drink
- pay - Pay your drink
$> machine brew --help
brew <subcommand>
> Brew a cup of a selection of hot drinks.
Lorem ipsum dolor sit amet...
Where <subcommand> can be one of:
- coffee - Brew a cup of coffee
- cocoa - Brew a cup of hot cocoa
$> machine pay --help
pay
> Pay for a drink.
Lorem ipsum dolor sit amet...
and so on.
Usecase:
Say I'm using cmd-ts
to wrap another CLI call.
And I don't want to map each of the 2nd CLI's arguments, but just forward them from the original call.
something like:
cmdTs.command({
name: 'runner',
acceptUnknownArguments: true,
args: {
myArg: flag({
type: boolean,
long: 'my-arg',
}),
handler: ({ myArg, unknownArgs }) => {
if (myArg) {
execa(`node ./otherCli ${unknownArgs.join(' ')}`)
}
},
},
})
Hi ๐ Neat project!
It'd be great to be able to write
import { command, run, string, positional } from 'cmd-ts';
// here, we have a handler decoupled from the means of acquiring input
import { myHandler } from './handler'
const app = command({
name: 'my-first-app',
args: {
someArg: positional({ type: string, displayName: 'some arg' }),
},
handler: myHandler // I guess this makes me a sucker for tacit programming
});
run(app, process.argv.slice(2));
Thereby allowing myHandler
to be invoked in tests without the cmd-ts
integration, but unless we can derive the type of args
that will be passed to the handler
function, we'll have to duplicate the type of args
manually, which is brittle and error-prone.
Is there currently a way to export the args
type?
I'd like to write regression tests for my command to test how arguments are parsed, required, and how the help message prints.
I see run
, runSafely
, and dryRun
but all three call the handler.
I am looking for the right TS command framework for me and am curious about the future of this project.
Hey, if you wouldn't mind, could you show me a little example of how to use cmd-ts such that I could get back a value from the executed command, or otherwise capture its output in an asynchronously-safe way?
I'm wanting to use cmd-ts outside of the context of the CLI, such as backing Discord/IRC bots and the like. It would be really helpful to see a short example if it is possible.
Thanks.
As a convenience factor on binaries that contain many subcommands, I find it very helpful to print out help if someone calls the command with zero arguments.
Would be really helpful to have an option (or example) to allow this!
import * as cmd from "cmd-ts";
cmd.command({
name: 'regression',
args: {
target: cmd.option({
type: cmd.oneOf(['stable', 'canary']),
long: 'target',
defaultValue() {
return 'stable'; // Error
}
}),
});
TS Result: Type '() => string' is not assignable to type '() => "stable" | "canary"'.
No Error.
Accept return value: 'stable'
as 'stable' | 'carary'
Currently, the objects returned by command
and subcommand
do not feature properties that let you crawl the command structure. It would be helpful if this structure's interface allowed for iterating over it and getting metadata, such as names, descriptions, child commands, etc.
This issue lists Renovate updates and detected dependencies. Read the Dependency Dashboard docs to learn more.
Warning
These dependencies are deprecated:
Datasource | Name | Replacement PR? |
---|---|---|
npm | request |
These updates are currently rate-limited. Click on a checkbox below to force their creation now.
@typescript-eslint/eslint-plugin
, @typescript-eslint/parser
)These updates have been manually edited so Renovate will no longer make changes. To discard all commits and start over, click on a checkbox.
These updates have all been created already. Click a checkbox below to force a retry/rebase of any.
@changesets/cli
, @swc-node/register
, @typescript-eslint/eslint-plugin
, @typescript-eslint/parser
, docs-ts
, eslint
, eslint-config-prettier
, eslint-plugin-import
, node
, node-fetch
, pnpm/action-setup
, prettier
, tempy
, typedoc
, typescript
, vitest
)fs-extra
, @types/fs-extra
).github/workflows/build.yml
actions/checkout v3
actions/setup-node v3
pnpm/action-setup v2.2.2
actions/cache v3
.github/workflows/release.yml
actions/checkout v3
pnpm/action-setup v2.2.2
actions/setup-node v3
changesets/action v1
.node-version
node 16.16.0
package.json
chalk ^4.0.0
debug ^4.3.4
didyoumean ^1.2.2
strip-ansi ^6.0.0
@changesets/cli 2.23.2
@swc-node/register 1.5.1
@types/debug 4.1.7
@types/didyoumean 1.2.0
@types/fs-extra 9.0.13
@types/node-fetch 2.6.2
@types/request 2.48.8
@typescript-eslint/eslint-plugin 5.30.7
@typescript-eslint/parser 5.30.7
cargo-mdbook 0.4.4
docs-ts 0.6.10
eslint 8.20.0
eslint-config-prettier 8.5.0
eslint-plugin-import 2.26.0
eslint-plugin-prettier 4.2.1
esm 3.2.25
execa 6.1.0
fs-extra 10.1.0
husky 8.0.1
infer-types 0.0.2
node-fetch 2.6.7
prettier 2.7.1
request 2.88.2
tempy 3.0.0
typedoc 0.23.8
typescript 4.7.4
vitest 0.18.1
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.