Giter Club home page Giter Club logo

clipanion's Introduction

Clipanion

Type-safe CLI library with no runtime dependencies

npm version Licence Yarn

Installation

yarn add clipanion

Why

  • Clipanion supports advanced typing mechanisms
  • Clipanion supports nested commands (yarn workspaces list)
  • Clipanion supports transparent option proxying without -- (for example yarn dlx eslint --fix)
  • Clipanion supports all option types you could think of (including negations, batches, ...)
  • Clipanion offers a Typanion integration for increased validation capabilities
  • Clipanion generates an optimized state machine out of your commands
  • Clipanion generates good-looking help pages out of the box
  • Clipanion offers common optional command entries out-of-the-box (e.g. version command, help command)

Clipanion is used in Yarn with great success.

Documentation

Check the website for our documentation: mael.dev/clipanion.

Migration

You can use clipanion-v3-codemod to migrate a Clipanion v2 codebase to v3.

Overview

Commands are declared by extending from the Command abstract base class, and more specifically by implementing its execute method which will then be called by Clipanion. Whatever exit code it returns will then be set as the exit code for the process:

class SuccessCommand extends Command {
    async execute() {
        return 0;
    }
}

Commands can also be exposed via one or many arbitrary paths using the paths static property:

class FooCommand extends Command {
    static paths = [[`foo`]];
    async execute() {
        this.context.stdout.write(`Foo\n`);
    }
}

class BarCommand extends Command {
    static paths = [[`bar`]];
    async execute() {
        this.context.stdout.write(`Bar\n`);
    }
}

Options are defined as regular class properties, annotated by the helpers provided in the Option namespace. If you use TypeScript, all property types will then be properly inferred with no extra work required:

class HelloCommand extends Command {
    // Positional option
    name = Option.String();

    async execute() {
        this.context.stdout.write(`Hello ${this.name}!\n`);
    }
}

Option arguments can be validated and coerced using the Typanion library:

class AddCommand extends Command {
    a = Option.String({required: true, validator: t.isNumber()});
    b = Option.String({required: true, validator: t.isNumber()});

    async execute() {
        this.context.stdout.write(`${this.a + this.b}\n`);
    }
}

License (MIT)

Copyright © 2019 Mael Nison

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:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

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.

clipanion's People

Contributors

abarisain avatar arcanis avatar arthuro555 avatar ayc0 avatar bgotink avatar cometkim avatar cptpackrat avatar d3lm avatar drarig29 avatar eps1lon avatar iansu avatar jakub-g avatar kevinkhill avatar kherock avatar lordofthelake avatar merceyz avatar noseworthy avatar paul-soporan avatar regevbr avatar samverschueren avatar skimi avatar theoludwig avatar tkamenoko avatar wormwlrm avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

clipanion's Issues

The "Options" section will always have rich formatting enabled

I'm using [email protected].
In testing my CLI using clipanion, I tried to use the environment variables NO_COLOR and FORCE_COLOR to force disabling color output. However, only the "Options" section was not in text format.

スクリーンショット 2021-12-09 13 24 26
スクリーンショット 2021-12-09 13 24 10

The reason for this is that only the "Options" section uses the richFormat.header() method directly.

