{"id":48187017,"url":"https://github.com/danajanezic/zli","last_synced_at":"2026-04-04T17:51:01.145Z","repository":{"id":169140802,"uuid":"644152765","full_name":"danajanezic/zli","owner":"danajanezic","description":"ZLI - a simple ZX based CLI framework","archived":false,"fork":false,"pushed_at":"2025-06-13T17:22:16.000Z","size":532,"stargazers_count":2,"open_issues_count":2,"forks_count":0,"subscribers_count":4,"default_branch":"master","last_synced_at":"2025-08-08T23:51:49.404Z","etag":null,"topics":["cli","google-zx","nodejs"],"latest_commit_sha":null,"homepage":"","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/danajanezic.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2023-05-22T23:51:15.000Z","updated_at":"2025-06-13T17:22:21.000Z","dependencies_parsed_at":null,"dependency_job_id":"4ac16909-7403-4646-ba3c-3b835672094f","html_url":"https://github.com/danajanezic/zli","commit_stats":null,"previous_names":["qandamedia/zli","danajanezic/zli"],"tags_count":4,"template":false,"template_full_name":null,"purl":"pkg:github/danajanezic/zli","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/danajanezic%2Fzli","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/danajanezic%2Fzli/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/danajanezic%2Fzli/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/danajanezic%2Fzli/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/danajanezic","download_url":"https://codeload.github.com/danajanezic/zli/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/danajanezic%2Fzli/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31407655,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-04T10:20:44.708Z","status":"ssl_error","status_checked_at":"2026-04-04T10:20:06.846Z","response_time":60,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cli","google-zx","nodejs"],"created_at":"2026-04-04T17:51:01.002Z","updated_at":"2026-04-04T17:51:01.135Z","avatar_url":"https://github.com/danajanezic.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# ZLI\n\nA [Google ZX](https://github.com/google/zx) based CLI that also utilizes\n[Commander's](https://github.com/tj/commander.js/) API to provide an intuitively\norganized CLI with easy to use declarative syntax.\n\n### !!! ONLY MJS IS SUPPORTED !!!\n\nIf your project is in common it will not work. There are no plans to support CJS as of now.\nZLI was designed to be used in an MJS mono-repo with CJS subprojects. As CJS doesn't\nsupport top level async/await, it is not preferable for NodeJS scripting since so much\ntooling requires reaching out to the OS.\n\nAlso of note, while there probably shouldn't be any issues using this in linux,\nas of now only OS X has been tested. Support for other OS's isn't a high priority at the moment, but\nwe're always open to PR's for adding support.\n\n## Motivation\n\nZX is a great scripting tool that has allowed our engineering team to quickly create tooling\nthat can leverage our node-based ecosystem. While ZX provides some rudimentary argument\nhandling, it is nowhere near as rich as Commander's. Initially, we added commander to our scripts\nfor this, but this provided no over-arching organization, nor did it provide easy discovery of the whole\nsuite of scripts that had been written. ZLI was written to solve this while also providing a declarative\nsyntax for describing commands, subcommands, and arguments.\n\n## Features\n\nZLI, as stated above, provides a simple declarative way of describing the command hierarchy and options with\nautomatic built-in help output. It also provides a suite of functional hooks for handling options and environment\nflags and a suite of helpers to handle common scripting functionality.\n\n## Getting Started\n\n### From NPM\n\n```\n$ npm i zli\n```\n\n### From Repo Clone\n\n```\n$ gh repo clone danajanezic/zli\n\n$ cd \u003cyour project name\u003e\n\n$ npm i \u003clocation  you cloned zli to\u003e\n```\n\nOnce installed you have to configure it:\n\n```\n$ npx zli --configure\n```\n\nThe documentation will assume that you used all the default configuration values.\n\nTo see an example project that uses ZLI [go here](https://github.com/qandamedia/example-zli-project).\n\nFrom there you can do the following:\n\n```\n$ ./zli --help\n$ ./zli show-globals --docs\n$ ./zli show-globals --list\n```\n\n## Creating Commands\n\nLet's create a command with the following signature:`./zli secrets --fetch-all`:\n\nFirst let's create a file in our command root called `secrets.js`\n\nInside this let's add the following:\n\n```javascript\nexport const OPTS = {\n    name: 'secrets',\n    description: 'Commands for fetching/updating secrets from secret store'\n    options: [\n        {\n            name: 'fetch-all',\n            description: 'Fetch all secrets from store',\n            type: OPTION_TYPES.BOOLEAN,\n        }\n    ]\n}\n```\n\nThis tells the ZLI interpreter the command structure we want. The interpreter\nlooks for exported constant object declarations named \"OPTS\". The key _name_\nat the top level of this object tells it the command name, the _description_ tells it\nwhat to display in the help output for this command. The _options_ array are all\nargument options for the command which follow the same _name/description_ pattern\nbut require an extra field: _type_ which tells the interpreter what type of options it is.\nTo see available types you can check the [source code](https://github.com/qandamedia/zli/blob/master/types/index.js).\n\nAt this point we can actually now run the following:\n\n```\n$ ./zli secrets --help\n\n__________.____    .___\n\\____    /|    |   |   |\n  /     / |    |   |   |\n /     /_ |    |___|   |\n/_______ \\|_______ \\___|\n        \\/        \\/\n\n\n\nUsage: ZLI secrets [options]\n\nCommands for fetching/updating secrets from secret store\n\nOptions:\n  -f, --fetch-all            Fetch all secrets from store\n  -h, --help                 display help for command\n\n```\n\nSo now that we have told the interpreter what our command looks like, we probably want to be able to\nhave our script actually do something when invoked. So let's now add the following to our file:\n\n```javascript\nuseOpts(() =\u003e {\n  echo('DO THE THING TO FETCH');\n}, 'fetchAll');\n```\n\nThe `useOpts` handler provides a way of scoping code to only be called when the arguments passed\nas strings. Note here that Commander transforms 'fetch-all' to 'fetchAll' as the argument name. Since\nZLI is built on GoogleZX, _the entire ZX API is within the scope of your scripts_. Hence `echo` being available.\n\nThe `useOpts` handler can be nested within other `useOpts` handlers or the runtime handlers which\nyou can learn more about by running `$ ./zli show-globals --docs`. If you want a block of code to run\nwhen multiple options are provided just pass each option name as a subsequent argument to the end\nof _useOpts_.\n\nLet's now make our command require a password option to be provided:\n\n```javascript\nexport const OPTS = {\n  name: 'secrets',\n  description: 'Commands for fetching/updating secrets from secret store',\n  options: [\n    {\n      name: 'fetch-all',\n      description: 'Fetch all secrets from store',\n      type: OPTION_TYPES.BOOLEAN,\n    },\n    {\n      name: 'password',\n      description: 'Password for secret store',\n      type: OPTION_TYPES.PASSWORD,\n      required: true,\n    },\n  ],\n};\n```\n\nNotice now we have two option items in our array with the new one being for a password.\nWe can see here that the type is _OPTION_TYPES.PASSWORD_ which tells the interpreter\nnot only that the user will be providing a value, but that value should be treated as a password.\nAs of now there is no special treatment, but it is on the roadmap to make sure that if there is\nlogging of this value (like say when an error is thrown) that it is masked by default, so\nit's still worth using this type over _OPTION_TYPES.INPUT_ for password values. Next\nwe see that there is a new field named _required_. This tells the interpreter not to\nproceed if the user has not provided this options.\n\n```\n$ ./zli secrets --fetch-all\n\nERROR:\n\n\tMissing required argument(s): password\n\n```\n\nNow let's update our code to use the password:\n\n```javascript\nuseOpts(\n  ({ password }) =\u003e {\n    echo(`Authenticating with password ${password}`);\n    echo('DO THE THING TO FETCH');\n  },\n  'fetchAll',\n  'password'\n);\n```\n\nSo as you can see we added _password_ to the args for useOpts and now our\ncallback function takes an argument: `({ password }) =\u003e {}`. ZLI passes all the\narguments to the callback function's first argument as an object. If we wanted to just\nscope our callback within the password and test _fetchAll_ independently we could\nrewrite our usage of _useOpts_ to be the following should we so choose:\n\n```javascript\nuseOpts(({ password, fetchAll }) =\u003e {\n  echo(`Authenticating with password ${password}`);\n\n  if (fetchAll) {\n    echo('DO THE THING TO FETCH');\n  }\n}, 'password');\n```\n\nOr we could also do rewrite it like this:\n\n```javascript\nuseOpts(({ password, fetchAll }) =\u003e {\n  echo(`Authenticating with password ${password}`);\n\n  useOpts(() =\u003e {\n    echo('DO THE THING TO FETCH');\n  }, 'fetchAll');\n}, 'password');\n```\n\nIt's up to you're preference. Very often the pattern we end up using is creating\nfunctions to handle specific operations and then wire up those functions with `useOpts`.\nThe idea behind _useOpts_ is to prevent garden path if/then code for handling all\nthe possible configurations of options.\n\nIf you need to code to always run inside a command script you just write it like you\nwould normally:\n\n```javascript\n//ONLY CALLED when options 'fetchAll' and 'password' are present\nuseOpts(\n  ({ password }) =\u003e {\n    echo(`Authenticating with password ${password}`);\n    echo('DO THE THING TO FETCH');\n  },\n  'fetchAll',\n  'password'\n);\n\n//Always called if no required options are defined or required options are defined and provided by user\necho(`You should see this output whether or not you provide the fetch options`);\n```\n\n```\n$ ./zli secrets -p 12341234\nYou should see this output whether or not you provide the fetch options\n```\n\nNow it needs to be pointed out that that code _will not_ be called if the password is not provided\nbecause the interpreter will not execute your script if a required field is not provided. This hints at\nthe underlying design goal of _minimizing side-effects_. That is, you should be able to do _very dangerous\nthings in your scripts that will never run unless explicitly told to_. Implementing this goal as well\nas the goal of providing simple declarative ways of describing command structure meant that\nthe CLI's interpreter works in two primary phases: 1. parse all the exported Opts objects to determine\ncommand structure and which script to run, 2. evaluate script within the context provided by\nthe interpreter.\n\nAs a result of this two-phased execution, code _outside_ of `export const OPTS = {}` does not run\nduring the first phase. This means you cannot use imports inside of it unless you use the async `import()`\nfunction or the `require` function provided by ZX (since ZX is within the scope of OPTS too).\n\n### Calling Commands in Other Files\n\nLet's say we need to call our`secrets` script from another script. ZLI provides the `withZliCmd` hook for just\nthis use-case. In our new script we will want to do the following:\n\n```javascript\nwithZliCmd((secretsCmd) =\u003e {\n  // Don't hardcode passwords this is just an example\n  secretsCmd({ fetchAll: true, password: 1234 });\n}, 'secrets.js');\n```\n\nLike `useOpts`, `withZliCmd` takes a callback as it's first argument which when executed\nis provided the command as a function to call with the arguments you would normally pass\nas cli options. The rest of the arguments to `withZliCmd` are the path to the command\nyou want to use. This example assumes that it is in a file that is next to 'secrets.js'.\n\n## Debugging\n\nIf you are having trouble figuring out why commands, subcommands and/or options\naren't as expected you can run the following command to see the a representation\nof the command hierarchy:\n\n```\n# ./zli --show-arg-name-map\n\u003e ZLI\n----\u003e [\n  [ 'v', 'verbose' ],\n  [ 'l', 'local' ],\n  [ 'd', 'develop' ],\n  [ 's', 'staging' ],\n  [ 'p', 'prod' ]\n]\nZLI \u003e secrets\n----\u003e [ [ 'f', 'fetch-all' ], [ 'p', 'password' ] ]\nZLI \u003e show-globals\n----\u003e [ [ 'd', 'docs' ], [ 'l', 'list' ] ]\n```\n\n## Experimental Features\n\n### Command Tree Caching\n\nIf you're CLI grows to be many files you might find that caching the command tree\nwill provide a performance lift. To do so you can run:\n\n```\n$ ./zli --write-cache\n```\n\nThis will create a file named `.zli.cache.js` which you'll probably want to add to your\n.gitignore file because the command paths will be absolute in the file and most definitely\nwon't work on other computers.\n\nIf you do this, remember to run the write command any time the cli commands change\nto update the cache, otherwise the CLI won't know about new commands that have\nbeen added since the cache was last updated.\n\nThere is a known issue where multiline line backtick string literals are not serialized\nproperly. This is an issue with the`code-stringify` package and is the reason why\nthis feature is still experimental.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdanajanezic%2Fzli","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdanajanezic%2Fzli","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdanajanezic%2Fzli/lists"}