{"id":18041303,"url":"https://github.com/ligurio/elle-cli","last_synced_at":"2025-04-09T16:50:35.945Z","repository":{"id":39710752,"uuid":"441733817","full_name":"ligurio/elle-cli","owner":"ligurio","description":"The command-line frontend to transactional consistency checkers for black-box databases","archived":false,"fork":false,"pushed_at":"2025-03-04T07:00:01.000Z","size":2942,"stargazers_count":29,"open_issues_count":2,"forks_count":5,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-03-23T18:54:18.819Z","etag":null,"topics":["clojure","elle","jepsen","knossos"],"latest_commit_sha":null,"homepage":"","language":"Clojure","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ligurio.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2021-12-25T18:12:31.000Z","updated_at":"2025-03-05T06:43:01.000Z","dependencies_parsed_at":"2024-10-28T12:52:09.252Z","dependency_job_id":"5139fa13-9285-4788-b0cc-037180540624","html_url":"https://github.com/ligurio/elle-cli","commit_stats":null,"previous_names":[],"tags_count":9,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ligurio%2Felle-cli","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ligurio%2Felle-cli/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ligurio%2Felle-cli/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ligurio%2Felle-cli/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ligurio","download_url":"https://codeload.github.com/ligurio/elle-cli/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248072532,"owners_count":21043256,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["clojure","elle","jepsen","knossos"],"created_at":"2024-10-30T15:09:05.167Z","updated_at":"2025-04-09T16:50:35.919Z","avatar_url":"https://github.com/ligurio.png","language":"Clojure","funding_links":[],"categories":[],"sub_categories":[],"readme":"# elle-cli\n\n[![Testing](https://github.com/ligurio/elle-cli/actions/workflows/test.yaml/badge.svg)](https://github.com/ligurio/elle-cli/actions/workflows/test.yaml)\n\nis a command-line frontend to transactional consistency checkers for black-box\ndatabases.\nIn comparison to Jepsen library it is standalone and language-agnostic tool. You\ncan use it with tests written in any programming language and everywhere where\nJVM is available. Under the hood `elle-cli` uses libraries\n[Elle](https://github.com/jepsen-io/elle),\n[Knossos](https://github.com/jepsen-io/knossos) and\n[Jepsen](https://github.com/jepsen-io/jepsen) and provides the same correctness\nguarantees.\n\nJepsen, Elle and Knossos supports histories only in [EDN (Extensible Data\nNotation)](https://github.com/edn-format/edn), it is a format for serializing\ndata that was invented by Rich Hickey, author of Clojure, for using in Clojure\napplications. Typical data serialized to EDN looks quite similar to JSON:\n\n```clojure\n{:type :invoke, :f :read,     :process 2, :time 53137939465, :index 0}\n{:type :invoke, :f :transfer, :process 3, :time 53137939133, :index 1, :value {:from 5, :to 2, :amount 2}}\n{:type :invoke, :f :read,     :process 1, :time 53139785248, :index 2}\n{:type :invoke, :f :transfer, :process 0, :time 53139856763, :index 3, :value {:from 7, :to 9, :amount 4}}\n{:type :invoke, :f :read,     :process 4, :time 53155597745, :index 4}\n```\n\nHowever, outside of the Clojure ecosystem EDN format is practically not used.\n`elle-cli` operates with operations history both in EDN and [JSON (JavaScript\nObject Notation)](https://www.json.org/) formats and can be successfully used\nwith histories produced by Jepsen tests in EDN format as well as with other\nJepsen-similar frameworks that produce histories in JSON format.\n\n## Usage\n\nIf you have a file with history written in EDN or JSON format, either as a\nseries of operation maps, or as a single vector or list containing those\noperations, you can ask `elle-cli` to check it for you at the command line like\nso:\n\n```sh\n$ git clone https://github.com/ligurio/elle-cli\n$ cd elle-cli\n$ lein deps\n$ lein uberjar\nCompiling elle_cli.cli\nCreated /home/sergeyb/sources/elle-cli/target/elle-cli-0.1.8.jar\nCreated /home/sergeyb/sources/elle-cli/target/elle-cli-0.1.8-standalone.jar\n$ java -jar target/elle-cli-0.1.8-standalone.jar --model rw-register histories/elle/rw-register.json\nhistories/elle/rw-register.json       true\n```\n\n`elle-cli` converts files with histories in JSON format automatically to\nClojure data structures and prints out the names of all files you asked it to\ncheck, followed by a tab, and then whether the history was valid. There are\nthree validity states:\n\n- `true`      means the history was valid\n- `false`     means the history was invalid\n- `:unknown`  means checker was unable to complete the analysis; e.g. it ran\n              out of memory.\n\nIn some cases conversion of history from JSON format to Clojure data structures\nmay fail and it is definitely a bug that should be reported. To workaround I\nrecommend to use a tool [jet](https://github.com/borkdude/jet), it is a CLI to\ntransform between JSON and EDN, and then pass file in EDN format to `elle-cli`.\n\n## Supported models\n\n### rw-register\n\nAn Elle's checker for write-read registers. Options are:\n\n- **consistency-models** - a collection of consistency models we expect this\n  history to obey. Defaults to `strict-serializable`. Possible values are:\n  `consistent-view`, `conflict-serializable`, `cursor-stability`,\n  `forward-consistent-view`, `monotonic-snapshot-read`, `monotonic-view`,\n  `read-committed`, `read-uncommitted`, `repeatable-read`, `serializable`,\n  `snapshot-isolation`, `strict-serializable`, `strong-serializable`,\n  `update-serializable`.\n- **anomalies** - a collection of specific anomalies you'd like to look for.\n  Defaults to `G0`. Possible values are: `G0`, `G0-process`, `G0-realtime`,\n  `G1a`, `G1b`, `G1c`, `G1c-process`, `G-single`, `G-single-process`,\n  `G-single-realtime`, `G-nonadjacent`, `G-nonadjacent-process`,\n  `G-nonadjacent-realtime`, `G2-item`, `G2-item-process`, `G2-item-realtime`,\n  `G2-process`, `GSIa`, `GSIb`, `incompatible-order`, `dirty-update`,\n  `lost-update`, `write-skew`.\n- **cycle-search-timeout** - how many milliseconds are we willing to search a\n  single SCC for a cycle? Default value is `1000`.\n- **directory** - where to output files, if desired. Default value is `nil`.\n- **plot-format** - either `png` or `svg`. Default value is `svg`.\n- **plot-timeout** - how many milliseconds will we wait to render a SCC plot?\n  Default value is `5000`.\n- **max-plot-bytes** - maximum size of a cycle graph (in bytes of DOT) which\n  we're willing to try and render. Default value is `65536`.\n\nExample of history:\n\n```clojure\n{:type :invoke, :f :txn :value [[:w :x 2]],   :process 0, :index 1}\n{:type :ok,     :f :txn :value [[:w :x 2]],   :process 0, :index 2}\n{:type :invoke, :f :txn :value [[:r :x nil]], :process 0, :index 3}\n{:type :ok,     :f :txn :value [[:r :x 3]],   :process 0, :index 4}\n{:type :invoke, :f :txn :value [[:r :x nil]], :process 0, :index 5}\n{:type :ok,     :f :txn :value [[:r :x 2]],   :process 0, :index 6}\n```\n\n### list-append\n\nAn Elle's checker for append and read histories. It checks for dependency\ncycles in append/read transactions.\n\nThe *append* test models the database as a collection of named lists, and\nperforms transactions comprised of read and append operations. A read returns\nthe value of a particular list, and an append adds a single unique element to\nthe end of a particular list. We derive ordering dependencies between these\ntransactions, and search for cycles in that dependency graph to identify\nconsistency anomalies.\n\nIn terms of Elle values in operation are lists of integers. Each operation\nperforms a transaction, comprised of micro-operations which are either reads of\nsome value (returning the entire list) or appends (adding a single number to\nwhatever the present value of the given list is). We detect cycles in these\ntransactions using Elle's cycle-detection system.\n\nOptions are:\n\n- **consistency-models** - a collection of consistency models we expect this\n  history to obey. Defaults to `strict-serializable`. Possible values are:\n  `consistent-view`, `conflict-serializable`, `cursor-stability`,\n  `forward-consistent-view`, `monotonic-snapshot-read`, `monotonic-view`,\n  `read-committed`, `read-uncommitted`, `repeatable-read`, `serializable`,\n  `snapshot-isolation`, `strict-serializable`, `strong-serializable`,\n  `update-serializable`.\n- **anomalies** - a collection of specific anomalies you'd like to look for.\n  Defaults to `G0`. Possible values are: `G0`, `G0-process`, `G0-realtime`,\n  `G1a`, `G1b`, `G1c`, `G1c-process`, `G-single`, `G-single-process`,\n  `G-single-realtime`, `G-nonadjacent`, `G-nonadjacent-process`,\n  `G-nonadjacent-realtime`, `G2-item`, `G2-item-process`, `G2-item-realtime`,\n  `G2-process`, `GSIa`, `GSIb`, `incompatible-order`, `dirty-update`,\n  `lost-update`, `write-skew`.\n- **cycle-search-timeout** - how many milliseconds are we willing to search a\n  single SCC for a cycle? Default value is `1000`.\n- **directory** - where to output files, if desired. Default value is `nil`.\n- **plot-format** - either `png` or `svg`. Default value is `svg`.\n- **plot-timeout** - how many milliseconds will we wait to render a SCC plot?\n  Default value is `5000`.\n- **max-plot-bytes** - maximum size of a cycle graph (in bytes of DOT) which\n  we're willing to try and render. Default value is `65536`.\n\nExample of history:\n\n```clojure\n{:index 2 :type :invoke, :value [[:append 255 8] [:r 253 nil]]}\n{:index 3 :type :ok,     :value [[:append 255 8] [:r 253 [1 3 4]]]}\n{:index 4 :type :invoke, :value [[:append 256 4] [:r 255 nil] [:r 256 nil] [:r 253 nil]]}\n{:index 5 :type :ok,     :value [[:append 256 4] [:r 255 [2 3 4 5 8]] [:r 256 [1 2 4]] [:r 253 [1 3 4]]]}\n{:index 6 :type :invoke, :value [[:append 250 10] [:r 253 nil] [:r 255 nil] [:append 256 3]]}\n{:index 7 :type :ok      :value [[:append 250 10] [:r 253 [1 3 4]] [:r 255 [2 3 4 5]] [:append 256 3]]}\n```\n\n### bank\n\nA Jepsen's checker for bank histories.\n\nTest simulates a set of bank accounts, one per row, and transfers money\nbetween them at random, ensuring that no account goes negative.\nAn option `--allow-negative-balances` change this behaviour.\nUnder snapshot isolation, one can prove that transfers must serialize, and the sum of all\naccounts is conserved. Meanwhile, read transactions select the current balance\nof all accounts. Snapshot isolation ensures those reads see a consistent\nsnapshot, which implies the sum of accounts in any read is constant as well.\n\nExample of history:\n\n```clojure\n{:type :invoke, :f :transfer, :process 0, :time 12613722542, :index 34, :value {:from 1, :to 0, :amount 5}}\n{:type :fail,   :f :transfer, :process 0, :time 12686176735, :index 35, :value {:from 1, :to 0, :amount 5}}\n{:type :invoke, :f :read,     :process 0, :time 12686563291, :index 36}\n{:type :ok,     :f :read,     :process 0, :time 12799165489, :index 37, :value {0 97, 1 0, 2 0, 3 0, 4 0, 5 3, 6 0, 7 0, 8 0, 9 0}}\n{:type :invoke, :f :transfer, :process 0, :time 12799587097, :index 38, :value {:from 6, :to 5, :amount 3}}\n{:type :fail,   :f :transfer, :process 0, :time 12903632203, :index 39, :value {:from 6, :to 5, :amount 3}}\n{:type :invoke, :f :read,     :process 0, :time 12903998176, :index 40}\n{:type :ok,     :f :read,     :process 0, :time 13005165731, :index 41, :value {0 97, 1 0, 2 0, 3 0, 4 0, 5 3, 6 0, 7 0, 8 0, 9 0}}\n{:type :invoke, :f :read,     :process 0, :time 13005675266, :index 42}\n{:type :ok,     :f :read,     :process 0, :time 13109721155, :index 43, :value {0 97, 1 0, 2 0, 3 0, 4 0, 5 3, 6 0, 7 0, 8 0, 9 0}}\n{:type :invoke, :f :read,     :process 0, :time 13110070211, :index 44}\n{:type :ok,     :f :read,     :process 0, :time 13210540811, :index 45, :value {0 97, 1 0, 2 0, 3 0, 4 0, 5 3, 6 0, 7 0, 8 0, 9 0}}\n{:type :invoke, :f :read,     :process 0, :time 13210921850, :index 46}\n```\n\n### counter\n\nA Jepsen's checker for counter histories.\n\nIn the counter test, we create a single record with a counter field, and\nexecute concurrent increments and reads of that counter. We look for cases,\nwhere the observed value is greater than the sum of all `:ok` increments, and\nlower than the sum of all attempted increments. Note that this counter verifier\nassumes the value monotonically increases and decrements are not allowed.\n\nExample of history:\n\n```clojure\n{:type :invoke, :f :add, :value 1, :op-index 1, :process 0, :time 10474104701, :index 0}\n{:type :ok,     :f :add, :value 1, :op-index 1, :process 0, :time 10584742951, :index 1}\n{:type :invoke, :f :add, :value 1, :op-index 2, :process 0, :time 10686291797, :index 2}\n{:type :ok,     :f :add, :value 1, :op-index 2, :process 0, :time 10810489852, :index 3}\n{:type :invoke, :f :add, :value 1, :op-index 3, :process 0, :time 10912309790, :index 4}\n{:type :ok,     :f :add, :value 1, :op-index 3, :process 0, :time 11040666263, :index 5}\n```\n\n### long-fork\n\nA Jepsen's checker for an anomaly in parallel snapshot isolation (but which is\nprohibited in normal snapshot isolation). In *long-fork*, concurrent write\ntransactions are observed in conflicting order.\n\nFor performance reasons, some database systems implement parallel\nsnapshot isolation, rather than standard snapshot isolation. Parallel\nsnapshot isolation allows an anomaly prevented by standard SI: a long\nfork, in which non-conflicting write transactions may be visible in\nincompatible orders. As an example, consider four transactions over an\nempty initial state:\n\n```\n(write x 1)\n(write y 1)\n(read x nil) (read y 1)\n(read x 1) (read y nil)\n```\n\nHere, we insert two records, `x` and `y`. In a serializable system, one record\nshould have been inserted before the other. However, transaction 3 observes `y`\ninserted before `x`, and transaction 4 observes `x` inserted before `y`. These\nobservations are incompatible with a total order of inserts.\n\nTo test for this behavior, we insert a sequence of unique keys, and\nconcurrently query for small batches of those keys, hoping to observe a pair of\nstates in which the implicit order of insertion conflicts.\n\nLong fork is an anomaly prohibited by snapshot isolation, but allowed by the\nslightly weaker model parallel snapshot isolation. In a long fork, updates to\nindependent keys become visible to reads in a way that isn't consistent with a\ntotal order of those updates. For instance:\n\n```\nT1: w(x, 1)\nT2: w(y, 1)\nT3: r(x, 1), r(y, nil)\nT4: r(x, nil), r(y, 1)\n```\n\nUnder snapshot isolation, T1 and T2 may execute concurrently, because their\nwrite sets don't intersect. However, every transaction should observe a\nsnapshot consistent with applying those writes in some order. Here, T3 implies\nT1 happened before T2, but T4 implies the opposite. We run an n-key\ngeneralization of these transactions continuously in our long fork test, and\nlook for cases where some keys are updated out of order.\n\nIn snapshot isolated systems, reads should observe a state consistent with a\ntotal order of transactions. A long fork anomaly occurs when a pair of reads\nobserves contradictory orders of events on distinct records - for instance, T1\nobserving record x before record y was created, and T2 observing y before x. In\nthe long fork test, we insert unique rows into a table, and query small groups\nof those rows, looking for cases where two reads observe incompatible orders.\n\n### set\n\nA Jepsen's checker for a set histories.\n\nGiven a set of `:add` operations, that inserts a sequence of unique records\ninto a table, followed by a final `:read`, that concurrently attempts to read\nall of those records back, verifies that every successfully added element is\npresent in the read, and that the read contains only elements for which an `:add`\nwas attempted. We measure how long it takes for a record to become\ndurably visible, or, if it lost, how long it takes to disappear.\nA linearizable `set` should make every inserted element\nimmediately visible.\n\nExample of history:\n\n```clojure\n{:type :invoke, :f :add, :value [0 0], :process 0, :time 10529279413, :index 0}\n{:type :ok,     :f :add, :value [0 0], :process 0, :time 10661777878, :index 1}\n{:type :invoke, :f :add, :value [0 1], :process 0, :time 10761664977, :index 2}\n{:type :ok,     :f :add, :value [0 1], :process 0, :time 10888511828, :index 3}\n{:type :invoke, :f :add, :value [0 2], :process 0, :time 11077906807, :index 4}\n{:type :ok,     :f :add, :value [0 2], :process 0, :time 11209256522, :index 5}\n```\n\nBeware, `set` and `set-full` have a different computational\ncomplexities. The `set-full` checker is a lot more expensive, but\ngives you precise bounds on latencies and stability of records\nover time, whereas `set` assumes a single read at the end of the\ntest.\n\n### set-full\n\nA Jepsen's checker for a set histories. It is a more rigorous set analysis. We\nallow `:add` operations which add a single element, and `:read` which return\nall elements present at that time.\n\n```clojure\n{:type :invoke, :f :add, :value [0 0], :process 0, :time 10529279413, :index 0}\n{:type :ok,     :f :add, :value [0 0], :process 0, :time 10661777878, :index 1}\n{:type :invoke, :f :add, :value [0 1], :process 0, :time 10761664977, :index 2}\n{:type :ok,     :f :add, :value [0 1], :process 0, :time 10888511828, :index 3}\n{:type :invoke, :f :add, :value [0 2], :process 0, :time 11077906807, :index 4}\n{:type :ok,     :f :add, :value [0 2], :process 0, :time 11209256522, :index 5}\n{:type :invoke, :f :add, :value [0 3], :process 0, :time 11330024782, :index 6}\n{:type :ok,     :f :add, :value [0 3], :process 0, :time 11457989603, :index 7}\n{:type :invoke, :f :add, :value [0 4], :process 0, :time 11620593669, :index 8}\n{:type :ok,     :f :add, :value [0 4], :process 0, :time 11745589449, :index 9}\n{:type :invoke, :f :add, :value [0 5], :process 0, :time 11786251931, :index 10}\n```\n\nBeware, `set` and `set-full` have a different computational\ncomplexities. The `set-full` checker is a lot more expensive, but\ngives you precise bounds on latencies and stability of records\nover time, whereas `set` assumes a single read at the end of the\ntest.\n\n### cas-register\n\nA Knossos checker for CAS (Compare-And-Set) registers. By default\n`competition/analysis` algorithm is used.\n\nExample of history:\n\n```clojure\n{:process 7, :type :invoke, :f :cas,   :value [2 3]}\n{:process 7, :type :fail,   :f :cas,   :value [2 3]}\n{:process 8, :type :invoke, :f :write, :value 2}\n{:process 8, :type :ok,     :f :write, :value 2}\n{:process 1, :type :invoke, :f :read,  :value nil}\n{:process 1, :type :ok,     :f :read,  :value 2}\n{:process 4, :type :invoke, :f :read,  :value nil}\n{:process 4, :type :ok,     :f :read,  :value 2}\n```\n\n### mutex\n\nA Knossos checker for a mutex histories. Applicable to a test with single mutex\nresponding to `:acquire` and `:release` messages. By default\n`competition/analysis` algorithm is used.\n\nExample of history:\n\n```clojure\n{:type :invoke, :f :release, :process 1, :time 341187643, :index 0}\n{:type :fail,   :f :release, :process 1, :time 342667129, :error :not-held, :index 1}\n{:type :invoke, :f :acquire, :process 3, :time 371408519, :index 2}\n{:type :invoke, :f :release, :process 4, :time 584312016, :index 3}\n{:type :fail,   :f :release, :process 4, :time 585400396, :error :not-held, :index 4}\n{:type :invoke, :f :release, :process 0, :time 584353142, :index 5}\n{:type :fail,   :f :release, :process 0, :time 585436373, :error :not-held, :index 6}\n{:type :invoke, :f :release, :process 1, :time 584300961, :index 7}\n{:type :fail,   :f :release, :process 1, :time 585478186, :error :not-held, :index 8}\n{:type :invoke, :f :acquire, :process 2, :time 584335820, :index 9}\n{:type :invoke, :f :release, :process 0, :time 679093895, :index 10}\n```\n\n### comments\n\nA custom checker for a comments histories.\n\nImagine an application which has a sequential stream of comments. Users make\ncomments by inserting new rows into a table. Because each request is\nload-balanced to a different server, two transactions from the same user may\nexecute on different nodes. Now imagine that a user makes a comment `C1` in\ntransaction `T1`. `T1` completes successfully. The user then realizes they made\na mistake, and posts a correction comment `C2`, in transaction `T2`. Meanwhile,\nsomeone attempts to read all comments in a third transaction `T3`, concurrent\nwith both `T1` and `T2`.\n\nExample of history:\n\n```clojure\n{:index 1, :type :invoke :f :read   :value nil}\n{:index 2, :type :invoke :f :write  :value 425}\n{:index 3, :type :ok     :f :write  :value 425}\n{:index 4, :type :invoke :f :write  :value 430}\n{:index 5, :type :ok     :f :write  :value 430}\n{:index 6, :type :ok     :f :read   :value #{2 10 15 20 34 35 38 42 43 47 51 53 59 61 71 72 82 88 89\n                                           113 119 123 129 132 145 146 163 167 176 206 216 224 230\n                                           238 243 244 255 260 292 294 299 312 316 324 325 327 330\n                                           350 356 359 360 361 363 366 367 371 376 403 410 419 422\n                                           430}}\n```\n\n### sequential\n\nA standalone checker for a sequential consistency, it checks that the effective\norder of transactions should be consistent with the order on every client. Any\nexecution is the same as if all `read` and `write` ops were executed in some\nglobal ordering, and the ops of each client process appear in the order\nspecified by its program. If a process order enforces that `x` must be visible\nbefore `y`, we should always read both or neither.\n\nTo verify this, we have a single client perform a sequence of independent\ntransactions, inserting `k1`, `k2`, ..., `kn` into different tables.\nConcurrently, a different client attempts to read each of `kn`, ..., `k2`, `k1`\nin turn. Because all inserts occur from the same process, they must also be\nvisible to any single process in that order. This implies that once a process\nobserves `kn`, any subsequent read must see `kn − 1`, and by induction, all\nsmaller keys.\n\nLike G2 and the bank tests, this test does not verify consistency *in general*.\n\n## License\n\nCopyright © 2021-2024 Sergey Bronnikov\n\nDistributed under the Eclipse Public License either version 1.0 or (at\nyour option) any later version.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fligurio%2Felle-cli","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fligurio%2Felle-cli","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fligurio%2Felle-cli/lists"}