if (options.length > 0) {
result += `\n`;
result += `${richFormat.header(`Options`)}\n`;

I think this is a bug because all the other sections use the this.format(colored).header() method.

Add `details` property to `@Command.{String,Boolean,Array}`

Description

At the moment the documentation created for a cli command is based on the static usage property. It also shows the following auto generated line.

tsproj create [--template,-t #0] [--pkg,-p #0] [--template-library,-T #0] [--verbose] <name>

I would like to propose that the @Command decorator properties adds a new option to provide a details string that is automatically added to the generated docs.

Example

import { Command } from 'clipanion';

export class CreateCommand extends Command {
  public static usage = Command.Usage({
    description: 'Create a new TypeScript project.',
    category: 'Create',
  });

  @Command.String({ required: true, details: `The name you would like to give the created project` })
  public name: string = '';

  @Command.String('--template,-t', { details: 'The template to use for the provided project. Defaults to `minimal`', })
  public template: string = 'minimal';

  Command.Path('create')
  public async execute() {
    this.context.stdout.write(
      JSON.stringify({
        name: this.name,
        template: this.template,
      }),
    );
  }
}

Would output the following docs when run with the -h flag:

Create a new TypeScript project.

Usage:

$ tsproj create [--template,-t] <name>

Positional: 

    <name>                              the name you would like to give the created project

Options:

    --template <string>                 The template to use for the provided project. Defaults to `minimal`

I'm happy to help make this happen, but I might need a few pointers in the right direction.

Multiline descriptions do not preserve space when removing newlines

I'm not sure if this is intended behavior or not, but I assume it's a bug.

details: `
    This is a
    multi-line description.
`,

Renders like this:

━━━ Details ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

This is amulti-line description.

There is no space between a and multi-line.

Validation errors that make sense to end-user

Using the yup integration, I see that the validation errors refer to internal implementation details. Ideally, the end-user would see errors referring to the --flags they provided rather than to the internal structure of the Command subclass.

Are there any plans to add those sorts of meaningful errors, or any constraints preventing this feature from being added?

Disallow registering the same command several times

import {Builtins, Cli} from 'clipanion';

const cli = new Cli({
	binaryLabel: `cli`,
	binaryName: `cli`,
	binaryVersion: `1.0.0`,
});

cli.register(Builtins.VersionCommand);
cli.register(Builtins.VersionCommand);
cli.register(Builtins.VersionCommand);
cli.register(Builtins.VersionCommand);
cli.register(Builtins.VersionCommand);

This should not be possible.

Documentation?

I was wondering if you plan to add more documentation? I know Clipanion is... extensive, but more documentation would help. Lately I've just been tearing apart Yarn Modern's code to figure out how to use it better. Like throwing UsageError or extending it to make something like WorkspaceRequiredError. But these smaller things aren't mentioned in the README and there isn't a wiki.

[Documentation] add cli examples

for example on https://mael.dev/clipanion/docs/paths

what is the command I would run? the reason I want this is I've been trying to figure out how to do --port, but from these docs I don't see what's obviously an example of that, there's this https://mael.dev/clipanion/docs/options but it doesn't show any examples of how to code those. I imagine it's on the previous page but it's hard to correlate the 2. Also, I don't see an example of how to create a command with no paths, although simply not defining paths seems to work. If I have an option, how do I pass it to a command?

class InstallCommand extends Command {
    static paths = [[`install`], [`i`]];
    async execute() {
        // ...
    }
}

also, https://mael.dev/clipanion/docs/contexts appears to be out of date, as BaseContext no longer contains some of that
https://mael.dev/clipanion/docs/validation apply cascade signature is marked as deprecated

from the code I assumed this would create a port option, but it instead creates an argument

export class App extends Command {
  readonly port = Option.String({ validator: isPort, required: true })

  static readonly paths = [Command.Default]
❯ yarn workspace @cof/e1-fake-chamber-of-secrets run serve
Unknown Syntax Error: Unsupported option name ("--port").

✨ Thanks; TS; daemon ✨

At first, this looks really nice, thank you for share!

I see you are not on typed wave (?) (at least on fully typed), but you provide TS definitions..
Do you plan to annotate sources of this nice tool with typescript in future? (IMHO flow is loosing, sadly)

I also really appreciate peg grammar and

really very, very nice daemon idea. (Unfortunately, daemon looks not working from current npm install)

[Feature Request/Question] Using with dependency injection library

First, thanks for Clipanion and yarn 2!!

I want to add DI library, that will inject stuff into the constructor
InversifyJS or tsyringe

In that context, I think we can see clipanion as "properties injector", and I want to add constructor injector

I need to get in the middle of the "new commandClass" and then let clipanion do his thin'

const command = new commandClass();
command.path = state.path;

Is there a way to "make it work" without modifying/patching clipanion?

Background:
The injection can be used for easier decoupling of logic, use less singletons, and make testing easier with out mocks in the modules level

The first thing I have in mind to put in the DI is config file and lookup.
pseudo code to emphasise what i mean:

@injectable()
class ConfigFile {
    constructor(@inject('clipanionContext'), context:  ClipanionContext) {
    }
    async getConfig(): IMyConfig {

        return ....;
    }
}

class SomeCommand extends Command {
    constructor(private configFile: ConfigFile) {
    }

    async exec() {
        const config = this.configFile.getConfig();

        this.context.stdout.write(chalk[config.color].(`Hello World\n`));
    }
}

cli.register(SomeCommand);

cli.commandCreationMiddleware({
    // This will be called when runExit is called.
    // Note, this is async, some async work can be done here
    // Not sure about error handing here thu
    async init(clipanionContext) {
        const inversifyContext = new inversify.Context();

        // make clipanionContext available in the DI as well
        inversifyContext.bind("ClipanionContext", clipanionContext);
        inversifyContext.bind(ConfigFile);

        // Opaque value that will be passed to provide
        return inversifyContext;
    },
    // Note, this is async, inversifyContext.get might have async providers.
    // Not sure about error handing here thu
    async provide(inversifyContext, klass) {
        return inversifyContext.get(klass);
    }
});

Additional thoughts

The idea is somewhat influenced from nestjs,
With nestjs you can also define transformers and validators using decorators and keep the "controller" itself more about the command and less about the validations
https://docs.nestjs.com/pipes#transformation-use-case
Notable difference is that in nest each class member can be a "command"

Help flag on command doesn't work with required flags

I'm using clipanion in https://github.com/Embraser01/typoas and the -h flag at the end of a command like yarn dlx @typoas/cli generator -h display:

Unknown Syntax Error: Command not found; did you mean one of:

  0. @typoas/cli generate <-i,--input #0> <-o,--output #0> <-n,--name #0> [--js-doc] [--only-types]
  1. @typoas/cli -h
  2. @typoas/cli -v

While running generator -h

instead of displaying the usage of the command.
I succeeded to reproduce the bug in the should display the usage per-command:

https://github.com/arcanis/clipanion/blob/master/tests/advanced.test.ts#L95-L110

By changing it to:

it(`should display the usage per-command`, async () => {
  const cli = new Cli();
  cli.register(Builtins.HelpCommand);

  class CommandA extends Command {
    static paths = [[`foo`]];

    static usage: Usage = {description: `My command A`};

    foo = Option.Boolean(`--foo`, {required:true});
    async execute() {log(this);}
  }

  cli.register(CommandA);

  expect(await runCli(cli, [`foo`, `-h`])).to.equal(cli.usage(CommandA));
  expect(await runCli(cli, [`foo`, `--help`])).to.equal(cli.usage(CommandA));
});

I found 2 issues here:

  • If I add usage here, the description will not be printed.
  • If I set the boolean flag to required, the Unknown Syntax Error is thrown

Some issues getting example code working

I dumped the example code and tried running it and had some issues:

➜  agnostic git:(master) ✗ ts-node ./packages/cerba/example.ts greet --name 'hi' 
Type Error: Class constructor Command cannot be invoked without 'new'
    at new GreetCommand (/Users/thomas/Desktop/agnostic/packages/cerba/example.ts:5:1)
    at Cli.process (/Users/thomas/Desktop/agnostic/node_modules/clipanion/lib/index.js:1194:37)
    at Cli.run (/Users/thomas/Desktop/agnostic/node_modules/clipanion/lib/index.js:1210:32)
    at Cli.runExit (/Users/thomas/Desktop/agnostic/node_modules/clipanion/lib/index.js:1252:39)
    at Object.<anonymous> (/Users/thomas/Desktop/agnostic/packages/cerba/example.ts:50:5)
    at Module._compile (internal/modules/cjs/loader.js:1137:30)
    at Module.m._compile (/Users/thomas/.config/yarn/global/node_modules/ts-node/src/index.ts:814:23)
    at Module._extensions..js (internal/modules/cjs/loader.js:1157:10)
    at Object.require.extensions.<computed> [as .ts] (/Users/thomas/.config/yarn/global/node_modules/ts-node/src/index.ts:817:12)
    at Module.load (internal/modules/cjs/loader.js:985:32)

Running `yarn` after pulling errors

Hello!

I'm getting

➤ YN0018: │ typescript@patch:typescript@npm%3A3.7.4#builtin<compat/typescript>::version=3.7.4&hash=226bd1: The remote archive doesn't match the expected checksum

when running "yarn" after install.

I found yarnpkg/berry#1142, and YARN_CHECKSUM_BEHAVIOR=update yarn fixes it. I'm opening an issue because I'm not sure if this is an appropriate fix to commit.

Clipanion 2 Yup Validation Context

It'd be nice if the Clipanion context could be supplied as a Yup validate context so that we could have additional abilities in .when conditions. My use case is to validate based on the user's cwd

Of course this is Clipanion 2. Would you accept a PR (with tests haha)?

Confusing type inference for Option.String

It looks like my fix for this issue in #59 was partially reverted in b121c16 and I was wondering what the exact problem it was causing with implementing #68.

Currently, the type definitions act as if tolerateBoolean is always true when an initial value is provided to Option.String. A workaround that works right now is to define arity: 1 for the option.

image

Wrong help behavior for commands with proxy options

This is a toy implementation of a command that is supposed to execute a sub-command for each package in a monorepo. It takes as arguments the sub-command to run in the context of each package, and an optional flag to exclude certain packages from the process.

export class WorkspaceEachCommand extends Command<BaseContext> {
  static paths = [["each"]];

  exclusions = Option.Array("-x,--exclude");
  subcommand = Option.Proxy();

  async execute(): Promise<number | void> {
    const { stdout } = this.context;
    const { exclusions, subcommand } = this;
    stdout.write(JSON.stringify({ exclusions, subcommand }));
  }

The usage (after taking into account #85, having the flags preceding the command) would be:

$ mytool -x ugly-package -x other-package-i-dont-like each do-something-interesting
{"exclusions":["ugly-package","other-package-i-dont-like"],"subcommand":["do-something-interesting"]}

This works correctly, or at least according to the expectations lined out in #85. If I try to get help for this command, though, things go wrong:

$ mytool each --help
{"subcommand":["--help"]}

Inverting the command and the flag also goes wrong – for different reasons:

$ mytool --help each
Unknown Syntax Error: Extraneous positional argument ("each").

$ mytool -h

If I type a wrong command, the help text suggests the wrong syntax, with the flags after the command. Given how the usage function is structured, I think it would show up also in the help for the single command, if it could be accessed.

$ mytool wrong 
Unknown Syntax Error: Command not found; did you mean one of:

  0. mytool -h
  1. mytool -v
  2. mytool each [-x,--exclude #0] ...

While running wrong

Any suggestions for that?

TypeScript types do not match actually exported objects

TypeScript types seem to export something different than sources/core/index.js. For example, this is in the README:

const { clipanion } = require('clipanion');

clipanion.command(...);

But this is what actually works in a TypeScript project:

import { Clipanion } from 'clipanion';

const clipanion = new Clipanion();
clipanion.command(...);

I'm happy to submit a PR but wanted to discuss first what is actually correct – should types be updated or exports in index.js?

Detailed help for default command is inaccessible

When running clitool --help with a default command and other subcommands, clipanion will output a list of commands. It will say You can also print more details about any of these commands by calling them with the -h,--help flag right after the command name.

However, the default command does not have a command name, so there is no way to get more details about the default command.

I'm not sure if/how this should be supported. If the default command is given an additional, non-default path, and if that non-default path is specified before Command.Default, then the path will appear in --help documentation. The output will not, however, indicate that this command is the default.

Proxy option captures also flags defined before it

When using clipanion 3.0.0-rc.11, given the following command definition:

import { Command, Option, Cli } from "clipanion";

class ProxiedCommand extends Command {
  static paths = [Command.Default];

  configPath = Option.String("-C,--config", {
    description: "configuration to load",
  });

  proxy = Option.Proxy();

  async execute(): Promise<void> {
    const { stdout } = this.context;
    stdout.write(`Configuration path was: ${this.configPath}.\n`);
    stdout.write(`Proxied arguments were: ${this.proxy}\n`);
  }
}

const [, , ...args] = process.argv;

const cli = new Cli();
cli.register(ProxiedCommand);
cli.runExit(args, Cli.defaultContext);

The behavior that I would have expected, according to the documentation, was:

$ ./proxied-command -C configPath command 1 2 3
Configuration path was: configPath.
Proxied arguments were: command,1,2,3

But the actual result was:

$ ./proxied-command -C configPath command 1 2 3
Configuration path was: undefined.
Proxied arguments were: -C,configPath,command,1,2,3

Is the expected behavior or a bug?

Class constructor Command cannot be invoked without 'new'

I'm unable to get a very basic setup working, although when I clone the repo and run the tests everything seems fine.

Reproduction Steps

(Using npm instead of Yarn here to reduce variables)

mkdir clip-test && cd clip-test
npm init -y
npm i clipanion typescript ts-node @types/node
curl https://gist.githubusercontent.com/BinaryMuse/bbb9bdb6f3c4658838792fb44926b8f2/raw/284dbb07157100215a0d7914e9e5e4f285f17ef9/tsconfig.json -o tsconfig.json
curl https://gist.githubusercontent.com/BinaryMuse/bbb9bdb6f3c4658838792fb44926b8f2/raw/284dbb07157100215a0d7914e9e5e4f285f17ef9/index.ts -o index.ts
./node_modules/.bin/ts-node index.ts

Here's the Gist with the two files: https://gist.github.com/BinaryMuse/bbb9bdb6f3c4658838792fb44926b8f2

Error

$ ./node_modules/.bin/ts-node index.ts
Type Error: Class constructor Command cannot be invoked without 'new'
    at new CommandHelp (/Users/mtilley/src/clip-test/index.ts:62:42)
    at Cli.process (/Users/mtilley/src/clip-test/node_modules/clipanion/lib/advanced/Cli.js:47:37)
    at Cli.run (/Users/mtilley/src/clip-test/node_modules/clipanion/lib/advanced/Cli.js:64:32)
    at Cli.runExit (/Users/mtilley/src/clip-test/node_modules/clipanion/lib/advanced/Cli.js:94:39)
    at Object.<anonymous> (/Users/mtilley/src/clip-test/index.ts:16:5)
    at Module._compile (internal/modules/cjs/loader.js:956:30)
    at Module.m._compile (/Users/mtilley/src/clip-test/node_modules/ts-node/src/index.ts:814:23)
    at Module._extensions..js (internal/modules/cjs/loader.js:973:10)
    at Object.require.extensions.<computed> [as .ts] (/Users/mtilley/src/clip-test/node_modules/ts-node/src/index.ts:817:12)
    at Module.load (internal/modules/cjs/loader.js:812:32)

Please provide tags

Hi,

to be able to reproduce your build, is it possible to add tags in this repo ? Last tag is 0.17 while clipanion has 3.2.0-rc.3 in npm.

Cheers,
Yadd (Debian Developer)

Better support for `console.log`

The doc seems to be recommending to use this.context.stdout.write instead of console.log:

image

But those 2 don't do exactly the same things:

code console.log stdout.write
image image image
image image image
image image image

Here there a plan to include a fully compatible console (like this.context.console) that would work with clipanion and that would be 100% compatible with the console.* APIs?

Extending commands which already have a command path defined.

Could there be a way to inherit a command with a path already defined? Achieving the code reuse is possible by refactoring the options into a shared base class but if you are dealing with commands from a third party this isn't really possible.

Something like below fail.

await runCli(() => {
        class CommandA extends Command {
            @Command.Boolean('-a')
            a = false
            @Command.Path('p1')
            async execute() {
                log(this)
            }
        }

        class CommandB extends CommandA {
            @Command.Boolean('-b')
            b = false

            @Command.Path('p2')
            async execute() {
                log(this)
            }
        }

        return [
            CommandA,
            CommandB
        ];
    }, [
        "p1", "-a"
    ])
  1) inheriting an existing command:
     Error: Ambiguous Syntax Error: Cannot find who to pick amongst the following alternatives:

  0. ... p1 [-a]
  1. ... p2 [-b] [-a]

While running p1 -a

Explicit category order

When you have an "Advanced" command category, you often want that category to appear last in --help. However, it will be alphabetized first.

It would be nice to offer a way to explicitly order command categories and, when #35 is implemented, option categories.

For command categories, perhaps cli.orderCategories([array of category names]) is sufficient.

For option categories, we could accept a similar array on the Usage object, or we could accept an option new Cli({alphabetizeOptions: false}) and document categories in the order they are declared on a command, similar to how positionals are ordered based on declaration order.

Variadic options

I'm trying to emulate the behaviour of the scaffolding tool plop where a freeform list of options may be supplied via the cli.

The way it's achieved there is to parse all options following -- as a collection. Another approach could be just having a "strict: boolean" switch within CommandClass that would collate all non-specified options as a collection (kinda like a variadic parameter).

Does this sound a suitable feature for clipanion?

can not turn off a boolean option / flag with default true value

metadata = Option.Boolean('--metadata', true, {
  description: 'keep metadata',
})

I have a Boolean option, and defaults to true, in case we want to turn off default true, when called --metadata false, It shows error like

Unknown Syntax Error: Extraneous positional argument ("false")

cannot work with --name=123

class GreetCommand extends Command {
    @Command.Boolean(`-v,--verbose`)
    public verbose: boolean = false;

    @Command.String(`-n,--name`)
    public name?: string;

    @Command.Path(`greet`)
    async execute() {
        if (typeof this.name === `undefined`) {
            this.context.stdout.write(`You're not registered.\n`);
        } else {
            this.context.stdout.write(`Hello, ${this.name}!\n`);
        }
    }
}

Works like this: bin greet -n=123 and bin greet --name 123
Cannot work like this: bin greet --name=123

When multiple commands with common prefix are registered, calling `--help` with the prefix should list them all

Context

Consider this code:

const { Command } = require('clipanion');

class TypecheckCommand extends Command {
    static paths = [['typecheck']];
    ...
}

class TypecheckStopCommand extends Command {
    static paths = [['typecheck', 'stop']];
    ...
}

module.exports = [TypecheckCommand, TypecheckStopCommand];

Current behavior

yarn cli typecheck --help prints only info about typecheck itself.

Expected behavior

It should at least indicate the existence of the other subcommands (in this case yarn cli typecheck stop).

bug: ts issues in d.ts file

Hi @arcanis
My PR in snyk/nodejs-lockfile-parser#57 is using @yarnpkg/core which in turn depends on this library (2.4.1) but the d.ts file of the published npm files causes compilation issues.

node_modules/clipanion/lib/index.d.ts:2:23 - error TS2688: Cannot find type definition file for 'mocha'.

2 /// <reference types="mocha" />
                        ~~~~~

node_modules/clipanion/lib/index.d.ts:449:32 - error TS2339: Property 'binaryName$0' does not exist on type '{ binaryLabel?: string | undefined; binaryName?: string | undefined; binaryVersion?: string | undefined; enableColors?: boolean | undefined; }'.

449     constructor({ binaryLabel, binaryName$0, binaryVersion, enableColors }?: {
          

I will try to create a quick PR to fix it

RFC: What if we didn't use decorators?

My idea is, what if we were to replace:

class MyCommand extends Command {
  @Command.Boolean(`-v,--verbose`)
  verbose: boolean = false;

  @Command.String(`--email`)
  email?: string;

  @Command.String()
  name!: string;

  async execute() {
    console.log(this.verbose);
    console.log(this.email);
    console.log(this.name);
  }
}

By the following:

class MyCommand extends Command {
  verbose = Command.Boolean(`-v,--verbose`, false);
  email = Command.String(`--email`);
  name = Command.String();

  async execute() {
    console.log(this.verbose.value);
    console.log(this.email.value);
    console.log(this.name.value);
  }
}

Presumably, the Command.Boolean function would be implemented kinda like this:

function Boolean(name: string): OptionProperty<boolean> {
  return {
    value: undefined as any as boolean,
    definition(...) {
      ...
    }
    transformer(state, command) {
      command[propertyName].value = true;
    }
  };
}

And on setup, instead of instantiating one command, the runner would first instantiate them all (in order to call the install property from each option), but would only call the execute method on the one that would end up being used (it would probably create a fresh one, since instantiation should be fairly cheap anyway).

Benefits I can see:

  • The "new decorators" would provide the option type via inference, without the user having to make sure they match (including the assertions, such as this name!: string).

  • No decorators, so we wouldn't need special TS settings, and it would even work the exact same way without TS (no need to manually call the decorators).

  • Less complexity, in particular we could remove the whole metaclass thing, the inheritance magic, etc.

  • Less vertical spaces and more common formatting, which means it can be used to format other things without having to stay on a single line:

    verbose = Command.Boolean(`-v,--verbose`, false, {
      description: `Print more stuff when running`,
    });

Of course, this would be a change worth of a major bump.

Thank you

Hi! I just wanted to say thank you for building Clipanion. I know that probably seems weird, but long story short, I hated Commander, and definitely hate method chaining. I set out to build a class-based command-line framework called Strategos but I could never get the logic figured out in a scalable way. See, a simple for-loop was never going to cut it. I had a few ideas, highly inspired by Minecraft's Command system, BaseCommand, AddCommand, etc...

A finite state machine? Never would have thought about it! Anyway, I struggled for a long time as a self-taught developer with lackluster CS skills, and Clipanion solved my hatred of Commander.

So, thank you.

workspaces throwing warnings in yarn

Tracked this down to clipanion and typanion. Leaving workspaces in 'package.json' causes yarn to whine, even though they're in node_modules, which I figured it'd ignore, but noooooooo

➜ root git:(master) ✗ yarn install
yarn install v1.22.10
[1/4] 🔍 Resolving packages...
[2/4] 🚚 Fetching packages...
[3/4] 🔗 Linking dependencies...
warning Workspaces can only be enabled in private projects.
warning Workspaces can only be enabled in private projects.
warning Workspaces can only be enabled in private projects.
warning Workspaces can only be enabled in private projects.

Usage from README is incorrect

Testing this snippet from the README throws multiple TypeScript compilation errors:

import {Cli, Command, Context} from 'clipanion';

class GreetCommand extends Command {
    @Command.Boolean(`-v,--verbose`)
    public verbose: boolean = false;

    @Command.String(`--name`)
    public name?: string;

    @Command.Path(`greet`)
    async execute(cli: Cli, context: Context) {
        if (typeof this.name === `undefined`) {
            context.stdout.write(`You're not registered.\n`);
        } else {
            context.stdout.write(`Hello, ${this.name}!\n`);
        }
    }
}

Errors are the following:

  • Module '"../../node_modules/clipanion/lib/advanced"' has no exported member 'Context'

  • Property 'execute' in type 'GreetCommand' is not assignable to the same property in base type 'Command<BaseContext>'. Type '(cli: Cli<BaseContext>, context: any) => Promise<void>' is not assignable to type '() => Promise<number | void>'

Multiple multi-level paths in one command causes duplicates in help text

Description

If you define multiple nested paths for a command, and then try to execute a shared partial path of it, you get an error message where the first path is displayed multiple times.

Source

const { Builtins, Command, runExit } = require('clipanion');

runExit(
    { binaryName: 'test' },
    [
        Builtins.HelpCommand,
        class extends Command {
            static paths = [['foo', 'bar'], ['foo', 'baz']];
            async execute() {
                console.log('hi');
            }
        }
    ],
    ['foo']
);

Expected result

Unknown Syntax Error: Command not found; did you mean:

$ test foo bar

While running foo

or

Unknown Syntax Error: Command not found; did you mean one of:

  0. test foo bar
  1. test foo baz

While running foo

It's really up to you (or a config option?) whether to include multiple paths for one command in the help text.

Actual result

Unknown Syntax Error: Command not found; did you mean one of:

  0. test foo bar
  1. test foo bar

While running foo

As you can see, while baz isn't shown at all (which, arguably, might be fine), bar is shown twice.

Behaviour of Command.Proxy and forwarding arguments

I am looking for a way to forward arguments to another command and only arguments that do not exist already on the defined command. Currently proxy will forward all arguments passed into the command and will not register flags passed into the command. Is this the expected behavour of Proxy? Is there another way for this to work? In the below example, the assertion is what I am looking for. This test will fail.

it(`proxy behaviour`, async () => {
    const output = await runCli(() => {
        class CommandA extends Command {
            @Command.Boolean('-a')
            a = false
            @Command.Proxy()
            args: string[] = []

            @Command.Path('p1')
            async execute() {
                log(this, ['a', 'args'])
            }
        }

        return [
            CommandA,
        ];
    }, [
        "p1", "-a", "-b", "positionalArg"
    ]);

    expect(output).to.equal(
        [
            `Running CommandA`,
            `true`,
            `["-b","positionalArg"]\n`
        ].join('\n')
    );
})

required flags prevent subcommand `--help`; raise cryptic error

Reproduction: https://replit.com/@AndrewBradley/clipanion-repro (clicking "run" is not wired up, but the code is in "index.ts" and the shell can be used to run node ./dist/index.js)

When a subcommand foo has a required option --bar, and I try to run mycli foo --help I get a cryptic error:

~/clipanion-repro$ node ./dist/index.js foo --help
Unknown Syntax Error: Command not found; did you mean:

$ ... foo <--bar #0>
While running foo --help
~/clipanion-repro$ 

I get the same error when I try to run mycli foo. In the former case, with --help, I want to see help output about the command. In the latter, instead of the cryptic error, it would be ideal for the error to specifically mention that --bar is missing, but not to suggest that the foo command is unrecognized.


// index.ts
#!/usr/bin/env node

import { Command, Cli, Builtins, Usage, Option } from 'clipanion';
import { isEnum } from 'typanion';

const envs = ['qa', 'stage', 'prod'] as const;
const envOption = Option.String('--bar', {
    required: true,
    description: 'bar option',
    validator: isEnum(envs)
});

async function main() {
    const cli = new Cli();
    cli.register(Builtins.VersionCommand);
    cli.register(Builtins.HelpCommand);
    cli.register(FooCommand);
    await cli.runExit(process.argv.slice(2), Cli.defaultContext);
}

class FooCommand extends Command {
    static paths = [['foo']];
    static usage: Usage = {
        description: `
            foo stuff
        `
    }

    env = envOption;

    async execute() {}
}


main();

Typepanion Literal Number and Option.String do not play nice together

// don't use well known ports
const isPort = isOneOf(
  [isLiteral(0), cascade(isNumber(), isInteger(), isPositive(), isInInclusiveRange(1024, 65535))],
  { exclusive: true },
)

export class App extends Command {
  readonly port = Option.String('--port', '0', { validator: isPort })

  static readonly paths = [Command.Default]
 }

unfortunately OptionString cannot take a number, and so literal 0 will fail

❯ yarn workspace @cof/e1-fake-chamber-of-secrets serve
Usage Error: Invalid value for port:

- .#1: Expected 0 (got "0")
- .#2: Expected to be in the [1024; 65535] range (got 0)

I guess my suggestion for a fix would be to allow a subset of primitives to Option.String fortunately express seems ok with the string

Group options into categories

Pending implementation of #12

Just as commands can be grouped into categories, options/flags should be groupable as well. Groups have some sort of ordering, with more commonly-used groups at the top, and more niche options further down, so there should be a way to specify the ordering of groups in the output.

To get an idea of what the output might look like, I threw together this yargs demo:

$ yarn ts-node ./src/index.ts exec --help
bin.js exec <args...>

Run external commands within our administrative environment

Positionals:
  args  Command to run.  Will be executed as an external process.
                                                [array] [required] [default: []]

Options:
  --swallow-exit-code, -s  a silly flag which swallows exit codes for some
                           reason                                      [boolean]

Credentials:
  --foo  authenticate against the foo environment                      [boolean]
  --bar  authenticate against the bar environment                      [boolean]
  --env  authenticate against a custom environment declared in your rc file
                                                                        [string]

Misc:
  --version  Show version number                                       [boolean]
  --help     Show help                                                 [boolean]

The yargs source code is below:

yargs demo
import type _yargs from 'yargs/yargs';
const yargs: typeof _yargs = require('yargs/yargs');

const credentialsGroup = 'Credentials:';
yargs(process.argv.slice(2))
    .strict()
    .help()
    // Global options available to all commands
    .options({
        foo: {
            group: credentialsGroup,
            type: 'boolean',
            describe: 'authenticate against the foo environment'
        },
        bar: {
            group: credentialsGroup,
            type: 'boolean',
            describe: 'authenticate against the bar environment'
        },
        env: {
            group: credentialsGroup,
            type: 'string',
            describe: 'authenticate against a custom environment declared in your rc file'
        }
    })
    .group('version', 'Misc:')
    .group('help', 'Misc:')
    .command('exec <args...>', 'Run external commands within our administrative environment', {
        builder(yargs) {
            return yargs
                .positional('args', {
                    describe: 'Command to run.  Will be executed as an external process.'
                })
                .options({
                    'swallow-exit-code': {
                        type: 'boolean',
                        alias: 's',
                        group: 'Options:',
                        describe: 'a silly flag which swallows exit codes for some reason'
                    }
                }).help();
        },
        handler(args) {

        }
    })
    .parse();

Custom error (fail) hook

Yargs Equivalent

Yargs allowed me to override the fail method in which I could handle errors and have a custom format for the error message, rather than it just printing a default error message.

Feature Description

The user can specify a method that runs when a command throws an error. This would override the default printing of an error message.

Clipanion doesn't work with TypeScript 4.8

The latest version of clipanion (3.2.0-rc.11) doesn't work with the latest version of TypeScript (4.8).

Reproduce

  1. Use [email protected]
  2. Use [email protected]
  3. Enable strictNullChecks in your code
  4. Compile

Expected Result

Compiles fine

Result

Clipanion type check errors.

It's due to stricter generic check introduced in 4.8:
https://devblogs.microsoft.com/typescript/announcing-typescript-4-8/#unconstrained-generics-no-longer-assignable-to

Unconstrained Generics No Longer Assignable to {}

In TypeScript 4.8, for projects with strictNullChecks enabled, TypeScript will now correctly issue an error when an unconstrained type parameter is used in a position where null or undefined are not legal values. That will include any type that expects {}, object, or an object type with all-optional properties.

Clipanion now errors because the first parameter of WithArity is restricted with Type extends { length?: number; }
clipanion3
But in a lot of places it gets non restricted generics
clipanion2

Which results in these errors:
clipanion1

It's possible to bypass this for now with skipLibCheck but unfortunately this turns off type checking for all libraries.

e:
Looks like adding extends {} like so gets rid of the errors:
clipanion4
However I cannot setup the dev environment to create a PR

`-` value seem not to work when passed to `Option.String` as option

Hello and first thank you for the good work on this library.

I wanted to report an issue that I found while working a small utility :
I wanted to add an option to my tool which can optionally take - as value (a pattern commonly seen on cli tools for redirecting some output on stdout instead of a file)

class BuildContextCommand extends clipanion.Command {
    public static paths = [ [ 'docker', 'build-context' ] ]
    public static usage = {
      description: 'builds an efficient docker context .tgz from a yarn workspace'
    }

    public output = clipanion.Option.String(`-o,--output`, {
      description: `the tgz output, use '-' to write contents to stdout (useful for piping to docker)`,
      required: false
    })
    public execute = async () => { console.log(this.output) }
}

clipanion seem to fail not recognizing this value:

hb docker build-context -o -
Unknown Syntax Error: Not enough arguments to option -o.

$ hb docker build-context [-o,--output #0] [input]

It's working when using the = form though:

hb docker build-context -o=-
-

maybe it's interpreted as another (positional/option) argument ?

macos 11.3
node v14.15.4
clipanion: 3.0.0-rc.12

Thank you,

Options cannot be used during object construction

Options appear as regular public class fields but do not function as such during class instance construction.

class DefaultCommand extends Command {
  static paths = [Command.Default];

  s = Option.String('-s', {
    required: true,
  });

  uppercase = this.s.toUpperCase(); // TypeError: this.s.toUpperCase is not a function

  async execute() {}
}

https://stackblitz.com/edit/node-gbmupy?file=cli.ts

This behavior is understandable given the way options are declared, but still might take the user by surprise. Especially when it fails silently (optional property usage, for example).

route unexpected command when single user command registered.

src/test.ts

#!/usr/bin/env ts-node-script

import {Cli, Command} from 'clipanion'

// greet [-v,--verbose] [--name ARG]
class GreetCommand extends Command {
  @Command.Boolean(`-v,--verbose`)
  verbose: boolean

  @Command.String(`--name`)
  name: string

  static paths = [[`greet`]]
  async execute() {
    if (typeof this.name === `undefined`) {
      this.context.stdout.write(`You're not registered.\n`)
    } else {
      this.context.stdout.write(`Hello, ${this.name}!\n`)
    }
  }
}

const cli = new Cli({
  binaryLabel: `My Utility`,
  binaryName: `bin`,
  binaryVersion: `1.0.0`,
})

cli.register(Command.Entries.Help)
cli.register(Command.Entries.Version)

cli.register(GreetCommand)

cli.runExit(process.argv.slice(2), {
  ...Cli.defaultContext,
})

when execute

$ ./src/test.ts             
You're not registered.

tsconfig.json

{
  "include": ["src/**/*"],
  "compilerOptions": {
    "rootDir": "src",
    "outDir": "lib",
    "allowSyntheticDefaultImports": true,
    "esModuleInterop": true,
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "target": "es2019",
    "module": "CommonJS",
    "skipLibCheck": true,
    "incremental": true,
    "experimentalDecorators": true
  }
}

the greet command should be called when execute ./src/test.ts greet, but not ./src/test.ts

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.