diff --git a/.gitignore b/.gitignore index 3a77a4be3..fd051b72e 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,6 @@ *-error.log /.nyc_output /dist -/artifacts /tmp /--fix /yarn.lock @@ -12,6 +11,7 @@ oclif.manifest.json /fluence.yaml .fluence /src/aqua +/src/services .idea .DS_Store **/node_modules diff --git a/README.md b/README.md index adf1aa045..22cf0562d 100644 --- a/README.md +++ b/README.md @@ -7,55 +7,54 @@ A tool that makes working with Fluence network more convenient * [Prerequisites](#prerequisites) +* [Usage](#usage) * [Currently supported workflow example](#currently-supported-workflow-example) * [Contributing](#contributing) -* [Usage](#usage) * [Commands](#commands) # Prerequisites +- Linux or MacOS (currently have some bugs on windows) - [Node.js >=16.0.0](https://nodejs.org/) -# Currently supported workflow example +# Usage -A lot of what is described next will be improved and automated in the future (e.g. development and building of marine services, key-management etc.) Currently Fluence CLI is a convenience wrapper around Aqua CLI - -1. Use `fluence init` to initialize new project -2. Write your own service using rust or use an example service from [examples repository](https://github.com/fluencelabs/examples) -3. Use [marine](https://doc.fluence.dev/marine-book/) to build `.wasm` service modules -4. Create a directory with the name of your service in `artifacts` directory (e.g. if your service name is `adder` - create `artifacts/adder` directory) -5. Put `.wasm` files into `artifacts/adder` directory -6. Create `artifacts/adder/deploy.json` file. You can find examples of such files in [examples repository](https://github.com/fluencelabs/examples), where they are usually called `deployment_cfg.json`. The name of the directory and the name of the service in `deploy.json` must match. Example of `artifacts/adder/deploy.json`: - -```json -{ - "adder": { - "modules": [ - { - "name": "adder", - "path": "adder.wasm", - "logger_enabled": true - } - ] - } -} + +```sh-session +$ npm install -g @fluencelabs/cli +$ fluence COMMAND +running command... +$ fluence (--version) +@fluencelabs/cli/0.0.0 linux-x64 node-v16.14.0 +$ fluence --help [COMMAND] +USAGE + $ fluence COMMAND +... ``` + -7. Add name of your service to the `fluence.yaml` config. Example of `fluence.yaml`: +# Currently supported workflow example +A lot of what is described next will be improved and automated in the future (key-management etc.) Currently Fluence CLI is a convenience wrapper around Aqua CLI and Marine + +1. Run `fluence init` to initialize new project +2. Run `fluence service add 'https://github.com/fluencelabs/services/blob/master/adder.tar.gz?raw=true'`. Config `fluence.yaml` in the root of the project directory will be updated to look like this: ```yaml -version: 0 +version: 1 services: - - name: adder - count: 2 # Optional. Number of services to deploy -``` - -8. Execute `fluence deploy` to deploy the application you described in `fluence.yaml`. Random peer will be selected for deployment of all of your services (can be overridden using `--on` flag) -User-level secret key from `~/.fluence/secrets.yaml` will be used to deploy each service (can be overridden using `-k` flag) -You can also add project-level secret key to your project `.fluence/secrets.yaml` manually (key-pair management coming soon) - -9. Write some aqua in `src/aqua/main.aqua`. Example `src/aqua/main.aqua`: + adder: + get: https://github.com/fluencelabs/services/blob/master/adder.tar.gz?raw=true + deploy: + - deployId: default +``` +`deployId` can be any unique string in camelCase. It is used in aqua to access ids of deployed services as you will see in a moment. +You can edit `fluence.yaml` manually if you want to deploy multiple times, deploy on specific network, deploy on specific peerId or if you want to override `service.yaml` + +3. Run `fluence service new ./src/services/newService` to generate new service template. You will be asked if you want to add the service to `fluence.yaml` - say yes. +4. Run `fluence service repl newService` to get service into the repl +5. Run `fluence deploy` to deploy the application you described in `fluence.yaml`. Services written in rust will be automatically built before deployment. User-level secret key from `~/.fluence/secrets.yaml` will be used to deploy each service (can be overridden using `-k` flag). You can also add project-level secret key to your project `.fluence/secrets.yaml` manually (key-pair management coming soon) +6. Write some aqua in `src/aqua/main.aqua`. Example `src/aqua/main.aqua`: ```aqua module Main @@ -67,18 +66,18 @@ service AddOne: add_one: u64 -> u64 func add_one(value: u64) -> u64: - serviceIds <- App.serviceIds() + services <- App.services() - on serviceIds.adder!.peerId: - AddOne serviceIds.adder!.serviceId + on services.adder.default!.peerId: + AddOne services.adder.default!.serviceId res <- AddOne.add_one(value) <- res ``` +`"deployed.app.aqua"` file is located at `.fluence/aqua/deployed.app.aqua`. `App.services()` method returns ids of the previously deployed services that you can use in your aqua code (this info is stored at `.fluence/app.yaml`. -10. Execute `fluence run -f 'add_one(1)'`. -Function with this name will be searched inside the `src/aqua/main.aqua` (can be overridden with `--input` flag) and then it will be executed on the peer that was used for deployment when you executed `fluence deploy` (can be overridden with `--on` flag). `"deployed.app.aqua"` file is located at `.fluence/aqua`. `App.serviceIds()` method returns ids of the previously deployed services that you can utilize in your aqua code (this info is stored at `.fluence/app.yaml`). Alternatively, if you are js developer - import generated `registerApp` function from `.fluence/ts/app.ts` or `.fluence/js/app.js` and execute it after `Fluence.run()` in your js application in order to give access to deployed services ids to your aqua code. Then compile `src/aqua/main.aqua` using Aqua CLI. Import and run `add_one(1)` in your js code. - -11. Remove the previously deployed fluence application using `fluence remove` +7. Run `fluence run -f 'add_one(1)'`. (function with this name will be searched inside the `src/aqua/main.aqua` (can be overridden with `--input` flag) and executed). +Alternatively, if you are js developer - import generated `registerApp` function from `.fluence/ts/app.ts` or `.fluence/js/app.js` and execute it after `Fluence.run()` in your js application in order to give access to deployed services ids to your aqua code. Then compile `src/aqua/main.aqua` using Aqua CLI. Import and run `add_one(1)` in your js code. +8. Run `fluence remove` to remove the previously deployed fluence application # Contributing @@ -94,41 +93,32 @@ Don't name arguments or flags with names that contain underscore symbols, becaus pre-commit runs each time before you commit. It includes prettier and generates this README.md file. If you want README.md file to be correctly generated please don't forget to run `npm run build` before committing -# Usage - - -```sh-session -$ npm install -g @fluencelabs/cli -$ fluence COMMAND -running command... -$ fluence (--version) -@fluencelabs/cli/0.0.0 linux-x64 node-v16.14.0 -$ fluence --help [COMMAND] -USAGE - $ fluence COMMAND -... -``` - +Pull request and release process: +1. Run `npm run check` to make sure everything ok with the code +2. Only after that commit your changes to trigger pre-commit hook that updates `README.md`. Read `README.md` to make sure it is correctly updated +3. Push your changes +4. Create pull request and merge your changes to `main` +5. Switch to `main` locally and pull merged changes +6. Run `git tag -a v0.0.0 -m ""` with version number that you want instead of `0.0.0` +5. Run `git push origin v0.0.0` with version number that you want instead of `0.0.0` to trigger release # Commands * [`fluence autocomplete [SHELL]`](#fluence-autocomplete-shell) -* [`fluence dependency [NAME] [-v] [--use ] [--no-input]`](#fluence-dependency-name--v---use-version--recommended---no-input) -* [`fluence deploy [--on ] [--relay ] [--force] [--timeout ] [-k ] [--no-input]`](#fluence-deploy---on-peer_id---relay-multiaddr---force---timeout-milliseconds--k-name---no-input) +* [`fluence dependency [NAME]`](#fluence-dependency-name) +* [`fluence deploy`](#fluence-deploy) * [`fluence help [COMMAND]`](#fluence-help-command) -* [`fluence init [PATH] [--no-input]`](#fluence-init-path---no-input) -* [`fluence plugins`](#fluence-plugins) -* [`fluence plugins:install PLUGIN...`](#fluence-pluginsinstall-plugin) -* [`fluence plugins:inspect PLUGIN...`](#fluence-pluginsinspect-plugin) -* [`fluence plugins:install PLUGIN...`](#fluence-pluginsinstall-plugin-1) -* [`fluence plugins:link PLUGIN`](#fluence-pluginslink-plugin) -* [`fluence plugins:uninstall PLUGIN...`](#fluence-pluginsuninstall-plugin) -* [`fluence plugins:uninstall PLUGIN...`](#fluence-pluginsuninstall-plugin-1) -* [`fluence plugins:uninstall PLUGIN...`](#fluence-pluginsuninstall-plugin-2) -* [`fluence plugins update`](#fluence-plugins-update) -* [`fluence remove [--timeout ] [--no-input]`](#fluence-remove---timeout-milliseconds---no-input) -* [`fluence run [--relay ] [--data ] [--data-path ] [--import ] [--json-service ] [--on ] [-i ] [-f ] [--timeout ] [--no-input]`](#fluence-run---relay-multiaddr---data-json---data-path-path---import-path---json-service-path---on-peer_id--i-path--f-function-call---timeout-milliseconds---no-input) +* [`fluence init [PATH]`](#fluence-init-path) +* [`fluence module add [PATH | URL]`](#fluence-module-add-path--url) +* [`fluence module new [PATH]`](#fluence-module-new-path) +* [`fluence module remove [NAME | PATH | URL]`](#fluence-module-remove-name--path--url) +* [`fluence remove`](#fluence-remove) +* [`fluence run`](#fluence-run) +* [`fluence service add [PATH | URL]`](#fluence-service-add-path--url) +* [`fluence service new [PATH]`](#fluence-service-new-path) +* [`fluence service remove [NAME | PATH | URL]`](#fluence-service-remove-name--path--url) +* [`fluence service repl [NAME | PATH | URL]`](#fluence-service-repl-name--path--url) ## `fluence autocomplete [SHELL]` @@ -159,22 +149,24 @@ EXAMPLES _See code: [@oclif/plugin-autocomplete](https://github.com/oclif/plugin-autocomplete/blob/v1.3.0/src/commands/autocomplete/index.ts)_ -## `fluence dependency [NAME] [-v] [--use ] [--no-input]` +## `fluence dependency [NAME]` Manage dependencies stored inside .fluence directory of the current user ``` USAGE - $ fluence dependency [NAME] [-v] [--use ] [--no-input] + $ fluence dependency [NAME] [-v | --use ] [--no-input] ARGUMENTS - NAME Dependency name. Currently the only dependency is aqua + NAME Dependency name. One of: aqua, marine, mrepl, cargo-generate. If you omit NAME argument and include --use + recommended - all dependencies will be reset to recommended versions FLAGS -v, --version Show current version of the dependency --no-input Don't interactively ask for any input from the user - --use= Set version of the dependency that you want to use. Use recommended keyword if you want - to use recommended version + --use= Set dependency version. Use recommended keyword to set recommended version for the + dependency. If you omit NAME argument and include --use recommended - all dependencies + will be reset to recommended versions DESCRIPTION Manage dependencies stored inside .fluence directory of the current user @@ -185,24 +177,23 @@ EXAMPLES _See code: [dist/commands/dependency.ts](https://github.com/fluencelabs/fluence-cli/blob/v0.0.0/dist/commands/dependency.ts)_ -## `fluence deploy [--on ] [--relay ] [--force] [--timeout ] [-k ] [--no-input]` +## `fluence deploy` -Deploy service to the remote peer +Deploy application, described in fluence.yaml ``` USAGE - $ fluence deploy [--on ] [--relay ] [--force] [--timeout ] [-k ] [--no-input] + $ fluence deploy [--relay ] [--force] [--timeout ] [-k ] [--no-input] FLAGS -k, --key-pair-name= Key pair name --force Force removing of previously deployed app --no-input Don't interactively ask for any input from the user - --on= PeerId of the peer where you want to deploy - --relay= Relay node MultiAddress + --relay= Relay node multiaddr --timeout= Timeout used for command execution DESCRIPTION - Deploy service to the remote peer + Deploy application, described in fluence.yaml EXAMPLES $ fluence deploy @@ -230,7 +221,7 @@ DESCRIPTION _See code: [@oclif/plugin-help](https://github.com/oclif/plugin-help/blob/v5.1.12/src/commands/help.ts)_ -## `fluence init [PATH] [--no-input]` +## `fluence init [PATH]` Initialize fluence project @@ -253,286 +244,209 @@ EXAMPLES _See code: [dist/commands/init.ts](https://github.com/fluencelabs/fluence-cli/blob/v0.0.0/dist/commands/init.ts)_ -## `fluence plugins` - -List installed plugins. - -``` -USAGE - $ fluence plugins [--core] - -FLAGS - --core Show core plugins. - -DESCRIPTION - List installed plugins. - -EXAMPLES - $ fluence plugins -``` - -_See code: [@oclif/plugin-plugins](https://github.com/oclif/plugin-plugins/blob/v2.1.0/src/commands/plugins/index.ts)_ - -## `fluence plugins:install PLUGIN...` +## `fluence module add [PATH | URL]` -Installs a plugin into the CLI. +Add module to service.yaml ``` USAGE - $ fluence plugins:install PLUGIN... + $ fluence module add [PATH | URL] [--no-input] [--name ] [--service ] ARGUMENTS - PLUGIN Plugin to install. + PATH | URL Path to a module or url to .tar.gz archive FLAGS - -f, --force Run yarn install with force flag. - -h, --help Show CLI help. - -v, --verbose + --name= Unique module name + --no-input Don't interactively ask for any input from the user + --service= Service name from fluence.yaml or path to the service directory DESCRIPTION - Installs a plugin into the CLI. - - Can be installed from npm or a git url. - - Installation of a user-installed plugin will override a core plugin. - - e.g. If you have a core plugin that has a 'hello' command, installing a user-installed plugin with a 'hello' command - will override the core plugin implementation. This is useful if a user needs to update core plugin functionality in - the CLI without the need to patch and update the whole CLI. - -ALIASES - $ fluence plugins add + Add module to service.yaml EXAMPLES - $ fluence plugins:install myplugin - - $ fluence plugins:install https://github.com/someuser/someplugin - - $ fluence plugins:install someuser/someplugin + $ fluence module add ``` -## `fluence plugins:inspect PLUGIN...` +## `fluence module new [PATH]` -Displays installation properties of a plugin. +Create new marine module template ``` USAGE - $ fluence plugins:inspect PLUGIN... + $ fluence module new [PATH] [--no-input] ARGUMENTS - PLUGIN [default: .] Plugin to inspect. + PATH Path to a module FLAGS - -h, --help Show CLI help. - -v, --verbose + --no-input Don't interactively ask for any input from the user DESCRIPTION - Displays installation properties of a plugin. + Create new marine module template EXAMPLES - $ fluence plugins:inspect myplugin + $ fluence module new ``` -## `fluence plugins:install PLUGIN...` +## `fluence module remove [NAME | PATH | URL]` -Installs a plugin into the CLI. +Remove module from service.yaml ``` USAGE - $ fluence plugins:install PLUGIN... + $ fluence module remove [NAME | PATH | URL] [--no-input] [--service ] ARGUMENTS - PLUGIN Plugin to install. + NAME | PATH | URL Module name from service.yaml, path to a module or url to .tar.gz archive FLAGS - -f, --force Run yarn install with force flag. - -h, --help Show CLI help. - -v, --verbose + --no-input Don't interactively ask for any input from the user + --service= Service name from fluence.yaml or path to the service directory DESCRIPTION - Installs a plugin into the CLI. - - Can be installed from npm or a git url. - - Installation of a user-installed plugin will override a core plugin. - - e.g. If you have a core plugin that has a 'hello' command, installing a user-installed plugin with a 'hello' command - will override the core plugin implementation. This is useful if a user needs to update core plugin functionality in - the CLI without the need to patch and update the whole CLI. - -ALIASES - $ fluence plugins add + Remove module from service.yaml EXAMPLES - $ fluence plugins:install myplugin - - $ fluence plugins:install https://github.com/someuser/someplugin - - $ fluence plugins:install someuser/someplugin + $ fluence module remove ``` -## `fluence plugins:link PLUGIN` +## `fluence remove` -Links a plugin into the CLI for development. +Remove previously deployed config ``` USAGE - $ fluence plugins:link PLUGIN - -ARGUMENTS - PATH [default: .] path to plugin + $ fluence remove [--relay ] [--timeout ] [--no-input] FLAGS - -h, --help Show CLI help. - -v, --verbose + --no-input Don't interactively ask for any input from the user + --relay= Relay node multiaddr + --timeout= Timeout used for command execution DESCRIPTION - Links a plugin into the CLI for development. - - Installation of a linked plugin will override a user-installed or core plugin. - - e.g. If you have a user-installed or core plugin that has a 'hello' command, installing a linked plugin with a 'hello' - command will override the user-installed or core plugin implementation. This is useful for development work. + Remove previously deployed config EXAMPLES - $ fluence plugins:link myplugin + $ fluence remove ``` -## `fluence plugins:uninstall PLUGIN...` +_See code: [dist/commands/remove.ts](https://github.com/fluencelabs/fluence-cli/blob/v0.0.0/dist/commands/remove.ts)_ + +## `fluence run` -Removes a plugin from the CLI. +Run aqua script ``` USAGE - $ fluence plugins:uninstall PLUGIN... - -ARGUMENTS - PLUGIN plugin to uninstall + $ fluence run [--relay ] [--data ] [--data-path ] [--import ] + [--json-service ] [--on ] [-i ] [-f ] [--timeout ] [--no-input] FLAGS - -h, --help Show CLI help. - -v, --verbose + -f, --func= Function call + -i, --input= Path to an aqua file or to a directory that contains aqua files + --data= JSON in { [argumentName]: argumentValue } format. You can call a function using these + argument names + --data-path= Path to a JSON file in { [argumentName]: argumentValue } format. You can call a function + using these argument names + --import=... Path to a directory to import from. May be used several times + --json-service= Path to a file that contains a JSON formatted service + --no-input Don't interactively ask for any input from the user + --on= PeerId of a peer where you want to run the function + --relay= Relay node multiaddr + --timeout= Timeout used for command execution DESCRIPTION - Removes a plugin from the CLI. + Run aqua script -ALIASES - $ fluence plugins unlink - $ fluence plugins remove +EXAMPLES + $ fluence run ``` -## `fluence plugins:uninstall PLUGIN...` +_See code: [dist/commands/run.ts](https://github.com/fluencelabs/fluence-cli/blob/v0.0.0/dist/commands/run.ts)_ + +## `fluence service add [PATH | URL]` -Removes a plugin from the CLI. +Add service to fluence.yaml ``` USAGE - $ fluence plugins:uninstall PLUGIN... + $ fluence service add [PATH | URL] [--no-input] [--name ] ARGUMENTS - PLUGIN plugin to uninstall + PATH | URL Path to a service or url to .tar.gz archive FLAGS - -h, --help Show CLI help. - -v, --verbose + --name= Unique service name + --no-input Don't interactively ask for any input from the user DESCRIPTION - Removes a plugin from the CLI. + Add service to fluence.yaml -ALIASES - $ fluence plugins unlink - $ fluence plugins remove +EXAMPLES + $ fluence service add ``` -## `fluence plugins:uninstall PLUGIN...` +## `fluence service new [PATH]` -Removes a plugin from the CLI. +Create new marine service template ``` USAGE - $ fluence plugins:uninstall PLUGIN... + $ fluence service new [PATH] [--no-input] [--name ] ARGUMENTS - PLUGIN plugin to uninstall + PATH Path to a service FLAGS - -h, --help Show CLI help. - -v, --verbose + --name= Unique service name + --no-input Don't interactively ask for any input from the user DESCRIPTION - Removes a plugin from the CLI. + Create new marine service template -ALIASES - $ fluence plugins unlink - $ fluence plugins remove +EXAMPLES + $ fluence service new ``` -## `fluence plugins update` +## `fluence service remove [NAME | PATH | URL]` -Update installed plugins. +Remove service from fluence.yaml ``` USAGE - $ fluence plugins update [-h] [-v] - -FLAGS - -h, --help Show CLI help. - -v, --verbose - -DESCRIPTION - Update installed plugins. -``` - -## `fluence remove [--timeout ] [--no-input]` + $ fluence service remove [NAME | PATH | URL] [--no-input] -Remove previously deployed config - -``` -USAGE - $ fluence remove [--timeout ] [--no-input] +ARGUMENTS + NAME | PATH | URL Service name from fluence.yaml, path to a service or url to .tar.gz archive FLAGS - --no-input Don't interactively ask for any input from the user - --timeout= Timeout used for command execution + --no-input Don't interactively ask for any input from the user DESCRIPTION - Remove previously deployed config + Remove service from fluence.yaml EXAMPLES - $ fluence remove + $ fluence service remove ``` -_See code: [dist/commands/remove.ts](https://github.com/fluencelabs/fluence-cli/blob/v0.0.0/dist/commands/remove.ts)_ - -## `fluence run [--relay ] [--data ] [--data-path ] [--import ] [--json-service ] [--on ] [-i ] [-f ] [--timeout ] [--no-input]` +## `fluence service repl [NAME | PATH | URL]` -Run aqua script +Open service inside repl ``` USAGE - $ fluence run [--relay ] [--data ] [--data-path ] [--import ] [--json-service ] - [--on ] [-i ] [-f ] [--timeout ] [--no-input] + $ fluence service repl [NAME | PATH | URL] [--no-input] + +ARGUMENTS + NAME | PATH | URL Service name from fluence.yaml, path to a service or url to .tar.gz archive FLAGS - -f, --func= Function call - -i, --input= Path to an aqua file or to a directory that contains aqua files - --data= JSON in { [argumentName]: argumentValue } format. You can call a function using these - argument names - --data-path= Path to a JSON file in { [argumentName]: argumentValue } format. You can call a function - using these argument names - --import=... Path to a directory to import from. May be used several times - --json-service= Path to a file that contains a JSON formatted service - --no-input Don't interactively ask for any input from the user - --on= PeerId of a peer where you want to run the function - --relay= Relay node MultiAddress - --timeout= Timeout used for command execution + --no-input Don't interactively ask for any input from the user DESCRIPTION - Run aqua script + Open service inside repl EXAMPLES - $ fluence run + $ fluence service repl ``` - -_See code: [dist/commands/run.ts](https://github.com/fluencelabs/fluence-cli/blob/v0.0.0/dist/commands/run.ts)_ diff --git a/package-lock.json b/package-lock.json index 09c692249..0c3792d4c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,16 +11,20 @@ "dependencies": { "@fluencelabs/fluence": "^0.23.0", "@fluencelabs/fluence-network-environment": "^1.0.13", + "@iarna/toml": "^2.2.5", "@oclif/color": "^1.0.1", "@oclif/core": "^1.9.0", "@oclif/errors": "^1.3.5", "@oclif/plugin-autocomplete": "^1.3.0", "@oclif/plugin-help": "^5", "@oclif/plugin-not-found": "^2.3.1", - "@oclif/plugin-plugins": "^2.1.0", "ajv": "^8.11.0", "camelcase": "^5.2.0", + "decompress": "^4.2.1", + "filenamify": "^4", "inquirer": "^8.2.4", + "multiaddr": "^10.0.1", + "node-fetch": "^2.6.7", "platform": "^1.3.6", "replace-homedir": "^2.0.0", "yaml": "^2.1.1", @@ -34,9 +38,12 @@ "@tsconfig/node16-strictest": "^1.0.1", "@types/camelcase": "^5.2.0", "@types/chai": "^4", + "@types/decompress": "^4.2.4", + "@types/iarna__toml": "^2.0.2", "@types/inquirer": "^8.2.1", "@types/mocha": "^9.1.1", "@types/node": "^17.0.41", + "@types/node-fetch": "^2.6.2", "@types/platform": "^1.3.4", "chai": "^4", "eslint": "^7.32.0", @@ -867,6 +874,11 @@ "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", "dev": true }, + "node_modules/@iarna/toml": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/@iarna/toml/-/toml-2.2.5.tgz", + "integrity": "sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg==" + }, "node_modules/@isaacs/string-locale-compare": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@isaacs/string-locale-compare/-/string-locale-compare-1.1.0.tgz", @@ -1660,27 +1672,6 @@ "node": ">=12.0.0" } }, - "node_modules/@oclif/plugin-plugins": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@oclif/plugin-plugins/-/plugin-plugins-2.1.0.tgz", - "integrity": "sha512-Bgt+QpTlX7+Q0HkVgtbUGYQlo/hyzNBAaXH5l16ou9Ji5wfi5T+niV5AzQ14R7JF8ZDOTbUOU/NRBJ2bzLCaZQ==", - "dependencies": { - "@oclif/color": "^1.0.1", - "@oclif/core": "^1.2.0", - "chalk": "^4.1.2", - "debug": "^4.1.0", - "fs-extra": "^9.0", - "http-call": "^5.2.2", - "load-json-file": "^5.3.0", - "npm-run-path": "^4.0.1", - "semver": "^7.3.2", - "tslib": "^2.0.0", - "yarn": "^1.22.17" - }, - "engines": { - "node": ">=12.0.0" - } - }, "node_modules/@oclif/plugin-warn-if-update-available": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@oclif/plugin-warn-if-update-available/-/plugin-warn-if-update-available-2.0.4.tgz", @@ -2145,6 +2136,15 @@ "integrity": "sha512-/zPMqDkzSZ8t3VtxOa4KPq7uzzW978M9Tvh+j7GHKuo6k6GTLxPJ4J5gE5cjfJ26pnXst0N5Hax8Sr0T2Mi9zQ==", "dev": true }, + "node_modules/@types/decompress": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@types/decompress/-/decompress-4.2.4.tgz", + "integrity": "sha512-/C8kTMRTNiNuWGl5nEyKbPiMv6HA+0RbEXzFhFBEzASM6+oa4tJro9b8nj7eRlOFfuLdzUU+DS/GPDlvvzMOhA==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/expect": { "version": "1.20.4", "resolved": "https://registry.npmjs.org/@types/expect/-/expect-1.20.4.tgz", @@ -2170,6 +2170,15 @@ "@types/node": "*" } }, + "node_modules/@types/iarna__toml": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@types/iarna__toml/-/iarna__toml-2.0.2.tgz", + "integrity": "sha512-Q3obxKhBLVVbEQ8zsAmsQVobAAZhi8dFFFjF0q5xKXiaHvH8IkSxcbM27e46M9feUMieR03SPpmp5CtaNzpdBg==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/inquirer": { "version": "8.2.1", "resolved": "https://registry.npmjs.org/@types/inquirer/-/inquirer-8.2.1.tgz", @@ -2240,6 +2249,16 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.41.tgz", "integrity": "sha512-xA6drNNeqb5YyV5fO3OAEsnXLfO7uF0whiOfPTz5AeDo8KeZFmODKnvwPymMNO8qE/an8pVY/O50tig2SQCrGw==" }, + "node_modules/@types/node-fetch": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.2.tgz", + "integrity": "sha512-DHqhlq5jeESLy19TYhLakJ07kNumXWjcDdxXsLUMJZ6ue8VZJj4kLPQVE/2mdHh3xZziNF1xppu5lwmS53HR+A==", + "dev": true, + "dependencies": { + "@types/node": "*", + "form-data": "^3.0.0" + } + }, "node_modules/@types/normalize-package-data": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz", @@ -3325,11 +3344,38 @@ "ieee754": "^1.2.1" } }, + "node_modules/buffer-alloc": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz", + "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==", + "dependencies": { + "buffer-alloc-unsafe": "^1.1.0", + "buffer-fill": "^1.0.0" + } + }, + "node_modules/buffer-alloc-unsafe": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz", + "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==" + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "engines": { + "node": "*" + } + }, "node_modules/buffer-es6": { "version": "4.9.3", "resolved": "https://registry.npmjs.org/buffer-es6/-/buffer-es6-4.9.3.tgz", "integrity": "sha512-Ibt+oXxhmeYJSsCkODPqNpPmyegefiD8rfutH1NYGhMZQhSp95Rz7haemgnJ6dxa6LT+JLLbtgOMORRluwKktw==" }, + "node_modules/buffer-fill": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz", + "integrity": "sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ==" + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -4044,6 +4090,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", + "dev": true, "engines": { "node": ">= 0.6" } @@ -4208,6 +4255,219 @@ "integrity": "sha512-V0pfhfr8suzyPGOx3nmq4aHqabehUZn6Ch9kyFpV79TGDTWFmHqUqXdabR7QHqxzrYolF4+tVmJhUG4OURg5dQ==", "peer": true }, + "node_modules/decompress": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/decompress/-/decompress-4.2.1.tgz", + "integrity": "sha512-e48kc2IjU+2Zw8cTb6VZcJQ3lgVbS4uuB1TfCHbiZIP/haNXm+SVyhu+87jts5/3ROpd82GSVCoNs/z8l4ZOaQ==", + "dependencies": { + "decompress-tar": "^4.0.0", + "decompress-tarbz2": "^4.0.0", + "decompress-targz": "^4.0.0", + "decompress-unzip": "^4.0.1", + "graceful-fs": "^4.1.10", + "make-dir": "^1.0.0", + "pify": "^2.3.0", + "strip-dirs": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/decompress-tar": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/decompress-tar/-/decompress-tar-4.1.1.tgz", + "integrity": "sha512-JdJMaCrGpB5fESVyxwpCx4Jdj2AagLmv3y58Qy4GE6HMVjWz1FeVQk1Ct4Kye7PftcdOo/7U7UKzYBJgqnGeUQ==", + "dependencies": { + "file-type": "^5.2.0", + "is-stream": "^1.1.0", + "tar-stream": "^1.5.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/decompress-tar/node_modules/bl": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.3.tgz", + "integrity": "sha512-pvcNpa0UU69UT341rO6AYy4FVAIkUHuZXRIWbq+zHnsVcRzDDjIAhGuuYoi0d//cwIwtt4pkpKycWEfjdV+vww==", + "dependencies": { + "readable-stream": "^2.3.5", + "safe-buffer": "^5.1.1" + } + }, + "node_modules/decompress-tar/node_modules/is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decompress-tar/node_modules/readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/decompress-tar/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/decompress-tar/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/decompress-tar/node_modules/tar-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.6.2.tgz", + "integrity": "sha512-rzS0heiNf8Xn7/mpdSVVSMAWAoy9bfb1WOTYC78Z0UQKeKa/CWS8FOq0lKGNa8DWKAn9gxjCvMLYc5PGXYlK2A==", + "dependencies": { + "bl": "^1.0.0", + "buffer-alloc": "^1.2.0", + "end-of-stream": "^1.0.0", + "fs-constants": "^1.0.0", + "readable-stream": "^2.3.0", + "to-buffer": "^1.1.1", + "xtend": "^4.0.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/decompress-tarbz2": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/decompress-tarbz2/-/decompress-tarbz2-4.1.1.tgz", + "integrity": "sha512-s88xLzf1r81ICXLAVQVzaN6ZmX4A6U4z2nMbOwobxkLoIIfjVMBg7TeguTUXkKeXni795B6y5rnvDw7rxhAq9A==", + "dependencies": { + "decompress-tar": "^4.1.0", + "file-type": "^6.1.0", + "is-stream": "^1.1.0", + "seek-bzip": "^1.0.5", + "unbzip2-stream": "^1.0.9" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/decompress-tarbz2/node_modules/file-type": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-6.2.0.tgz", + "integrity": "sha512-YPcTBDV+2Tm0VqjybVd32MHdlEGAtuxS3VAYsumFokDSMG+ROT5wawGlnHDoz7bfMcMDt9hxuXvXwoKUx2fkOg==", + "engines": { + "node": ">=4" + } + }, + "node_modules/decompress-tarbz2/node_modules/is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decompress-targz": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/decompress-targz/-/decompress-targz-4.1.1.tgz", + "integrity": "sha512-4z81Znfr6chWnRDNfFNqLwPvm4db3WuZkqV+UgXQzSngG3CEKdBkw5jrv3axjjL96glyiiKjsxJG3X6WBZwX3w==", + "dependencies": { + "decompress-tar": "^4.1.1", + "file-type": "^5.2.0", + "is-stream": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/decompress-targz/node_modules/is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decompress-unzip": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/decompress-unzip/-/decompress-unzip-4.0.1.tgz", + "integrity": "sha512-1fqeluvxgnn86MOh66u8FjbtJpAFv5wgCT9Iw8rcBqQcCo5tO8eiJw7NNTrvt9n4CRBVq7CstiS922oPgyGLrw==", + "dependencies": { + "file-type": "^3.8.0", + "get-stream": "^2.2.0", + "pify": "^2.3.0", + "yauzl": "^2.4.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/decompress-unzip/node_modules/file-type": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-3.9.0.tgz", + "integrity": "sha512-RLoqTXE8/vPmMuTI88DAzhMYC99I8BWv7zYP4A1puo5HIjEJ5EX48ighy4ZyKMG9EDXxBgW6e++cn7d1xuFghA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decompress-unzip/node_modules/get-stream": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-2.3.1.tgz", + "integrity": "sha512-AUGhbbemXxrZJRD5cDvKtQxLuYaIbNtDTK8YqupCI393Q2KSTreEsLUN3ZxAWFGiKTzL6nKuzfcIvieflUX9qA==", + "dependencies": { + "object-assign": "^4.0.1", + "pinkie-promise": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decompress-unzip/node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decompress/node_modules/make-dir": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", + "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", + "dependencies": { + "pify": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/decompress/node_modules/make-dir/node_modules/pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", + "engines": { + "node": ">=4" + } + }, + "node_modules/decompress/node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/dedent": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", @@ -5545,6 +5805,14 @@ "bser": "2.1.1" } }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dependencies": { + "pend": "~1.2.0" + } + }, "node_modules/figures": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", @@ -5579,6 +5847,14 @@ "node": "^10.12.0 || >=12.0.0" } }, + "node_modules/file-type": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-5.2.0.tgz", + "integrity": "sha512-Iq1nJ6D2+yIO4c8HHg4fyVb8mAJieo1Oloy1mLLaB2PvezNedhBVm+QU7g0qM42aiMbRXTxKKwGD17rjKNJYVQ==", + "engines": { + "node": ">=4" + } + }, "node_modules/file-uri-to-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", @@ -5611,6 +5887,30 @@ "node": ">=10" } }, + "node_modules/filename-reserved-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-2.0.0.tgz", + "integrity": "sha512-lc1bnsSr4L4Bdif8Xb/qrtokGbq5zlsms/CYH8PP+WtCkGNF65DPiQY8vG3SakEdRn8Dlnm+gW/qWKKjS5sZzQ==", + "engines": { + "node": ">=4" + } + }, + "node_modules/filenamify": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/filenamify/-/filenamify-4.3.0.tgz", + "integrity": "sha512-hcFKyUG57yWGAzu1CMt/dPzYZuv+jAJUT85bL8mrXvNe6hWj6yEHEc4EdcgiA6Z3oi1/9wXJdZPXF2dZNgwgOg==", + "dependencies": { + "filename-reserved-regex": "^2.0.0", + "strip-outer": "^1.0.1", + "trim-repeated": "^1.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -5735,7 +6035,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", - "peer": true, "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", @@ -6280,6 +6579,7 @@ "version": "5.3.0", "resolved": "https://registry.npmjs.org/http-call/-/http-call-5.3.0.tgz", "integrity": "sha512-ahwimsC23ICE4kPl9xTBjKB4inbRaeLyZeRunC/1Jy/Z6X8tv22MEAjK+KBOMSVLaqXPTTmd8638waVIKLGx2w==", + "dev": true, "dependencies": { "content-type": "^1.0.4", "debug": "^4.1.1", @@ -6621,6 +6921,16 @@ "resolved": "https://registry.npmjs.org/any-signal/-/any-signal-3.0.1.tgz", "integrity": "sha512-xgZgJtKEa9YmDqXodIgl7Fl1C8yNXr8w6gXjqK3LW4GcEiYT+6AQfJSE/8SPsEpLLmcvbv8YU+qet94UewHxqg==" }, + "node_modules/ipfs-utils/node_modules/node-fetch": { + "name": "@achingbrain/node-fetch", + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/@achingbrain/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-iTASGs+HTFK5E4ZqcMsHmeJ4zodyq8L38lZV33jwqcBJYoUt3HjN4+ot+O9/0b+ke8ddE7UgOtVuZN/OkV19/g==", + "license": "MIT", + "engines": { + "node": "4.x || >=6.0.0" + } + }, "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", @@ -6827,6 +7137,11 @@ "resolved": "https://registry.npmjs.org/is-loopback-addr/-/is-loopback-addr-1.0.1.tgz", "integrity": "sha512-DhWU/kqY7X2F6KrrVTu7mHlbd2Pbo4D1YkAzasBMjQs6lJAoefxaA6m6CpSX0K6pjt9D0b9PNFI5zduy/vzOYw==" }, + "node_modules/is-natural-number": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-natural-number/-/is-natural-number-4.0.1.tgz", + "integrity": "sha512-Y4LTamMe0DDQIIAlaer9eKebAlDSV6huy+TWhJVPlzZh2o4tRP5SQWFlLn5N0To4mDD22/qdOq+veo1cSISLgQ==" + }, "node_modules/is-negative-zero": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", @@ -6916,6 +7231,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.2.0.tgz", "integrity": "sha512-RUbUeKwvm3XG2VYamhJL1xFktgjvPzL0Hq8C+6yrWIswDy3BIXGqCxhxkc30N9jqK311gVU137K8Ei55/zVJRg==", + "dev": true, "engines": { "node": ">=0.10.0" } @@ -7033,8 +7349,7 @@ "node_modules/isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" }, "node_modules/isbinaryfile": { "version": "4.0.10", @@ -8104,7 +8419,8 @@ "node_modules/json-parse-better-errors": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", - "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==" + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", + "dev": true }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", @@ -8699,37 +9015,6 @@ "node": ">=8" } }, - "node_modules/load-json-file": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-5.3.0.tgz", - "integrity": "sha512-cJGP40Jc/VXUsp8/OrnyKyTZ1y6v/dphm3bioS+RrKXjK2BB6wHUd6JptZEFDGgGahMT+InnZO5i1Ei9mpC8Bw==", - "dependencies": { - "graceful-fs": "^4.1.15", - "parse-json": "^4.0.0", - "pify": "^4.0.1", - "strip-bom": "^3.0.0", - "type-fest": "^0.3.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/load-json-file/node_modules/strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", - "engines": { - "node": ">=4" - } - }, - "node_modules/load-json-file/node_modules/type-fest": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.3.1.tgz", - "integrity": "sha512-cUGJnCdr4STbePCgqNFbpVNCepa+kAVohJs1sLhxzdH+gnEoOd8VhbYa7pD3zZYGiURWM2xzEII3fQcRizDkYQ==", - "engines": { - "node": ">=6" - } - }, "node_modules/load-yaml-file": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/load-yaml-file/-/load-yaml-file-0.2.0.tgz", @@ -9780,16 +10065,44 @@ "integrity": "sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA==" }, "node_modules/node-fetch": { - "name": "@achingbrain/node-fetch", "version": "2.6.7", - "resolved": "https://registry.npmjs.org/@achingbrain/node-fetch/-/node-fetch-2.6.7.tgz", - "integrity": "sha512-iTASGs+HTFK5E4ZqcMsHmeJ4zodyq8L38lZV33jwqcBJYoUt3HjN4+ot+O9/0b+ke8ddE7UgOtVuZN/OkV19/g==", - "license": "MIT", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, "engines": { "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } } }, - "node_modules/node-forge": { + "node_modules/node-fetch/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "node_modules/node-fetch/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/node-fetch/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/node-forge": { "version": "0.10.0", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.10.0.tgz", "integrity": "sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA==", @@ -10271,7 +10584,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -10823,6 +11135,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", "integrity": "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==", + "dev": true, "dependencies": { "error-ex": "^1.3.1", "json-parse-better-errors": "^1.0.1" @@ -11000,6 +11313,11 @@ "node": ">=5.10.0" } }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==" + }, "node_modules/performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", @@ -11037,10 +11355,30 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true, "engines": { "node": ">=6" } }, + "node_modules/pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw==", + "dependencies": { + "pinkie": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/pirates": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.5.tgz", @@ -11306,8 +11644,7 @@ "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, "node_modules/progress": { "version": "2.0.3", @@ -12493,6 +12830,23 @@ "node": ">=10.0.0" } }, + "node_modules/seek-bzip": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/seek-bzip/-/seek-bzip-1.0.6.tgz", + "integrity": "sha512-e1QtP3YL5tWww8uKaOCQ18UxIT2laNBXHjV/S2WYCiK4udiv8lkG89KRIoCjUagnAmCBurjF4zEVX2ByBbnCjQ==", + "dependencies": { + "commander": "^2.8.1" + }, + "bin": { + "seek-bunzip": "bin/seek-bunzip", + "seek-table": "bin/seek-bzip-table" + } + }, + "node_modules/seek-bzip/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" + }, "node_modules/semver": { "version": "7.3.7", "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", @@ -12987,6 +13341,14 @@ "node": ">=0.10.0" } }, + "node_modules/strip-dirs": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/strip-dirs/-/strip-dirs-2.1.0.tgz", + "integrity": "sha512-JOCxOeKLm2CAS73y/U4ZeZPTkE+gNVCzKt7Eox84Iej1LT/2pTWYpZKJuxwQpvX1LiZb1xokNR7RLfuBAa7T3g==", + "dependencies": { + "is-natural-number": "^4.0.1" + } + }, "node_modules/strip-eof": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", @@ -13015,6 +13377,25 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strip-outer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/strip-outer/-/strip-outer-1.0.1.tgz", + "integrity": "sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg==", + "dependencies": { + "escape-string-regexp": "^1.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-outer/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/supports-color": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", @@ -13323,6 +13704,11 @@ "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", "peer": true }, + "node_modules/to-buffer": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.1.1.tgz", + "integrity": "sha512-lx9B5iv7msuFYE3dytT+KE5tap+rNYw+K4jVkb9R/asAb+pbBSM17jtunHplhBe6RRJdZx3Pn2Jph24O32mOVg==" + }, "node_modules/to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", @@ -13392,6 +13778,25 @@ "integrity": "sha512-whw60l7r+8ZU8Tu/Uc2yxtc4ZTZbR/PF3u1IPNKGQ6p8EICLb3Z2lAgoqw9bqYd8IkgnsaOcLzYHFckjqNsf0g==", "dev": true }, + "node_modules/trim-repeated": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/trim-repeated/-/trim-repeated-1.0.0.tgz", + "integrity": "sha512-pkonvlKk8/ZuR0D5tLW8ljt5I8kmxp2XKymhepUeOdCEfKpZaktSArkLHZt76OB1ZvO9bssUsDty4SWhLvZpLg==", + "dependencies": { + "escape-string-regexp": "^1.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/trim-repeated/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/truncate-utf8-bytes": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", @@ -13669,6 +14074,38 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/unbzip2-stream": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz", + "integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==", + "dependencies": { + "buffer": "^5.2.1", + "through": "^2.3.8" + } + }, + "node_modules/unbzip2-stream/node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, "node_modules/unique-filename": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", @@ -14182,6 +14619,14 @@ "resolved": "https://registry.npmjs.org/xsalsa20/-/xsalsa20-1.2.0.tgz", "integrity": "sha512-FIr/DEeoHfj7ftfylnoFt3rAIRoWXpx2AoDfrT2qD2wtp7Dp+COajvs/Icb7uHqRW9m60f5iXZwdsJJO3kvb7w==" }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "engines": { + "node": ">=0.4" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -14273,17 +14718,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/yarn": { - "version": "1.22.19", - "resolved": "https://registry.npmjs.org/yarn/-/yarn-1.22.19.tgz", - "integrity": "sha512-/0V5q0WbslqnwP91tirOvldvYISzaqhClxzyUKXYxs07yUILIs5jx/k6CFe8bvKSkds5w+eiOqta39Wk3WxdcQ==", - "hasInstallScript": true, - "bin": { - "yarn": "bin/yarn.js", - "yarnpkg": "bin/yarn.js" - }, - "engines": { - "node": ">=4.0.0" + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" } }, "node_modules/yeoman-environment": { @@ -15360,6 +15801,11 @@ "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", "dev": true }, + "@iarna/toml": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/@iarna/toml/-/toml-2.2.5.tgz", + "integrity": "sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg==" + }, "@isaacs/string-locale-compare": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@isaacs/string-locale-compare/-/string-locale-compare-1.1.0.tgz", @@ -16005,24 +16451,6 @@ "lodash": "^4.17.21" } }, - "@oclif/plugin-plugins": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@oclif/plugin-plugins/-/plugin-plugins-2.1.0.tgz", - "integrity": "sha512-Bgt+QpTlX7+Q0HkVgtbUGYQlo/hyzNBAaXH5l16ou9Ji5wfi5T+niV5AzQ14R7JF8ZDOTbUOU/NRBJ2bzLCaZQ==", - "requires": { - "@oclif/color": "^1.0.1", - "@oclif/core": "^1.2.0", - "chalk": "^4.1.2", - "debug": "^4.1.0", - "fs-extra": "^9.0", - "http-call": "^5.2.2", - "load-json-file": "^5.3.0", - "npm-run-path": "^4.0.1", - "semver": "^7.3.2", - "tslib": "^2.0.0", - "yarn": "^1.22.17" - } - }, "@oclif/plugin-warn-if-update-available": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@oclif/plugin-warn-if-update-available/-/plugin-warn-if-update-available-2.0.4.tgz", @@ -16466,6 +16894,15 @@ "integrity": "sha512-/zPMqDkzSZ8t3VtxOa4KPq7uzzW978M9Tvh+j7GHKuo6k6GTLxPJ4J5gE5cjfJ26pnXst0N5Hax8Sr0T2Mi9zQ==", "dev": true }, + "@types/decompress": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@types/decompress/-/decompress-4.2.4.tgz", + "integrity": "sha512-/C8kTMRTNiNuWGl5nEyKbPiMv6HA+0RbEXzFhFBEzASM6+oa4tJro9b8nj7eRlOFfuLdzUU+DS/GPDlvvzMOhA==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/expect": { "version": "1.20.4", "resolved": "https://registry.npmjs.org/@types/expect/-/expect-1.20.4.tgz", @@ -16491,6 +16928,15 @@ "@types/node": "*" } }, + "@types/iarna__toml": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@types/iarna__toml/-/iarna__toml-2.0.2.tgz", + "integrity": "sha512-Q3obxKhBLVVbEQ8zsAmsQVobAAZhi8dFFFjF0q5xKXiaHvH8IkSxcbM27e46M9feUMieR03SPpmp5CtaNzpdBg==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/inquirer": { "version": "8.2.1", "resolved": "https://registry.npmjs.org/@types/inquirer/-/inquirer-8.2.1.tgz", @@ -16561,6 +17007,16 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.41.tgz", "integrity": "sha512-xA6drNNeqb5YyV5fO3OAEsnXLfO7uF0whiOfPTz5AeDo8KeZFmODKnvwPymMNO8qE/an8pVY/O50tig2SQCrGw==" }, + "@types/node-fetch": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.2.tgz", + "integrity": "sha512-DHqhlq5jeESLy19TYhLakJ07kNumXWjcDdxXsLUMJZ6ue8VZJj4kLPQVE/2mdHh3xZziNF1xppu5lwmS53HR+A==", + "dev": true, + "requires": { + "@types/node": "*", + "form-data": "^3.0.0" + } + }, "@types/normalize-package-data": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz", @@ -17379,11 +17835,35 @@ "ieee754": "^1.2.1" } }, + "buffer-alloc": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz", + "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==", + "requires": { + "buffer-alloc-unsafe": "^1.1.0", + "buffer-fill": "^1.0.0" + } + }, + "buffer-alloc-unsafe": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz", + "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==" + }, + "buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==" + }, "buffer-es6": { "version": "4.9.3", "resolved": "https://registry.npmjs.org/buffer-es6/-/buffer-es6-4.9.3.tgz", "integrity": "sha512-Ibt+oXxhmeYJSsCkODPqNpPmyegefiD8rfutH1NYGhMZQhSp95Rz7haemgnJ6dxa6LT+JLLbtgOMORRluwKktw==" }, + "buffer-fill": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz", + "integrity": "sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ==" + }, "buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -17926,7 +18406,8 @@ "content-type": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", - "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", + "dev": true }, "convert-source-map": { "version": "1.8.0", @@ -18050,6 +18531,183 @@ "integrity": "sha512-V0pfhfr8suzyPGOx3nmq4aHqabehUZn6Ch9kyFpV79TGDTWFmHqUqXdabR7QHqxzrYolF4+tVmJhUG4OURg5dQ==", "peer": true }, + "decompress": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/decompress/-/decompress-4.2.1.tgz", + "integrity": "sha512-e48kc2IjU+2Zw8cTb6VZcJQ3lgVbS4uuB1TfCHbiZIP/haNXm+SVyhu+87jts5/3ROpd82GSVCoNs/z8l4ZOaQ==", + "requires": { + "decompress-tar": "^4.0.0", + "decompress-tarbz2": "^4.0.0", + "decompress-targz": "^4.0.0", + "decompress-unzip": "^4.0.1", + "graceful-fs": "^4.1.10", + "make-dir": "^1.0.0", + "pify": "^2.3.0", + "strip-dirs": "^2.0.0" + }, + "dependencies": { + "make-dir": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", + "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", + "requires": { + "pify": "^3.0.0" + }, + "dependencies": { + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==" + } + } + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==" + } + } + }, + "decompress-tar": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/decompress-tar/-/decompress-tar-4.1.1.tgz", + "integrity": "sha512-JdJMaCrGpB5fESVyxwpCx4Jdj2AagLmv3y58Qy4GE6HMVjWz1FeVQk1Ct4Kye7PftcdOo/7U7UKzYBJgqnGeUQ==", + "requires": { + "file-type": "^5.2.0", + "is-stream": "^1.1.0", + "tar-stream": "^1.5.2" + }, + "dependencies": { + "bl": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.3.tgz", + "integrity": "sha512-pvcNpa0UU69UT341rO6AYy4FVAIkUHuZXRIWbq+zHnsVcRzDDjIAhGuuYoi0d//cwIwtt4pkpKycWEfjdV+vww==", + "requires": { + "readable-stream": "^2.3.5", + "safe-buffer": "^5.1.1" + } + }, + "is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==" + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "tar-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.6.2.tgz", + "integrity": "sha512-rzS0heiNf8Xn7/mpdSVVSMAWAoy9bfb1WOTYC78Z0UQKeKa/CWS8FOq0lKGNa8DWKAn9gxjCvMLYc5PGXYlK2A==", + "requires": { + "bl": "^1.0.0", + "buffer-alloc": "^1.2.0", + "end-of-stream": "^1.0.0", + "fs-constants": "^1.0.0", + "readable-stream": "^2.3.0", + "to-buffer": "^1.1.1", + "xtend": "^4.0.0" + } + } + } + }, + "decompress-tarbz2": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/decompress-tarbz2/-/decompress-tarbz2-4.1.1.tgz", + "integrity": "sha512-s88xLzf1r81ICXLAVQVzaN6ZmX4A6U4z2nMbOwobxkLoIIfjVMBg7TeguTUXkKeXni795B6y5rnvDw7rxhAq9A==", + "requires": { + "decompress-tar": "^4.1.0", + "file-type": "^6.1.0", + "is-stream": "^1.1.0", + "seek-bzip": "^1.0.5", + "unbzip2-stream": "^1.0.9" + }, + "dependencies": { + "file-type": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-6.2.0.tgz", + "integrity": "sha512-YPcTBDV+2Tm0VqjybVd32MHdlEGAtuxS3VAYsumFokDSMG+ROT5wawGlnHDoz7bfMcMDt9hxuXvXwoKUx2fkOg==" + }, + "is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==" + } + } + }, + "decompress-targz": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/decompress-targz/-/decompress-targz-4.1.1.tgz", + "integrity": "sha512-4z81Znfr6chWnRDNfFNqLwPvm4db3WuZkqV+UgXQzSngG3CEKdBkw5jrv3axjjL96glyiiKjsxJG3X6WBZwX3w==", + "requires": { + "decompress-tar": "^4.1.1", + "file-type": "^5.2.0", + "is-stream": "^1.1.0" + }, + "dependencies": { + "is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==" + } + } + }, + "decompress-unzip": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/decompress-unzip/-/decompress-unzip-4.0.1.tgz", + "integrity": "sha512-1fqeluvxgnn86MOh66u8FjbtJpAFv5wgCT9Iw8rcBqQcCo5tO8eiJw7NNTrvt9n4CRBVq7CstiS922oPgyGLrw==", + "requires": { + "file-type": "^3.8.0", + "get-stream": "^2.2.0", + "pify": "^2.3.0", + "yauzl": "^2.4.2" + }, + "dependencies": { + "file-type": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-3.9.0.tgz", + "integrity": "sha512-RLoqTXE8/vPmMuTI88DAzhMYC99I8BWv7zYP4A1puo5HIjEJ5EX48ighy4ZyKMG9EDXxBgW6e++cn7d1xuFghA==" + }, + "get-stream": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-2.3.1.tgz", + "integrity": "sha512-AUGhbbemXxrZJRD5cDvKtQxLuYaIbNtDTK8YqupCI393Q2KSTreEsLUN3ZxAWFGiKTzL6nKuzfcIvieflUX9qA==", + "requires": { + "object-assign": "^4.0.1", + "pinkie-promise": "^2.0.0" + } + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==" + } + } + }, "dedent": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", @@ -19081,6 +19739,14 @@ "bser": "2.1.1" } }, + "fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "requires": { + "pend": "~1.2.0" + } + }, "figures": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", @@ -19105,6 +19771,11 @@ "flat-cache": "^3.0.4" } }, + "file-type": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-5.2.0.tgz", + "integrity": "sha512-Iq1nJ6D2+yIO4c8HHg4fyVb8mAJieo1Oloy1mLLaB2PvezNedhBVm+QU7g0qM42aiMbRXTxKKwGD17rjKNJYVQ==" + }, "file-uri-to-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", @@ -19136,6 +19807,21 @@ } } }, + "filename-reserved-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-2.0.0.tgz", + "integrity": "sha512-lc1bnsSr4L4Bdif8Xb/qrtokGbq5zlsms/CYH8PP+WtCkGNF65DPiQY8vG3SakEdRn8Dlnm+gW/qWKKjS5sZzQ==" + }, + "filenamify": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/filenamify/-/filenamify-4.3.0.tgz", + "integrity": "sha512-hcFKyUG57yWGAzu1CMt/dPzYZuv+jAJUT85bL8mrXvNe6hWj6yEHEc4EdcgiA6Z3oi1/9wXJdZPXF2dZNgwgOg==", + "requires": { + "filename-reserved-regex": "^2.0.0", + "strip-outer": "^1.0.1", + "trim-repeated": "^1.0.0" + } + }, "fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -19244,7 +19930,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", - "peer": true, "requires": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", @@ -19650,6 +20335,7 @@ "version": "5.3.0", "resolved": "https://registry.npmjs.org/http-call/-/http-call-5.3.0.tgz", "integrity": "sha512-ahwimsC23ICE4kPl9xTBjKB4inbRaeLyZeRunC/1Jy/Z6X8tv22MEAjK+KBOMSVLaqXPTTmd8638waVIKLGx2w==", + "dev": true, "requires": { "content-type": "^1.0.4", "debug": "^4.1.1", @@ -19902,6 +20588,10 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/any-signal/-/any-signal-3.0.1.tgz", "integrity": "sha512-xgZgJtKEa9YmDqXodIgl7Fl1C8yNXr8w6gXjqK3LW4GcEiYT+6AQfJSE/8SPsEpLLmcvbv8YU+qet94UewHxqg==" + }, + "node-fetch": { + "version": "https://registry.npmjs.org/@achingbrain/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-iTASGs+HTFK5E4ZqcMsHmeJ4zodyq8L38lZV33jwqcBJYoUt3HjN4+ot+O9/0b+ke8ddE7UgOtVuZN/OkV19/g==" } } }, @@ -20034,6 +20724,11 @@ "resolved": "https://registry.npmjs.org/is-loopback-addr/-/is-loopback-addr-1.0.1.tgz", "integrity": "sha512-DhWU/kqY7X2F6KrrVTu7mHlbd2Pbo4D1YkAzasBMjQs6lJAoefxaA6m6CpSX0K6pjt9D0b9PNFI5zduy/vzOYw==" }, + "is-natural-number": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-natural-number/-/is-natural-number-4.0.1.tgz", + "integrity": "sha512-Y4LTamMe0DDQIIAlaer9eKebAlDSV6huy+TWhJVPlzZh2o4tRP5SQWFlLn5N0To4mDD22/qdOq+veo1cSISLgQ==" + }, "is-negative-zero": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", @@ -20089,7 +20784,8 @@ "is-retry-allowed": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.2.0.tgz", - "integrity": "sha512-RUbUeKwvm3XG2VYamhJL1xFktgjvPzL0Hq8C+6yrWIswDy3BIXGqCxhxkc30N9jqK311gVU137K8Ei55/zVJRg==" + "integrity": "sha512-RUbUeKwvm3XG2VYamhJL1xFktgjvPzL0Hq8C+6yrWIswDy3BIXGqCxhxkc30N9jqK311gVU137K8Ei55/zVJRg==", + "dev": true }, "is-scoped": { "version": "2.1.0", @@ -20168,8 +20864,7 @@ "isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" }, "isbinaryfile": { "version": "4.0.10", @@ -21034,7 +21729,8 @@ "json-parse-better-errors": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", - "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==" + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", + "dev": true }, "json-parse-even-better-errors": { "version": "2.3.1", @@ -21510,30 +22206,6 @@ } } }, - "load-json-file": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-5.3.0.tgz", - "integrity": "sha512-cJGP40Jc/VXUsp8/OrnyKyTZ1y6v/dphm3bioS+RrKXjK2BB6wHUd6JptZEFDGgGahMT+InnZO5i1Ei9mpC8Bw==", - "requires": { - "graceful-fs": "^4.1.15", - "parse-json": "^4.0.0", - "pify": "^4.0.1", - "strip-bom": "^3.0.0", - "type-fest": "^0.3.0" - }, - "dependencies": { - "strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==" - }, - "type-fest": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.3.1.tgz", - "integrity": "sha512-cUGJnCdr4STbePCgqNFbpVNCepa+kAVohJs1sLhxzdH+gnEoOd8VhbYa7pD3zZYGiURWM2xzEII3fQcRizDkYQ==" - } - } - }, "load-yaml-file": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/load-yaml-file/-/load-yaml-file-0.2.0.tgz", @@ -22345,9 +23017,33 @@ "integrity": "sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA==" }, "node-fetch": { - "version": "npm:@achingbrain/node-fetch@2.6.7", - "resolved": "https://registry.npmjs.org/@achingbrain/node-fetch/-/node-fetch-2.6.7.tgz", - "integrity": "sha512-iTASGs+HTFK5E4ZqcMsHmeJ4zodyq8L38lZV33jwqcBJYoUt3HjN4+ot+O9/0b+ke8ddE7UgOtVuZN/OkV19/g==" + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "requires": { + "whatwg-url": "^5.0.0" + }, + "dependencies": { + "tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "requires": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + } + } }, "node-forge": { "version": "0.10.0", @@ -22733,8 +23429,7 @@ "object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" }, "object-inspect": { "version": "1.12.2", @@ -23140,6 +23835,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", "integrity": "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==", + "dev": true, "requires": { "error-ex": "^1.3.1", "json-parse-better-errors": "^1.0.1" @@ -23268,6 +23964,11 @@ "asn1.js": "^5.0.1" } }, + "pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==" + }, "performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", @@ -23292,7 +23993,21 @@ "pify": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", - "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==" + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true + }, + "pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==" + }, + "pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw==", + "requires": { + "pinkie": "^2.0.0" + } }, "pirates": { "version": "4.0.5", @@ -23478,8 +24193,7 @@ "process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, "progress": { "version": "2.0.3", @@ -24369,6 +25083,21 @@ "node-gyp-build": "^4.2.0" } }, + "seek-bzip": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/seek-bzip/-/seek-bzip-1.0.6.tgz", + "integrity": "sha512-e1QtP3YL5tWww8uKaOCQ18UxIT2laNBXHjV/S2WYCiK4udiv8lkG89KRIoCjUagnAmCBurjF4zEVX2ByBbnCjQ==", + "requires": { + "commander": "^2.8.1" + }, + "dependencies": { + "commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" + } + } + }, "semver": { "version": "7.3.7", "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", @@ -24755,6 +25484,14 @@ } } }, + "strip-dirs": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/strip-dirs/-/strip-dirs-2.1.0.tgz", + "integrity": "sha512-JOCxOeKLm2CAS73y/U4ZeZPTkE+gNVCzKt7Eox84Iej1LT/2pTWYpZKJuxwQpvX1LiZb1xokNR7RLfuBAa7T3g==", + "requires": { + "is-natural-number": "^4.0.1" + } + }, "strip-eof": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", @@ -24771,6 +25508,21 @@ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==" }, + "strip-outer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/strip-outer/-/strip-outer-1.0.1.tgz", + "integrity": "sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg==", + "requires": { + "escape-string-regexp": "^1.0.2" + }, + "dependencies": { + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==" + } + } + }, "supports-color": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", @@ -25013,6 +25765,11 @@ "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", "peer": true }, + "to-buffer": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.1.1.tgz", + "integrity": "sha512-lx9B5iv7msuFYE3dytT+KE5tap+rNYw+K4jVkb9R/asAb+pbBSM17jtunHplhBe6RRJdZx3Pn2Jph24O32mOVg==" + }, "to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", @@ -25066,6 +25823,21 @@ "integrity": "sha512-whw60l7r+8ZU8Tu/Uc2yxtc4ZTZbR/PF3u1IPNKGQ6p8EICLb3Z2lAgoqw9bqYd8IkgnsaOcLzYHFckjqNsf0g==", "dev": true }, + "trim-repeated": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/trim-repeated/-/trim-repeated-1.0.0.tgz", + "integrity": "sha512-pkonvlKk8/ZuR0D5tLW8ljt5I8kmxp2XKymhepUeOdCEfKpZaktSArkLHZt76OB1ZvO9bssUsDty4SWhLvZpLg==", + "requires": { + "escape-string-regexp": "^1.0.2" + }, + "dependencies": { + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==" + } + } + }, "truncate-utf8-bytes": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", @@ -25251,6 +26023,26 @@ "which-boxed-primitive": "^1.0.2" } }, + "unbzip2-stream": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz", + "integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==", + "requires": { + "buffer": "^5.2.1", + "through": "^2.3.8" + }, + "dependencies": { + "buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + } + } + }, "unique-filename": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", @@ -25674,6 +26466,11 @@ "resolved": "https://registry.npmjs.org/xsalsa20/-/xsalsa20-1.2.0.tgz", "integrity": "sha512-FIr/DEeoHfj7ftfylnoFt3rAIRoWXpx2AoDfrT2qD2wtp7Dp+COajvs/Icb7uHqRW9m60f5iXZwdsJJO3kvb7w==" }, + "xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" + }, "y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -25738,10 +26535,14 @@ } } }, - "yarn": { - "version": "1.22.19", - "resolved": "https://registry.npmjs.org/yarn/-/yarn-1.22.19.tgz", - "integrity": "sha512-/0V5q0WbslqnwP91tirOvldvYISzaqhClxzyUKXYxs07yUILIs5jx/k6CFe8bvKSkds5w+eiOqta39Wk3WxdcQ==" + "yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "requires": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } }, "yeoman-environment": { "version": "3.9.1", diff --git a/package.json b/package.json index 4ab89532e..ea7788d37 100644 --- a/package.json +++ b/package.json @@ -35,16 +35,20 @@ "dependencies": { "@fluencelabs/fluence": "^0.23.0", "@fluencelabs/fluence-network-environment": "^1.0.13", + "@iarna/toml": "^2.2.5", "@oclif/color": "^1.0.1", "@oclif/core": "^1.9.0", "@oclif/errors": "^1.3.5", "@oclif/plugin-autocomplete": "^1.3.0", "@oclif/plugin-help": "^5", "@oclif/plugin-not-found": "^2.3.1", - "@oclif/plugin-plugins": "^2.1.0", "ajv": "^8.11.0", "camelcase": "^5.2.0", + "decompress": "^4.2.1", + "filenamify": "^4", "inquirer": "^8.2.4", + "multiaddr": "^10.0.1", + "node-fetch": "^2.6.7", "platform": "^1.3.6", "replace-homedir": "^2.0.0", "yaml": "^2.1.1", @@ -55,9 +59,12 @@ "@tsconfig/node16-strictest": "^1.0.1", "@types/camelcase": "^5.2.0", "@types/chai": "^4", + "@types/decompress": "^4.2.4", + "@types/iarna__toml": "^2.0.2", "@types/inquirer": "^8.2.1", "@types/mocha": "^9.1.1", "@types/node": "^17.0.41", + "@types/node-fetch": "^2.6.2", "@types/platform": "^1.3.4", "chai": "^4", "eslint": "^7.32.0", @@ -83,7 +90,6 @@ "commands": "./dist/commands", "plugins": [ "@oclif/plugin-help", - "@oclif/plugin-plugins", "@oclif/plugin-not-found", "@oclif/plugin-autocomplete" ], diff --git a/src/commands/dependency.ts b/src/commands/dependency.ts index 32627b674..c68da759c 100644 --- a/src/commands/dependency.ts +++ b/src/commands/dependency.ts @@ -16,28 +16,45 @@ import color from "@oclif/color"; import { Command, Flags } from "@oclif/core"; +import { Separator } from "inquirer"; -import { initAquaCli } from "../lib/aquaCli"; import { - AQUA_NPM_DEPENDENCY, dependencyList, + DependencyName, initDependencyConfig, initReadonlyDependencyConfig, isDependency, } from "../lib/configs/user/dependency"; -import { FLUENCE_DIR_NAME, NO_INPUT_FLAG } from "../lib/const"; +import { + AQUA_NPM_DEPENDENCY, + cargoDependencyList, + CARGO_GENERATE_CARGO_DEPENDENCY, + CommandObj, + FLUENCE_DIR_NAME, + MARINE_CARGO_DEPENDENCY, + MREPL_CARGO_DEPENDENCY, + NO_INPUT_FLAG, + npmDependencyList, +} from "../lib/const"; import { getIsInteractive } from "../lib/helpers/getIsInteractive"; -import { usage } from "../lib/helpers/usage"; -import { npmDependencies } from "../lib/npm"; +import { ensureNpmDependency, npmDependencies } from "../lib/npm"; import { confirm, input, list } from "../lib/prompt"; +import { cargoDependencies, ensureCargoDependency } from "../lib/rust"; const NAME = "NAME"; const RECOMMENDED = "recommended"; const VERSION_FLAG_NAME = "version"; const USE_FLAG_NAME = "use"; +const RESET_ALL_MESSAGE = `If you omit ${color.yellow( + NAME +)} argument and include ${color.yellow( + `--${USE_FLAG_NAME} ${RECOMMENDED}` +)} - all dependencies will be reset to ${RECOMMENDED} versions`; export default class Dependency extends Command { - static override description = `Manage dependencies stored inside ${FLUENCE_DIR_NAME} directory of the current user`; + static override description = `Manage dependencies stored inside ${color.yellow( + FLUENCE_DIR_NAME + )} directory of the current user`; static override examples = ["<%= config.bin %> <%= command.id %>"]; static override flags = { version: Flags.boolean({ @@ -46,9 +63,9 @@ export default class Dependency extends Command { exclusive: [USE_FLAG_NAME], }), use: Flags.string({ - description: `Set version of the dependency that you want to use. Use ${color.yellow( + description: `Set dependency version. Use ${color.yellow( RECOMMENDED - )} keyword if you want to use ${RECOMMENDED} version`, + )} keyword to set ${RECOMMENDED} version for the dependency. ${RESET_ALL_MESSAGE}`, helpValue: ``, exclusive: [VERSION_FLAG_NAME], }), @@ -57,39 +74,47 @@ export default class Dependency extends Command { static override args = [ { name: NAME, - description: `Dependency name. Currently the only dependency is ${color.yellow( - AQUA_NPM_DEPENDENCY - )}`, + description: `Dependency name. One of: ${color.yellow( + dependencyList.join(", ") + )}. ${RESET_ALL_MESSAGE}`, }, ]; - static override usage: string = usage(this); async run(): Promise { const { args, flags } = await this.parse(Dependency); const isInteractive = getIsInteractive(flags); let name: unknown = args[NAME]; - if (name === undefined) { - if (flags.use === RECOMMENDED) { - const resetAllDependencies = await confirm({ - isInteractive, - message: `Do you want to reset all dependencies to their ${color.yellow( - RECOMMENDED - )} versions`, - }); - - if (resetAllDependencies) { - const dependencyConfig = await initDependencyConfig(this); - dependencyConfig.dependency = {}; - await dependencyConfig.$commit(); - this.log( - `Successfully reset all dependencies to their ${color.yellow( - RECOMMENDED - )} versions` - ); - return; - } + if ( + name === undefined && + flags.use === RECOMMENDED && + (await confirm({ + isInteractive, + message: `Do you want to reset all dependencies to ${color.yellow( + RECOMMENDED + )} versions`, + })) + ) { + const dependencyConfig = await initDependencyConfig(this); + dependencyConfig.dependency = {}; + await dependencyConfig.$commit(); + for (const dependencyName of dependencyList) { + // eslint-disable-next-line no-await-in-loop + await ensureDependency(dependencyName, this); } + this.log( + `Successfully reset all dependencies to ${color.yellow( + RECOMMENDED + )} versions` + ); + return; + } + if ( + name === undefined || + (!isDependency(name) && + typeof this.warn(`Unknown dependency ${color.yellow(name)}`) === + "string") + ) { name = await list({ isInteractive, message: "Select dependency", @@ -97,48 +122,42 @@ export default class Dependency extends Command { `Do you want to manage ${color.yellow(name)}`, onNoChoices: (): void => this.error("You have to select dependency to manage"), - options: [...dependencyList], + options: [ + new Separator("NPM dependencies:"), + ...npmDependencyList, + new Separator("Cargo dependencies:"), + ...cargoDependencyList, + ], }); } if (!isDependency(name)) { - this.error(`Unknown dependency ${color.yellow(name)}`); + this.error("Unreachable"); } const dependencyName = name; - const { recommendedVersion, packageName } = npmDependencies[dependencyName]; - - const handleVersion = async (): Promise => { - const result = - (await initReadonlyDependencyConfig(this)).dependency[dependencyName] ?? - recommendedVersion; - - this.log( - `Using version ${color.yellow(result)}${ - result.includes(recommendedVersion) ? ` (${RECOMMENDED})` : "" - } of ${packageName}` - ); - }; + const { recommendedVersion, packageName } = { + ...npmDependencies, + ...cargoDependencies, + }[dependencyName]; if (flags.version === true) { - return handleVersion(); + return handleVersion({ + commandObj: this, + dependencyName, + packageName, + recommendedVersion, + }); } - const handleUse = async (version: string): Promise => { - const dependencyConfig = await initDependencyConfig(this); - if (version === RECOMMENDED) { - delete dependencyConfig.dependency[dependencyName]; - } else { - dependencyConfig.dependency[dependencyName] = version; - } - - await dependencyConfig.$commit(); - await initAquaCli(this); - await handleVersion(); - }; - if (typeof flags.use === "string") { - return handleUse(flags.use); + return handleUse({ + commandObj: this, + dependencyName, + packageName, + recommendedVersion, + version: flags.use, + }); } return ( @@ -151,23 +170,122 @@ export default class Dependency extends Command { onNoChoices: (): never => this.error("Unreachable"), options: [ { - value: handleVersion, - name: `Print version of ${color.yellow(name)}`, + value: (): Promise => + handleVersion({ + commandObj: this, + dependencyName, + packageName, + recommendedVersion, + }), + name: `Print version of ${color.yellow(dependencyName)}`, }, { value: async (): Promise => - handleUse( - await input({ + handleUse({ + commandObj: this, + dependencyName, + packageName, + recommendedVersion, + version: await input({ isInteractive, message: `Enter version of ${color.yellow( name )} that you want to use`, - }) - ), - name: `Set version of ${color.yellow(name)} that you want to use`, + }), + }), + name: `Set version of ${color.yellow( + dependencyName + )} that you want to use`, }, ], }) )(); } } + +type HandleVersionArg = { + recommendedVersion: string; + dependencyName: DependencyName; + packageName: string; + commandObj: CommandObj; +}; + +const handleVersion = async ({ + recommendedVersion, + dependencyName, + packageName, + commandObj, +}: HandleVersionArg): Promise => { + const result = + (await initReadonlyDependencyConfig(commandObj)).dependency?.[ + dependencyName + ] ?? recommendedVersion; + + commandObj.log( + `Using version ${color.yellow(result)}${ + result.includes(recommendedVersion) ? ` (${RECOMMENDED})` : "" + } of ${packageName}` + ); +}; + +const ensureDependency = async ( + dependencyName: DependencyName, + commandObj: CommandObj +): Promise => { + switch (dependencyName) { + case AQUA_NPM_DEPENDENCY: { + await ensureNpmDependency({ name: dependencyName, commandObj }); + break; + } + case MARINE_CARGO_DEPENDENCY: + case MREPL_CARGO_DEPENDENCY: + case CARGO_GENERATE_CARGO_DEPENDENCY: { + await ensureCargoDependency({ + name: dependencyName, + commandObj, + }); + break; + } + default: { + const _exhaustiveCheck: never = dependencyName; + return _exhaustiveCheck; + } + } +}; + +type HandleUseArg = HandleVersionArg & { + version: string; +}; + +const handleUse = async ({ + recommendedVersion, + dependencyName, + packageName, + commandObj, + version, +}: HandleUseArg): Promise => { + const dependencyConfig = await initDependencyConfig(commandObj); + const updatedDependencyVersionsConfig = { + ...dependencyConfig.dependency, + [dependencyName]: version === RECOMMENDED ? undefined : version, + }; + + const isConfigEmpty = Object.values(updatedDependencyVersionsConfig).every( + (value): boolean => value === undefined + ); + + if (isConfigEmpty) { + delete dependencyConfig.dependency; + } else { + dependencyConfig.dependency = updatedDependencyVersionsConfig; + } + + await dependencyConfig.$commit(); + await ensureDependency(dependencyName, commandObj); + return handleVersion({ + commandObj, + dependencyName, + packageName, + recommendedVersion, + }); +}; diff --git a/src/commands/deploy.ts b/src/commands/deploy.ts index 51beef6d7..1be110d04 100644 --- a/src/commands/deploy.ts +++ b/src/commands/deploy.ts @@ -14,50 +14,79 @@ * limitations under the License. */ +import fsPromises from "node:fs/promises"; import path from "node:path"; import color from "@oclif/color"; -import { Command, Flags } from "@oclif/core"; +import { CliUx, Command, Flags } from "@oclif/core"; +import camelcase from "camelcase"; import { AquaCLI, initAquaCli } from "../lib/aquaCli"; import { DeployedServiceConfig, initAppConfig, initNewReadonlyAppConfig, - Services, + ServicesV2, } from "../lib/configs/project/app"; -import { initReadonlyFluenceConfig } from "../lib/configs/project/fluence"; import { - ARTIFACTS_DIR_NAME, + FluenceConfigReadonly, + initReadonlyFluenceConfig, + OverrideModules, + ServiceDeployV1, +} from "../lib/configs/project/fluence"; +import { + initReadonlyModuleConfig, + ModuleConfigReadonly, +} from "../lib/configs/project/module"; +import { + FACADE_MODULE_NAME, + initReadonlyServiceConfig, + ModuleV0, + ServiceConfigReadonly, +} from "../lib/configs/project/service"; +import { CommandObj, - DEPLOYMENT_CONFIG_FILE_NAME, + DEFAULT_DEPLOY_NAME, FLUENCE_CONFIG_FILE_NAME, FORCE_FLAG_NAME, + FS_OPTIONS, KEY_PAIR_FLAG, + MODULE_CONFIG_FILE_NAME, NO_INPUT_FLAG, + SERVICE_CONFIG_FILE_NAME, TIMEOUT_FLAG, + TIMEOUT_FLAG_NAME, } from "../lib/const"; -import { updateDeployedAppAqua, generateRegisterApp } from "../lib/deployedApp"; +import { + generateDeployedAppAqua, + generateRegisterApp, +} from "../lib/deployedApp"; +import { + downloadModule, + downloadService, + getModuleUrlOrAbsolutePath, + getModuleWasmPath, + isUrl, +} from "../lib/helpers/downloadFile"; +import { ensureFluenceProject } from "../lib/helpers/ensureFluenceProject"; import { getIsInteractive } from "../lib/helpers/getIsInteractive"; -import { usage } from "../lib/helpers/usage"; -import { getKeyPairFromFlags } from "../lib/keyPairs/getKeyPair"; -import { getRandomRelayId, getRandomRelayAddr } from "../lib/multiaddr"; -import { getArtifactsPath } from "../lib/pathsGetters/getArtifactsPath"; -import { ensureProjectFluenceDirPath } from "../lib/pathsGetters/getProjectFluenceDirPath"; +import { replaceHomeDir } from "../lib/helpers/replaceHomeDir"; +import { getKeyPairFromFlags } from "../lib/keypairs"; +import { initMarineCli } from "../lib/marineCli"; +import { getRandomRelayAddr, getRandomRelayId } from "../lib/multiaddr"; +import { ensureFluenceTmpDeployJsonPath } from "../lib/paths"; import { confirm } from "../lib/prompt"; import { removeApp } from "./remove"; export default class Deploy extends Command { - static override description = "Deploy service to the remote peer"; + static override description = `Deploy application, described in ${color.yellow( + FLUENCE_CONFIG_FILE_NAME + )}`; static override examples = ["<%= config.bin %> <%= command.id %>"]; static override flags = { - on: Flags.string({ - description: "PeerId of the peer where you want to deploy", - helpValue: "", - }), relay: Flags.string({ - description: "Relay node MultiAddress", + description: "Relay node multiaddr", helpValue: "", }), [FORCE_FLAG_NAME]: Flags.boolean({ @@ -67,91 +96,97 @@ export default class Deploy extends Command { ...KEY_PAIR_FLAG, ...NO_INPUT_FLAG, }; - static override usage: string = usage(this); - async run(): Promise { const { flags } = await this.parse(Deploy); const isInteractive = getIsInteractive(flags); - await ensureProjectFluenceDirPath(this, isInteractive); + await ensureFluenceProject(this, isInteractive); - const deployedConfig = await initAppConfig(this); - - if (deployedConfig !== null) { + const keyPair = await getKeyPairFromFlags(flags, this, isInteractive); + if (keyPair instanceof Error) { + this.error(keyPair.message); + } + const fluenceConfig = await initReadonlyFluenceConfig(this); + if (fluenceConfig === null) { + this.error("You must init Fluence project first to deploy"); + } + const relay = flags.relay ?? getRandomRelayAddr(fluenceConfig.relays); + const preparedForDeployItems = await prepareForDeploy({ + commandObj: this, + fluenceConfig, + }); + const aquaCli = await initAquaCli(this); + const tmpDeployJSONPath = await ensureFluenceTmpDeployJsonPath(); + const appConfig = await initAppConfig(this); + if (appConfig !== null) { // Prompt user to remove previously deployed app if // it was already deployed before const doRemove = flags[FORCE_FLAG_NAME] || (await confirm({ - message: - "Currently you need to remove your app to deploy again. Do you want to remove?", + message: `Previously deployed app described in ${color.yellow( + replaceHomeDir(appConfig.$getPath()) + )} needs to be removed to continue. Do you want to remove?`, isInteractive, flagName: FORCE_FLAG_NAME, })); if (!doRemove) { - this.error("You have to confirm in order to continue"); + this.error("You have to confirm to continue"); } await removeApp({ - appConfig: deployedConfig, + appConfig, commandObj: this, timeout: flags.timeout, + relay: flags.relay, isInteractive, }); } - const keyPair = await getKeyPairFromFlags(flags, this, isInteractive); - if (keyPair instanceof Error) { - this.error(keyPair.message); - } - - const fluenceConfig = await initReadonlyFluenceConfig(this); - if (fluenceConfig.services.length === 0) { - this.error( - `No services to deploy. Add services you want to deploy to ${color.yellow( - ARTIFACTS_DIR_NAME - )} directory (${getArtifactsPath()}) and also list them in ${color.yellow( - `${FLUENCE_CONFIG_FILE_NAME}.yaml` - )} (${fluenceConfig.$getPath()})` - ); - } - const artifactsPath = getArtifactsPath(); - const cwd = process.cwd(); - const addr = flags.relay ?? getRandomRelayAddr(); - const peerId = flags.on ?? getRandomRelayId(); - if (flags.on === undefined) { - this.log(`Random peer ${color.yellow(peerId)} selected for deployment`); - } - - const aquaCli = await initAquaCli(this); - const successfullyDeployedServices: Services = {}; - for (const { name, count = 1 } of fluenceConfig.services) { - process.chdir(path.join(artifactsPath, name)); - // eslint-disable-next-line no-await-in-loop - const services = await deployServices({ - count, - deployServiceOptions: { - name, - artifactsPath, + const successfullyDeployedServices: ServicesV2 = {}; + this.log( + `Going to deploy project described in ${color.yellow( + replaceHomeDir(fluenceConfig.$getPath()) + )}` + ); + for (const { + count, + deployJSON, + deployId, + peerId = getRandomRelayId(fluenceConfig.relays), + serviceName, + } of preparedForDeployItems) { + for (let i = 0; i < count; i = i + 1) { + // eslint-disable-next-line no-await-in-loop + const res = await deployService({ + deployJSON, + peerId, + serviceName, + deployId, + relay, secretKey: keyPair.secretKey, aquaCli, - peerId, - timeout: flags.timeout, - addr, - }, - commandObj: this, - }); - if (services !== null) { - successfullyDeployedServices[name] = services; + timeout: flags[TIMEOUT_FLAG_NAME], + tmpDeployJSONPath, + commandObj: this, + }); + if (res !== null) { + const { deployedServiceConfig, deployId, serviceName } = res; + const successfullyDeployedServicesByName = + successfullyDeployedServices[serviceName] ?? {}; + successfullyDeployedServicesByName[deployId] = [ + ...(successfullyDeployedServicesByName[deployId] ?? []), + deployedServiceConfig, + ]; + successfullyDeployedServices[serviceName] = + successfullyDeployedServicesByName; + } } } - - process.chdir(cwd); - if (Object.keys(successfullyDeployedServices).length === 0) { this.error("No services were deployed successfully"); } - await updateDeployedAppAqua(successfullyDeployedServices); + await generateDeployedAppAqua(successfullyDeployedServices); await generateRegisterApp({ deployedServices: successfullyDeployedServices, aquaCli, @@ -159,147 +194,380 @@ export default class Deploy extends Command { await initNewReadonlyAppConfig( { - version: 1, + version: 2, services: successfullyDeployedServices, keyPairName: keyPair.name, timestamp: new Date().toISOString(), + relays: fluenceConfig.relays, }, this ); } } -type DeployServiceOptions = Readonly<{ +type DeployInfo = { + serviceName: string; + serviceDirPath: string; + deployId: string; + count: number; + peerId: string | undefined; + modules: Array; +}; + +const overrideModule = ( + mod: ModuleV0, + overrideModules: OverrideModules | undefined, + moduleName: string +): ModuleV0 => ({ ...mod, ...overrideModules?.[moduleName] }); + +type PreparedForDeploy = Omit & { + deployJSON: DeployJSONConfig; +}; + +type GetDeployJSONsArg = { + commandObj: CommandObj; + fluenceConfig: FluenceConfigReadonly; +}; + +const prepareForDeploy = async ({ + commandObj, + fluenceConfig, +}: GetDeployJSONsArg): Promise> => { + if ( + fluenceConfig.services === undefined || + Object.keys(fluenceConfig.services).length === 0 + ) { + throw new Error( + `Use ${color.yellow( + "fluence service add" + )} command to add services you want to deploy to ${color.yellow( + FLUENCE_CONFIG_FILE_NAME + )}` + ); + } + + type ServicePathPromises = Promise<{ + serviceName: string; + serviceDirPath: string; + get: string; + deploy: Array; + }>; + + CliUx.ux.action.start("Making sure all services are downloaded"); + const servicePaths = await Promise.all( + Object.entries(fluenceConfig.services).map( + ([serviceName, { get, deploy }]): ServicePathPromises => + (async (): ServicePathPromises => ({ + serviceName: + camelcase(serviceName) === serviceName + ? serviceName + : commandObj.error( + `Service name ${color.yellow(serviceName)} not in camelCase` + ), + deploy, + get, + serviceDirPath: isUrl(get) + ? await downloadService(get) + : path.resolve(get), + }))() + ) + ); + CliUx.ux.action.stop(); + + type ServiceConfigPromises = Promise<{ + serviceName: string; + serviceConfig: ServiceConfigReadonly; + serviceDirPath: string; + deploy: Array; + }>; + + const serviceConfigs = await Promise.all( + servicePaths.map( + ({ serviceName, serviceDirPath, deploy, get }): ServiceConfigPromises => + (async (): ServiceConfigPromises => ({ + serviceName, + deploy, + serviceConfig: + (await initReadonlyServiceConfig(serviceDirPath, commandObj)) ?? + commandObj.error( + `Service ${color.yellow(serviceName)} must have ${color.yellow( + SERVICE_CONFIG_FILE_NAME + )}. ${ + isUrl(get) + ? `Not able to find it after downloading and decompressing ${color.yellow( + get + )}` + : `Not able to find it at ${color.yellow(get)}` + }` + ), + serviceDirPath, + }))() + ) + ); + + const allDeployInfos = serviceConfigs.flatMap( + ({ + serviceName, + deploy, + serviceConfig, + serviceDirPath, + }): Array => + deploy.map( + ({ deployId, count = 1, peerId, overrideModules }): DeployInfo => ({ + serviceName, + serviceDirPath, + deployId: + camelcase(deployId) === deployId + ? deployId + : commandObj.error( + `DeployId ${color.yellow(deployId)} not in camelCase` + ), + count, + peerId: fluenceConfig?.peerIds?.[peerId ?? ""] ?? peerId, + modules: ((): Array => { + const modulesNotFoundInServiceYaml = Object.keys( + overrideModules ?? {} + ).filter( + (moduleName): boolean => !(moduleName in serviceConfig.modules) + ); + if (modulesNotFoundInServiceYaml.length > 0) { + commandObj.error( + `${color.yellow( + FLUENCE_CONFIG_FILE_NAME + )} has service ${color.yellow( + serviceName + )} with deployId ${color.yellow( + deployId + )} that has moduleOverrides for modules that don't exist in the service ${color.yellow( + serviceDirPath + )}. Please make sure ${color.yellow( + modulesNotFoundInServiceYaml.join(", ") + )} spelled correctly ` + ); + } + const { [FACADE_MODULE_NAME]: facadeModule, ...otherModules } = + serviceConfig.modules; + return [ + ...Object.entries(otherModules).map( + ([moduleName, mod]): ModuleV0 => + overrideModule(mod, overrideModules, moduleName) + ), + overrideModule(facadeModule, overrideModules, FACADE_MODULE_NAME), + ]; + })(), + }) + ) + ); + + const setOfAllGets = [ + ...new Set( + allDeployInfos.flatMap( + ({ modules, serviceDirPath }): Array => + modules.map(({ get }): string => + getModuleUrlOrAbsolutePath(get, serviceDirPath) + ) + ) + ), + ]; + const marineCli = await initMarineCli(commandObj); + CliUx.ux.action.start("Making sure all modules are downloaded and built"); + const mapOfAllModuleConfigs = new Map( + await Promise.all( + setOfAllGets.map( + (get): Promise<[string, ModuleConfigReadonly]> => + (async (): Promise<[string, ModuleConfigReadonly]> => { + const moduleConfig = + (isUrl(get) + ? await initReadonlyModuleConfig( + await downloadModule(get), + commandObj + ) + : await initReadonlyModuleConfig(get, commandObj)) ?? + CliUx.ux.action.stop(color.red("error")) ?? + commandObj.error( + `Module with get: ${color.yellow( + get + )} doesn't have ${color.yellow(MODULE_CONFIG_FILE_NAME)}` + ); + + if (moduleConfig.type === "rust") { + await marineCli({ + command: "build", + flags: { release: true }, + workingDir: path.dirname(moduleConfig.$getPath()), + }); + } + return [get, moduleConfig]; + })() + ) + ) + ); + CliUx.ux.action.stop(); + + return allDeployInfos.map( + ({ modules, serviceDirPath, ...rest }): PreparedForDeploy => { + const deployJSON = { + [DEFAULT_DEPLOY_NAME]: { + modules: modules.map(({ get, ...overrides }): JSONModuleConf => { + const moduleConfig = + mapOfAllModuleConfigs.get( + getModuleUrlOrAbsolutePath(get, serviceDirPath) + ) ?? + commandObj.error( + `Unreachable. Wasn't able to find module config for ${get}` + ); + return serviceModuleToJSONModuleConfig(moduleConfig, overrides); + }), + }, + }; + return { + ...rest, + deployJSON, + }; + } + ); +}; + +/* eslint-disable camelcase */ +type JSONModuleConf = { name: string; - peerId: string; - artifactsPath: string; - addr: string; + path: string; + max_heap_size?: string; + logger_enabled?: boolean; + logging_mask?: number; + mapped_dirs?: Array<[string, string]>; + preopened_files?: Array; + envs?: Array<[string, string]>; + mounted_binaries?: Array<[string, string]>; +}; + +type DeployJSONConfig = Record< + string, + { + modules: Array; + } +>; + +const serviceModuleToJSONModuleConfig = ( + moduleConfig: ModuleConfigReadonly, + overrides: Omit +): JSONModuleConf => { + const overriddenConfig = { ...moduleConfig, ...overrides }; + const { + name, + loggerEnabled, + loggingMask, + volumes, + envs, + maxHeapSize, + mountedBinaries, + preopenedFiles, + } = overriddenConfig; + + const jsonModuleConfig: JSONModuleConf = { + name, + path: getModuleWasmPath(overriddenConfig), + }; + if (loggerEnabled === true) { + jsonModuleConfig.logger_enabled = true; + } + if (typeof loggingMask === "number") { + jsonModuleConfig.logging_mask = loggingMask; + } + if (typeof maxHeapSize === "string") { + jsonModuleConfig.max_heap_size = maxHeapSize; + } + if (volumes !== undefined) { + jsonModuleConfig.mapped_dirs = Object.entries(volumes); + jsonModuleConfig.preopened_files = [...new Set(Object.values(volumes))]; + } + if (preopenedFiles !== undefined) { + jsonModuleConfig.preopened_files = [ + ...new Set([...Object.values(volumes ?? {}), ...preopenedFiles]), + ]; + } + if (envs !== undefined) { + jsonModuleConfig.envs = Object.entries(envs); + } + if (mountedBinaries !== undefined) { + jsonModuleConfig.mounted_binaries = Object.entries(mountedBinaries); + } + return jsonModuleConfig; +}; +/* eslint-enable camelcase */ + +type DeployServiceArg = Readonly<{ + deployJSON: DeployJSONConfig; + relay: string; secretKey: string; aquaCli: AquaCLI; - timeout: string | undefined; + timeout: number | undefined; + serviceName: string; + deployId: string; + tmpDeployJSONPath: string; + commandObj: CommandObj; }>; /** * Deploy by first uploading .wasm files and configs, possibly creating a new blueprint * @param param0 DeployServiceOptions - * @returns Promise + * @returns Promise */ const deployService = async ({ - name, + deployJSON, peerId, - artifactsPath, - addr, + serviceName, + deployId, + relay, secretKey, aquaCli, + tmpDeployJSONPath, timeout, -}: DeployServiceOptions): Promise => { + commandObj, +}: DeployServiceArg & { peerId: string }): Promise<{ + deployedServiceConfig: DeployedServiceConfig; + serviceName: string; + deployId: string; +} | null> => { + await fsPromises.writeFile( + tmpDeployJSONPath, + JSON.stringify(deployJSON, null, 2), + FS_OPTIONS + ); let result: string; try { result = await aquaCli( { command: "remote deploy_service", flags: { - "config-path": path.join( - artifactsPath, - name, - DEPLOYMENT_CONFIG_FILE_NAME - ), - service: name, - addr, + "config-path": tmpDeployJSONPath, + service: DEFAULT_DEPLOY_NAME, + addr: relay, sk: secretKey, on: peerId, timeout, }, }, - "Deploying service", - { name, on: peerId, relay: addr } + "Deploying", + { service: serviceName, deployId, on: peerId } ); } catch (error) { - return new Error(`Wasn't able to deploy service\n${String(error)}`); + commandObj.warn(`Wasn't able to deploy service\n${String(error)}`); + return null; } const [, blueprintId] = /Blueprint id:\n(.*)/.exec(result) ?? []; const [, serviceId] = /And your service id is:\n"(.*)"/.exec(result) ?? []; if (blueprintId === undefined || serviceId === undefined) { - return new Error( + commandObj.warn( `Deployment finished without errors but not able to parse serviceId or blueprintId from aqua cli output:\n\n${result}` ); - } - - return { blueprintId, serviceId, peerId }; -}; - -/** - * Deploy a service and then deploy zero or more services using the blueprint - * id of the first service that was deployed - * @param param0 Readonly<{ deployServiceOptions: DeployServiceOptions; count: number; commandObj: CommandObj;}> - * @returns Promise - */ -const deployServices = async ({ - count, - deployServiceOptions, - commandObj, -}: Readonly<{ - deployServiceOptions: DeployServiceOptions; - count: number; - commandObj: CommandObj; -}>): Promise => { - const result = await deployService(deployServiceOptions); - - if (result instanceof Error) { - commandObj.warn(result.message); return null; } - const { blueprintId } = result; - const { secretKey, peerId, addr, aquaCli, name, timeout } = - deployServiceOptions; - - const services = [result]; - - let servicesToDeployCount = count - 1; - - // deploy by blueprintId 'servicesToDeployCount' number of times - while (servicesToDeployCount > 0) { - let result: string; - try { - // eslint-disable-next-line no-await-in-loop - result = await aquaCli( - { - command: "remote create_service", - flags: { - id: blueprintId, - addr, - sk: secretKey, - on: peerId, - timeout, - }, - }, - "Deploying service", - { - name, - blueprintId, - on: peerId, - relay: addr, - } - ); - } catch (error) { - commandObj.warn(`Wasn't able to deploy service\n${String(error)}`); - continue; - } - - const [, serviceId] = /"(.*)"/.exec(result) ?? []; - - if (serviceId === undefined) { - commandObj.warn( - `Deployment finished without errors but not able to parse serviceId from aqua cli output:\n\n${result}` - ); - continue; - } - - services.push({ blueprintId, serviceId, peerId }); - servicesToDeployCount = servicesToDeployCount - 1; - } - - return services; + return { + deployedServiceConfig: { blueprintId, serviceId, peerId }, + serviceName, + deployId, + }; }; diff --git a/src/commands/init.ts b/src/commands/init.ts index 106289a79..0b81babf1 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -21,30 +21,24 @@ import path from "node:path"; import { color } from "@oclif/color"; import { Command } from "@oclif/core"; import type { JSONSchemaType } from "ajv"; -import replaceHomedir from "replace-homedir"; import { ajv } from "../lib/ajv"; -import { initReadonlyFluenceConfig } from "../lib/configs/project/fluence"; +import { initNewReadonlyFluenceConfig } from "../lib/configs/project/fluence"; import { CommandObj, - ARTIFACTS_DIR_NAME, - EXTENSIONS_JSON_FILE_NAME, - FLUENCE_DIR_NAME, FS_OPTIONS, - SRC_DIR_NAME, - VSCODE_DIR_NAME, - GITIGNORE_FILE_NAME, - GIT_IGNORE_CONTENT, - AQUA_DIR_NAME, - SETTINGS_JSON_FILE_NAME, - DEFAULT_SRC_AQUA_FILE_NAME, + RECOMMENDED_GIT_IGNORE_CONTENT, NO_INPUT_FLAG, } from "../lib/const"; import { getIsInteractive } from "../lib/helpers/getIsInteractive"; -import { usage } from "../lib/helpers/usage"; -import { getArtifactsPath } from "../lib/pathsGetters/getArtifactsPath"; -import { getDefaultAquaPath } from "../lib/pathsGetters/getDefaultAquaPath"; -import { getSrcAquaDirPath } from "../lib/pathsGetters/getSrcAquaDirPath"; +import { replaceHomeDir } from "../lib/helpers/replaceHomeDir"; +import { + ensureFluenceAquaDir, + ensureVSCodeExtensionsJsonPath, + ensureVSCodeSettingsJsonPath, + ensureSrcAquaMainPath, + getGitignorePath, +} from "../lib/paths"; import { input } from "../lib/prompt"; export const PATH = "PATH"; @@ -61,7 +55,6 @@ export default class Init extends Command { description: "Project path", }, ]; - static override usage: string = usage(this); async run(): Promise { const { args, flags } = await this.parse(Init); const isInteractive = getIsInteractive(flags); @@ -75,40 +68,36 @@ export default class Init extends Command { } } +const RECOMMENDATIONS = "recommendations"; + type ExtensionsJson = { - recommendations: Array; + [RECOMMENDATIONS]?: Array; }; const extensionsJsonSchema: JSONSchemaType = { type: "object", properties: { - recommendations: { type: "array", items: { type: "string" } }, + [RECOMMENDATIONS]: { + type: "array", + items: { type: "string" }, + nullable: true, + }, }, - required: ["recommendations"], + required: [], }; const validateExtensionsJson = ajv.compile(extensionsJsonSchema); const extensionsConfig: ExtensionsJson = { - recommendations: ["redhat.vscode-yaml", "FluenceLabs.aqua"], + [RECOMMENDATIONS]: ["redhat.vscode-yaml", "FluenceLabs.aqua"], }; -const ensureRecommendedExtensions = async ( - projectPath: string -): Promise => { - const vscodeDirPath = path.join(projectPath, VSCODE_DIR_NAME); - await fsPromises.mkdir(vscodeDirPath, { recursive: true }); - const extensionsJsonPath = path.join( - vscodeDirPath, - EXTENSIONS_JSON_FILE_NAME - ); +const ensureRecommendedExtensions = async (): Promise => { + const extensionsJsonPath = await ensureVSCodeExtensionsJsonPath(); let fileContent: string; try { fileContent = await fsPromises.readFile(extensionsJsonPath, FS_OPTIONS); } catch { - await fsPromises.writeFile( - extensionsJsonPath, - JSON.stringify(extensionsConfig, null, 2) + "\n", - FS_OPTIONS - ); + fileContent = JSON.stringify({}); + await fsPromises.writeFile(extensionsJsonPath, fileContent, FS_OPTIONS); return; } @@ -120,11 +109,12 @@ const ensureRecommendedExtensions = async ( } if (validateExtensionsJson(parsedFileContent)) { - for (const recommendation of extensionsConfig.recommendations) { - if (!parsedFileContent.recommendations.includes(recommendation)) { - parsedFileContent.recommendations.push(recommendation); - } - } + parsedFileContent[RECOMMENDATIONS] = [ + ...new Set([ + ...(parsedFileContent[RECOMMENDATIONS] ?? []), + ...(extensionsConfig[RECOMMENDATIONS] ?? []), + ]), + ]; await fsPromises.writeFile( extensionsJsonPath, JSON.stringify(parsedFileContent, null, 2) + "\n", @@ -133,37 +123,36 @@ const ensureRecommendedExtensions = async ( } }; +const AQUA_SETTINGS_IMPORTS = "aquaSettings.imports"; + type SettingsJson = { - "aquaSettings.imports": Array; + [AQUA_SETTINGS_IMPORTS]?: Array; }; const settingsJsonSchema: JSONSchemaType = { type: "object", properties: { - "aquaSettings.imports": { type: "array", items: { type: "string" } }, + [AQUA_SETTINGS_IMPORTS]: { + type: "array", + items: { type: "string" }, + nullable: true, + }, }, - required: ["aquaSettings.imports"], + required: [], }; const validateSettingsJson = ajv.compile(settingsJsonSchema); -const getSettingsConfig = (): SettingsJson => ({ - "aquaSettings.imports": [getDefaultAquaPath(), getArtifactsPath()], +const initSettingsConfig = async (): Promise => ({ + [AQUA_SETTINGS_IMPORTS]: [await ensureFluenceAquaDir()], }); -const ensureRecommendedSettings = async ( - projectPath: string -): Promise => { - const vscodeDirPath = path.join(projectPath, VSCODE_DIR_NAME); - await fsPromises.mkdir(vscodeDirPath, { recursive: true }); - const settingsJsonPath = path.join(vscodeDirPath, SETTINGS_JSON_FILE_NAME); +const ensureRecommendedSettings = async (): Promise => { + const settingsJsonPath = await ensureVSCodeSettingsJsonPath(); let fileContent: string; try { fileContent = await fsPromises.readFile(settingsJsonPath, FS_OPTIONS); } catch { - await fsPromises.writeFile( - settingsJsonPath, - JSON.stringify(getSettingsConfig(), null, 2) + "\n", - FS_OPTIONS - ); + fileContent = JSON.stringify({}); + await fsPromises.writeFile(settingsJsonPath, fileContent, FS_OPTIONS); return; } @@ -175,12 +164,12 @@ const ensureRecommendedSettings = async ( } if (validateSettingsJson(parsedFileContent)) { - const settingsConfig = getSettingsConfig(); - for (const importItem of settingsConfig["aquaSettings.imports"]) { - if (!parsedFileContent["aquaSettings.imports"].includes(importItem)) { - parsedFileContent["aquaSettings.imports"].push(importItem); - } - } + parsedFileContent[AQUA_SETTINGS_IMPORTS] = [ + ...new Set([ + ...(parsedFileContent[AQUA_SETTINGS_IMPORTS] ?? []), + ...((await initSettingsConfig())[AQUA_SETTINGS_IMPORTS] ?? []), + ]), + ]; await fsPromises.writeFile( settingsJsonPath, JSON.stringify(parsedFileContent, null, 2) + "\n", @@ -189,36 +178,38 @@ const ensureRecommendedSettings = async ( } }; -const ensureGitIgnore = async (projectPath: string): Promise => { - let gitIgnoreContent: string; - const gitIgnorePath = path.join(projectPath, GITIGNORE_FILE_NAME); +const ensureGitIgnore = async (): Promise => { + const gitIgnorePath = getGitignorePath(); + let newGitIgnoreContent: string; try { - const currentGitIgnore = await fsPromises.readFile( + const currentGitIgnoreContent = await fsPromises.readFile( gitIgnorePath, FS_OPTIONS ); - const currentGitIgnoreEntries = new Set(currentGitIgnore.split("\n")); - const missingGitIgnoreEntries = GIT_IGNORE_CONTENT.split("\n") + const currentGitIgnoreEntries = new Set( + currentGitIgnoreContent.split("\n") + ); + const missingGitIgnoreEntries = RECOMMENDED_GIT_IGNORE_CONTENT.split("\n") .filter((entry): boolean => !currentGitIgnoreEntries.has(entry)) .join("\n"); - gitIgnoreContent = + newGitIgnoreContent = missingGitIgnoreEntries === "" - ? currentGitIgnore - : `${currentGitIgnore}\n# recommended by Fluence Labs:\n${missingGitIgnoreEntries}\n`; + ? currentGitIgnoreContent + : `${currentGitIgnoreContent}\n# recommended by Fluence Labs:\n${missingGitIgnoreEntries}\n`; } catch { - gitIgnoreContent = GIT_IGNORE_CONTENT; + newGitIgnoreContent = RECOMMENDED_GIT_IGNORE_CONTENT; } - return fsPromises.writeFile(gitIgnorePath, gitIgnoreContent, FS_OPTIONS); + return fsPromises.writeFile(gitIgnorePath, newGitIgnoreContent, FS_OPTIONS); }; -type InitOptions = { +type InitArg = { commandObj: CommandObj; isInteractive: boolean; projectPath?: string | undefined; }; -export const init = async (options: InitOptions): Promise => { +export const init = async (options: InitArg): Promise => { const { commandObj, isInteractive } = options; const projectPath = @@ -234,40 +225,26 @@ export const init = async (options: InitOptions): Promise => { ); try { - const aquaDefaultDirPath = path.join( - projectPath, - FLUENCE_DIR_NAME, - AQUA_DIR_NAME - ); - await fsPromises.mkdir(aquaDefaultDirPath, { recursive: true }); + await fsPromises.mkdir(projectPath, { recursive: true }); process.chdir(projectPath); - await initReadonlyFluenceConfig(commandObj); + await initNewReadonlyFluenceConfig(commandObj); - const aquaSrcDirPath = path.join(projectPath, SRC_DIR_NAME, AQUA_DIR_NAME); - await fsPromises.mkdir(aquaSrcDirPath, { recursive: true }); - const defaultSrcAquaFilePath = path.join( - getSrcAquaDirPath(), - DEFAULT_SRC_AQUA_FILE_NAME - ); + const srcMainAquaPath = await ensureSrcAquaMainPath(); try { - await fsPromises.access(defaultSrcAquaFilePath); + await fsPromises.access(srcMainAquaPath); } catch { - await fsPromises.writeFile(defaultSrcAquaFilePath, ""); + await fsPromises.writeFile(srcMainAquaPath, ""); } - const artifactsDirPath = path.join(projectPath, ARTIFACTS_DIR_NAME); - await fsPromises.mkdir(artifactsDirPath, { recursive: true }); - - await ensureRecommendedExtensions(projectPath); - await ensureRecommendedSettings(projectPath); - await ensureGitIgnore(projectPath); + await ensureRecommendedExtensions(); + await ensureRecommendedSettings(); + await ensureGitIgnore(); commandObj.log( color.magentaBright( - `\nFluence project successfully initialized at ${replaceHomedir( - projectPath, - "~" + `\nSuccessfully initialized Fluence project template at ${replaceHomeDir( + projectPath )}\n` ) ); diff --git a/src/commands/module/add.ts b/src/commands/module/add.ts new file mode 100644 index 000000000..52df76ec8 --- /dev/null +++ b/src/commands/module/add.ts @@ -0,0 +1,146 @@ +/** + * Copyright 2022 Fluence Labs Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import assert from "node:assert"; +import path from "node:path"; + +import color from "@oclif/color"; +import { Command, Flags } from "@oclif/core"; +import camelcase from "camelcase"; + +import { initFluenceConfig } from "../../lib/configs/project/fluence"; +import { initServiceConfig } from "../../lib/configs/project/service"; +import { + FLUENCE_CONFIG_FILE_NAME, + NO_INPUT_FLAG, + SERVICE_CONFIG_FILE_NAME, +} from "../../lib/const"; +import { isUrl, stringToCamelCaseName } from "../../lib/helpers/downloadFile"; +import { getIsInteractive } from "../../lib/helpers/getIsInteractive"; +import { replaceHomeDir } from "../../lib/helpers/replaceHomeDir"; +import { input } from "../../lib/prompt"; +import { hasKey } from "../../lib/typeHelpers"; + +const PATH_OR_URL = "PATH | URL"; +const NAME_FLAG_NAME = "name"; + +export default class Add extends Command { + static override description = `Add module to ${color.yellow( + SERVICE_CONFIG_FILE_NAME + )}`; + static override examples = ["<%= config.bin %> <%= command.id %>"]; + static override flags = { + ...NO_INPUT_FLAG, + [NAME_FLAG_NAME]: Flags.string({ + description: "Unique module name", + helpValue: "", + }), + service: Flags.directory({ + description: `Service name from ${color.yellow( + FLUENCE_CONFIG_FILE_NAME + )} or path to the service directory`, + helpValue: "", + }), + }; + static override args = [ + { + name: PATH_OR_URL, + description: "Path to a module or url to .tar.gz archive", + }, + ]; + async run(): Promise { + const { args, flags } = await this.parse(Add); + const isInteractive = getIsInteractive(flags); + const pathToModule: unknown = + args[PATH_OR_URL] ?? + (await input({ + isInteractive, + message: "Enter path to a module or url to .tar.gz archive", + })); + assert(typeof pathToModule === "string"); + const serviceNameOrPath = + flags.service ?? + (await input({ + isInteractive, + message: `Enter service name from ${color.yellow( + FLUENCE_CONFIG_FILE_NAME + )} or path to the service directory`, + })); + const fluenceConfig = await initFluenceConfig(this); + let servicePath = serviceNameOrPath; + if (hasKey(serviceNameOrPath, fluenceConfig?.services)) { + const serviceGet = fluenceConfig?.services[serviceNameOrPath]?.get; + assert(typeof serviceGet === "string"); + servicePath = serviceGet; + } + if (isUrl(servicePath)) { + this.error( + `Can't modify downloaded service ${color.yellow(servicePath)}` + ); + } + const serviceConfig = await initServiceConfig( + path.resolve(servicePath), + this + ); + if (serviceConfig === null) { + this.error( + `Directory ${color.yellow(servicePath)} does not contain ${color.yellow( + SERVICE_CONFIG_FILE_NAME + )}` + ); + } + const moduleName = + flags[NAME_FLAG_NAME] ?? + stringToCamelCaseName(path.basename(pathToModule)); + if (camelcase(moduleName) !== moduleName) { + this.error( + `Module name ${color.yellow( + moduleName + )} not in camelCase. Please use ${color.yellow( + `--${NAME_FLAG_NAME}` + )} flag to specify service name` + ); + } + if (moduleName in serviceConfig.modules) { + this.error( + `You already have ${color.yellow(moduleName)} in ${color.yellow( + SERVICE_CONFIG_FILE_NAME + )}. Provide a unique name for the new module using ${color.yellow( + `--${NAME_FLAG_NAME}` + )} flag or edit the existing module in ${color.yellow( + SERVICE_CONFIG_FILE_NAME + )} manually` + ); + } + serviceConfig.modules = { + ...serviceConfig.modules, + [moduleName]: { + get: isUrl(pathToModule) + ? pathToModule + : path.relative( + path.resolve(servicePath), + path.resolve(pathToModule) + ), + }, + }; + await serviceConfig.$commit(); + this.log( + `Added ${color.yellow(moduleName)} to ${color.yellow( + replaceHomeDir(path.resolve(servicePath)) + )}` + ); + } +} diff --git a/src/commands/module/new.ts b/src/commands/module/new.ts new file mode 100644 index 000000000..4cced443e --- /dev/null +++ b/src/commands/module/new.ts @@ -0,0 +1,72 @@ +/** + * Copyright 2022 Fluence Labs Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import assert from "node:assert"; +import fsPromises from "node:fs/promises"; +import path from "node:path"; + +import color from "@oclif/color"; +import { Command } from "@oclif/core"; + +import { initNewReadonlyModuleConfig } from "../../lib/configs/project/module"; +import { CommandObj, NO_INPUT_FLAG } from "../../lib/const"; +import { ensureFluenceProject } from "../../lib/helpers/ensureFluenceProject"; +import { getIsInteractive } from "../../lib/helpers/getIsInteractive"; +import { initMarineCli } from "../../lib/marineCli"; + +const PATH = "PATH"; + +export default class New extends Command { + static override description = "Create new marine module template"; + static override examples = ["<%= config.bin %> <%= command.id %>"]; + static override flags = { + ...NO_INPUT_FLAG, + }; + static override args = [ + { + name: PATH, + description: "Path to a module", + }, + ]; + async run(): Promise { + const { args, flags } = await this.parse(New); + const isInteractive = getIsInteractive(flags); + await ensureFluenceProject(this, isInteractive); + const pathToModuleDir: unknown = args[PATH]; + assert(typeof pathToModuleDir === "string"); + await generateNewModule(pathToModuleDir, this); + this.log( + `Successfully generated template for new module at ${color.yellow( + pathToModuleDir + )}` + ); + } +} + +export const generateNewModule = async ( + pathToModuleDir: string, + commandObj: CommandObj +): Promise => { + await fsPromises.mkdir(pathToModuleDir, { recursive: true }); + const marineCli = await initMarineCli(commandObj); + const name = path.basename(pathToModuleDir); + await marineCli({ + command: "generate", + flags: { init: true, name }, + workingDir: pathToModuleDir, + }); + await initNewReadonlyModuleConfig(pathToModuleDir, commandObj, name); +}; diff --git a/src/commands/module/remove.ts b/src/commands/module/remove.ts new file mode 100644 index 000000000..8a2beee04 --- /dev/null +++ b/src/commands/module/remove.ts @@ -0,0 +1,143 @@ +/** + * Copyright 2022 Fluence Labs Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import assert from "node:assert"; +import path from "node:path"; + +import color from "@oclif/color"; +import { Command, Flags } from "@oclif/core"; + +import { initFluenceConfig } from "../../lib/configs/project/fluence"; +import { + FACADE_MODULE_NAME, + initServiceConfig, +} from "../../lib/configs/project/service"; +import { + FLUENCE_CONFIG_FILE_NAME, + NO_INPUT_FLAG, + SERVICE_CONFIG_FILE_NAME, +} from "../../lib/const"; +import { isUrl } from "../../lib/helpers/downloadFile"; +import { getIsInteractive } from "../../lib/helpers/getIsInteractive"; +import { input } from "../../lib/prompt"; +import { hasKey } from "../../lib/typeHelpers"; + +const NAME_OR_PATH_OR_URL = "NAME | PATH | URL"; + +export default class Remove extends Command { + static override description = `Remove module from ${color.yellow( + SERVICE_CONFIG_FILE_NAME + )}`; + static override examples = ["<%= config.bin %> <%= command.id %>"]; + static override flags = { + ...NO_INPUT_FLAG, + service: Flags.directory({ + description: `Service name from ${color.yellow( + FLUENCE_CONFIG_FILE_NAME + )} or path to the service directory`, + helpValue: "", + }), + }; + static override args = [ + { + name: NAME_OR_PATH_OR_URL, + description: `Module name from ${color.yellow( + SERVICE_CONFIG_FILE_NAME + )}, path to a module or url to .tar.gz archive`, + }, + ]; + async run(): Promise { + const { args, flags } = await this.parse(Remove); + const isInteractive = getIsInteractive(flags); + const nameOrPathOrUrlFromArgs: unknown = args[NAME_OR_PATH_OR_URL]; + assert( + typeof nameOrPathOrUrlFromArgs === "string" || + typeof nameOrPathOrUrlFromArgs === "undefined" + ); + const nameOrPathOrUrl = + nameOrPathOrUrlFromArgs ?? + (await input({ + isInteractive, + message: `Enter module name from ${color.yellow( + SERVICE_CONFIG_FILE_NAME + )}, path to a module or url to .tar.gz archive`, + })); + assert(typeof nameOrPathOrUrl === "string"); + const serviceNameOrPath = + flags.service ?? + (await input({ + isInteractive, + message: `Enter service name from ${color.yellow( + FLUENCE_CONFIG_FILE_NAME + )} or path to the service directory`, + })); + const fluenceConfig = await initFluenceConfig(this); + let servicePath = serviceNameOrPath; + if (hasKey(serviceNameOrPath, fluenceConfig?.services)) { + const serviceGet = fluenceConfig?.services[serviceNameOrPath]?.get; + assert(typeof serviceGet === "string"); + servicePath = serviceGet; + } + if (isUrl(servicePath)) { + this.error( + `Can't modify downloaded service ${color.yellow(servicePath)}` + ); + } + const serviceConfig = await initServiceConfig( + path.resolve(servicePath), + this + ); + if (serviceConfig === null) { + this.error( + `Directory ${color.yellow(servicePath)} does not contain ${color.yellow( + SERVICE_CONFIG_FILE_NAME + )}` + ); + } + if (nameOrPathOrUrl === FACADE_MODULE_NAME) { + this.error( + `Each service must have a facade module, if you want to change it either override it in ${color.yellow( + FLUENCE_CONFIG_FILE_NAME + )} or replace it manually in ${color.yellow(SERVICE_CONFIG_FILE_NAME)}` + ); + } else if (nameOrPathOrUrl in serviceConfig.modules) { + delete serviceConfig.modules[nameOrPathOrUrl]; + } else if ( + Object.values(serviceConfig.modules).some( + ({ get }): boolean => get === nameOrPathOrUrl + ) + ) { + const [moduleName] = + Object.entries(serviceConfig.modules).find( + ([, { get }]): boolean => get === nameOrPathOrUrl + ) ?? []; + assert(typeof moduleName === "string"); + delete serviceConfig.modules[moduleName]; + } else { + this.error( + `There is no module ${color.yellow(nameOrPathOrUrl)} in ${color.yellow( + servicePath + )}` + ); + } + await serviceConfig.$commit(); + this.log( + `Removed module ${color.yellow(nameOrPathOrUrl)} from ${color.yellow( + servicePath + )}` + ); + } +} diff --git a/src/commands/remove.ts b/src/commands/remove.ts index 07f090176..f6020c077 100644 --- a/src/commands/remove.ts +++ b/src/commands/remove.ts @@ -16,52 +16,64 @@ import fsPromises from "node:fs/promises"; -import { Command } from "@oclif/core"; +import color from "@oclif/color"; +import { Command, Flags } from "@oclif/core"; import { initAquaCli } from "../lib/aquaCli"; -import { AppConfig, initAppConfig, Services } from "../lib/configs/project/app"; +import { + AppConfig, + initAppConfig, + ServicesV2, +} from "../lib/configs/project/app"; import { CommandObj, NO_INPUT_FLAG, TIMEOUT_FLAG } from "../lib/const"; -import { updateDeployedAppAqua, generateRegisterApp } from "../lib/deployedApp"; +import { + generateDeployedAppAqua, + generateRegisterApp, +} from "../lib/deployedApp"; +import { ensureFluenceProject } from "../lib/helpers/ensureFluenceProject"; import { getIsInteractive } from "../lib/helpers/getIsInteractive"; -import { getMessageWithKeyValuePairs } from "../lib/helpers/getMessageWithKeyValuePairs"; -import { usage } from "../lib/helpers/usage"; -import { getKeyPair } from "../lib/keyPairs/getKeyPair"; +import { replaceHomeDir } from "../lib/helpers/replaceHomeDir"; +import { getKeyPair } from "../lib/keypairs"; import { getRandomRelayAddr } from "../lib/multiaddr"; -import { getDeployedAppAquaPath } from "../lib/pathsGetters/getDefaultAquaPath"; -import { - getAppJsPath, - getDeployedAppJsPath, -} from "../lib/pathsGetters/getJsPath"; -import { ensureProjectFluenceDirPath } from "../lib/pathsGetters/getProjectFluenceDirPath"; import { - getAppTsPath, - getDeployedAppTsPath, -} from "../lib/pathsGetters/getTsPath"; + ensureFluenceJSAppPath, + ensureFluenceTSAppPath, + ensureFluenceAquaDeployedAppPath, + ensureFluenceJSDeployedAppPath, + ensureFluenceTSDeployedAppPath, +} from "../lib/paths"; import { confirm } from "../lib/prompt"; export default class Remove extends Command { static override description = "Remove previously deployed config"; static override examples = ["<%= config.bin %> <%= command.id %>"]; static override flags = { + relay: Flags.string({ + description: "Relay node multiaddr", + helpValue: "", + }), ...TIMEOUT_FLAG, ...NO_INPUT_FLAG, }; - static override usage: string = usage(this); async run(): Promise { const { flags } = await this.parse(Remove); const isInteractive = getIsInteractive(flags); - await ensureProjectFluenceDirPath(this, isInteractive); + await ensureFluenceProject(this, isInteractive); const appConfig = await initAppConfig(this); if (appConfig === null) { - this.error("There is nothing to remove"); + this.error( + "Seems like project is not currently deployed. Nothing to remove" + ); } if ( - isInteractive && + isInteractive && // when isInteractive is false - removeApp without asking !(await confirm({ - message: "Are you sure you want to remove your app?", + message: `Are you sure you want to remove app described in ${color.yellow( + replaceHomeDir(appConfig.$getPath()) + )}?`, isInteractive, })) ) { @@ -72,6 +84,7 @@ export default class Remove extends Command { appConfig, commandObj: this, timeout: flags.timeout, + relay: flags.relay, isInteractive, }); } @@ -90,82 +103,82 @@ export const removeApp = async ({ timeout, appConfig, isInteractive, + relay, }: Readonly<{ commandObj: CommandObj; - timeout: string | undefined; + timeout: number | undefined; appConfig: AppConfig; isInteractive: boolean; + relay: string | undefined; }>): Promise => { - const { keyPairName, timestamp, services } = appConfig; + commandObj.log( + `Going to remove app described in ${color.yellow( + replaceHomeDir(appConfig.$getPath()) + )}` + ); + const { keyPairName, services, relays } = appConfig; const keyPair = await getKeyPair({ commandObj, keyPairName, isInteractive }); - - if (keyPair instanceof Error) { - commandObj.warn( - getMessageWithKeyValuePairs(`${keyPair.message}. From config`, { - "deployed at": timestamp, - }) - ); - return; - } - const aquaCli = await initAquaCli(commandObj); - const notRemovedServices: Services = {}; - const addr = getRandomRelayAddr(); - - for (const [name, servicesByName] of Object.entries(services)) { - const notRemovedServicesByName = []; - - for (const service of servicesByName) { - const { serviceId, peerId, blueprintId } = service; - try { - // eslint-disable-next-line no-await-in-loop - await aquaCli( - { - command: "remote remove_service", - flags: { - addr, - id: serviceId, - sk: keyPair.secretKey, - on: peerId, - timeout, + const notRemovedServices: ServicesV2 = {}; + const addr = relay ?? getRandomRelayAddr(relays); + + for (const [serviceName, servicesByName] of Object.entries(services)) { + const notRemovedServicesByName: typeof servicesByName = {}; + for (const [deployId, services] of Object.entries(servicesByName)) { + for (const service of services) { + const { serviceId, peerId } = service; + try { + // eslint-disable-next-line no-await-in-loop + await aquaCli( + { + command: "remote remove_service", + flags: { + addr, + id: serviceId, + sk: keyPair.secretKey, + on: peerId, + timeout, + }, }, - }, - `Removing service`, - { - name, - id: serviceId, - blueprintId, - relay: addr, - "deployed on": peerId, - "deployed at": timestamp, - } - ); - } catch (error) { - commandObj.warn(`When removing service\n${String(error)}`); - notRemovedServicesByName.push(service); + "Removing", + { + service: serviceName, + deployId, + serviceId, + } + ); + } catch (error) { + commandObj.warn(`When removing service\n${String(error)}`); + notRemovedServicesByName[deployId] = [ + ...(notRemovedServicesByName[deployId] ?? []), + service, + ]; + } } } - if (notRemovedServicesByName.length > 0) { - notRemovedServices[name] = notRemovedServicesByName; + if (Object.keys(notRemovedServicesByName).length > 0) { + notRemovedServices[serviceName] = notRemovedServicesByName; } } if (Object.keys(notRemovedServices).length === 0) { + const pathsToRemove = await Promise.all([ + ensureFluenceAquaDeployedAppPath(), + ensureFluenceTSAppPath(), + ensureFluenceJSAppPath(), + ensureFluenceTSDeployedAppPath(), + ensureFluenceJSDeployedAppPath(), + Promise.resolve(appConfig.$getPath()), + ]); + await Promise.allSettled( - [ - getDeployedAppAquaPath(), - getAppTsPath(), - getAppJsPath(), - getDeployedAppTsPath(), - getDeployedAppJsPath(), - appConfig.$getPath(), - ].map((path): Promise => fsPromises.unlink(path)) + pathsToRemove.map((path): Promise => fsPromises.unlink(path)) ); return; } - await updateDeployedAppAqua(notRemovedServices); + await generateDeployedAppAqua(notRemovedServices); await generateRegisterApp({ deployedServices: notRemovedServices, aquaCli, diff --git a/src/commands/run.ts b/src/commands/run.ts index 3a5e3a190..c69b8570d 100644 --- a/src/commands/run.ts +++ b/src/commands/run.ts @@ -30,33 +30,30 @@ import { TIMEOUT_FLAG, } from "../lib/const"; import { getAppJson } from "../lib/deployedApp"; +import { ensureFluenceProject } from "../lib/helpers/ensureFluenceProject"; import { getIsInteractive } from "../lib/helpers/getIsInteractive"; -import { usage } from "../lib/helpers/usage"; -import { getRandomRelayId, getRandomRelayAddr } from "../lib/multiaddr"; -import { getMaybeArtifactsPath } from "../lib/pathsGetters/getArtifactsPath"; -import { getDefaultAquaPath } from "../lib/pathsGetters/getDefaultAquaPath"; -import { getSrcMainAquaPath } from "../lib/pathsGetters/getSrcAquaDirPath"; +import { getRandomRelayAddr } from "../lib/multiaddr"; import { - getAppServiceJsonPath, - getTmpPath, -} from "../lib/pathsGetters/getTmpPath"; -import { confirm, input, list } from "../lib/prompt"; + ensureFluenceTmpAppServiceJsonPath, + ensureFluenceAquaDir, + ensureSrcAquaMainPath, +} from "../lib/paths"; +import { input } from "../lib/prompt"; const FUNC_FLAG_NAME = "func"; const INPUT_FLAG_NAME = "input"; const ON_FLAG_NAME = "on"; +const DATA_FLAG_NAME = "data"; export default class Run extends Command { static override description = "Run aqua script"; - static override examples = ["<%= config.bin %> <%= command.id %>"]; - static override flags = { relay: Flags.string({ - description: "Relay node MultiAddress", + description: "Relay node multiaddr", helpValue: "", }), - data: Flags.string({ + [DATA_FLAG_NAME]: Flags.string({ description: "JSON in { [argumentName]: argumentValue } format. You can call a function using these argument names", helpValue: "", @@ -94,15 +91,12 @@ export default class Run extends Command { ...TIMEOUT_FLAG, ...NO_INPUT_FLAG, }; - - static override usage: string = usage(this); - async run(): Promise { const { flags } = await this.parse(Run); const isInteractive = getIsInteractive(flags); + await ensureFluenceProject(this, isInteractive); - const on = await ensurePeerId(flags.on, this, isInteractive); - const aqua = await ensureAquaPath(flags[INPUT_FLAG_NAME], isInteractive); + const aqua = await ensureAquaPath(flags[INPUT_FLAG_NAME]); const func = flags[FUNC_FLAG_NAME] ?? @@ -112,18 +106,16 @@ export default class Run extends Command { flagName: FUNC_FLAG_NAME, })); - const relay = flags.relay ?? getRandomRelayAddr(); + const appConfig = await initReadonlyAppConfig(this); + const relay = flags.relay ?? getRandomRelayAddr(appConfig?.relays); const data = await getRunData(flags, this); - const imports = [ + const imports: Array = [ ...(flags.import ?? []), - getDefaultAquaPath(), - await getMaybeArtifactsPath(), + await ensureFluenceAquaDir(), ]; - const appConfig = await initReadonlyAppConfig(this); - await fsPromises.mkdir(getTmpPath(), { recursive: true }); - const appJsonServicePath = getAppServiceJsonPath(); + const appJsonServicePath = await ensureFluenceTmpAppServiceJsonPath(); if (appConfig !== null) { await fsPromises.writeFile( appJsonServicePath, @@ -141,7 +133,6 @@ export default class Run extends Command { addr: relay, func, input: aqua, - on, timeout: flags.timeout, import: imports, "json-service": appJsonServicePath, @@ -149,7 +140,7 @@ export default class Run extends Command { }, }, "Running", - { function: func, on, relay } + { function: func, relay } ); } finally { if (appConfig !== null) { @@ -161,79 +152,14 @@ export default class Run extends Command { } } -const ensurePeerId = async ( - onFromArgs: string | undefined, - commandObj: CommandObj, - isInteractive: boolean -): Promise => { - if (typeof onFromArgs === "string") { - return onFromArgs; - } - const appConfig = await initReadonlyAppConfig(commandObj); - - const peerIdsFromDeployed = [ - ...new Set( - Object.values(appConfig?.services ?? {}).flatMap( - (deployedServiceConfig): Array => - deployedServiceConfig.map(({ peerId }): string => peerId) - ) - ), - ]; - const firstPeerId = peerIdsFromDeployed[0]; - if (peerIdsFromDeployed.length === 1 && firstPeerId !== undefined) { - return firstPeerId; - } - - const options = - peerIdsFromDeployed.length > 1 && - (await confirm({ - message: - "Do you want to select one of the peers from your app to run the function?", - isInteractive, - flagName: ON_FLAG_NAME, - })) - ? peerIdsFromDeployed - : [getRandomRelayId()]; - - return list({ - message: "Select peerId of the peer where you want to run the function", - options, - onNoChoices: (): Promise => - input({ - message: - "Enter a peerId of the peer where you want to run your function", - isInteractive, - flagName: ON_FLAG_NAME, - }), - oneChoiceMessage: (peerId): string => - `Do you want to run your function on a random peer ${color.yellow( - peerId - )}`, - isInteractive, - flagName: ON_FLAG_NAME, - }); -}; - const ensureAquaPath = async ( - aquaPathFromArgs: string | undefined, - isInteractive: boolean + aquaPathFromArgs: string | undefined ): Promise => { if (typeof aquaPathFromArgs === "string") { return aquaPathFromArgs; } - try { - const srcMainAquaPath = getSrcMainAquaPath(); - await fsPromises.access(srcMainAquaPath); - return srcMainAquaPath; - } catch { - return input({ - message: - "Enter a path to an aqua file or to a directory that contains aqua files", - isInteractive, - flagName: INPUT_FLAG_NAME, - }); - } + return ensureSrcAquaMainPath(); }; type RunData = Record; @@ -286,11 +212,11 @@ const getRunData = async ( try { parsedData = JSON.parse(data); } catch { - commandObj.error("Unable to parse --data"); + commandObj.error(`Unable to parse --${DATA_FLAG_NAME}`); } if (!validateRunData(parsedData)) { commandObj.error( - `Invalid --data: ${JSON.stringify(validateRunData.errors)}` + `Invalid --${DATA_FLAG_NAME}: ${JSON.stringify(validateRunData.errors)}` ); } for (const key in parsedData) { diff --git a/src/commands/service/add.ts b/src/commands/service/add.ts new file mode 100644 index 000000000..de4d4b1d3 --- /dev/null +++ b/src/commands/service/add.ts @@ -0,0 +1,133 @@ +/** + * Copyright 2022 Fluence Labs Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import assert from "node:assert"; + +import color from "@oclif/color"; +import { Command, Flags } from "@oclif/core"; +import camelcase from "camelcase"; + +import { initFluenceConfig } from "../../lib/configs/project/fluence"; +import { + CommandObj, + DEFAULT_DEPLOY_NAME, + FLUENCE_CONFIG_FILE_NAME, + NO_INPUT_FLAG, +} from "../../lib/const"; +import { stringToCamelCaseName } from "../../lib/helpers/downloadFile"; +import { ensureFluenceProject } from "../../lib/helpers/ensureFluenceProject"; +import { getIsInteractive } from "../../lib/helpers/getIsInteractive"; +import { input } from "../../lib/prompt"; + +const PATH_OR_URL = "PATH | URL"; +const NAME_FLAG_NAME = "name"; + +export default class Add extends Command { + static override description = `Add service to ${color.yellow( + FLUENCE_CONFIG_FILE_NAME + )}`; + static override examples = ["<%= config.bin %> <%= command.id %>"]; + static override flags = { + ...NO_INPUT_FLAG, + [NAME_FLAG_NAME]: Flags.string({ + description: "Unique service name", + helpValue: "", + }), + }; + static override args = [ + { + name: PATH_OR_URL, + description: "Path to a service or url to .tar.gz archive", + }, + ]; + async run(): Promise { + const { args, flags } = await this.parse(Add); + const isInteractive = getIsInteractive(flags); + await ensureFluenceProject(this, isInteractive); + const pathOrUrlFromArgs: unknown = args[PATH_OR_URL]; + assert( + typeof pathOrUrlFromArgs === "string" || + typeof pathOrUrlFromArgs === "undefined" + ); + await addService({ + commandObj: this, + nameFromFlags: flags[NAME_FLAG_NAME], + pathOrUrl: + pathOrUrlFromArgs ?? + (await input({ isInteractive, message: "Enter service path or url" })), + }); + } +} + +type AddServiceArg = { + commandObj: CommandObj; + nameFromFlags: string | undefined; + pathOrUrl: string; +}; + +export const addService = async ({ + commandObj, + nameFromFlags, + pathOrUrl: pathOrUrlFromArgs, +}: AddServiceArg): Promise => { + const fluenceConfig = await initFluenceConfig(commandObj); + if (fluenceConfig === null) { + return commandObj.error( + "You must init Fluence project first to add services" + ); + } + if (fluenceConfig.services === undefined) { + fluenceConfig.services = {}; + } + const serviceName = nameFromFlags ?? stringToCamelCaseName(pathOrUrlFromArgs); + if (camelcase(serviceName) !== serviceName) { + commandObj.error( + `Service name ${color.yellow( + serviceName + )} not in camelCase. Please use ${color.yellow( + `--${NAME_FLAG_NAME}` + )} flag to specify service name` + ); + } + if (serviceName in fluenceConfig.services) { + commandObj.error( + `You already have ${color.yellow(serviceName)} in ${color.yellow( + FLUENCE_CONFIG_FILE_NAME + )}. Provide a unique name for the new service using ${color.yellow( + `--${NAME_FLAG_NAME}` + )} flag or edit the existing service in ${color.yellow( + FLUENCE_CONFIG_FILE_NAME + )} manually` + ); + } + fluenceConfig.services = { + ...fluenceConfig.services, + [serviceName]: { + get: pathOrUrlFromArgs, + deploy: [ + { + deployId: DEFAULT_DEPLOY_NAME, + }, + ], + }, + }; + await fluenceConfig.$commit(); + commandObj.log( + `Added ${color.yellow(serviceName)} to ${color.yellow( + FLUENCE_CONFIG_FILE_NAME + )}` + ); +}; diff --git a/src/commands/service/new.ts b/src/commands/service/new.ts new file mode 100644 index 000000000..3a27b204a --- /dev/null +++ b/src/commands/service/new.ts @@ -0,0 +1,98 @@ +/** + * Copyright 2022 Fluence Labs Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import assert from "node:assert"; +import path from "node:path"; + +import color from "@oclif/color"; +import { Command, Flags } from "@oclif/core"; + +import { + FACADE_MODULE_NAME, + initNewReadonlyServiceConfig, +} from "../../lib/configs/project/service"; +import { FLUENCE_CONFIG_FILE_NAME, NO_INPUT_FLAG } from "../../lib/const"; +import { ensureFluenceProject } from "../../lib/helpers/ensureFluenceProject"; +import { getIsInteractive } from "../../lib/helpers/getIsInteractive"; +import { confirm, input } from "../../lib/prompt"; +import { generateNewModule } from "../module/new"; + +import { addService } from "./add"; + +const PATH = "PATH"; +const NAME_FLAG_NAME = "name"; + +export default class New extends Command { + static override description = "Create new marine service template"; + static override examples = ["<%= config.bin %> <%= command.id %>"]; + static override flags = { + ...NO_INPUT_FLAG, + [NAME_FLAG_NAME]: Flags.string({ + description: "Unique service name", + helpValue: "", + }), + }; + static override args = [ + { + name: PATH, + description: "Path to a service", + }, + ]; + async run(): Promise { + const { args, flags } = await this.parse(New); + const isInteractive = getIsInteractive(flags); + await ensureFluenceProject(this, isInteractive); + const servicePathFromArgs: unknown = args[PATH]; + assert( + typeof servicePathFromArgs === "string" || + typeof servicePathFromArgs === "undefined" + ); + const servicePath = + servicePathFromArgs ?? + (await input({ isInteractive, message: "Enter service path" })); + const pathToModuleDir = path.join( + servicePath, + "modules", + FACADE_MODULE_NAME + ); + await generateNewModule(pathToModuleDir, this); + await initNewReadonlyServiceConfig( + servicePath, + this, + path.relative(servicePath, pathToModuleDir) + ); + this.log( + `Successfully generated template for new service at ${color.yellow( + servicePath + )}` + ); + if ( + isInteractive && + (await confirm({ + isInteractive, + message: `Do you want add ${color.yellow( + servicePath + )} to ${color.yellow(FLUENCE_CONFIG_FILE_NAME)}?`, + })) + ) { + await addService({ + commandObj: this, + nameFromFlags: flags[NAME_FLAG_NAME], + pathOrUrl: servicePath, + }); + } + } +} diff --git a/src/commands/service/remove.ts b/src/commands/service/remove.ts new file mode 100644 index 000000000..70e40c3c4 --- /dev/null +++ b/src/commands/service/remove.ts @@ -0,0 +1,102 @@ +/** + * Copyright 2022 Fluence Labs Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import assert from "node:assert"; + +import color from "@oclif/color"; +import { Command } from "@oclif/core"; + +import { initFluenceConfig } from "../../lib/configs/project/fluence"; +import { FLUENCE_CONFIG_FILE_NAME, NO_INPUT_FLAG } from "../../lib/const"; +import { ensureFluenceProject } from "../../lib/helpers/ensureFluenceProject"; +import { getIsInteractive } from "../../lib/helpers/getIsInteractive"; +import { input } from "../../lib/prompt"; + +const NAME_OR_PATH_OR_URL = "NAME | PATH | URL"; + +export default class Remove extends Command { + static override description = `Remove service from ${color.yellow( + FLUENCE_CONFIG_FILE_NAME + )}`; + static override examples = ["<%= config.bin %> <%= command.id %>"]; + static override flags = { + ...NO_INPUT_FLAG, + }; + static override args = [ + { + name: NAME_OR_PATH_OR_URL, + description: `Service name from ${color.yellow( + FLUENCE_CONFIG_FILE_NAME + )}, path to a service or url to .tar.gz archive`, + }, + ]; + async run(): Promise { + const { args, flags } = await this.parse(Remove); + const isInteractive = getIsInteractive(flags); + await ensureFluenceProject(this, isInteractive); + const nameOrPathOrUrlFromArgs: unknown = args[NAME_OR_PATH_OR_URL]; + assert( + typeof nameOrPathOrUrlFromArgs === "string" || + typeof nameOrPathOrUrlFromArgs === "undefined" + ); + const nameOrPathOrUrl = + nameOrPathOrUrlFromArgs ?? + (await input({ + isInteractive, + message: `Enter service name from ${color.yellow( + FLUENCE_CONFIG_FILE_NAME + )}, path to a service or url to .tar.gz archive`, + })); + const fluenceConfig = await initFluenceConfig(this); + if (fluenceConfig === null) { + this.error("You must init Fluence project first to remove services"); + } + if (fluenceConfig.services === undefined) { + this.error( + `There are no services in ${color.yellow(FLUENCE_CONFIG_FILE_NAME)}` + ); + } + if (nameOrPathOrUrl in fluenceConfig.services) { + delete fluenceConfig.services[nameOrPathOrUrl]; + } else if ( + Object.values(fluenceConfig.services).some( + ({ get }): boolean => get === nameOrPathOrUrl + ) + ) { + const [serviceName] = + Object.entries(fluenceConfig.services).find( + ([, { get }]): boolean => get === nameOrPathOrUrl + ) ?? []; + assert(typeof serviceName === "string"); + delete fluenceConfig.services[serviceName]; + } else { + this.error( + `There is no service ${color.yellow(nameOrPathOrUrl)} in ${color.yellow( + FLUENCE_CONFIG_FILE_NAME + )}` + ); + } + if (Object.keys(fluenceConfig.services).length === 0) { + delete fluenceConfig.services; + } + await fluenceConfig.$commit(); + this.log( + `Removed service ${color.yellow(nameOrPathOrUrl)} from ${color.yellow( + FLUENCE_CONFIG_FILE_NAME + )}` + ); + } +} diff --git a/src/commands/service/repl.ts b/src/commands/service/repl.ts new file mode 100644 index 000000000..ca4c551b9 --- /dev/null +++ b/src/commands/service/repl.ts @@ -0,0 +1,253 @@ +/** + * Copyright 2022 Fluence Labs Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import assert from "node:assert"; +import { spawn } from "node:child_process"; +import fsPromises from "node:fs/promises"; +import path from "node:path"; + +import stringifyToTOML from "@iarna/toml/stringify"; +import color from "@oclif/color"; +import { CliUx, Command } from "@oclif/core"; + +import { initReadonlyFluenceConfig } from "../../lib/configs/project/fluence"; +import { initReadonlyModuleConfig } from "../../lib/configs/project/module"; +import { + initReadonlyServiceConfig, + ModuleV0 as ServiceModule, +} from "../../lib/configs/project/service"; +import { + CommandObj, + FLUENCE_CONFIG_FILE_NAME, + FS_OPTIONS, + MODULE_CONFIG_FILE_NAME, + MREPL_CARGO_DEPENDENCY, + NO_INPUT_FLAG, + SERVICE_CONFIG_FILE_NAME, +} from "../../lib/const"; +import { + downloadModule, + downloadService, + getModuleUrlOrAbsolutePath, + getModuleWasmPath, + isUrl, +} from "../../lib/helpers/downloadFile"; +import { getIsInteractive } from "../../lib/helpers/getIsInteractive"; +import { initMarineCli, MarineCLI } from "../../lib/marineCli"; +import { ensureFluenceTmpConfigTomlPath } from "../../lib/paths"; +import { input } from "../../lib/prompt"; +import { ensureCargoDependency } from "../../lib/rust"; + +const NAME_OR_PATH_OR_URL = "NAME | PATH | URL"; + +export default class REPL extends Command { + static override description = "Open service inside repl"; + static override examples = ["<%= config.bin %> <%= command.id %>"]; + static override flags = { + ...NO_INPUT_FLAG, + }; + static override args = [ + { + name: NAME_OR_PATH_OR_URL, + description: `Service name from ${color.yellow( + FLUENCE_CONFIG_FILE_NAME + )}, path to a service or url to .tar.gz archive`, + }, + ]; + async run(): Promise { + const { args, flags } = await this.parse(REPL); + const isInteractive = getIsInteractive(flags); + const nameOrPathOrUrlFromArgs: unknown = args[NAME_OR_PATH_OR_URL]; + assert( + typeof nameOrPathOrUrlFromArgs === "string" || + typeof nameOrPathOrUrlFromArgs === "undefined" + ); + const nameOrPathOrUrl = + nameOrPathOrUrlFromArgs ?? + (await input({ + isInteractive, + message: `Enter service name from ${color.yellow( + FLUENCE_CONFIG_FILE_NAME + )}, path to a service or url to .tar.gz archive`, + })); + + CliUx.ux.action.start( + "Making sure service and modules are downloaded and built" + ); + const serviceModules = await ensureServiceConfig({ + commandObj: this, + nameOrPathOrUrl, + }); + + const marineCli = await initMarineCli(this); + const moduleConfigs = await ensureModuleConfigs({ + serviceModules, + commandObj: this, + marineCli, + }); + CliUx.ux.action.stop(); + + const fluenceTmpConfigTomlPath = await ensureFluenceTmpConfigTomlPath(); + + await fsPromises.writeFile( + fluenceTmpConfigTomlPath, + stringifyToTOML({ module: moduleConfigs }), + FS_OPTIONS + ); + + spawn( + await ensureCargoDependency({ + name: MREPL_CARGO_DEPENDENCY, + commandObj: this, + }), + [fluenceTmpConfigTomlPath], + { stdio: "inherit" } + ); + } +} + +type EnsureServiceConfigArg = { + nameOrPathOrUrl: string; + commandObj: CommandObj; +}; + +const ensureServiceConfig = async ({ + commandObj, + nameOrPathOrUrl, +}: EnsureServiceConfigArg): Promise> => { + const fluenceConfig = await initReadonlyFluenceConfig(commandObj); + const get = + fluenceConfig?.services?.[nameOrPathOrUrl]?.get ?? nameOrPathOrUrl; + const serviceDirPath = isUrl(get) + ? await downloadService(get) + : path.resolve(get); + const { facade, ...otherModules } = + (await initReadonlyServiceConfig(serviceDirPath, commandObj))?.modules ?? + CliUx.ux.action.stop(color.red("error")) ?? + commandObj.error( + `Service ${color.yellow(nameOrPathOrUrl)} doesn't have ${color.yellow( + SERVICE_CONFIG_FILE_NAME + )}` + ); + return [...Object.values(otherModules), facade].map( + (mod): ServiceModule => ({ + ...mod, + get: getModuleUrlOrAbsolutePath(mod.get, serviceDirPath), + }) + ); +}; +/* eslint-disable camelcase */ +type TomlModuleConfig = { + name: string; + load_from?: string; + max_heap_size?: string; + logger_enabled?: boolean; + logging_mask?: number; + wasi?: { + preopened_files?: Array; + mapped_dirs?: Record; + envs?: Record; + }; + mounted_binaries?: Record; +}; + +type EnsureModuleConfigsArg = { + serviceModules: Array; + commandObj: CommandObj; + marineCli: MarineCLI; +}; + +const ensureModuleConfigs = ({ + commandObj, + serviceModules, + marineCli, +}: EnsureModuleConfigsArg): Promise> => + Promise.all( + serviceModules.map( + ({ get, ...overrides }): Promise => + (async (): Promise => { + const modulePath = isUrl(get) ? await downloadModule(get) : get; + const moduleConfig = + (await initReadonlyModuleConfig(modulePath, commandObj)) ?? + CliUx.ux.action.stop(color.red("error")) ?? + commandObj.error( + `Module with get: ${color.yellow( + get + )} doesn't have ${color.yellow(MODULE_CONFIG_FILE_NAME)}` + ); + + const overridenModules = { ...moduleConfig, ...overrides }; + + const { + name, + envs, + loggerEnabled, + volumes, + type, + preopenedFiles, + mountedBinaries, + maxHeapSize, + loggingMask, + } = overridenModules; + + if (type === "rust") { + await marineCli({ + command: "build", + flags: { release: true }, + workingDir: path.dirname(moduleConfig.$getPath()), + }); + } + const load_from = getModuleWasmPath(overridenModules); + const tomlModuleConfig: TomlModuleConfig = { + name, + load_from, + }; + if (loggerEnabled === true) { + tomlModuleConfig.logger_enabled = true; + } + if (typeof loggingMask === "number") { + tomlModuleConfig.logging_mask = loggingMask; + } + if (typeof maxHeapSize === "string") { + tomlModuleConfig.max_heap_size = maxHeapSize; + } + if (volumes !== undefined) { + tomlModuleConfig.wasi = { + mapped_dirs: volumes, + preopened_files: [...new Set(Object.values(volumes))], + }; + } + if (preopenedFiles !== undefined) { + tomlModuleConfig.wasi = { + preopened_files: [ + ...new Set([ + ...Object.values(volumes ?? {}), + ...preopenedFiles, + ]), + ], + }; + } + if (envs !== undefined) { + tomlModuleConfig.wasi = { envs }; + } + if (mountedBinaries !== undefined) { + tomlModuleConfig.mounted_binaries = mountedBinaries; + } + return tomlModuleConfig; + })() + ) + ); +/* eslint-enable camelcase */ diff --git a/src/lib/ajv.ts b/src/lib/ajv.ts index d64232b2f..b223c3c62 100644 --- a/src/lib/ajv.ts +++ b/src/lib/ajv.ts @@ -16,4 +16,4 @@ import Ajv from "ajv"; -export const ajv = new Ajv({ allErrors: true }); +export const ajv = new Ajv({ allowUnionTypes: true }); diff --git a/src/lib/aquaCli.ts b/src/lib/aquaCli.ts index 924ef4e11..0a3a512d0 100644 --- a/src/lib/aquaCli.ts +++ b/src/lib/aquaCli.ts @@ -14,20 +14,17 @@ * limitations under the License. */ -import { AQUA_NPM_DEPENDENCY } from "./configs/user/dependency"; -import type { CommandObj } from "./const"; +import { AQUA_NPM_DEPENDENCY, CommandObj } from "./const"; import { execPromise } from "./execPromise"; import { getMessageWithKeyValuePairs } from "./helpers/getMessageWithKeyValuePairs"; import { unparseFlags } from "./helpers/unparseFlags"; import { ensureNpmDependency } from "./npm"; +import type { Flags, OptionalFlags } from "./typeHelpers"; -type Flags = Record< - T, - string | boolean | Array ->; -type OptionalFlags = Partial< - Record> ->; +/** + * Execution timeout in milliseconds + */ +const AQUA_CLI_EXECUTION_TIMEOUT = 90_000; export type AquaCliInput = | { @@ -74,11 +71,11 @@ export const initAquaCli = async (commandObj: CommandObj): Promise => { const timeoutNumber = Number(flags.timeout); return execPromise( - `${aquaCliPath} ${command ?? ""}${unparseFlags(flags)}`, + `${aquaCliPath} ${command ?? ""}${unparseFlags(flags, commandObj)}`, message === undefined ? undefined : getMessageWithKeyValuePairs(message, keyValuePairs), - Number.isNaN(timeoutNumber) ? undefined : timeoutNumber + Number.isNaN(timeoutNumber) ? AQUA_CLI_EXECUTION_TIMEOUT : timeoutNumber ); }; }; diff --git a/src/lib/configs/initConfig.ts b/src/lib/configs/initConfig.ts index ae49f932a..d3d6cdf8c 100644 --- a/src/lib/configs/initConfig.ts +++ b/src/lib/configs/initConfig.ts @@ -19,28 +19,30 @@ import path from "node:path"; import color from "@oclif/color"; import type { AnySchema, JSONSchemaType, ValidateFunction } from "ajv"; -import replaceHomedir from "replace-homedir"; import { parse } from "yaml"; import { yamlDiffPatch } from "yaml-diff-patch"; import { ajv } from "../ajv"; import { CommandObj, FS_OPTIONS, SCHEMAS_DIR_NAME } from "../const"; +import { replaceHomeDir } from "../helpers/replaceHomeDir"; import type { ValidationResult } from "../helpers/validations"; import type { Mutable } from "../typeHelpers"; +type SchemaArg = { + name: string; + configDirPath: string; + getSchemaDirPath: GetPath | undefined; + commandObj: CommandObj; + schema: AnySchema; +}; + const ensureSchema = async ({ name, configDirPath, getSchemaDirPath, commandObj, schema, -}: { - name: string; - configDirPath: string; - getSchemaDirPath: GetPath | undefined; - commandObj: CommandObj; - schema: AnySchema; -}): Promise => { +}: SchemaArg): Promise => { const schemaDir = path.join( getSchemaDirPath === undefined ? configDirPath @@ -57,31 +59,51 @@ const ensureSchema = async ({ return path.relative(configDirPath, schemaPath); }; -const getConfigString = async ( - configPath: string, - schemaRelativePath: string, - commandObj: CommandObj, - getDefaultConfig: GetDefaultConfig | undefined -): Promise => { +type GetConfigString = { + configPath: string; + schemaRelativePath: string; + commandObj: CommandObj; + getDefaultConfig: GetDefaultConfig | undefined; + examples: string | undefined; +}; + +const getConfigString = async ({ + configPath, + schemaRelativePath, + commandObj, + getDefaultConfig, + examples, +}: GetConfigString): Promise => { + const schemaPathCommentStart = "# yaml-language-server: $schema="; + const schemaPathComment = `${schemaPathCommentStart}${schemaRelativePath}`; + let configString: string; try { const fileContent = await fsPromises.readFile(configPath, FS_OPTIONS); - return fileContent; + configString = fileContent.startsWith(schemaPathCommentStart) + ? `${[schemaPathComment, ...fileContent.split("\n").slice(1)] + .join("\n") + .trim()}` + : `${schemaPathComment}\n${fileContent}`; } catch { if (getDefaultConfig === undefined) { return null; } - const defaultConfigString = yamlDiffPatch( - `# yaml-language-server: $schema=${schemaRelativePath}`, + configString = yamlDiffPatch( + `${schemaPathComment}\n\n${ + examples === undefined + ? "" + : `EXAMPLES:${examples}` + .split("\n") + .map((ex): string => `# ${ex}`) + .join("\n") + .trim() + }`, {}, await getDefaultConfig(commandObj) ); - await fsPromises.writeFile( - configPath, - defaultConfigString + "\n", - FS_OPTIONS - ); - return defaultConfigString; } + await fsPromises.writeFile(configPath, `${configString}\n`, FS_OPTIONS); + return configString; }; type MigrateConfigOptions< @@ -93,7 +115,7 @@ type MigrateConfigOptions< configPath: string; validateLatestConfig: ValidateFunction; config: Config; - validate: undefined | ((config: LatestConfig) => ValidationResult); + validate: undefined | ConfigValidateFunction; }; const migrateConfig = async < @@ -110,9 +132,11 @@ const migrateConfig = async < latestConfig: LatestConfig; configString: string; }> => { - const migratedConfig = migrations - .slice(config.version) - .reduce((config, migration): Config => migration(config), config); + let migratedConfig = config; + for (const migration of migrations.slice(config.version)) { + // eslint-disable-next-line no-await-in-loop + migratedConfig = await migration(migratedConfig); + } const migratedConfigString = yamlDiffPatch( configString, @@ -128,7 +152,8 @@ const migrateConfig = async < )}. Errors: ${JSON.stringify(validateLatestConfig.errors, null, 2)}` ); } - const maybeValidationError = validate !== undefined && validate(latestConfig); + const maybeValidationError = + validate !== undefined && (await validate(latestConfig, configPath)); if (typeof maybeValidationError === "string") { // eslint-disable-next-line unicorn/prefer-type-error @@ -158,10 +183,10 @@ type EnsureConfigOptions< configPath: string; validateLatestConfig: ValidateFunction; config: Config; - validate: undefined | ((config: LatestConfig) => ValidationResult); + validate: undefined | ConfigValidateFunction; }; -const ensureConfigIsValidLatest = < +const ensureConfigIsValidLatest = async < Config extends BaseConfig, LatestConfig extends BaseConfig >({ @@ -169,7 +194,7 @@ const ensureConfigIsValidLatest = < validateLatestConfig, config, validate, -}: EnsureConfigOptions): LatestConfig => { +}: EnsureConfigOptions): Promise => { if (!validateLatestConfig(config)) { throw new Error( `Invalid config ${color.yellow(configPath)}. Errors: ${JSON.stringify( @@ -179,7 +204,8 @@ const ensureConfigIsValidLatest = < )}` ); } - const maybeValidationError = validate !== undefined && validate(config); + const maybeValidationError = + validate !== undefined && (await validate(config, configPath)); if (typeof maybeValidationError === "string") { // eslint-disable-next-line unicorn/prefer-type-error @@ -204,12 +230,19 @@ export type InitializedConfig = Mutable< $commit(): Promise; }; type BaseConfig = { version: number }; -export type Migrations = Array<(config: Config) => Config>; +export type Migrations = Array< + (config: Config) => Config | Promise +>; export type GetDefaultConfig = ( commandObj: CommandObj ) => LatestConfig | Promise; type GetPath = (commandObj: CommandObj) => string | Promise; +export type ConfigValidateFunction = ( + config: LatestConfig, + configPath: string +) => ValidationResult | Promise; + export type InitConfigOptions< Config extends BaseConfig, LatestConfig extends BaseConfig @@ -218,9 +251,10 @@ export type InitConfigOptions< latestSchema: JSONSchemaType; migrations: Migrations; name: string; - getPath: GetPath; + getConfigDirPath: GetPath; getSchemaDirPath?: GetPath; - validate?: (config: LatestConfig) => ValidationResult; + validate?: ConfigValidateFunction; + examples?: string; }; type InitFunction = ( @@ -239,8 +273,8 @@ type InitReadonlyFunctionWithDefault = ( commandObj: CommandObj ) => Promise>; -const getConfigPath = (configDirPath: string, name: string): string => - path.join(configDirPath, `${name}.yaml`); +export const getConfigPath = (configDirPath: string, name: string): string => + path.join(configDirPath, name); export function getReadonlyConfigInitFunction< Config extends BaseConfig, @@ -271,32 +305,36 @@ export function getReadonlyConfigInitFunction< latestSchema, migrations, name, - getPath, + getConfigDirPath, validate, getSchemaDirPath, + examples, } = options; - const configDirPath = await getPath(commandObj); + const configDirPath = await getConfigDirPath(commandObj); const configPath = getConfigPath(configDirPath, name); const validateAllConfigVersions = ajv.compile({ oneOf: allSchemas, }); + const validateLatestConfig = ajv.compile(latestSchema); + const schemaRelativePath = await ensureSchema({ name, configDirPath, getSchemaDirPath, commandObj, - schema: validateAllConfigVersions.schema, + schema: validateLatestConfig.schema, }); - const maybeConfigString = await getConfigString( + const maybeConfigString = await getConfigString({ configPath, schemaRelativePath, commandObj, - getDefaultConfig - ); + getDefaultConfig, + examples, + }); if (maybeConfigString === null) { return null; } @@ -315,8 +353,6 @@ export function getReadonlyConfigInitFunction< ); } - const validateLatestConfig = ajv.compile(latestSchema); - let latestConfig: LatestConfig; if (config.version < migrations.length) { ({ latestConfig, configString } = await migrateConfig({ @@ -328,7 +364,7 @@ export function getReadonlyConfigInitFunction< validate, })); } else { - latestConfig = ensureConfigIsValidLatest({ + latestConfig = await ensureConfigIsValidLatest({ config, configPath, validateLatestConfig, @@ -375,14 +411,15 @@ export function getConfigInitFunction< return async ( commandObj: CommandObj ): Promise | null> => { - const configDirPath = await options.getPath(commandObj); - const configPath = getConfigPath(configDirPath, options.name); + const configPath = getConfigPath( + await options.getConfigDirPath(commandObj), + options.name + ); if (initializedConfigs.has(configPath)) { throw new Error( - `Mutable config ${replaceHomedir( - configPath, - "~" + `Mutable config ${replaceHomeDir( + configPath )} was already initialized. Please initialize readonly config instead or use previously initialized mutable config` ); } diff --git a/src/lib/configs/project/app.ts b/src/lib/configs/project/app.ts index 70b1cefe9..dd7e05e97 100644 --- a/src/lib/configs/project/app.ts +++ b/src/lib/configs/project/app.ts @@ -17,8 +17,9 @@ import type { JSONSchemaType } from "ajv"; import { ajv } from "../../ajv"; -import { APP_FILE_NAME, CommandObj } from "../../const"; -import { getProjectFluenceDirPath } from "../../pathsGetters/getProjectFluenceDirPath"; +import { APP_CONFIG_FILE_NAME, CommandObj } from "../../const"; +import { NETWORKS, Relays } from "../../multiaddr"; +import { ensureFluenceDir } from "../../paths"; import { getConfigInitFunction, InitConfigOptions, @@ -79,14 +80,15 @@ const configSchemaV0: JSONSchemaType = { required: ["version", "services", "keyPairName", "timestamp"], }; -export type Services = Record>; +type ServicesV1 = Record>; type ConfigV1 = { version: 1; - services: Services; + services: ServicesV1; keyPairName: string; timestamp: string; knownRelays?: Array; + relays?: Relays; }; const configSchemaV1: JSONSchemaType = { @@ -118,11 +120,71 @@ const configSchemaV1: JSONSchemaType = { nullable: true, items: { type: "string" }, }, + relays: { + type: ["string", "array"], + oneOf: [ + { type: "string", enum: NETWORKS }, + { type: "array", items: { type: "string" } }, + ], + nullable: true, + }, + }, + required: ["version", "services", "keyPairName", "timestamp"], +}; + +export type ServicesV2 = Record< + string, + Record> +>; + +type ConfigV2 = { + version: 2; + services: ServicesV2; + keyPairName: string; + timestamp: string; + relays?: Relays; +}; + +const configSchemaV2: JSONSchemaType = { + type: "object", + properties: { + version: { type: "number", enum: [2] }, + services: { + type: "object", + additionalProperties: { + type: "object", + additionalProperties: { + type: "array", + items: { + type: "object", + properties: { + peerId: { type: "string" }, + serviceId: { type: "string" }, + blueprintId: { type: "string" }, + }, + required: ["peerId", "serviceId", "blueprintId"], + }, + }, + required: [], + }, + required: [], + }, + keyPairName: { type: "string" }, + timestamp: { type: "string" }, + relays: { + type: ["string", "array"], + oneOf: [ + { type: "string", enum: NETWORKS }, + { type: "array", items: { type: "string" } }, + ], + nullable: true, + }, }, required: ["version", "services", "keyPairName", "timestamp"], }; const validateConfigSchemaV0 = ajv.compile(configSchemaV0); +const validateConfigSchemaV1 = ajv.compile(configSchemaV1); const migrations: Migrations = [ (config: Config): ConfigV1 => { @@ -136,7 +198,7 @@ const migrations: Migrations = [ const { keyPairName, knownRelays, timestamp, services } = config; - const newServices: Services = {}; + const newServices: ServicesV1 = {}; for (const { name, peerId, serviceId, blueprintId } of services) { const service = { peerId, @@ -162,19 +224,50 @@ const migrations: Migrations = [ ...(knownRelays === undefined ? {} : { knownRelays }), }; }, + (config: Config): ConfigV2 => { + if (!validateConfigSchemaV1(config)) { + throw new Error( + `Migration error. Errors: ${JSON.stringify( + validateConfigSchemaV0.errors + )}` + ); + } + + const { + keyPairName, + knownRelays, + timestamp, + services, + relays: relaysFromConfig, + } = config; + const relays = + typeof relaysFromConfig === "string" + ? relaysFromConfig + : [...(relaysFromConfig ?? []), ...(knownRelays ?? [])]; + + return { + version: 2, + keyPairName, + timestamp, + services: { + default: services, + }, + ...(typeof relays === "string" || relays.length > 0 ? { relays } : {}), + }; + }, ]; -type Config = ConfigV0 | ConfigV1; -type LatestConfig = ConfigV1; +type Config = ConfigV0 | ConfigV1 | ConfigV2; +type LatestConfig = ConfigV2; export type AppConfig = InitializedConfig; export type AppConfigReadonly = InitializedReadonlyConfig; const initConfigOptions: InitConfigOptions = { - allSchemas: [configSchemaV0, configSchemaV1], - latestSchema: configSchemaV1, + allSchemas: [configSchemaV0, configSchemaV1, configSchemaV2], + latestSchema: configSchemaV2, migrations, - name: APP_FILE_NAME, - getPath: getProjectFluenceDirPath, + name: APP_CONFIG_FILE_NAME, + getConfigDirPath: ensureFluenceDir, }; export const initAppConfig = getConfigInitFunction(initConfigOptions); diff --git a/src/lib/configs/project/fluence.ts b/src/lib/configs/project/fluence.ts index 90eb798a7..c8bafc9b6 100644 --- a/src/lib/configs/project/fluence.ts +++ b/src/lib/configs/project/fluence.ts @@ -14,16 +14,15 @@ * limitations under the License. */ -import fsPromises from "node:fs/promises"; +import path from "node:path"; import color from "@oclif/color"; import type { JSONSchemaType } from "ajv"; +import { ajv } from "../../ajv"; import { FLUENCE_CONFIG_FILE_NAME } from "../../const"; -import { validateUnique, ValidationResult } from "../../helpers/validations"; -import { getArtifactsPath } from "../../pathsGetters/getArtifactsPath"; -import { getProjectFluenceDirPath } from "../../pathsGetters/getProjectFluenceDirPath"; -import { getProjectRootDir } from "../../pathsGetters/getProjectRootDir"; +import { NETWORKS, Relays } from "../../multiaddr"; +import { ensureFluenceDir, getProjectRootDir } from "../../paths"; import { GetDefaultConfig, getConfigInitFunction, @@ -32,13 +31,16 @@ import { InitializedReadonlyConfig, getReadonlyConfigInitFunction, Migrations, + ConfigValidateFunction, } from "../initConfig"; -type Service = { name: string; count?: number }; +import type { ModuleV0 as ServiceModuleConfig } from "./service"; + +type ServiceV0 = { name: string; count?: number }; type ConfigV0 = { version: 0; - services: Array; + services: Array; }; const configSchemaV0: JSONSchemaType = { @@ -60,58 +62,235 @@ const configSchemaV0: JSONSchemaType = { required: ["version", "services"], }; -const getDefault: GetDefaultConfig< - LatestConfig -> = async (): Promise => { - const artifactsPath = getArtifactsPath(); - - let services: Array = []; - try { - services = ( - await fsPromises.readdir(artifactsPath, { withFileTypes: true }) - ) - .filter((item): boolean => item.isDirectory()) - .map(({ name }): ConfigV0["services"][0] => ({ - name, - })); - } catch {} - - return { - version: 0, - services, - }; +export type OverrideModules = Record; +export type ServiceDeployV1 = { + deployId: string; + count?: number; + peerId?: string; + overrideModules?: OverrideModules; +}; +export type FluenceConfigModule = Partial; + +type ServiceV1 = { + get: string; + deploy: Array; +}; + +type ConfigV1 = { + version: 1; + services?: Record; + relays?: Relays; + peerIds?: Record; +}; + +const configSchemaV1: JSONSchemaType = { + type: "object", + properties: { + version: { type: "number", enum: [1] }, + services: { + type: "object", + additionalProperties: { + type: "object", + properties: { + get: { type: "string" }, + deploy: { + type: "array", + items: { + type: "object", + properties: { + deployId: { + type: "string", + }, + count: { + type: "number", + minimum: 1, + nullable: true, + }, + peerId: { + type: "string", + nullable: true, + }, + overrideModules: { + type: "object", + additionalProperties: { + type: "object", + properties: { + get: { type: "string", nullable: true }, + type: { type: "string", nullable: true, enum: ["rust"] }, + name: { type: "string", nullable: true }, + maxHeapSize: { type: "string", nullable: true }, + loggerEnabled: { type: "boolean", nullable: true }, + loggingMask: { type: "number", nullable: true }, + volumes: { + type: "object", + nullable: true, + required: [], + }, + preopenedFiles: { + type: "array", + nullable: true, + items: { type: "string" }, + }, + envs: { + type: "object", + nullable: true, + required: [], + }, + mountedBinaries: { + type: "object", + nullable: true, + required: [], + }, + }, + required: [], + nullable: true, + }, + nullable: true, + required: [], + }, + }, + required: ["deployId"], + }, + }, + }, + required: ["get", "deploy"], + }, + required: [], + nullable: true, + }, + relays: { + type: ["string", "array"], + oneOf: [ + { type: "string", enum: NETWORKS }, + { type: "array", items: { type: "string" } }, + ], + nullable: true, + }, + peerIds: { + type: "object", + nullable: true, + required: [], + additionalProperties: { type: "string" }, + }, + }, + required: ["version"], }; -const migrations: Migrations = []; +const getDefault: GetDefaultConfig = (): LatestConfig => ({ + version: 1, +}); + +const validateConfigSchemaV0 = ajv.compile(configSchemaV0); -const validate = (config: LatestConfig): ValidationResult => - validateUnique( - config.services, - ({ name }): string => name, - (name): string => - `There are multiple services with the same name ${color.yellow(name)}` - ); +const migrations: Migrations = [ + (config: Config): ConfigV1 => { + if (!validateConfigSchemaV0(config)) { + throw new Error( + `Migration error. Errors: ${JSON.stringify( + validateConfigSchemaV0.errors + )}` + ); + } -type Config = ConfigV0; -type LatestConfig = ConfigV0; + const services = config.services.reduce>( + (acc, { name, count = 1 }, i): Record => ({ + ...acc, + [name]: { + get: path.relative( + getProjectRootDir(), + path.join(getProjectRootDir(), "artifacts", name) + ), + deploy: [ + { deployId: `default_${i}`, ...(count > 1 ? { count } : {}) }, + ], + }, + }), + {} + ); + + return { + version: 1, + services, + }; + }, +]; + +type Config = ConfigV0 | ConfigV1; +type LatestConfig = ConfigV1; export type FluenceConfig = InitializedConfig; export type FluenceConfigReadonly = InitializedReadonlyConfig; -const initConfigOptions: InitConfigOptions = { - allSchemas: [configSchemaV0], - latestSchema: configSchemaV0, +const examples = ` +services: + someService: # Service name in camelCase + get: https://github.com/fluencelabs/services/blob/master/adder.tar.gz?raw=true # URL or path + deploy: + - deployId: default # any unique string in camelCase. Used in aqua to access deployed service ids + # You can access deployment info in aqua like this: + # services <- App.services() + # on services.someService.default!.peerId: + peerId: MY_PEER # Peer id or peer id name to deploy on. Default: Random peer id is selected for each deploy + count: 1 # How many times to deploy. Default: 1 + # overrideModules: # Override modules from service.yaml + # facade: + # get: ./relative/path # Override facade module +peerIds: # A map of named peerIds. Optional. + MY_PEER: 12D3KooWCMr9mU894i8JXAFqpgoFtx6qnV1LFPSfVc3Y34N4h4LS +relays: kras # Array of relay multi-addresses or keywords: kras, testnet, stage. Default: kras`; + +const validate: ConfigValidateFunction = ( + config +): ReturnType> => { + if (config.services === undefined) { + return true; + } + const notUnique: Array<{ + serviceName: string; + notUniqueDeployIds: Set; + }> = []; + for (const [serviceName, { deploy }] of Object.entries(config.services)) { + const deployIds = new Set(); + const notUniqueDeployIds = new Set(); + for (const { deployId } of deploy) { + if (deployIds.has(deployId)) { + notUniqueDeployIds.add(deployId); + } + deployIds.add(deployId); + } + if (notUniqueDeployIds.size > 0) { + notUnique.push({ serviceName, notUniqueDeployIds }); + } + } + if (notUnique.length > 0) { + return `Deploy ids must be unique. Not unique deploy ids found:\n${notUnique + .map( + ({ serviceName, notUniqueDeployIds }): string => + `${color.yellow(serviceName)}: ${[...notUniqueDeployIds].join(", ")}` + ) + .join("\n")}`; + } + return true; +}; + +export const initConfigOptions: InitConfigOptions = { + allSchemas: [configSchemaV0, configSchemaV1], + latestSchema: configSchemaV1, migrations, name: FLUENCE_CONFIG_FILE_NAME, - getPath: getProjectRootDir, - getSchemaDirPath: (): string => getProjectFluenceDirPath(), + getConfigDirPath: getProjectRootDir, + getSchemaDirPath: ensureFluenceDir, + examples, validate, }; -export const initFluenceConfig = getConfigInitFunction( +export const initNewFluenceConfig = getConfigInitFunction( initConfigOptions, getDefault ); -export const initReadonlyFluenceConfig = getReadonlyConfigInitFunction( +export const initNewReadonlyFluenceConfig = getReadonlyConfigInitFunction( initConfigOptions, getDefault ); +export const initFluenceConfig = getConfigInitFunction(initConfigOptions); +export const initReadonlyFluenceConfig = + getReadonlyConfigInitFunction(initConfigOptions); diff --git a/src/lib/configs/project/module.ts b/src/lib/configs/project/module.ts new file mode 100644 index 000000000..a9a7bb4ce --- /dev/null +++ b/src/lib/configs/project/module.ts @@ -0,0 +1,133 @@ +/** + * Copyright 2022 Fluence Labs Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { JSONSchemaType } from "ajv"; + +import { CommandObj, MODULE_CONFIG_FILE_NAME } from "../../const"; +import { ensureFluenceDir } from "../../paths"; +import { + getConfigInitFunction, + InitConfigOptions, + InitializedConfig, + InitializedReadonlyConfig, + getReadonlyConfigInitFunction, + Migrations, + GetDefaultConfig, +} from "../initConfig"; + +export type ConfigV0 = { + version: 0; + name: string; + type?: "rust"; + maxHeapSize?: string; + loggerEnabled?: boolean; + loggingMask?: number; + volumes?: Record; + preopenedFiles?: Array; + envs?: Record; + mountedBinaries?: Record; +}; + +const configSchemaV0: JSONSchemaType = { + type: "object", + properties: { + version: { type: "number", enum: [0] }, + type: { type: "string", enum: ["rust"], nullable: true }, + name: { type: "string" }, + maxHeapSize: { type: "string", nullable: true }, + loggerEnabled: { type: "boolean", nullable: true }, + loggingMask: { type: "number", nullable: true }, + volumes: { type: "object", nullable: true, required: [] }, + preopenedFiles: { + type: "array", + items: { type: "string" }, + nullable: true, + }, + envs: { type: "object", nullable: true, required: [] }, + mountedBinaries: { type: "object", nullable: true, required: [] }, + }, + required: ["version", "name"], +}; + +const migrations: Migrations = []; + +const examples = ` +name: facade +type: rust # use this for modules written in rust and expected to be built with marine +maxHeapSize: "100" # 100 bytes +# maxHeapSize: 100K # 100 kilobytes +# maxHeapSize: 100 Ki # 100 kibibytes +# Max size of the heap that a module can allocate in format: +# where ? is an optional field and specificator is one from the following (case-insensitive): +# K, Kb - kilobyte; Ki, KiB - kibibyte; M, Mb - megabyte; Mi, MiB - mebibyte; G, Gb - gigabyte; Gi, GiB - gibibyte; +# Current limit is 4 GiB +loggerEnabled: true # true, if it allows module to use the Marine SDK logger. +loggingMask: 0 # manages the logging targets, described in here: https://doc.fluence.dev/marine-book/marine-rust-sdk/developing/logging#using-target-map +mountedBinaries: + curl: /usr/bin/curl # a map of mounted binary executable files +preopenedFiles: # a list of files and directories that this module could access with WASI + - ./dir +volumes: # a map of accessible files and their aliases. +# Aliases should be normally used in Marine module development because it's hard to know the full path to a file. + aliasForSomePath: ./some/path +envs: # environment variables accessible by a particular module with standard Rust env API like this std::env::var(IPFS_ADDR_ENV_NAME). + # Please note that Marine adds three additional environment variables. Module environment variables could be examined with repl + ENV1: arg1 + ENV2: arg2`; + +type Config = ConfigV0; +type LatestConfig = ConfigV0; +export type ModuleConfig = InitializedConfig; +export type ModuleConfigReadonly = InitializedReadonlyConfig; + +const getInitConfigOptions = ( + configPath: string +): InitConfigOptions => ({ + allSchemas: [configSchemaV0], + latestSchema: configSchemaV0, + migrations, + name: MODULE_CONFIG_FILE_NAME, + getSchemaDirPath: ensureFluenceDir, + getConfigDirPath: (): string => configPath, + examples, +}); + +export const initModuleConfig = ( + configPath: string, + commandObj: CommandObj +): Promise | null> => + getConfigInitFunction(getInitConfigOptions(configPath))(commandObj); +export const initReadonlyModuleConfig = ( + configPath: string, + commandObj: CommandObj +): Promise | null> => + getReadonlyConfigInitFunction(getInitConfigOptions(configPath))(commandObj); +const getDefault: (name: string) => GetDefaultConfig = + (name: string): GetDefaultConfig => + (): LatestConfig => ({ + version: 0, + type: "rust", + name, + }); +export const initNewReadonlyModuleConfig = ( + configPath: string, + commandObj: CommandObj, + name: string +): Promise | null> => + getReadonlyConfigInitFunction( + getInitConfigOptions(configPath), + getDefault(name) + )(commandObj); diff --git a/src/lib/configs/project/projectSecrets.ts b/src/lib/configs/project/projectSecrets.ts index 1cf4c1aea..61203c906 100644 --- a/src/lib/configs/project/projectSecrets.ts +++ b/src/lib/configs/project/projectSecrets.ts @@ -17,18 +17,15 @@ import color from "@oclif/color"; import type { JSONSchemaType } from "ajv"; -import { SECRETS_FILE_NAME } from "../../const"; +import { SECRETS_CONFIG_FILE_NAME } from "../../const"; import { validateHasDefault, validateMultiple, validateUnique, ValidationResult, } from "../../helpers/validations"; -import { - ConfigKeyPair, - configKeyPairSchema, -} from "../../keyPairs/generateKeyPair"; -import { getProjectFluenceDirPath } from "../../pathsGetters/getProjectFluenceDirPath"; +import { ConfigKeyPair, configKeyPairSchema } from "../../keypairs"; +import { ensureFluenceDir } from "../../paths"; import { GetDefaultConfig, getConfigInitFunction, @@ -95,8 +92,8 @@ const initConfigOptions: InitConfigOptions = { allSchemas: [configSchemaV0], latestSchema: configSchemaV0, migrations, - name: SECRETS_FILE_NAME, - getPath: getProjectFluenceDirPath, + name: SECRETS_CONFIG_FILE_NAME, + getConfigDirPath: ensureFluenceDir, validate, }; diff --git a/src/lib/configs/project/service.ts b/src/lib/configs/project/service.ts new file mode 100644 index 000000000..e897307cd --- /dev/null +++ b/src/lib/configs/project/service.ts @@ -0,0 +1,166 @@ +/** + * Copyright 2022 Fluence Labs Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { JSONSchemaType } from "ajv"; + +import { CommandObj, SERVICE_CONFIG_FILE_NAME } from "../../const"; +import { ensureFluenceDir } from "../../paths"; +import { + getConfigInitFunction, + InitConfigOptions, + InitializedConfig, + InitializedReadonlyConfig, + getReadonlyConfigInitFunction, + Migrations, + GetDefaultConfig, +} from "../initConfig"; + +import type { ConfigV0 as ModuleConfig } from "./module"; + +export type ModuleV0 = { + get: string; +} & Partial>; + +export type Module = ModuleV0; + +const moduleSchema: JSONSchemaType = { + type: "object", + properties: { + get: { type: "string" }, + type: { type: "string", nullable: true, enum: ["rust"] }, + name: { type: "string", nullable: true }, + maxHeapSize: { type: "string", nullable: true }, + loggerEnabled: { type: "boolean", nullable: true }, + loggingMask: { type: "number", nullable: true }, + volumes: { type: "object", nullable: true, required: [] }, + preopenedFiles: { + type: "array", + nullable: true, + items: { type: "string" }, + }, + envs: { type: "object", nullable: true, required: [] }, + mountedBinaries: { type: "object", nullable: true, required: [] }, + }, + required: ["get"], +}; + +export const FACADE_MODULE_NAME = "facade"; + +export type ConfigV0 = { + version: 0; + modules: { [FACADE_MODULE_NAME]: ModuleV0 } & Record; +}; + +const configSchemaV0: JSONSchemaType = { + type: "object", + properties: { + version: { type: "number", enum: [0] }, + modules: { + type: "object", + additionalProperties: moduleSchema, + properties: { + [FACADE_MODULE_NAME]: moduleSchema, + }, + required: [FACADE_MODULE_NAME], + }, + }, + required: ["version", "modules"], +}; + +const migrations: Migrations = []; + +const examples = ` +modules: + facade: + get: modules/facade + + # Overrides for module: + maxHeapSize: "100" # 100 bytes + # maxHeapSize: 100K # 100 kilobytes + # maxHeapSize: 100 Ki # 100 kibibytes + # Max size of the heap that a module can allocate in format: + # where ? is an optional field and specificator is one from the following (case-insensitive): + # K, Kb - kilobyte; Ki, KiB - kibibyte; M, Mb - megabyte; Mi, MiB - mebibyte; G, Gb - gigabyte; Gi, GiB - gibibyte; + # Current limit is 4 GiB + loggerEnabled: true # true, if it allows module to use the Marine SDK logger. + loggingMask: 0 # manages the logging targets, described in here: https://doc.fluence.dev/marine-book/marine-rust-sdk/developing/logging#using-target-map + mountedBinaries: + curl: /usr/bin/curl # a map of mounted binary executable files + preopenedFiles: # a list of files and directories that this module could access with WASI + - ./dir + volumes: # a map of accessible files and their aliases. + # Aliases should be normally used in Marine module development because it's hard to know the full path to a file. + aliasForSomePath: ./some/path + envs: # environment variables accessible by a particular module with standard Rust env API like this std::env::var(IPFS_ADDR_ENV_NAME). + # Please note that Marine adds three additional environment variables. Module environment variables could be examined with repl + ENV1: arg1 + ENV2: arg2`; + +type Config = ConfigV0; +type LatestConfig = ConfigV0; + +export type ServiceConfig = InitializedConfig; + +export type ServiceConfigReadonly = InitializedReadonlyConfig; + +const getInitConfigOptions = ( + configDirPath: string +): InitConfigOptions => ({ + allSchemas: [configSchemaV0], + latestSchema: configSchemaV0, + migrations, + name: SERVICE_CONFIG_FILE_NAME, + getSchemaDirPath: ensureFluenceDir, + getConfigDirPath: (): string => configDirPath, + examples, +}); + +export const initServiceConfig = ( + configDirPath: string, + commandObj: CommandObj +): Promise | null> => + getConfigInitFunction(getInitConfigOptions(configDirPath))(commandObj); + +export const initReadonlyServiceConfig = ( + configDirPath: string, + commandObj: CommandObj +): Promise | null> => + getReadonlyConfigInitFunction(getInitConfigOptions(configDirPath))( + commandObj + ); + +const getDefault: ( + relativePathToFacade: string +) => GetDefaultConfig = + (relativePathToFacade: string): GetDefaultConfig => + (): LatestConfig => ({ + version: 0, + modules: { + [FACADE_MODULE_NAME]: { + get: relativePathToFacade, + }, + }, + }); + +export const initNewReadonlyServiceConfig = ( + configPath: string, + commandObj: CommandObj, + relativePathToFacade: string +): Promise | null> => + getReadonlyConfigInitFunction( + getInitConfigOptions(configPath), + getDefault(relativePathToFacade) + )(commandObj); diff --git a/src/lib/configs/user/dependency.ts b/src/lib/configs/user/dependency.ts index 0052b6c1b..c009cb42e 100644 --- a/src/lib/configs/user/dependency.ts +++ b/src/lib/configs/user/dependency.ts @@ -16,8 +16,17 @@ import type { JSONSchemaType } from "ajv"; -import { DEPENDENCY_FILE_NAME } from "../../const"; -import { ensureUserFluenceDir } from "../../pathsGetters/ensureUserFluenceDir"; +import { + AQUA_NPM_DEPENDENCY, + cargoDependencyList, + CARGO_GENERATE_CARGO_DEPENDENCY, + CommandObj, + DEPENDENCY_CONFIG_FILE_NAME, + MARINE_CARGO_DEPENDENCY, + MREPL_CARGO_DEPENDENCY, + npmDependencyList, +} from "../../const"; +import { ensureUserFluenceDir } from "../../paths"; import { getIsStringUnion } from "../../typeHelpers"; import { GetDefaultConfig, @@ -29,20 +38,28 @@ import { Migrations, } from "../initConfig"; -export const AQUA_NPM_DEPENDENCY = "aqua"; - -const npmDependencyList = [AQUA_NPM_DEPENDENCY] as const; -export type NPMDependency = typeof npmDependencyList[number]; +export const dependencyList = [ + ...npmDependencyList, + ...cargoDependencyList, +] as const; +export type DependencyName = typeof dependencyList[number]; +type DependencyMap = Partial>; -export const dependencyList = [...npmDependencyList] as const; -export type Dependency = typeof dependencyList[number]; -type DependencyMap = Partial>; +export const getVersionToUse = async ( + recommendedVersion: string, + name: DependencyName, + commandObj: CommandObj +): Promise => { + const version = (await initReadonlyDependencyConfig(commandObj)) + ?.dependency?.[name]; + return typeof version === "string" ? version : recommendedVersion; +}; -export const isDependency = getIsStringUnion(npmDependencyList); +export const isDependency = getIsStringUnion(dependencyList); type ConfigV0 = { version: 0; - dependency: DependencyMap; + dependency?: DependencyMap; }; const configSchemaV0: JSONSchemaType = { @@ -53,16 +70,19 @@ const configSchemaV0: JSONSchemaType = { type: "object", properties: { [AQUA_NPM_DEPENDENCY]: { type: "string", nullable: true }, + [MARINE_CARGO_DEPENDENCY]: { type: "string", nullable: true }, + [MREPL_CARGO_DEPENDENCY]: { type: "string", nullable: true }, + [CARGO_GENERATE_CARGO_DEPENDENCY]: { type: "string", nullable: true }, }, required: [], + nullable: true, }, }, - required: ["version", "dependency"], + required: ["version"], }; const getDefault: GetDefaultConfig = (): LatestConfig => ({ version: 0, - dependency: {}, }); const migrations: Migrations = []; @@ -76,8 +96,8 @@ const initConfigOptions: InitConfigOptions = { allSchemas: [configSchemaV0], latestSchema: configSchemaV0, migrations, - name: DEPENDENCY_FILE_NAME, - getPath: ensureUserFluenceDir, + name: DEPENDENCY_CONFIG_FILE_NAME, + getConfigDirPath: ensureUserFluenceDir, }; export const initDependencyConfig = getConfigInitFunction( diff --git a/src/lib/configs/user/userSecrets.ts b/src/lib/configs/user/userSecrets.ts index daf084a60..a2042bcd8 100644 --- a/src/lib/configs/user/userSecrets.ts +++ b/src/lib/configs/user/userSecrets.ts @@ -17,7 +17,7 @@ import color from "@oclif/color"; import type { JSONSchemaType } from "ajv"; -import { AUTO_GENERATED, SECRETS_FILE_NAME } from "../../const"; +import { AUTO_GENERATED, SECRETS_CONFIG_FILE_NAME } from "../../const"; import { validateHasDefault, validateMultiple, @@ -28,8 +28,8 @@ import { ConfigKeyPair, configKeyPairSchema, generateKeyPair, -} from "../../keyPairs/generateKeyPair"; -import { ensureUserFluenceDir } from "../../pathsGetters/ensureUserFluenceDir"; +} from "../../keypairs"; +import { ensureUserFluenceDir } from "../../paths"; import { GetDefaultConfig, getConfigInitFunction, @@ -94,8 +94,8 @@ const initConfigOptions: InitConfigOptions = { allSchemas: [configSchemaV0], latestSchema: configSchemaV0, migrations, - name: SECRETS_FILE_NAME, - getPath: ensureUserFluenceDir, + name: SECRETS_CONFIG_FILE_NAME, + getConfigDirPath: ensureUserFluenceDir, validate, }; diff --git a/src/lib/const.ts b/src/lib/const.ts index 6fb896796..cb04f9eae 100644 --- a/src/lib/const.ts +++ b/src/lib/const.ts @@ -17,28 +17,42 @@ import { Command, Flags } from "@oclif/core"; import type { stringify } from "yaml"; -export const AQUA_RECOMMENDED_VERSION = "0.7.4-329"; +export const AQUA_RECOMMENDED_VERSION = "0.7.4-332"; +export const MARINE_RECOMMENDED_VERSION = "0.12.1"; +export const MREPL_RECOMMENDED_VERSION = "0.18.0"; +export const CARGO_GENERATE_RECOMMENDED_VERSION = "0.15.2"; +export const RUST_TOOLCHAIN_REQUIRED_TO_INSTALL_MARINE = "nightly-x86_64"; +export const RUST_WASM32_WASI_TARGET = "wasm32-wasi"; export const AQUA_EXT = "aqua"; export const TS_EXT = "ts"; export const JS_EXT = "js"; export const JSON_EXT = "json"; +export const YAML_EXT = "yaml"; +export const WASM_EXT = "wasm"; +export const TOML_EXT = "toml"; export const FLUENCE_DIR_NAME = ".fluence"; export const SCHEMAS_DIR_NAME = "schemas"; export const SRC_DIR_NAME = "src"; -export const ARTIFACTS_DIR_NAME = "artifacts"; export const TMP_DIR_NAME = "tmp"; export const VSCODE_DIR_NAME = ".vscode"; export const NODE_MODULES_DIR_NAME = "node_modules"; export const AQUA_DIR_NAME = "aqua"; export const TS_DIR_NAME = "ts"; export const JS_DIR_NAME = "js"; - -export const FLUENCE_CONFIG_FILE_NAME = "fluence"; -export const SECRETS_FILE_NAME = "secrets"; -export const DEPENDENCY_FILE_NAME = "dependency"; -export const APP_FILE_NAME = "app"; +export const MODULES_DIR_NAME = "modules"; +export const SERVICES_DIR_NAME = "services"; +export const NPM_DIR_NAME = "npm"; +export const CARGO_DIR_NAME = "cargo"; +export const BIN_DIR_NAME = "bin"; + +export const FLUENCE_CONFIG_FILE_NAME = `fluence.${YAML_EXT}`; +export const SECRETS_CONFIG_FILE_NAME = `secrets.${YAML_EXT}`; +export const MODULE_CONFIG_FILE_NAME = `module.${YAML_EXT}`; +export const SERVICE_CONFIG_FILE_NAME = `service.${YAML_EXT}`; +export const APP_CONFIG_FILE_NAME = `app.${YAML_EXT}`; +export const DEPENDENCY_CONFIG_FILE_NAME = `dependency.${YAML_EXT}`; const DEPLOYED_APP_FILE_NAME = "deployed.app"; @@ -51,7 +65,7 @@ export const GITIGNORE_FILE_NAME = ".gitignore"; export const PACKAGE_JSON_FILE_NAME = `package.${JSON_EXT}`; export const EXTENSIONS_JSON_FILE_NAME = `extensions.${JSON_EXT}`; export const SETTINGS_JSON_FILE_NAME = `settings.${JSON_EXT}`; -export const DEPLOYMENT_CONFIG_FILE_NAME = `deploy.${JSON_EXT}`; +export const DEPLOY_CONFIG_FILE_NAME = `deploy.${JSON_EXT}`; export const APP_SERVICE_JSON_FILE_NAME = `app-service.${JSON_EXT}`; export const APP_TS_FILE_NAME = `app.${TS_EXT}`; @@ -59,6 +73,9 @@ export const APP_JS_FILE_NAME = `app.${JS_EXT}`; export const DEPLOYED_APP_TS_FILE_NAME = `${DEPLOYED_APP_FILE_NAME}.${TS_EXT}`; export const DEPLOYED_APP_JS_FILE_NAME = `${DEPLOYED_APP_FILE_NAME}.${JS_EXT}`; +export const CRATES_TOML = `.crates.${TOML_EXT}`; +export const CONFIG_TOML = `Config.${TOML_EXT}`; + export const FS_OPTIONS = { encoding: "utf8", } as const; @@ -74,6 +91,7 @@ export const YAML_FORMAT: [ ]; export const AUTO_GENERATED = "auto-generated"; +export const DEFAULT_DEPLOY_NAME = "default"; export const KEY_PAIR_FLAG_NAME = "key-pair-name"; export const KEY_PAIR_FLAG = { @@ -93,7 +111,7 @@ export const NO_INPUT_FLAG = { export const TIMEOUT_FLAG_NAME = "timeout"; export const TIMEOUT_FLAG = { - [TIMEOUT_FLAG_NAME]: Flags.string({ + [TIMEOUT_FLAG_NAME]: Flags.integer({ description: "Timeout used for command execution", helpValue: "", }), @@ -103,7 +121,7 @@ export const FORCE_FLAG_NAME = "force"; export type CommandObj = Readonly>; -export const GIT_IGNORE_CONTENT = `.idea +export const RECOMMENDED_GIT_IGNORE_CONTENT = `.idea .DS_Store .fluence **/node_modules @@ -114,3 +132,17 @@ Cargo.lock export const IS_TTY = process.stdout.isTTY && process.stdin.isTTY; export const IS_DEVELOPMENT = process.env["NODE_ENV"] === "development"; + +export const MARINE_CARGO_DEPENDENCY = "marine"; +export const MREPL_CARGO_DEPENDENCY = "mrepl"; +export const CARGO_GENERATE_CARGO_DEPENDENCY = "cargo-generate"; +export const cargoDependencyList = [ + MARINE_CARGO_DEPENDENCY, + MREPL_CARGO_DEPENDENCY, + CARGO_GENERATE_CARGO_DEPENDENCY, +] as const; +export type CargoDependency = typeof cargoDependencyList[number]; + +export const AQUA_NPM_DEPENDENCY = "aqua"; +export const npmDependencyList = [AQUA_NPM_DEPENDENCY] as const; +export type NPMDependency = typeof npmDependencyList[number]; diff --git a/src/lib/deployedApp.ts b/src/lib/deployedApp.ts index 795df3583..ecd49be50 100644 --- a/src/lib/deployedApp.ts +++ b/src/lib/deployedApp.ts @@ -21,32 +21,23 @@ import { CliUx } from "@oclif/core"; import camelcase from "camelcase"; import type { AquaCLI } from "./aquaCli"; -import type { DeployedServiceConfig, Services } from "./configs/project/app"; +import type { ServicesV2 } from "./configs/project/app"; import { FS_OPTIONS } from "./const"; -import { getDeployedAppAquaPath } from "./pathsGetters/getDefaultAquaPath"; -import { getAppJsPath, getJsPath } from "./pathsGetters/getJsPath"; -import { getAppTsPath, getTsPath } from "./pathsGetters/getTsPath"; +import { replaceHomeDir } from "./helpers/replaceHomeDir"; +import { + ensureFluenceJSAppPath, + ensureFluenceTSAppPath, + ensureFluenceAquaDeployedAppPath, + ensureFluenceJSDir, + ensureFluenceTSDir, +} from "./paths"; const APP = "App"; -const SERVICE_IDS = "serviceIds"; +const SERVICE_IDS = "services"; const SERVICE_IDS_ITEM = "ServiceIdsItem"; -const SERVICE_IDS_LIST = "ServiceIdsList"; - -type ServiceJsonObj = Record< - string, - DeployedServiceConfig | Array ->; - -const getServicesJsonObj = (services: Services): ServiceJsonObj => - Object.entries(services).reduce( - (acc, [name, services]): ServiceJsonObj => { - acc[camelcase(name)] = services; - return acc; - }, - {} - ); +const SERVICES = "Services"; -export const getAppJson = (services: Services): string => +export const getAppJson = (services: ServicesV2): string => JSON.stringify( { name: APP, @@ -54,7 +45,7 @@ export const getAppJson = (services: Services): string => functions: [ { name: SERVICE_IDS, - result: getServicesJsonObj(services), + result: services, }, ], }, @@ -66,13 +57,13 @@ const generateRegisterAppTSorJS = async ({ deployedServices, aquaCli, isJS, -}: GenerateRegisterAppOptions & { +}: GenerateRegisterAppArg & { isJS: boolean; }): Promise => { await aquaCli({ flags: { - input: getDeployedAppAquaPath(), - output: isJS ? getJsPath() : getTsPath(), + input: await ensureFluenceAquaDeployedAppPath(), + output: await (isJS ? ensureFluenceJSDir() : ensureFluenceTSDir()), js: isJS, }, }); @@ -83,11 +74,7 @@ const generateRegisterAppTSorJS = async ({ import { registerApp as registerAppService } from "./deployed.app"; const service = { - serviceIds: () => (${JSON.stringify( - getServicesJsonObj(deployedServices), - null, - 2 - )}), + ${SERVICE_IDS}: () => (${JSON.stringify(deployedServices, null, 2)}), }; ${ @@ -120,30 +107,37 @@ export function registerApp( `; await fsPromises.writeFile( - isJS ? getAppJsPath() : getAppTsPath(), + await (isJS ? ensureFluenceJSAppPath() : ensureFluenceTSAppPath()), appContent, FS_OPTIONS ); }; -type GenerateRegisterAppOptions = { - deployedServices: Services; +type GenerateRegisterAppArg = { + deployedServices: ServicesV2; aquaCli: AquaCLI; }; export const generateRegisterApp = async ( - options: GenerateRegisterAppOptions + options: GenerateRegisterAppArg ): Promise => { - CliUx.ux.action.start(`Compiling ${color.yellow(getDeployedAppAquaPath())}`); + CliUx.ux.action.start( + `Compiling ${color.yellow( + replaceHomeDir(await ensureFluenceAquaDeployedAppPath()) + )}` + ); await generateRegisterAppTSorJS({ ...options, isJS: true }); await generateRegisterAppTSorJS({ ...options, isJS: false }); CliUx.ux.action.stop(); }; -export const updateDeployedAppAqua = async ( - services: Services +const getDeploysDataName = (serviceName: string): string => + `${camelcase(serviceName, { pascalCase: true })}Deploys`; + +export const generateDeployedAppAqua = async ( + services: ServicesV2 ): Promise => { - const appServicesFilePath = getDeployedAppAquaPath(); + const appServicesFilePath = await ensureFluenceAquaDeployedAppPath(); const appServicesAqua = // Codegeneration: `export App @@ -153,12 +147,28 @@ data ${SERVICE_IDS_ITEM}: peerId: string serviceId: string -data ${SERVICE_IDS_LIST}: -${Object.keys(getServicesJsonObj(services)) - .map((name): string => ` ${name}: []${SERVICE_IDS_ITEM}\n`) - .join("")} +${Object.entries(services) + .map( + ([serviceName, deployments]): string => + `data ${getDeploysDataName(serviceName)}:\n${Object.keys(deployments) + .map( + (deployName: string): string => + ` ${deployName}: []${SERVICE_IDS_ITEM}` + ) + .join("\n")}` + ) + .join("\n\n")} + +data ${SERVICES}: +${Object.keys(services) + .map( + (serviceName): string => + ` ${camelcase(serviceName)}: ${getDeploysDataName(serviceName)}` + ) + .join("\n")} + service ${APP}("${APP}"): - ${SERVICE_IDS}: -> ${SERVICE_IDS_LIST} + ${SERVICE_IDS}: -> ${SERVICES} `; await fsPromises.writeFile(appServicesFilePath, appServicesAqua, FS_OPTIONS); }; diff --git a/src/lib/execPromise.ts b/src/lib/execPromise.ts index c5f9bc7f0..445b864d0 100644 --- a/src/lib/execPromise.ts +++ b/src/lib/execPromise.ts @@ -21,11 +21,6 @@ import { CliUx } from "@oclif/core"; import { IS_DEVELOPMENT, TIMEOUT_FLAG_NAME } from "./const"; -/** - * Execution timeout in milliseconds - */ -const EXECUTION_TIMEOUT = 90_000; - export const execPromise = ( command: string, message?: string, @@ -45,26 +40,28 @@ export const execPromise = ( const failedCommandText = `Debug info:\n${commandToDisplay}\n`; - const executionTimeout = timeout ?? EXECUTION_TIMEOUT; - - const execTimeout = setTimeout((): void => { - if (typeof message === "string") { - CliUx.ux.action.stop(color.red("Timed out")); - } - childProcess.kill(); - rej( - new Error( - `Execution timed out: command didn't yield any result in ${color.yellow( - `${executionTimeout}ms` - )}\nIt's best to just try again or increase timeout using ${color.yellow( - `--${TIMEOUT_FLAG_NAME}` - )} flag\n${failedCommandText}` - ) - ); - }, executionTimeout); + const execTimeout = + timeout !== undefined && + setTimeout((): void => { + if (typeof message === "string") { + CliUx.ux.action.stop(color.red("Timed out")); + } + childProcess.kill(); + rej( + new Error( + `Execution timed out: command didn't yield any result in ${color.yellow( + `${timeout}ms` + )}\nIt's best to just try again or increase timeout using ${color.yellow( + `--${TIMEOUT_FLAG_NAME}` + )} flag\n${failedCommandText}` + ) + ); + }, timeout); const childProcess = exec(command, (error, stdout, stderr): void => { - clearTimeout(execTimeout); + if (execTimeout !== false) { + clearTimeout(execTimeout); + } if (error !== null) { if (typeof message === "string") { diff --git a/src/lib/helpers/downloadFile.ts b/src/lib/helpers/downloadFile.ts new file mode 100644 index 000000000..0ec96a0b8 --- /dev/null +++ b/src/lib/helpers/downloadFile.ts @@ -0,0 +1,114 @@ +/** + * Copyright 2022 Fluence Labs Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import crypto from "node:crypto"; +import fsPromises from "node:fs/promises"; +import path from "node:path"; + +import color from "@oclif/color"; +import camelcase from "camelcase"; +import decompress from "decompress"; +import filenamify from "filenamify"; +import fetch from "node-fetch"; + +import { WASM_EXT } from "../const"; +import { ensureFluenceModulesDir, ensureFluenceServicesDir } from "../paths"; + +export const getHashOfString = (str: string): Promise => { + const md5Hash = crypto.createHash("md5"); + return new Promise((resolve): void => { + md5Hash.on("readable", (): void => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const data = md5Hash.read(); + // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions + if (data) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call + resolve(data.toString("hex")); + } + }); + + md5Hash.write(str); + md5Hash.end(); + }); +}; + +const downloadFile = async (path: string, url: string): Promise => { + const res = await fetch(url); + if (res.status === 404) { + throw new Error(`Failed when downloading ${color.yellow(url)}`); + } + const buffer = await res.buffer(); + await fsPromises.writeFile(path, buffer); + return path; +}; + +export const stringToCamelCaseName = (string: string): string => { + const cleanString = string.replace(".tar.gz?raw=true", ""); + return camelcase( + filenamify( + cleanString.split(cleanString.includes("/") ? "/" : "\\").slice(-1)[0] ?? + "" + ) + ); +}; + +const ARCHIVE_FILE = "archive.tar.gz"; + +const getHashPath = async (get: string, dir: string): Promise => + path.join(dir, `${stringToCamelCaseName(get)}_${await getHashOfString(get)}`); + +const downloadAndDecompress = async ( + get: string, + dir: string +): Promise => { + const dirPath = await getHashPath(get, dir); + try { + await fsPromises.access(dirPath); + return dirPath; + } catch {} + await fsPromises.mkdir(dirPath, { recursive: true }); + const archivePath = path.join(dirPath, ARCHIVE_FILE); + await downloadFile(archivePath, get); + await decompress(archivePath, dirPath); + await fsPromises.unlink(archivePath); + return dirPath; +}; + +export const downloadModule = async (get: string): Promise => + downloadAndDecompress(get, await ensureFluenceModulesDir()); + +export const downloadService = async (get: string): Promise => + downloadAndDecompress(get, await ensureFluenceServicesDir()); + +export const isUrl = (unknown: string): boolean => + unknown.startsWith("http://") || unknown.startsWith("https://"); + +export const getModuleWasmPath = (config: { + type?: string; + name: string; + $getPath: () => string; +}): string => { + const fileName = `${config.name}.${WASM_EXT}`; + const configDirName = path.dirname(config.$getPath()); + return config.type === "rust" + ? path.resolve(configDirName, "target", "wasm32-wasi", "release", fileName) + : path.resolve(configDirName, fileName); +}; + +export const getModuleUrlOrAbsolutePath = ( + get: string, + serviceDirPath: string +): string => (isUrl(get) ? get : path.resolve(serviceDirPath, get)); diff --git a/src/lib/pathsGetters/getProjectFluenceDirPath.ts b/src/lib/helpers/ensureFluenceProject.ts similarity index 70% rename from src/lib/pathsGetters/getProjectFluenceDirPath.ts rename to src/lib/helpers/ensureFluenceProject.ts index 204f64080..f4068b8b8 100644 --- a/src/lib/pathsGetters/getProjectFluenceDirPath.ts +++ b/src/lib/helpers/ensureFluenceProject.ts @@ -18,23 +18,22 @@ import fsPromises from "node:fs/promises"; import path from "node:path"; import { init } from "../../commands/init"; -import { CommandObj, FLUENCE_DIR_NAME } from "../const"; +import { initConfigOptions as fluenceConfigInitOptions } from "../configs/project/fluence"; +import { CommandObj, FLUENCE_CONFIG_FILE_NAME } from "../const"; import { confirm } from "../prompt"; -import { getProjectRootDir } from "./getProjectRootDir"; - -export const getProjectFluenceDirPath = (): string => - path.join(getProjectRootDir(), FLUENCE_DIR_NAME); - -export const ensureProjectFluenceDirPath = async ( +export const ensureFluenceProject = async ( commandObj: CommandObj, isInteractive: boolean -): Promise => { - const projectFluenceDirPath = getProjectFluenceDirPath(); +): Promise => { + const projectFluencePath = path.join( + await fluenceConfigInitOptions.getConfigDirPath(commandObj), + FLUENCE_CONFIG_FILE_NAME + ); try { - await fsPromises.access(projectFluenceDirPath); - return projectFluenceDirPath; + await fsPromises.access(projectFluencePath); + return; } catch {} const errorMessage = "Not a fluence project"; @@ -56,7 +55,5 @@ export const ensureProjectFluenceDirPath = async ( ); } - await init({ commandObj, isInteractive }); - - return getProjectFluenceDirPath(); + return init({ commandObj, isInteractive }); }; diff --git a/src/lib/helpers/getMessageWithKeyValuePairs.ts b/src/lib/helpers/getMessageWithKeyValuePairs.ts index 78b9c23f6..a6a5c4226 100644 --- a/src/lib/helpers/getMessageWithKeyValuePairs.ts +++ b/src/lib/helpers/getMessageWithKeyValuePairs.ts @@ -18,7 +18,7 @@ import { color } from "@oclif/color"; export const getMessageWithKeyValuePairs = ( message: string, - keyValuePairs?: Record + keyValuePairs: Record | undefined ): string => `${color.yellow(message)}${ keyValuePairs === undefined diff --git a/src/lib/pathsGetters/getSrcAquaDirPath.ts b/src/lib/helpers/logAndFail.ts similarity index 59% rename from src/lib/pathsGetters/getSrcAquaDirPath.ts rename to src/lib/helpers/logAndFail.ts index 505b929d2..8bd63a688 100644 --- a/src/lib/pathsGetters/getSrcAquaDirPath.ts +++ b/src/lib/helpers/logAndFail.ts @@ -14,20 +14,7 @@ * limitations under the License. */ -import path from "node:path"; - -import { - AQUA_DIR_NAME, - DEFAULT_SRC_AQUA_FILE_NAME, - SRC_DIR_NAME, -} from "../const"; - -import { getProjectRootDir } from "./getProjectRootDir"; - -export const getSrcAquaDirPath = (): string => { - return path.join(getProjectRootDir(), SRC_DIR_NAME, AQUA_DIR_NAME); -}; - -export const getSrcMainAquaPath = (): string => { - return path.join(getSrcAquaDirPath(), DEFAULT_SRC_AQUA_FILE_NAME); +export const logAndFail = (...args: Parameters): void => { + console.log(...args); + throw new Error("fail"); }; diff --git a/src/lib/pathsGetters/getProjectRootDir.ts b/src/lib/helpers/replaceHomeDir.ts similarity index 82% rename from src/lib/pathsGetters/getProjectRootDir.ts rename to src/lib/helpers/replaceHomeDir.ts index b30cf1c0a..b627c5f96 100644 --- a/src/lib/pathsGetters/getProjectRootDir.ts +++ b/src/lib/helpers/replaceHomeDir.ts @@ -14,4 +14,7 @@ * limitations under the License. */ -export const getProjectRootDir = (): string => process.cwd(); +import replaceHomedir from "replace-homedir"; + +export const replaceHomeDir = (path: string): string => + replaceHomedir(path, "~"); diff --git a/src/lib/helpers/unparseFlags.ts b/src/lib/helpers/unparseFlags.ts index a20d313fe..d2730eabe 100644 --- a/src/lib/helpers/unparseFlags.ts +++ b/src/lib/helpers/unparseFlags.ts @@ -14,28 +14,36 @@ * limitations under the License. */ +import type { CommandObj } from "../const"; + const unparseFlag = ( flagName: string, - flagValue: string | boolean | undefined + flagValue: string | number | boolean | undefined, + commandObj: CommandObj ): string => { if (flagValue === undefined || flagValue === false) { return ""; } - return ` \\\n--${flagName}${flagValue === true ? "" : ` '${flagValue}'`}`; + return ` ${commandObj.config.windows ? "" : "\\\n"}-${ + flagName.length > 1 ? "-" : "" + }${flagName}${flagValue === true ? "" : ` '${flagValue}'`}`; }; export const unparseFlags = ( flags: Record< string, - string | boolean | undefined | Array - > + string | number | boolean | undefined | Array + >, + commandObj: CommandObj ): string => Object.entries(flags) .flatMap( ([flagName, flagValue]): Array => Array.isArray(flagValue) - ? flagValue.map((value): string => unparseFlag(flagName, value)) - : [unparseFlag(flagName, flagValue)] + ? flagValue.map((value): string => + unparseFlag(flagName, value, commandObj) + ) + : [unparseFlag(flagName, flagValue, commandObj)] ) .join(""); diff --git a/src/lib/helpers/usage.ts b/src/lib/helpers/usage.ts deleted file mode 100644 index 71cf9b174..000000000 --- a/src/lib/helpers/usage.ts +++ /dev/null @@ -1,53 +0,0 @@ -/** - * Copyright 2022 Fluence Labs Limited - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import type { Command } from "@oclif/core"; - -import { hasKey } from "../typeHelpers"; - -export const usage = (CommandClass: T): string => { - const command = CommandClass.name.toLowerCase(); - - const args = - CommandClass.args === undefined - ? "" - : CommandClass.args.reduce( - (acc, { name }): string => `${acc} [${name}]`, - "" - ); - - const flags = - CommandClass.flags === undefined - ? "" - : Object.entries(CommandClass.flags).reduce( - (acc, [fullKey, flag]): string => { - const key = flag.char ?? fullKey; - const prefix = flag.char === undefined ? "--" : "-"; - - if ( - hasKey("helpValue", flag) && - typeof flag.helpValue === "string" - ) { - return `${acc} [${prefix}${key} ${flag.helpValue}]`; - } - - return `${acc} [${prefix}${key}]`; - }, - "" - ); - - return `${command}${args}${flags}`; -}; diff --git a/src/lib/keyPairs/generateKeyPair.ts b/src/lib/keyPairs/generateKeyPair.ts deleted file mode 100644 index ae3aede13..000000000 --- a/src/lib/keyPairs/generateKeyPair.ts +++ /dev/null @@ -1,48 +0,0 @@ -/** - * Copyright 2022 Fluence Labs Limited - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { KeyPair } from "@fluencelabs/fluence"; -import type { JSONSchemaType } from "ajv"; - -export type ConfigKeyPair = { - peerId: string; - secretKey: string; - publicKey: string; - name: string; -}; - -export const configKeyPairSchema: JSONSchemaType = { - type: "object", - properties: { - peerId: { type: "string" }, - secretKey: { type: "string" }, - publicKey: { type: "string" }, - name: { type: "string" }, - }, - required: ["peerId", "secretKey", "publicKey", "name"], -}; - -export const generateKeyPair = async (name: string): Promise => { - const keyPair = await KeyPair.randomEd25519(); - return { - peerId: keyPair.Libp2pPeerId.toB58String(), - secretKey: Buffer.from(keyPair.toEd25519PrivateKey()).toString("base64"), - publicKey: Buffer.from(keyPair.Libp2pPeerId.pubKey.bytes).toString( - "base64" - ), - name, - }; -}; diff --git a/src/lib/keyPairs/getKeyPair.ts b/src/lib/keypairs.ts similarity index 71% rename from src/lib/keyPairs/getKeyPair.ts rename to src/lib/keypairs.ts index b68c2685b..9f59ebb76 100644 --- a/src/lib/keyPairs/getKeyPair.ts +++ b/src/lib/keypairs.ts @@ -15,20 +15,18 @@ */ import assert from "node:assert"; -import fsPromises from "node:fs/promises"; +import { KeyPair } from "@fluencelabs/fluence"; import color from "@oclif/color"; +import type { JSONSchemaType } from "ajv"; import { Separator } from "inquirer"; -import { initReadonlyProjectSecretsConfig } from "../configs/project/projectSecrets"; -import { initReadonlyUserSecretsConfig } from "../configs/user/userSecrets"; -import { CommandObj, KEY_PAIR_FLAG_NAME } from "../const"; -import { getProjectFluenceDirPath } from "../pathsGetters/getProjectFluenceDirPath"; -import { list, Choices } from "../prompt"; +import { initReadonlyProjectSecretsConfig } from "./configs/project/projectSecrets"; +import { initReadonlyUserSecretsConfig } from "./configs/user/userSecrets"; +import { CommandObj, KEY_PAIR_FLAG_NAME } from "./const"; +import { list, Choices } from "./prompt"; -import type { ConfigKeyPair } from "./generateKeyPair"; - -type GetUserKeyPairOptions = { +type GetUserKeyPairArg = { commandObj: CommandObj; isInteractive: boolean; keyPairName?: string | undefined; @@ -38,7 +36,7 @@ const getUserKeyPair = async ({ commandObj, keyPairName, isInteractive, -}: GetUserKeyPairOptions): Promise => { +}: GetUserKeyPairArg): Promise => { const userSecretsConfig = await initReadonlyUserSecretsConfig(commandObj); if (keyPairName === undefined) { @@ -105,7 +103,7 @@ const getUserKeyPair = async ({ }); }; -type GetKeyPairOptions = { +type GetKeyPairArg = { commandObj: CommandObj; isInteractive: boolean; keyPairName: string | undefined; @@ -114,7 +112,7 @@ type GetKeyPairOptions = { const getProjectKeyPair = async ({ commandObj, keyPairName, -}: GetKeyPairOptions): Promise => { +}: GetKeyPairArg): Promise => { const projectSecretsConfig = await initReadonlyProjectSecretsConfig( commandObj ); @@ -131,20 +129,9 @@ const getProjectKeyPair = async ({ }; export const getKeyPair = async ( - options: GetKeyPairOptions -): Promise => { - const projectFluenceDirPath = getProjectFluenceDirPath(); - - try { - await fsPromises.access(projectFluenceDirPath); - const projectKeyPair = await getProjectKeyPair(options); - if (projectKeyPair !== undefined) { - return projectKeyPair; - } - } catch {} - - return getUserKeyPair(options); -}; + options: GetKeyPairArg +): Promise => + (await getProjectKeyPair(options)) ?? getUserKeyPair(options); export const getKeyPairFromFlags = async ( { @@ -156,3 +143,33 @@ export const getKeyPairFromFlags = async ( isInteractive: boolean ): Promise => getKeyPair({ commandObj, keyPairName, isInteractive }); + +export type ConfigKeyPair = { + peerId: string; + secretKey: string; + publicKey: string; + name: string; +}; + +export const configKeyPairSchema: JSONSchemaType = { + type: "object", + properties: { + peerId: { type: "string" }, + secretKey: { type: "string" }, + publicKey: { type: "string" }, + name: { type: "string" }, + }, + required: ["peerId", "secretKey", "publicKey", "name"], +}; + +export const generateKeyPair = async (name: string): Promise => { + const keyPair = await KeyPair.randomEd25519(); + return { + peerId: keyPair.Libp2pPeerId.toB58String(), + secretKey: Buffer.from(keyPair.toEd25519PrivateKey()).toString("base64"), + publicKey: Buffer.from(keyPair.Libp2pPeerId.pubKey.bytes).toString( + "base64" + ), + name, + }; +}; diff --git a/src/lib/marineCli.ts b/src/lib/marineCli.ts new file mode 100644 index 000000000..c2de9721f --- /dev/null +++ b/src/lib/marineCli.ts @@ -0,0 +1,81 @@ +/** + * Copyright 2022 Fluence Labs Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + CARGO_GENERATE_CARGO_DEPENDENCY, + CommandObj, + MARINE_CARGO_DEPENDENCY, +} from "./const"; +import { execPromise } from "./execPromise"; +import { getMessageWithKeyValuePairs } from "./helpers/getMessageWithKeyValuePairs"; +import { unparseFlags } from "./helpers/unparseFlags"; +import { ensureCargoDependency } from "./rust"; +import type { Flags } from "./typeHelpers"; + +export type MarineCliInput = + | { + command: "generate"; + flags: Flags<"init" | "name">; + } + | { + command: "build"; + flags: Flags<"release">; + }; + +export type MarineCLI = { + ( + args: { + message?: string | undefined; + keyValuePairs?: Record; + workingDir?: string; + } & MarineCliInput + ): Promise; +}; + +export const initMarineCli = async ( + commandObj: CommandObj +): Promise => { + const marineCliPath = await ensureCargoDependency({ + name: MARINE_CARGO_DEPENDENCY, + commandObj, + }); + await ensureCargoDependency({ + name: CARGO_GENERATE_CARGO_DEPENDENCY, + commandObj, + }); + return async ({ + command, + flags, + message, + keyValuePairs, + workingDir, + }): Promise => { + const cwd = process.cwd(); + if (workingDir !== undefined) { + process.chdir(workingDir); + } + const result = await execPromise( + `${marineCliPath} ${command ?? ""}${unparseFlags(flags, commandObj)}`, + message === undefined + ? undefined + : getMessageWithKeyValuePairs(message, keyValuePairs) + ); + if (workingDir !== undefined) { + process.chdir(cwd); + } + return result; + }; +}; diff --git a/src/lib/multiaddr.ts b/src/lib/multiaddr.ts index 7a6f483f8..97c4176de 100644 --- a/src/lib/multiaddr.ts +++ b/src/lib/multiaddr.ts @@ -16,25 +16,63 @@ import assert from "node:assert"; -import { krasnodar } from "@fluencelabs/fluence-network-environment"; +import { + krasnodar, + stage, + testNet, + Node, +} from "@fluencelabs/fluence-network-environment"; +import { Multiaddr } from "multiaddr"; -export const defaultAddr = krasnodar.map(({ multiaddr }): string => multiaddr); -const defaultRelayIds = krasnodar.map(({ peerId }): string => peerId); +export const NETWORKS = ["kras", "stage", "testnet"] as const; +export type Network = typeof NETWORKS[number]; +export type Relays = Network | Array | undefined; -export const getRandomRelayAddr = (): string => { - const largestIndex = defaultAddr.length - 1; +const getAddrs = (nodes: Array): Array => + nodes.map(({ multiaddr }): string => multiaddr); + +const ADDR_MAP: Record> = { + kras: getAddrs(krasnodar), + stage: getAddrs(stage), + testnet: getAddrs(testNet), +}; + +const resolveAddrs = (relays: Relays): Array => { + if (relays === undefined) { + return ADDR_MAP.kras; + } + + if (Array.isArray(relays)) { + return relays; + } + + return ADDR_MAP[relays]; +}; + +export const getRandomRelayAddr = (relays: Relays): string => { + const addrs = resolveAddrs(relays); + const largestIndex = addrs.length - 1; const randomIndex = Math.round(Math.random() * largestIndex); - const randomRelayAddr = defaultAddr[randomIndex]; + const randomRelayAddr = addrs[randomIndex]; assert(randomRelayAddr !== undefined); return randomRelayAddr; }; -export const getRandomRelayId = (): string => { - const largestIndex = defaultRelayIds.length - 1; +const getIds = (nodes: Array): Array => + nodes.map((addr): string => { + const id = new Multiaddr(addr).getPeerId(); + assert(id !== null); + return id; + }); + +export const getRandomRelayId = (relays: Relays): string => { + const addrs = resolveAddrs(relays); + const ids = getIds(addrs); + const largestIndex = ids.length - 1; const randomIndex = Math.round(Math.random() * largestIndex); - const randomRelayId = defaultRelayIds[randomIndex]; + const randomRelayId = ids[randomIndex]; assert(randomRelayId !== undefined); return randomRelayId; diff --git a/src/lib/npm.ts b/src/lib/npm.ts index 4f0d53922..35dd1c953 100644 --- a/src/lib/npm.ts +++ b/src/lib/npm.ts @@ -18,18 +18,20 @@ import fsPromises from "node:fs/promises"; import path from "node:path"; import color from "@oclif/color"; -import replaceHomedir from "replace-homedir"; +import { getVersionToUse } from "./configs/user/dependency"; import { AQUA_NPM_DEPENDENCY, - initReadonlyDependencyConfig, + AQUA_RECOMMENDED_VERSION, + BIN_DIR_NAME, + CommandObj, NPMDependency, -} from "./configs/user/dependency"; -import { AQUA_RECOMMENDED_VERSION, CommandObj } from "./const"; +} from "./const"; import { execPromise } from "./execPromise"; -import { ensureUserFluenceDir } from "./pathsGetters/ensureUserFluenceDir"; +import { replaceHomeDir } from "./helpers/replaceHomeDir"; +import { ensureUserFluenceNpmDir } from "./paths"; -type NPMInstallOptions = { +type NPMInstallArg = { packageName: string; version: string; message: string; @@ -41,32 +43,14 @@ const npmInstall = async ({ version, message, commandObj, -}: NPMInstallOptions): Promise => +}: NPMInstallArg): Promise => execPromise( - `npm i ${packageName}@${version} -g --prefix ${await ensureNpmDir( + `npm i ${packageName}@${version} -g --prefix ${await ensureUserFluenceNpmDir( commandObj )}`, message ); -export const ensureNpmDir = async (commandObj: CommandObj): Promise => { - const userFluenceDir = await ensureUserFluenceDir(commandObj); - const npmPath = path.join(userFluenceDir, "npm"); - await fsPromises.mkdir(npmPath, { recursive: true }); - return npmPath; -}; - -const getVersionToUse = async ( - recommendedVersion: string, - name: NPMDependency, - commandObj: CommandObj -): Promise => { - const version = (await initReadonlyDependencyConfig(commandObj)).dependency[ - name - ]; - return typeof version === "string" ? version : recommendedVersion; -}; - export const npmDependencies: Record< NPMDependency, { recommendedVersion: string; bin: string; packageName: string } @@ -78,7 +62,7 @@ export const npmDependencies: Record< }, }; -type NpmDependencyOptions = { +type NpmDependencyArg = { name: NPMDependency; commandObj: CommandObj; }; @@ -86,15 +70,17 @@ type NpmDependencyOptions = { export const ensureNpmDependency = async ({ name, commandObj, -}: NpmDependencyOptions): Promise => { +}: NpmDependencyArg): Promise => { const { bin, packageName, recommendedVersion } = npmDependencies[name]; - const npmDirPath = await ensureNpmDir(commandObj); - const dependencyPath = path.join(npmDirPath, "bin", bin); + const npmDirPath = await ensureUserFluenceNpmDir(commandObj); + const dependencyPath = commandObj.config.windows + ? path.join(npmDirPath, bin) + : path.join(npmDirPath, BIN_DIR_NAME, bin); const version = await getVersionToUse(recommendedVersion, name, commandObj); try { await fsPromises.access(dependencyPath); - const result = await execPromise(`${dependencyPath} --version`); + const result = await getNpmDependencyVersion(dependencyPath); if (!result.includes(version)) { throw new Error("Outdated"); } @@ -104,10 +90,21 @@ export const ensureNpmDependency = async ({ version, message: `Installing version ${color.yellow( version - )} of ${packageName} to ${replaceHomedir(npmDirPath, "~")}`, + )} of ${packageName} to ${replaceHomeDir(npmDirPath)}`, commandObj, }); + const result = await getNpmDependencyVersion(dependencyPath); + if (!result.includes(version)) { + return commandObj.error( + `Not able to install version ${color.yellow( + version + )} of ${packageName} to ${replaceHomeDir(npmDirPath)}` + ); + } } return dependencyPath; }; + +const getNpmDependencyVersion = (dependencyPath: string): Promise => + execPromise(`${dependencyPath} --version`); diff --git a/src/lib/paths.ts b/src/lib/paths.ts new file mode 100644 index 000000000..2f010fa63 --- /dev/null +++ b/src/lib/paths.ts @@ -0,0 +1,157 @@ +/** + * Copyright 2022 Fluence Labs Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import fsPromises from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; + +import { + APP_JS_FILE_NAME, + APP_SERVICE_JSON_FILE_NAME, + APP_TS_FILE_NAME, + AQUA_DIR_NAME, + CARGO_DIR_NAME, + CommandObj, + CONFIG_TOML, + CRATES_TOML, + DEFAULT_SRC_AQUA_FILE_NAME, + DEPLOYED_APP_AQUA_FILE_NAME, + DEPLOYED_APP_JS_FILE_NAME, + DEPLOYED_APP_TS_FILE_NAME, + DEPLOY_CONFIG_FILE_NAME, + EXTENSIONS_JSON_FILE_NAME, + FLUENCE_DIR_NAME, + GITIGNORE_FILE_NAME, + JS_DIR_NAME, + MODULES_DIR_NAME, + NPM_DIR_NAME, + SERVICES_DIR_NAME, + SETTINGS_JSON_FILE_NAME, + SRC_DIR_NAME, + TMP_DIR_NAME, + TS_DIR_NAME, + VSCODE_DIR_NAME, +} from "./const"; + +const ensureDir = async (dirPath: string): Promise => { + await fsPromises.mkdir(dirPath, { recursive: true }); + return dirPath; +}; + +// User .fluence paths: + +export const ensureUserFluenceDir = async ( + commandObj: CommandObj +): Promise => + commandObj.config.windows + ? ensureDir(commandObj.config.configDir) + : ensureDir(path.join(os.homedir(), FLUENCE_DIR_NAME)); + +export const ensureUserFluenceNpmDir = async ( + commandObj: CommandObj +): Promise => + ensureDir(path.join(await ensureUserFluenceDir(commandObj), NPM_DIR_NAME)); + +export const ensureUserFluenceCargoDir = async ( + commandObj: CommandObj, + isGlobalDependency: true | undefined +): Promise => + ensureDir( + isGlobalDependency === true + ? path.join( + path.dirname(await ensureUserFluenceDir(commandObj)), + `.${CARGO_DIR_NAME}` + ) + : path.join(await ensureUserFluenceDir(commandObj), CARGO_DIR_NAME) + ); + +export const ensureUserFluenceCargoCratesPath = async ( + commandObj: CommandObj, + isGlobalDependency: true | undefined +): Promise => + path.join( + await ensureUserFluenceCargoDir(commandObj, isGlobalDependency), + CRATES_TOML + ); + +// Project paths: + +export const getProjectRootDir = (): string => process.cwd(); + +export const ensureSrcAquaDir = (): Promise => + ensureDir(path.join(getProjectRootDir(), SRC_DIR_NAME, AQUA_DIR_NAME)); + +export const ensureSrcAquaMainPath = async (): Promise => + path.join(await ensureSrcAquaDir(), DEFAULT_SRC_AQUA_FILE_NAME); + +export const ensureVSCodeDir = (): Promise => + ensureDir(path.join(getProjectRootDir(), VSCODE_DIR_NAME)); + +export const ensureVSCodeSettingsJsonPath = async (): Promise => + path.join(await ensureVSCodeDir(), SETTINGS_JSON_FILE_NAME); + +export const ensureVSCodeExtensionsJsonPath = async (): Promise => + path.join(await ensureVSCodeDir(), EXTENSIONS_JSON_FILE_NAME); + +export const getGitignorePath = (): string => + path.join(getProjectRootDir(), GITIGNORE_FILE_NAME); + +// Project .fluence paths: + +export const ensureFluenceDir = (): Promise => + ensureDir(path.join(getProjectRootDir(), FLUENCE_DIR_NAME)); + +export const ensureFluenceAquaDir = async (): Promise => + ensureDir(path.join(await ensureFluenceDir(), AQUA_DIR_NAME)); + +export const ensureFluenceAquaDeployedAppPath = async (): Promise => + path.join(await ensureFluenceAquaDir(), DEPLOYED_APP_AQUA_FILE_NAME); + +export const ensureFluenceJSDir = async (): Promise => + ensureDir(path.join(await ensureFluenceDir(), JS_DIR_NAME)); + +export const ensureFluenceJSAppPath = async (): Promise => + path.join(await ensureFluenceJSDir(), APP_JS_FILE_NAME); + +export const ensureFluenceJSDeployedAppPath = async (): Promise => + path.join(await ensureFluenceJSDir(), DEPLOYED_APP_JS_FILE_NAME); + +export const ensureFluenceTSDir = async (): Promise => + ensureDir(path.join(await ensureFluenceDir(), TS_DIR_NAME)); + +export const ensureFluenceTSAppPath = async (): Promise => + path.join(await ensureFluenceTSDir(), APP_TS_FILE_NAME); + +export const ensureFluenceTSDeployedAppPath = async (): Promise => + path.join(await ensureFluenceTSDir(), DEPLOYED_APP_TS_FILE_NAME); + +export const ensureFluenceModulesDir = async (): Promise => + ensureDir(path.join(await ensureFluenceDir(), MODULES_DIR_NAME)); + +export const ensureFluenceServicesDir = async (): Promise => + ensureDir(path.join(await ensureFluenceDir(), SERVICES_DIR_NAME)); + +export const ensureFluenceTmpDir = async (): Promise => + ensureDir(path.join(await ensureFluenceDir(), TMP_DIR_NAME)); + +export const ensureFluenceTmpAppServiceJsonPath = async (): Promise => + path.join(await ensureFluenceTmpDir(), APP_SERVICE_JSON_FILE_NAME); + +export const ensureFluenceTmpDeployJsonPath = async (): Promise => + path.join(await ensureFluenceTmpDir(), DEPLOY_CONFIG_FILE_NAME); + +export const ensureFluenceTmpConfigTomlPath = async (): Promise => + path.join(await ensureFluenceTmpDir(), CONFIG_TOML); diff --git a/src/lib/pathsGetters/ensureUserFluenceDir.ts b/src/lib/pathsGetters/ensureUserFluenceDir.ts deleted file mode 100644 index fb8b0fa31..000000000 --- a/src/lib/pathsGetters/ensureUserFluenceDir.ts +++ /dev/null @@ -1,34 +0,0 @@ -/** - * Copyright 2022 Fluence Labs Limited - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import fsPromises from "node:fs/promises"; -import os from "node:os"; -import path from "node:path"; - -import { CommandObj, FLUENCE_DIR_NAME } from "../const"; - -export const ensureUserFluenceDir = async ( - commandObj: CommandObj -): Promise => { - if (commandObj.config.windows) { - await fsPromises.mkdir(commandObj.config.configDir, { recursive: true }); - return commandObj.config.configDir; - } - - const fluenceDir = path.join(os.homedir(), FLUENCE_DIR_NAME); - await fsPromises.mkdir(fluenceDir, { recursive: true }); - return fluenceDir; -}; diff --git a/src/lib/pathsGetters/getArtifactsPath.ts b/src/lib/pathsGetters/getArtifactsPath.ts deleted file mode 100644 index 0a1ab3bde..000000000 --- a/src/lib/pathsGetters/getArtifactsPath.ts +++ /dev/null @@ -1,37 +0,0 @@ -/** - * Copyright 2022 Fluence Labs Limited - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import fsPromises from "node:fs/promises"; -import path from "node:path"; - -import { ARTIFACTS_DIR_NAME } from "../const"; - -import { getProjectRootDir } from "./getProjectRootDir"; - -export const getArtifactsPath = (): string => { - return path.join(getProjectRootDir(), ARTIFACTS_DIR_NAME); -}; - -export const getMaybeArtifactsPath = async (): Promise => { - const artifactsPath = getArtifactsPath(); - try { - await fsPromises.access(artifactsPath); - } catch { - return; - } - - return artifactsPath; -}; diff --git a/src/lib/pathsGetters/getDefaultAquaPath.ts b/src/lib/pathsGetters/getDefaultAquaPath.ts deleted file mode 100644 index f33533145..000000000 --- a/src/lib/pathsGetters/getDefaultAquaPath.ts +++ /dev/null @@ -1,29 +0,0 @@ -/** - * Copyright 2022 Fluence Labs Limited - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import path from "node:path"; - -import { DEPLOYED_APP_AQUA_FILE_NAME, AQUA_DIR_NAME } from "../const"; - -import { getProjectFluenceDirPath } from "./getProjectFluenceDirPath"; - -export const getDefaultAquaPath = (): string => { - const projectFluenceDir = getProjectFluenceDirPath(); - return path.join(projectFluenceDir, AQUA_DIR_NAME); -}; - -export const getDeployedAppAquaPath = (): string => - path.join(getDefaultAquaPath(), DEPLOYED_APP_AQUA_FILE_NAME); diff --git a/src/lib/pathsGetters/getJsPath.ts b/src/lib/pathsGetters/getJsPath.ts deleted file mode 100644 index c58853832..000000000 --- a/src/lib/pathsGetters/getJsPath.ts +++ /dev/null @@ -1,36 +0,0 @@ -/** - * Copyright 2022 Fluence Labs Limited - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import path from "node:path"; - -import { - APP_JS_FILE_NAME, - DEPLOYED_APP_JS_FILE_NAME, - JS_DIR_NAME, -} from "../const"; - -import { getProjectFluenceDirPath } from "./getProjectFluenceDirPath"; - -export const getJsPath = (): string => { - const projectFluenceDir = getProjectFluenceDirPath(); - return path.join(projectFluenceDir, JS_DIR_NAME); -}; - -export const getAppJsPath = (): string => - path.join(getJsPath(), APP_JS_FILE_NAME); - -export const getDeployedAppJsPath = (): string => - path.join(getJsPath(), DEPLOYED_APP_JS_FILE_NAME); diff --git a/src/lib/pathsGetters/getTmpPath.ts b/src/lib/pathsGetters/getTmpPath.ts deleted file mode 100644 index 7dde0aa43..000000000 --- a/src/lib/pathsGetters/getTmpPath.ts +++ /dev/null @@ -1,29 +0,0 @@ -/** - * Copyright 2022 Fluence Labs Limited - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import path from "node:path"; - -import { APP_SERVICE_JSON_FILE_NAME, TMP_DIR_NAME } from "../const"; - -import { getProjectFluenceDirPath } from "./getProjectFluenceDirPath"; - -export const getTmpPath = (): string => { - const projectFluenceDir = getProjectFluenceDirPath(); - return path.join(projectFluenceDir, TMP_DIR_NAME); -}; - -export const getAppServiceJsonPath = (): string => - path.join(getTmpPath(), APP_SERVICE_JSON_FILE_NAME); diff --git a/src/lib/pathsGetters/getTsPath.ts b/src/lib/pathsGetters/getTsPath.ts deleted file mode 100644 index 44c78254e..000000000 --- a/src/lib/pathsGetters/getTsPath.ts +++ /dev/null @@ -1,36 +0,0 @@ -/** - * Copyright 2022 Fluence Labs Limited - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import path from "node:path"; - -import { - APP_TS_FILE_NAME, - DEPLOYED_APP_TS_FILE_NAME, - TS_DIR_NAME, -} from "../const"; - -import { getProjectFluenceDirPath } from "./getProjectFluenceDirPath"; - -export const getTsPath = (): string => { - const projectFluenceDir = getProjectFluenceDirPath(); - return path.join(projectFluenceDir, TS_DIR_NAME); -}; - -export const getAppTsPath = (): string => - path.join(getTsPath(), APP_TS_FILE_NAME); - -export const getDeployedAppTsPath = (): string => - path.join(getTsPath(), DEPLOYED_APP_TS_FILE_NAME); diff --git a/src/lib/prompt.ts b/src/lib/prompt.ts index cf735ae9c..da8099bd4 100644 --- a/src/lib/prompt.ts +++ b/src/lib/prompt.ts @@ -86,7 +86,7 @@ const prompt = async ({ if (!isInteractive) { throw new Error( - `Can't prompt when ${color.yellow( + `Can't prompt when in non-interactive mode or when ${color.yellow( `--${NO_INPUT_FLAG_NAME}` )} is set.${advice}` ); @@ -101,7 +101,7 @@ const prompt = async ({ throw new Error("Prompt error"); }; -type ConfirmOptions = DistinctQuestion & { +type ConfirmArg = DistinctQuestion & { isInteractive: boolean; message: string; flagName?: string | undefined; @@ -111,7 +111,7 @@ export const confirm = ({ isInteractive, flagName, ...question -}: ConfirmOptions): Promise => +}: ConfirmArg): Promise => prompt({ ...question, type: "confirm", @@ -120,7 +120,7 @@ export const confirm = ({ flagName, }); -type InputOptions = DistinctQuestion & { +type InputArg = DistinctQuestion & { isInteractive: boolean; message: string; flagName?: string | undefined; @@ -130,7 +130,7 @@ export const input = ({ isInteractive, flagName, ...question -}: InputOptions): Promise => +}: InputArg): Promise => prompt({ ...question, type: "input", @@ -141,7 +141,7 @@ export const input = ({ type SeparatorObj = InstanceType; -export type Choices = T extends string +export type Choices = [T] extends [string] ? Array : Array<{ value: T; name: string } | SeparatorObj>; diff --git a/src/lib/rust.ts b/src/lib/rust.ts new file mode 100644 index 000000000..d95d0eafd --- /dev/null +++ b/src/lib/rust.ts @@ -0,0 +1,261 @@ +/** + * Copyright 2022 Fluence Labs Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import fsPromises from "node:fs/promises"; +import path from "node:path"; + +import color from "@oclif/color"; + +import { getVersionToUse } from "./configs/user/dependency"; +import { + BIN_DIR_NAME, + CargoDependency, + CARGO_GENERATE_CARGO_DEPENDENCY, + CARGO_GENERATE_RECOMMENDED_VERSION, + CommandObj, + FS_OPTIONS, + MARINE_CARGO_DEPENDENCY, + MARINE_RECOMMENDED_VERSION, + MREPL_CARGO_DEPENDENCY, + MREPL_RECOMMENDED_VERSION, + RUST_TOOLCHAIN_REQUIRED_TO_INSTALL_MARINE, + RUST_WASM32_WASI_TARGET, +} from "./const"; +import { execPromise } from "./execPromise"; +import { replaceHomeDir } from "./helpers/replaceHomeDir"; +import { unparseFlags } from "./helpers/unparseFlags"; +import { + ensureUserFluenceCargoCratesPath, + ensureUserFluenceCargoDir, +} from "./paths"; + +const CARGO = "cargo"; +const RUSTUP = "rustup"; + +const ensureRust = async (commandObj: CommandObj): Promise => { + if (!(await isRustInstalled())) { + if (commandObj.config.windows) { + commandObj.error( + "Rust needs to be installed. Please visit https://www.rust-lang.org/tools/install for installation instructions" + ); + } + + const rustupInitFlags = unparseFlags( + { + quiet: true, + y: true, + }, + commandObj + ); + + await execPromise( + `curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- ${rustupInitFlags}`, + "Installing rust" + ); + + if (!(await isRustInstalled())) { + commandObj.error( + `Installed rust without errors but ${color.yellow( + RUSTUP + )} or ${color.yellow(CARGO)} not in PATH` + ); + } + } + + if (!(await hasRequiredRustToolchain())) { + await execPromise( + `${RUSTUP} install ${RUST_TOOLCHAIN_REQUIRED_TO_INSTALL_MARINE}`, + `Installing ${color.yellow( + RUST_TOOLCHAIN_REQUIRED_TO_INSTALL_MARINE + )} rust toolchain` + ); + if (!(await hasRequiredRustToolchain())) { + commandObj.error( + `Not able to install ${color.yellow( + RUST_TOOLCHAIN_REQUIRED_TO_INSTALL_MARINE + )} rust toolchain` + ); + } + } + + if (!(await hasRequiredRustTarget())) { + await execPromise( + `${RUSTUP} target add ${RUST_WASM32_WASI_TARGET}`, + `Adding ${color.yellow(RUST_WASM32_WASI_TARGET)} rust target` + ); + if (!(await hasRequiredRustTarget())) { + commandObj.error( + `Not able to install ${color.yellow( + RUST_WASM32_WASI_TARGET + )} rust target` + ); + } + } +}; + +const isRustInstalled = async (): Promise => { + try { + await execPromise(`${CARGO} --version`); + await execPromise(`${RUSTUP} --version`); + return true; + } catch { + return false; + } +}; + +const hasRequiredRustToolchain = async (): Promise => + (await execPromise(`${RUSTUP} toolchain list`)).includes( + RUST_TOOLCHAIN_REQUIRED_TO_INSTALL_MARINE + ); + +const hasRequiredRustTarget = async (): Promise => + (await execPromise(`${RUSTUP} target list`)).includes( + `${RUST_WASM32_WASI_TARGET} (installed)` + ); + +const cargoInstall = async ({ + packageName, + version, + isNightly, + isGlobalDependency, + commandObj, + message, +}: CargoDependencyInfo & { + version: string; + commandObj: CommandObj; + message: string; +}): Promise => + execPromise( + `${CARGO}${ + isNightly === true ? " +nightly" : "" + } install ${packageName} ${unparseFlags( + { + version, + ...(isGlobalDependency === true + ? {} + : { + root: await ensureUserFluenceCargoDir( + commandObj, + isGlobalDependency + ), + }), + }, + commandObj + )}`, + message + ); + +type CargoDependencyInfo = { + recommendedVersion: string; + packageName: string; + isNightly?: true; + isGlobalDependency?: true; +}; + +export const cargoDependencies: Record = { + [MARINE_CARGO_DEPENDENCY]: { + recommendedVersion: MARINE_RECOMMENDED_VERSION, + packageName: MARINE_CARGO_DEPENDENCY, + isNightly: true, + }, + [MREPL_CARGO_DEPENDENCY]: { + recommendedVersion: MREPL_RECOMMENDED_VERSION, + packageName: MREPL_CARGO_DEPENDENCY, + isNightly: true, + }, + [CARGO_GENERATE_CARGO_DEPENDENCY]: { + recommendedVersion: CARGO_GENERATE_RECOMMENDED_VERSION, + packageName: CARGO_GENERATE_CARGO_DEPENDENCY, + isGlobalDependency: true, + }, +}; + +type CargoDependencyArg = { + name: CargoDependency; + commandObj: CommandObj; +}; + +const isCorrectVersionInstalled = async ({ + name, + commandObj, + isGlobalDependency, +}: CargoDependencyArg & { + isGlobalDependency: true | undefined; +}): Promise => { + const { packageName, recommendedVersion } = cargoDependencies[name]; + const cratesTomlPath = await ensureUserFluenceCargoCratesPath( + commandObj, + isGlobalDependency + ); + const version = await getVersionToUse(recommendedVersion, name, commandObj); + + try { + const cratesTomlContent = await fsPromises.readFile( + cratesTomlPath, + FS_OPTIONS + ); + return cratesTomlContent.includes(`${packageName} ${version}`); + } catch { + return false; + } +}; + +export const ensureCargoDependency = async ({ + name, + commandObj, +}: CargoDependencyArg): Promise => { + await ensureRust(commandObj); + const dependency = cargoDependencies[name]; + const { isGlobalDependency, packageName, recommendedVersion } = dependency; + const userFluenceCargoCratesPath = await ensureUserFluenceCargoCratesPath( + commandObj, + isGlobalDependency + ); + const dependencyPath = path.join( + await ensureUserFluenceCargoDir(commandObj, isGlobalDependency), + BIN_DIR_NAME, + packageName + ); + if ( + await isCorrectVersionInstalled({ name, commandObj, isGlobalDependency }) + ) { + return dependencyPath; + } + const version = await getVersionToUse(recommendedVersion, name, commandObj); + + await cargoInstall({ + version, + message: `Installing version ${color.yellow( + version + )} of ${packageName} to ${replaceHomeDir( + await ensureUserFluenceCargoDir(commandObj, isGlobalDependency) + )}`, + commandObj, + ...dependency, + }); + + if ( + await isCorrectVersionInstalled({ name, commandObj, isGlobalDependency }) + ) { + return dependencyPath; + } + + return commandObj.error( + `Not able to install ${color.yellow( + version + )} of ${packageName} to ${replaceHomeDir(userFluenceCargoCratesPath)}` + ); +}; diff --git a/src/lib/typeHelpers.ts b/src/lib/typeHelpers.ts index f829cbaaf..c9ad43292 100644 --- a/src/lib/typeHelpers.ts +++ b/src/lib/typeHelpers.ts @@ -20,6 +20,15 @@ export type Mutable = { -readonly [Key in keyof Type]: Type[Key]; }; +export type Flags = Record< + T, + string | number | boolean | Array +>; + +export type OptionalFlags = Partial< + Record> +>; + export const isObject = ( unknown: unknown ): unknown is Record =>