diff --git a/.dockerignore b/.dockerignore index a5e4eb39..612967f9 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,5 +1,4 @@ .dockerignore -package.json README.md Dockerfile.c12e-ci.cortex-cli c12e-ci.yml diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 00000000..06a1644b --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,36 @@ +{ + "parserOptions": { + "ecmaVersion": "latest", + "sourceType": "module" + }, + "extends": "airbnb-base", + "rules": { + "no-underscore-dangle": [ + 0 + ], + "import/extensions": "off", + "require-await": "error", + "max-classes-per-file": "off", + "no-console": "off", // This is a cli app.. + "indent": 0, + "global-require":"off", + "no-return-assign": "off", + "function-paren-newline": "off", + "function-call-argument-newline": "off", + "no-multiple-empty-lines": "off", + "no-param-reassign": "warn", + "no-undef": "error", + "no-trailing-spaces": "off", + "prefer-promise-reject-errors": "warn", + "max-len": [ + "warn", {"code":180} // High res display .. + ], + "no-restricted-properties": "warn", + "camelcase": "warn", + "class-methods-use-this": "off" + }, + "env": { + "node": true, + "mocha": true + } +} diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..ba1f2bd7 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Set the default behavior, in case people don't have core.autocrlf set +* text eol=lf diff --git a/.github/workflows/package-binary.yaml b/.github/workflows/package-binary.yaml new file mode 100644 index 00000000..3e09d904 --- /dev/null +++ b/.github/workflows/package-binary.yaml @@ -0,0 +1,65 @@ +name: package-standalone-binary +run-name: +on: + push: + branches: + - main + - develop +jobs: + package-linux: + runs-on: ubuntu-latest + steps: + - name: Check out source + uses: actions/checkout@v3 + - name: Install node + uses: actions/setup-node@v3 + with: + node-version: '18' + - name: Build, test, package + run: | + npm ci + npm test + npm run package-binary + - name: Upload binary + uses: actions/upload-artifact@v3 + with: + name: binary-linux + path: ./dist/cortex + package-mac: + runs-on: macos-latest + steps: + - name: Check out + uses: actions/checkout@v3 + - name: Install node + uses: actions/setup-node@v3 + with: + node-version: '18' + - name: Build, test, package + run: | + npm ci + npm test + npm run package-binary + - name: Upload binary + uses: actions/upload-artifact@v3 + with: + name: binary-macos + path: ./dist/cortex + package-windows: + runs-on: windows-latest + steps: + - name: Check out + uses: actions/checkout@v3 + - name: Install node + uses: actions/setup-node@v3 + with: + node-version: '18' + - name: Build, test, package + run: | + npm ci + npm test + npm run package-binary-windows + - name: Upload binary + uses: actions/upload-artifact@v3 + with: + name: binary-win64 + path: ./dist/cortex.exe diff --git a/.gitignore b/.gitignore index 03269ada..6e23bb49 100644 --- a/.gitignore +++ b/.gitignore @@ -66,6 +66,9 @@ jspm_packages # Mongo Explorer plugin: .idea/mongoSettings.xml +# Visual Studio Code +.vscode/ + ## File-based project format: *.iws @@ -93,5 +96,10 @@ test/cortex/local.json .local/ dist/ - -package-lock.json \ No newline at end of file +cortex-cli-docs.md +*.code-workspace +package/ +*.tgz +npm-shrinkwrap.json +*.amp +test/cortex/cache.dat diff --git a/.husky/pre-push b/.husky/pre-push new file mode 100755 index 00000000..449fcdee --- /dev/null +++ b/.husky/pre-push @@ -0,0 +1,4 @@ +#!/bin/sh +. "$(dirname "$0")/_/husky.sh" + +npm test diff --git a/.mocharc.yaml b/.mocharc.yaml new file mode 100644 index 00000000..b2197505 --- /dev/null +++ b/.mocharc.yaml @@ -0,0 +1,7 @@ +exit: true +extension: ['js'] +bail: false +slow: 1000 +timeout: 10000 +ui: 'bdd' +recursive: true diff --git a/.npmrc b/.npmrc index 43c97e71..9702fa6f 100644 --- a/.npmrc +++ b/.npmrc @@ -1 +1,2 @@ -package-lock=false +save-exact=true +engine-strict=true \ No newline at end of file diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 6f6e67bb..00000000 --- a/.travis.yml +++ /dev/null @@ -1,24 +0,0 @@ -language: node_js -node_js: -- lts/* -cache: - directories: - - node_modules -deploy: - provider: npm - email: msanchez@cognitivescale.com - skip_cleanup: true - on: - branch: master - api_key: - secure: q7P79e+krncuRo130vZJTZ4uG++yhYeWBYNZQC4/fDrta4qF7ER9W0CQ/CTzfnqnT6arkQS+l0QmIrzxEiB0VDh2Oa9ZFTlwHomOQYUaZon1g7+8vlfGUbU6t52fRwhDZDZElJjYRz24D9yI6WIf8RErTYp/zP4ezkm/hwHm1x7Usrea5kVdAz7sBzuUGWwf5G7+Kxav6tYhe8YE4+VNfCvrDqBaYlzh4/9kGnPMEPe2GaiLWoEvdhP35TgVQvZCe+pehp7EtuwreGBETB0TTsmVQSwkVGvgMHjncjFEsOO4ClCTnbe6OrQ3NDf+JMA4gjRPh22B731GQZDhIbVP1CH2ODqFNy6cVwoc7Bz778vYyedXWtzlX9BD1gB9qUXPFS+XtS59PY2CL1c+cMuIdfYDyrJ1CIYHoosVxao1S/dwt7lgufB4acDgwwcQwZLzM12Bl4H2ibDbCFikcQgXZWoeHCkVXL2rZ9tnd0G5y/UuWUnU5rgR4v5YG/D5lrnIU4E2OkoW3uTNjt+JBmLkqMkwe7W7YeXtCJ95bd97cc+uQetevNay+dIwDZ3wage7Dfih8gl3muFKdea/FFNsElLVo8kgF5lyM6Eaf1rc5G3Zoq+5B8led6CFLvfbeVkeq3fZIVsNuXD+LeuJENlxPmYsgg9VrJdtKhIn9V0kF28= -#deploy: -# provider: npm -# email: msanchez@cognitivescale.com -# skip_cleanup: true -# on: -# branch: staging -# api_key: -# secure: q7P79e+krncuRo130vZJTZ4uG++yhYeWBYNZQC4/fDrta4qF7ER9W0CQ/CTzfnqnT6arkQS+l0QmIrzxEiB0VDh2Oa9ZFTlwHomOQYUaZon1g7+8vlfGUbU6t52fRwhDZDZElJjYRz24D9yI6WIf8RErTYp/zP4ezkm/hwHm1x7Usrea5kVdAz7sBzuUGWwf5G7+Kxav6tYhe8YE4+VNfCvrDqBaYlzh4/9kGnPMEPe2GaiLWoEvdhP35TgVQvZCe+pehp7EtuwreGBETB0TTsmVQSwkVGvgMHjncjFEsOO4ClCTnbe6OrQ3NDf+JMA4gjRPh22B731GQZDhIbVP1CH2ODqFNy6cVwoc7Bz778vYyedXWtzlX9BD1gB9qUXPFS+XtS59PY2CL1c+cMuIdfYDyrJ1CIYHoosVxao1S/dwt7lgufB4acDgwwcQwZLzM12Bl4H2ibDbCFikcQgXZWoeHCkVXL2rZ9tnd0G5y/UuWUnU5rgR4v5YG/D5lrnIU4E2OkoW3uTNjt+JBmLkqMkwe7W7YeXtCJ95bd97cc+uQetevNay+dIwDZ3wage7Dfih8gl3muFKdea/FFNsElLVo8kgF5lyM6Eaf1rc5G3Zoq+5B8led6CFLvfbeVkeq3fZIVsNuXD+LeuJENlxPmYsgg9VrJdtKhIn9V0kF28= -# tag: -# staging diff --git a/Dockerfile b/Dockerfile new file mode 120000 index 00000000..2c1e1661 --- /dev/null +++ b/Dockerfile @@ -0,0 +1 @@ +Dockerfile.c12e-ci.cortex-cli \ No newline at end of file diff --git a/Dockerfile.c12e-ci.cortex-cli b/Dockerfile.c12e-ci.cortex-cli index 0a899cc0..e321e6f8 100644 --- a/Dockerfile.c12e-ci.cortex-cli +++ b/Dockerfile.c12e-ci.cortex-cli @@ -1,8 +1,11 @@ -FROM c12e/nodejs:8.9.4 +FROM node:18-bullseye-slim -COPY . /from_ci_cd_build - -# Make sure the cortex executable is on the path ... -RUN ln -s /from_ci_cd_build/bin/cortex.js /usr/local/bin/cortex - -ENTRYPOINT ["/from_ci_cd_build/entrypoint.sh"] +COPY cortex-cli.tgz /app/ +COPY scripts/entrypoint.sh / +RUN apt-get update +RUN apt-get install -y libsecret-1-dev +RUN npm install -g /app/cortex-cli.tgz \ + && echo "default:x:1001:0:Default Application User:/app:/sbin/nologin" >> /etc/passwd \ + && chown -R 1001 /app +USER 1001 +ENTRYPOINT ["/entrypoint.sh"] diff --git a/README.md b/README.md index 22b099e8..11420d83 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # Cortex CLI A command line utility for the Cortex Platform. + [![Build Status](https://secure.travis-ci.org/CognitiveScale/cortex-cli.svg)](http://travis-ci.org/CognitiveScale/cortex-cli) ### Installation: @@ -23,6 +24,18 @@ You can link your local copy of cortex-cli to your globally installed version: ```bash npm link ``` + +The cortex cli can be run directly from the source with a simple wrapper script +``` +git clone https://github.com/CognitiveScale/cortex-cli.git +cat > /usr/local/bin/cortex < [sub-command] [options] ### Getting Started ```bash -cortex configure -Cortex URL [https://api.cortex.insights.ai]: -Account []: -Username []: -Password: ********** +cortex configure --file /personal/access/token.json --project defaultProject + +or + +cortex configure --project defaultProject +Cortex Personal Access Token: ``` Upon successful login, a config file will be saved in your home directory with your authentication token for future use. (**NOTE:** currently, you will have to re-run the configure command when your token expires). @@ -81,12 +95,12 @@ cortex configure --profile myprofile cortex agents list --profile myprofile ``` -#### Filtering Results using the --query option -Many of the commands support a __--query__ option for filtering JSON responses. Queries use JMESPath to filter JSON documents. The specification for JMESPath can be found here: http://jmespath.org/. It is similar to the popular JQ tool and supported by Amazon AWS and some other notable services. +#### Filtering Results using the --json or --query option (DEPRECATION WARNING for --query) +Many of the commands support a __--json / --query__ option for filtering JSON responses. Queries use JMESPath to filter JSON documents. The specification for JMESPath can be found here: http://jmespath.org/. It is similar to the popular JQ tool and supported by Amazon AWS and some other notable services. For example, if I want to select just the name and title from my previous output: ```bash -cortex agents list --query "[].{name: name, title: title}" +cortex agents list --json "[].{name: name, title: title}" [ { "name": "tutorial/movie_recommendation", @@ -103,89 +117,84 @@ cortex agents list --query "[].{name: name, title: title}" ] ``` -### Command List and Implementation Status: - -- [x] configure - - [x] list -- [ ] deploy -- [x] functions - - [x] list - - [X] describe [functionName] - - [x] invoke [functionName] - - [X] deploy [functionName] - - [ ] delete [functionName] -- [ ] stacks - - [ ] list - - [ ] describe - - [ ] deploy - - [ ] delete -- [x] agents - - [x] list - - [x] save [agentDefinition] - - [x] describe [agentName] - - [ ] delete [agentName] - - [x] get-snapshots [agentName] - - [x] create-snapshots [snapshotDefinition] - - [x] invoke [serviceName] - - [x] get-service-activation [activationId] -- [x] types - - [x] list - - [x] save [typeDefinition] - - [x] describe [typeName] - - [ ] delete [typeName] -- [x] skills - - [x] list - - [x] save [skillDefinition] - - [x] describe [skillName] - - [ ] delete [skillName] - - [x] generate -- [x] processors - - [ ] save-runtime [runtimeDefinition] - - [x] list-runtime-types - - [x] list-runtimes - - [x] describe [runtimeName] - - [x] delete [runtimeName] - - [x] invoke [runtimeName] [actionId] - - [x] list-actions [runtimeName] -- [x] content - - [x] upload - - [x] download - - [x] delete - - [x] list -- [ ] jobs - - [x] list - - [x] save - - [x] describe - - [x] status - - [ ] delete -- [x] tasks - - [x] list - - [x] logs - - [x] cancel - - [x] describe -- [ ] connections - - [x] list - - [x] list-types - - [x] save [connectionDefinition] - - [ ] delete [connectionName] - - [x] describe [connectionName] - - [x] test [connectionName] - - [x] generate -- [ ] datasets - - [x] list - - [x] save [datasetDefinition] - - [ ] delete [datasetName] - - [x] describe [datasetName] - - [x] get-dataframe [datasetName] - - [x] get-stream [datasetName] - - [x] generate -- [ ] snapshots -- [ ] environments - - [ ] list - - [ ] save [environmentDefinition] - - [ ] delete [environmentDefinition] - - [ ] describe [environmentName] -- [ ] container-registries - - [ ] list - - [ ] save [registryName] - - [ ] delete [registryName] +## Standalone binary + +We use caxa https://github.com/leafac/caxa to build standalone binaries. +Previously, we used `pkg` but it doesn't support ESM without transpilation as of 07/2023 + +Executables are created for the following platforms: + - Linux 64bit intel + - Windows 64bit intel + - macOS 64 bit intel. + - macOS 64 bit M1/M2. + +### Signed binaries status +* windows: binaries are currently **unsigned** +* macOS: adhoc signature using [ldid](https://github.com/xerub/ldid) + +## Subcommands & Compatibility Checks + +The following sequene diagram shows an example running a command with Cortex CLI - namely which HTTP requests. ([This diagram can be viewered here](https://mermaid.live/view#pako:eNp9VE1z2jAQ_Ss7nh6aAeJLT54OHcZAQ4c4TEx64iLLa6IiS6ok0zCZ_PdKlg0k0Ookad--_XgrvUZUlhglkcHfDQqKU0a2mtQbAW6RxkrR1AXqcJ5QKzVMcR-OhXyBdLmAVNY1EaUJt34poi2jTBFhIQVigEpt8eU6IG-KEwQUU8iZwI4NRRk25x5zxtEcjMX60pZ7rhyFITBZLS7tmbdnq_tgcbWMxuNBmlyEB86M3YgAo5o5AsJhKUkJKy0rl0Ewpd7_lFACj7PJFD7dPdzP4ttAGlMpKrYN-BO0C5zyxp00PD0uh576F1I7BLT0to_ehsgTuFuvV_B9toa4IoVmNN5_iZmoZEDlHd-P_CGDzznqvSNdsxoht6RWQ5gjsY1GmHOyNTc99ah1WrKatUrQTksoiMESpICqc6u825ko3YhwCwtz5um2Svmyy28nuX0FDpJAiRy3xCJYCebo07Fyg-9dBk6eBGZaS53Ak9gJ-Uf003aRSSYdrfRFu0jDzAnRtGBFLCsYZ_YA6TPS3YWmVzFtR5vC53298fTcKyZKcUdmmRQmDqqPKGdnyrTlB20e3Vtj2nX3J2rjPG76lELAQXYWsWti9oFisieMk4Jj-wA7nl7UfzYlD01xz9ihL2fbDTys3j-__7ZAhWE18dduN4775zPSqKRh7rtgR6oPJRwjnbKOhlGNuiasdD_Sq7_eRPYZa9xEiduWRO820Ua8OZz_mvKDoFFidYPDqFGlG6vu9wqXb38B-i2SbA)). + +The Cortex CLI will limit the set of available subcommands, to those which are supported by the Cortex cluster that it is +communicating with. + +```mermaid +sequenceDiagram + autonumber + Actor Dev + box CLI Commands + participant C as cortex + participant Sub as cortex pipelines + end + participant Filesystem + participant S as Sensa API + participant N as NPM + Dev->>+C: cortex pipelines list + + critical Load Profile + C->>+Filesystem: READ $HOME/.cortex/config + Filesystem->>+C: Cluster URL, Project, etc. + + C->>+S: HTTP GET /fabric/v4/info + S->>+C: JSON (Server Time Stamp, Feature Flags) + C-->+C: Limit Subcommands based on feature flags + end + + alt Is Subcommand Supported? + C->>Sub: delegate to subcommand + else + C->>+Dev: Error: Unknown Command + end + + Note over Sub,N: Run Compatibility Check + + critical Compatibility Check + Sub->>S: HTTP GET /fabric/v4/compatibility/applications/cortex-cli + S->>+Sub: JSON (Required Version) + + Sub->>+N: HTTP GET + N->>+Sub: JSON (Available CLI Versions) + end + + Note over Sub,S: Run Action + + critical List Pipelines + Sub->>S: HTTP GET /fabric/v4/projects//pipeline-repositories + S->>+Sub: JSON (Pipelines) + end +``` + +Practical Notes: +* Only want to load the Profile & perform a Compatibility Check once during command execution +* Clear downside is that startup for initial help commands are slightly slower due to the dynamic nature of loading + subcommands + +### Preview Features + +All preview features cam be enabled in the CLI by setting the `PREVIEW_FEATURES_ENABLED` environment variable. + + Example + ``` + PREVIEW_FEATURES_ENABLED=* cortex -h + ``` diff --git a/bin/cortex-accounts.js b/bin/cortex-accounts.js deleted file mode 100755 index a7c57ad6..00000000 --- a/bin/cortex-accounts.js +++ /dev/null @@ -1,172 +0,0 @@ -#!/usr/bin/env node - -/* - * Copyright 2018 Cognitive Scale, Inc. All Rights Reserved. - * - * 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. - */ - -const helper = require('./utils.js'); -const program = require('commander'); -const chalk = require('chalk'); - -const { - CreateGroupCommand, - AddMembersToGroupCommand, - ListGroupsCommand, - DescribeGroupCommand, - DeleteGroupCommand, - RemoveMembersFromGroupCommand, - RegisterResourceCommand, - GrantGroupAccessToResourceCommand, -} = require('../src/commands/accounts'); - -let processed = false; -program.description('Work with Cortex Accounts'); - -// Create Group -program - .command('create-groups ') - .description('Create a group') - .option('--color [on/off]', 'Turn on/off colors for JSON output.', 'on') - .option('--profile [profile]', 'The profile to use') - .option('--description [description]', 'Group description') - .action((groupName, options) => { - try { - new CreateGroupCommand(program).execute(groupName, options); - processed = true; - } - catch (err) { - console.error(chalk.red(err.message)); - } - }); - -// Add Members to Group -program - .command('add-members-groups [members...]') - .description('Add members to group') - .option('--color [on/off]', 'Turn on/off colors for JSON output.', 'on') - .option('--profile [profile]', 'The profile to use') - .action((groupName, members, options) => { - try { - new AddMembersToGroupCommand(program).execute(groupName, members, options); - processed = true; - } - catch (err) { - console.error(chalk.red(err.message)); - } - }); - -// List Groups -program - .command('list-groups') - .description('List groups') - .option('--color [on/off]', 'Turn on/off colors for JSON output.', 'on') - .option('--profile [profile]', 'The profile to use') - .action((options) => { - try { - new ListGroupsCommand(program).execute(options); - processed = true; - } - catch (err) { - console.error(chalk.red(err.message)); - } - }); - -// Describe Group -program - .command('describe-groups ') - .description('Describe a group') - .option('--color [on/off]', 'Turn on/off colors for JSON output.', 'on') - .option('--profile [profile]', 'The profile to use') - .action((groupName, options) => { - try { - new DescribeGroupCommand(program).execute(groupName, options); - processed = true; - } - catch (err) { - console.error(chalk.red(err.message)); - } - }); - -// Delete Group -program - .command('delete-groups ') - .description('Delete a group') - .option('--color [on/off]', 'Turn on/off colors for JSON output.', 'on') - .option('--profile [profile]', 'The profile to use') - .action((groupName, options) => { - try { - new DeleteGroupCommand(program).execute(groupName, options); - processed = true; - } - catch (err) { - console.error(chalk.red(err.message)); - } - }); - -// Remove Members From Group -program - .command('remove-members-groups [members...]') - .description('Remove members from a group') - .option('--color [on/off]', 'Turn on/off colors for JSON output.', 'on') - .option('--profile [profile]', 'The profile to use') - .action((groupName, members, options) => { - try { - new RemoveMembersFromGroupCommand(program).execute(groupName, members, options); - processed = true; - } - catch (err) { - console.error(chalk.red(err.message)); - } - }); - -// Register Resource -program - .command('register-resources ') - .description('Register a resource') - .option('--color [on/off]', 'Turn on/off colors for JSON output.', 'on') - .option('--profile [profile]', 'The profile to use') - .option('--description [description]', 'Resource description') - .option('--access [access]', 'Access level of resource [read/write/admin/execute]') - .action((resourceName, options) => { - try { - new RegisterResourceCommand(program).execute(resourceName, options); - processed = true; - } - catch (err) { - console.error(chalk.red(err.message)); - } - }); - -// Grant Group Access To Resource -program - .command('grant-group-access-resources ') - .description('Grant group access to a resource') - .option('--color [on/off]', 'Turn on/off colors for JSON output.', 'on') - .option('--profile [profile]', 'The profile to use') - .action((resourceId, groupName, options) => { - try { - new GrantGroupAccessToResourceCommand(program).execute(resourceId, groupName, options); - processed = true; - } - catch (err) { - console.error(chalk.red(err.message)); - } - }); - -process.env.DOC && require('../src/commands/utils').exportDoc(program); - -program.parse(process.argv); -if (!processed) - ['string', 'undefined'].includes(typeof program.args[0]) && helper.helpAndExit(program); \ No newline at end of file diff --git a/bin/cortex-actions.js b/bin/cortex-actions.js index 9f1d3534..fd59cb21 100755 --- a/bin/cortex-actions.js +++ b/bin/cortex-actions.js @@ -1,146 +1,95 @@ #!/usr/bin/env node +import esMain from 'es-main'; +import { Command } from 'commander'; +import { callCommand } from '../src/compatibility.js'; +import { + ListActionsCommand, DescribeActionCommand, DeleteActionCommand, DeployActionCommand, +} from '../src/commands/actions.js'; +import { + DEFAULT_LIST_SKIP_COUNT, DEFAULT_LIST_LIMIT_COUNT, DEFAULT_LIST_SORT_PARAMS, GET_DEFAULT_SORT_CLI_OPTION, LIST_JSON_HELP_TEXT, QUERY_JSON_HELP_TEXT, +} from '../src/constants.js'; +import { checkForEmptyArgs } from '../src/commands/utils.js'; -/* - * Copyright 2018 Cognitive Scale, Inc. All Rights Reserved. - * - * 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. - */ - -const helper = require('./utils.js'); -const program = require('commander'); -const chalk = require('chalk'); -const { - ListActionsCommand, - DescribeActionCommand, - DeleteActionCommand, - InvokeActionCommand, - DeployActionCommand -} = require('../src/commands/actions'); - -let processed = false; -program.description('Work with Cortex Actions'); - +export function create() { + const program = new Command(); + program.name('cortex actions'); + program.description('Work with Cortex Actions'); // List Actions -program - .command('list') - .description('List the deployed actions') - .option('--color [on/off]', 'Turn on/off colors for JSON output.', 'on') - .option('--profile [profile]', 'The profile to use') - .option('--json', 'Output results using JSON') - .option('--query [query]', 'A JMESPath query to use in filtering the response data. Ignored if output format is not JSON.') - .action((options) => { - try { - new ListActionsCommand(program).execute(options); - processed = true; - } - catch (err) { - console.error(chalk.red(err.message)); - } - }); - + program + .command('list') + .description('List the deployed actions') + .alias('l') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--profile ', 'The profile to use') + .option('--project ', 'The project to use') + .option('--query ', `[DEPRECATION WARNING] ${QUERY_JSON_HELP_TEXT}`) + .option('--json [searchQuery]', LIST_JSON_HELP_TEXT) + .option('--filter ', 'A Mongo style filter to use.') + .option('--limit ', 'Limit number of records', DEFAULT_LIST_LIMIT_COUNT) + .option('--skip ', 'Skip number of records', DEFAULT_LIST_SKIP_COUNT) + .option('--sort ', 'A Mongo style sort statement to use in the query.', GET_DEFAULT_SORT_CLI_OPTION(DEFAULT_LIST_SORT_PARAMS.updatedAt)) + .action(callCommand((options) => { + new ListActionsCommand(program).execute(options); + })); // Describe Action -program - .command('describe ') - .description('Describe an action') - .option('--color [on/off]', 'Turn on/off colors for JSON output.', 'on') - .option('--profile [profile]', 'The profile to use') - .option('--query [query]', 'A JMESPath query to use in filtering the response data.') - .option('-d, --download', 'Download code binary in response') - .action((actionName, options) => { - try { - new DescribeActionCommand(program).execute(actionName, options); - processed = true; - } - catch (err) { - console.error(chalk.red(err.message)); - } - }); - + program + .command('describe ') + .alias('get') + .description('Describe an action') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--profile ', 'The profile to use') + .option('--project ', 'The project to use') + .option('--json [searchPath]', QUERY_JSON_HELP_TEXT) + .option('--query ', `[DEPRECATION WARNING] ${QUERY_JSON_HELP_TEXT}`) + .option('-d, --download', 'Download code binary in response') + .action(callCommand((actionName, options) => { + checkForEmptyArgs({ actionName }); + new DescribeActionCommand(program).execute(actionName, options); + })); // Delete Action -program - .command('delete ') - .description('Delete an action') - .option('--color [on/off]', 'Turn on/off colors for JSON output.', 'on') - .option('--profile [profile]', 'The profile to use') - .option('--query [query]', 'A JMESPath query to use in filtering the response data.') - .option('--actionType [actionType]', 'Type of action') - .action((actionName, options) => { - try { - new DeleteActionCommand(program).execute(actionName, options); - processed = true; - } - catch (err) { - console.error(chalk.red(err.message)); - } - }); - -// Invoke Action -program - .command('invoke ') - .description('Invoke an action') - .option('--params [params]', 'JSON params to send to the action') - .option('--params-file [paramsFile]', 'A file containing either JSON or YAML formatted params') - .option('--color [on/off]', 'Turn on/off colors for JSON output.', 'on') - .option('--profile [profile]', 'The profile to use') - .option('--query [query]', 'A JMESPath query to use in filtering the response data.') - .option('--actionType [actionType]', 'Type of action') - .option('--path [path]', 'Path to the daemon service url being invoked', '') - .option('--method [method]', 'HTTP method') // GET, POST ... - .action((actionName, options) => { - try { - new InvokeActionCommand(program).execute(actionName, options); - processed = true; - } - catch (err) { - console.error(chalk.red(err.message)); - } - }); - + program + .command('delete ') + .description('Delete an action') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--profile ', 'The profile to use') + .option('--project ', 'The project to use') + .action(callCommand((actionName, options) => { + checkForEmptyArgs({ actionName }); + new DeleteActionCommand(program).execute(actionName, options); + })); // Deploy Action -program - .command('deploy ') - .description('Deploy an action') - .option('--kind [kind]', 'Action runtime kind') // python:3, python:2, nodejs:default - .option('--code [code]', 'The code file or code archive to deploy') - .option('--docker [image]', 'Docker image to use as the runner') - .option('--memory [memory]', 'Action memory limit in megabytes', '256') - .option('--timeout [timeout]', 'Execution timeout in milliseconds', '60000') - .option('--color [on/off]', 'Turn on/off colors for JSON output.', 'on') - .option('--profile [profile]', 'The profile to use') - .option('--actionType [actionType]', 'Type of action') - .option('--ports [ports]', 'Docker ports') //'["9091/tcp"]' - .option('--environment [environment]', 'Environment') - .option('--cmd [cmd]', 'Command to be executed') //'["--daemon"]' - .action((actionName, options) => { - try { - if (!options.kind && !options.docker) { - throw new Error('--kind [kind] or --docker [image] required'); - } - - if (options.docker && options.kind) { - throw new Error('Use either --kind [kind] or --docker [image], but not both'); - } - - new DeployActionCommand(program).execute(actionName, options); - processed = true; - } - catch (err) { - console.error(chalk.red(err.message)); - } - }); - -process.env.DOC && require('../src/commands/utils').exportDoc(program); - -program.parse(process.argv); -if (!processed) - ['string', 'undefined'].includes(typeof program.args[0]) && helper.helpAndExit(program); \ No newline at end of file + program + .command('deploy [actionDefinition]') + .alias('save') + .description('Deploy an action') + .storeOptionsAsProperties(false) + .option('--name, --actionName ', 'Action name') + .option('--type, --actionType ', 'Type of action') + .option('--cmd ', 'Command to be executed') // '<"--daemon">' + .option('--image, --docker ', 'Docker image to use as the runner') + .option('--environmentVariables ', 'Docker container environment variables, only used for daemon action types') + .option('--jobTimeout ', 'Job Timeout in seconds until the job is marked as FAILED (default: no timeout), only used for job action types') + .option('-k, --k8sResource ', 'Additional kubernetes resources deployed and owned by the skill, provide as last option specified or end list of files with "--"') + .option('--no-compat', 'Ignore API compatibility checks') + // eslint-disable-next-line max-len + .option('--podspec ', 'A file containing either a JSON or YAML formatted pod spec to merge with the action definition, used for specifying resources (like memory, ephemeral storage, CPUs, and GPUs) and tolerations (like allowing pods to be scheduled on tainted nodes).') + .option('--port ', 'Docker port, only used for daemon action types') // '9091' + .option('--profile ', 'The profile to use') + .option('--project ', 'The project to use') + .option('--scaleCount ', 'Scale count, only used for daemon action types') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + // TODO this command is non-functional, comment for now to avoid confusion + // .option('--push-docker', 'Push Docker image to the Cortex registry.') + .option('-y, --yaml', 'Use YAML format') + .action(callCommand((actionName, options) => { + new DeployActionCommand(program).execute(actionName, options); + })); + return program; +} +if (esMain(import.meta)) { + create().showHelpAfterError().parseAsync(process.argv); +} +export default create(); diff --git a/bin/cortex-agents.js b/bin/cortex-agents.js index 3591a1a3..baf83f2e 100755 --- a/bin/cortex-agents.js +++ b/bin/cortex-agents.js @@ -1,267 +1,251 @@ #!/usr/bin/env node - -/* - * Copyright 2018 Cognitive Scale, Inc. All Rights Reserved. - * - * 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. - */ - -const helper = require('./utils.js'); -const program = require('commander'); -const chalk = require('chalk'); -const { - SaveAgentCommand, - ListAgentsCommand, +import esMain from 'es-main'; +import { Command } from 'commander'; +import { callCommand } from '../src/compatibility.js'; +import { + DEFAULT_LIST_LIMIT_COUNT, + DEFAULT_LIST_SKIP_COUNT, + DEFAULT_LIST_SORT_PARAMS, + GET_DEFAULT_SORT_CLI_OPTION, + LIST_JSON_HELP_TEXT, + QUERY_JSON_HELP_TEXT, +} from '../src/constants.js'; +import { + CreateAgentSnapshotCommand, + DeleteAgentCommand, + DeployAgentCommand, DescribeAgentCommand, + DescribeAgentSnapshotCommand, + GetActivationCommand, InvokeAgentServiceCommand, - GetServiceActivationCommand, - ListAgentInstancesCommand, - CreateAgentInstanceCommand, - GetAgentInstanceCommand, - DeleteAgentInstanceCommand, - StopAgentInstanceCommand, - ListTriggersCommand, + ListActivationsCommand, ListAgentSnapshotsCommand, - CreateAgentSnapshotCommand -} = require('../src/commands/agents'); - -let processed = false; -program.description('Work with Cortex Agents'); - + ListAgentsCommand, + ListServicesCommand, + SaveAgentCommand, + UndeployAgentCommand, + CancelActivationCommand, +} from '../src/commands/agents.js'; +import { checkForEmptyArgs } from '../src/commands/utils.js'; + +export function create() { + const program = new Command(); + program.name('cortex agents'); + program.description('Work with Cortex Agents'); // Save Agent -program - .command('save ') - .description('Save an agent definition') - .option('--color [on/off]', 'Turn on/off colors for JSON output.', 'on') - .option('--profile [profile]', 'The profile to use') - .option('-y, --yaml', 'Use YAML for agent definition format') - .action((agentDefinition, options) => { - try { - new SaveAgentCommand(program).execute(agentDefinition, options); - processed = true; - } - catch (err) { - console.error(chalk.red(err.message)); - } - }); - + program + .command('save ') + .description('Save an agent definition') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--profile ', 'The profile to use') + .option('--project ', 'The project to use') + .option('-y, --yaml', 'Use YAML for agent definition format') + .action(callCommand( + (agentDefinitions, options) => new SaveAgentCommand(program).execute(agentDefinitions, options))); // List Agents -program - .command('list') - .description('List agent definitions') - .option('--color [on/off]', 'Turn on/off colors for JSON output.', 'on') - .option('--profile [profile]', 'The profile to use') - .option('--json', 'Output results using JSON') - .option('--query [query]', 'A JMESPath query to use in filtering the response data. Ignored if output format is not JSON.') - .action((options) => { - try { + program + .command('list') + .description('List agent definitions') + .alias('l') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--profile ', 'The profile to use') + .option('--project ', 'The project to use') + .option('--json [searchQuery]', LIST_JSON_HELP_TEXT) + .option('--query ', `[DEPRECATION WARNING] ${QUERY_JSON_HELP_TEXT}`) + .option('--filter ', 'A Mongo style filter to use.') + .option('--limit ', 'Limit number of records', DEFAULT_LIST_LIMIT_COUNT) + .option('--skip ', 'Skip number of records', DEFAULT_LIST_SKIP_COUNT) + .option('--sort ', 'A Mongo style sort statement to use in the query.', GET_DEFAULT_SORT_CLI_OPTION(DEFAULT_LIST_SORT_PARAMS.updatedAt)) + .action(callCommand((options) => { new ListAgentsCommand(program).execute(options); - processed = true; - } - catch (err) { - console.error(chalk.red(err.message)); - } - }); - + })); +// Deploy agent + program + .command('deploy ') + .description('Deploy the agent resource to the cluster') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--profile ', 'The profile to use') + .option('--project ', 'The project to use') + .action(callCommand((agentNames, options) => { + checkForEmptyArgs({ agentNames }); + new DeployAgentCommand(program).execute(agentNames, options); + })); // Describe Agent -program - .command('describe ') - .description('Describe agent') - .option('--color [on/off]', 'Turn on/off colors for JSON output.', 'on') - .option('--profile [profile]', 'The profile to use') - .option('--query [query]', 'A JMESPath query to use in filtering the response data.') - .option('--versions', 'To get list of versions of an agent') - .action((agentName, options) => { - try { - new DescribeAgentCommand(program).execute(agentName, options); - processed = true; - } - catch (err) { - console.error(chalk.red(err.message)); - } - }); - + program + .command('describe ') + .description('Describe agent') + .alias('get') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('-f --output-file ', 'Write output to file instead of stdout') + .option('-o, --output ', 'Format output as yaml or k8s resource', 'json') + .option('--profile ', 'The profile to use') + .option('--project ', 'The project to use') + .option('--json [searchPath]', QUERY_JSON_HELP_TEXT) + .option('--query ', `[DEPRECATION WARNING] ${QUERY_JSON_HELP_TEXT}`) + .option('--versions', 'To get list of versions of an agent') + .option('--verbose', 'Verbose output', false) + .action(callCommand((agentName, options) => { + checkForEmptyArgs({ agentName }); + new DescribeAgentCommand(program).execute(agentName, options); + })); // Invoke Agent Service -program - .command('invoke ') - .description('Invoke an agent service') - .option('--color [on/off]', 'Turn on/off colors for JSON output.', 'on') - .option('--profile [profile]', 'The profile to use') - .option('--params [params]', 'JSON params to send to the action') - .option('--params-file [paramsFile]', 'A file containing either JSON or YAML formatted params') - .action((agentName, serviceName, options) => { - try { - new InvokeAgentServiceCommand(program).execute(agentName, serviceName, options); - processed = true; - } - catch (err) { - console.error(chalk.red(err.message)); - } - }); - -//Get Agent Service Activation -program - .command('get-service-activation ') - .description('Get service activation') - .option('--color [on/off]', 'Turn on/off colors for JSON output.', 'on') - .option('--profile [profile]', 'The profile to use') - .option('--query [query]', 'A JMESPath query to use in filtering the response data.') - .action((activationId, options) => { - try { - new GetServiceActivationCommand(program).execute(activationId, options); - processed = true; - } - catch (err) { - console.error(chalk.red(err.message)); - } - }); - - -program - .command('list-snapshots ') - .description('List agent snapshots') - .option('--color [on/off]', 'Turn on/off colors for JSON output.', 'on') - .option('--profile [profile]', 'The profile to use') - .action((agentName, options) => { - try { - new ListAgentSnapshotsCommand(program).execute(agentName, options); - processed = true; - } - catch (err) { - console.error(chalk.red(err.message)); - } - }); - -//Create Agent Snapshot -program - .command('create-snapshot [snapshotDefinition]') - .description('Create an agent snapshot') - .option('--color [on/off]', 'Turn on/off colors for JSON output.', 'on') - .option('--profile [profile]', 'The profile to use') - .option('--agentName [name[:version]]', 'The name of the agent to snapshot') - .option('--title [title]', 'A descriptive title for the snapshot') - - .action((snapshotDefinition, options) => { - try { - new CreateAgentSnapshotCommand(program).execute(snapshotDefinition, options); - processed = true; - } - catch (err) { - console.error(chalk.red(err.message)); - } - }); - - -//List agent instances -program - .command('list-instances ') - .description('List agent instances ') - .option('--color [on/off]', 'Turn on/off colors for JSON output.', 'on') - .option('--profile [profile]', 'The profile to use') - .action((agentName, options) => { - try { - new ListAgentInstancesCommand(program).execute(agentName, options); - processed = true; - } - catch (err) { - console.error(chalk.red(err.message)); - } - }); - -//Create agent instances -program - .command('create-instance ') - .description('Create agent instance') - .option('--color [on/off]', 'Turn on/off colors for JSON output.', 'on') - .option('--profile [profile]', 'The profile to use') - .action((instanceDefinition, options) => { - try { - new CreateAgentInstanceCommand(program).execute(instanceDefinition, options); - processed = true; - } - catch (err) { - console.error(chalk.red(err.message)); - } - }); - -//Get agent instance -program - .command('get-instance ') - .description('Get agent instance') - .option('--color [on/off]', 'Turn on/off colors for JSON output.', 'on') - .option('--profile [profile]', 'The profile to use') - .option('--json', 'Output results using JSON') - .action((instanceId, options) => { - try { - new GetAgentInstanceCommand(program).execute(instanceId, options); - processed = true; - } - catch (err) { - console.error(chalk.red(err.message)); - } - }); - -//Delete agent instance -program - .command('delete-instance ') - .description('Delete agent instance') - .option('--color [on/off]', 'Turn on/off colors for JSON output.', 'on') - .option('--profile [profile]', 'The profile to use') - .action((instanceId, options) => { - try { - new DeleteAgentInstanceCommand(program).execute(instanceId, options); - processed = true; - } - catch (err) { - console.error(chalk.red(err.message)); - } - }); - -//Stop agent instance -program - .command('stop-instance ') - .description('Stop agent instance') - .option('--color [on/off]', 'Turn on/off colors for JSON output.', 'on') - .option('--profile [profile]', 'The profile to use') - .action((instanceId, options) => { - try { - new StopAgentInstanceCommand(program).execute(instanceId, options); - processed = true; - } - catch (err) { - console.error(chalk.red(err.message)); - } - }); - -//List triggers -program - .command('list-triggers') - .description('List of triggers for the current tenant') - .option('--color [on/off]', 'Turn on/off colors for JSON output.', 'on') - .option('--profile [profile]', 'The profile to use') - .action((options) => { - try { - new ListTriggersCommand(program).execute(options); - processed = true; - } - catch (err) { - console.error(chalk.red(err.message)); - } - }); - - -process.env.DOC && require('../src/commands/utils').exportDoc(program); - -program.parse(process.argv); -if (!processed) - ['string', 'undefined'].includes(typeof program.args[0]) && helper.helpAndExit(program); + program + .command('invoke ') + .description('Invoke an agent service') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', true) + .option('--profile ', 'The profile to use') + .option('--project ', 'The project to use') + .option('--params ', 'JSON params to send to the action') + .option('--params-file ', 'A file containing either JSON or YAML formatted params') + .option('--scheduleName ', 'Identifier to uniquely identify an agents schedule, defaults to the agent\'s name') + .option('--scheduleCron ', 'Schedule agent invoke periodically using a cron schedule string for example: "0 0 * * *", @hourly, or @daily') + .option('--sync', 'Invoke the agent synchronously', false) + .action(callCommand((agentName, serviceName, options) => { + checkForEmptyArgs({ agentName, serviceName }); + new InvokeAgentServiceCommand(program).execute(agentName, serviceName, options); + })); +// Delete Agent + program + .command('delete ') + .description('Delete an agent') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--profile ', 'The profile to use') + .option('--project ', 'The project to use') + .action(callCommand((agentName, options) => { + checkForEmptyArgs({ agentName }); + new DeleteAgentCommand(program).execute(agentName, options); + })); +// Undeploy agent + program + .command('undeploy ') + .description('Undeploy the agent resource from the cluster') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--profile ', 'The profile to use') + .option('--project ', 'The project to use') + .action(callCommand((agentNames, options) => { + checkForEmptyArgs({ agentNames }); + new UndeployAgentCommand(program).execute(agentNames, options); + })); +// Get activation + program + .command('get-activation ') + .description('Get dataset or service activation') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--profile ', 'The profile to use') + .option('--project ', 'The project to use') + .option('--verbose [boolean]', 'Get debugging info in activation response') + .option('--report [boolean]', 'Get additional debugging info in activation response') + .option('--json [searchPath]', QUERY_JSON_HELP_TEXT) + .option('--query ', `[DEPRECATION WARNING] ${QUERY_JSON_HELP_TEXT}`) + .action(callCommand((activationId, options) => { + checkForEmptyArgs({ activationId }); + new GetActivationCommand(program).execute(activationId, options); + })); +// Cancel activation + program + .command('cancel-activation ') + .description('Cancel pending/running activation') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--profile ', 'The profile to use') + .option('--project ', 'The project to use') + .option('--inFlight [boolean]', 'Leave inflight jobs running', false) + .action(callCommand((activationId, options) => { + checkForEmptyArgs({ activationId }); + new CancelActivationCommand(program).execute(activationId, options); + })); + +// List activations + program + .command('list-activations') + .description('List activations for an agent') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--profile ', 'The profile to use') + .option('--project ', 'The project to use') + .option('--json [searchQuery]', LIST_JSON_HELP_TEXT) + .option('--query ', `[DEPRECATION WARNING] ${QUERY_JSON_HELP_TEXT}`) + .option('--agentName ', 'Query activations by agentName') + .option('--skillName ', 'Query activations by skillName') + .option('--startBefore ', 'Filters activations to include those that started before the specified timestamp.') + .option('--startAfter ', 'Filters activations to include those that started after the specified timestamp.') + .option('--endBefore ', 'Filters activations to include those that ended before the specified timestamp.') + .option('--endAfter ', 'Filters activations to include those that ended after the specified timestamp.') + .option('--correlationId ', 'Query activations with same correlationId') + .option('--status ', 'Filters activations by status [complete|error].') + .option('--limit ', 'Limit number of records', DEFAULT_LIST_LIMIT_COUNT) + .option('--skip ', 'Skip number of records', DEFAULT_LIST_SKIP_COUNT) + // eslint-disable-next-line max-len + .option('--sort ', 'A Mongo style sort statement to use in the query.', GET_DEFAULT_SORT_CLI_OPTION(DEFAULT_LIST_SORT_PARAMS.start)) + .option('--filter ', 'A Mongo style filter to use.') + .action(callCommand((options) => { + new ListActivationsCommand(program).execute(options); + })); + program + .command('list-services ') + .description('List agent service endpoints') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--profile ', 'The profile to use') + .option('--project ', 'The project to use') + .option('--json [searchQuery]', LIST_JSON_HELP_TEXT) + .option('--query ', `[DEPRECATION WARNING] ${QUERY_JSON_HELP_TEXT}`) + .action(callCommand((agentName, options) => { + checkForEmptyArgs({ agentName }); + new ListServicesCommand(program).execute(agentName, options); + })); +// Create Agent Snapshot + program + .command('create-snapshot [snapshotDefinition]') + .description('Create an agent snapshot') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--profile ', 'The profile to use') + .option('--project ', 'The project to use') + .option('--agentName ', 'The name of the agent to snapshot') + .option('--title ', 'A descriptive title for the snapshot') + .action(callCommand((snapshotDefinition, options) => { + new CreateAgentSnapshotCommand(program).execute(snapshotDefinition, options); + })); + program + .command('list-snapshots <agentName>') + .description('List agent snapshots') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--profile <profile>', 'The profile to use') + .option('--project <project>', 'The project to use') + .option('--json [searchQuery]', LIST_JSON_HELP_TEXT) + .option('--query <query>', `[DEPRECATION WARNING] ${QUERY_JSON_HELP_TEXT}`) + .option('--filter <filter>', 'A Mongo style filter to use.') + .option('--limit <limit>', 'Limit number of records', DEFAULT_LIST_LIMIT_COUNT) + .option('--skip <skip>', 'Skip number of records', DEFAULT_LIST_SKIP_COUNT) + .option('--sort <sort>', 'A Mongo style sort statement to use in the query.', GET_DEFAULT_SORT_CLI_OPTION(DEFAULT_LIST_SORT_PARAMS._updatedAt)) + .action(callCommand((agentName, options) => { + checkForEmptyArgs({ agentName }); + new ListAgentSnapshotsCommand(program).execute(agentName, options); + })); + program + .command('describe-snapshot <snapshotId>') + .alias('get-snapshot') + .description('Describe agent snapshot') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--profile <profile>', 'The profile to use') + .option('--project <project>', 'The project to use') + .option('-o, --output <json|yaml|k8s>', 'Format output as yaml or k8s resources') + .action(callCommand((snapshotId, options) => { + checkForEmptyArgs({ snapshotId }); + new DescribeAgentSnapshotCommand(program).execute(snapshotId, options); + })); + return program; +} +if (esMain(import.meta)) { + create().showHelpAfterError().parseAsync(process.argv); +} +export default create(); diff --git a/bin/cortex-applications.js b/bin/cortex-applications.js new file mode 100755 index 00000000..f8fba527 --- /dev/null +++ b/bin/cortex-applications.js @@ -0,0 +1,124 @@ +#!/usr/bin/env node +import esMain from 'es-main'; +import process from 'node:process'; +import { Command } from 'commander'; +import { callCommand } from '../src/compatibility.js'; +import { + DEFAULT_LIST_LIMIT_COUNT, + DEFAULT_LIST_SKIP_COUNT, + DEFAULT_LIST_SORT_PARAMS, + GET_DEFAULT_SORT_CLI_OPTION, + LIST_JSON_HELP_TEXT, + QUERY_JSON_HELP_TEXT, +} from '../src/constants.js'; +import { + DeleteAppCommand, + DeployAppCommand, + DescribeAppCommand, + ListAppsCommand, + SaveAppCommand, + AppLogsCommand, + UndeployAppCommand, +} from '../src/commands/applications.js'; + +export function create() { + const program = new Command(); + program.name('cortex applications'); + program.description('Applications are a mechanism for exposing additional Web UIs and APIS behind Sensa\'s api gateway. \nNOTE: Applications cannot be added to Agents like normal Skills'); +// Deploy Application + program + .command('deploy <names...>') + .description('Deploy the application resource to the cluster') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--profile <profile>', 'The profile to use') + .option('--project <project>', 'The project to use') + .action(callCommand(async (names, options) => { + await new DeployAppCommand(program).execute(names, options); + })); +// Describe Application + program + .command('describe <name>') + .alias('get') + .description('Describe application') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--profile <profile>', 'The profile to use') + .option('--project <project>', 'The project to use') + .option('-f, --output-file <file>', 'Write output to file instead of stdout') + .option('-o, --output <pretty|json|yaml|k8s>', 'Format output as yaml or k8s resource', 'pretty') + .option('--json [searchPath]', QUERY_JSON_HELP_TEXT) + .option('--query <query>', `[DEPRECATION WARNING] ${QUERY_JSON_HELP_TEXT}`) + .option('--verbose', 'Verbose output', false) + .action(callCommand(async (name, options) => { + await new DescribeAppCommand(program).execute(name, options); + })); +// List Applications + program + .command('list') + .description('List applications') + .alias('l') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--profile <profile>', 'The profile to use') + .option('--project <project>', 'The project to use') + .option('--json [searchQuery]', LIST_JSON_HELP_TEXT) + .option('--query <query>', `[DEPRECATION WARNING] ${QUERY_JSON_HELP_TEXT}`) + .option('--nostatus', 'skip extra call for application status') + .option('--filter <filter>', 'A Mongo style filter to use.') + .option('--limit <limit>', 'Limit number of records', DEFAULT_LIST_LIMIT_COUNT) + .option('--skip <skip>', 'Skip number of records', DEFAULT_LIST_SKIP_COUNT) + .option('--sort <sort>', 'A Mongo style sort statement to use in the query.', GET_DEFAULT_SORT_CLI_OPTION(DEFAULT_LIST_SORT_PARAMS.updatedAt)) + .action(callCommand(async (options) => { + await new ListAppsCommand(program).execute(options); + })); +// Delete Application + program + .command('delete <name>') + .description('Delete an application') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--json', 'Output results using JSON') + .option('--profile <profile>', 'The profile to use') + .option('--project <project>', 'The project to use') + .action(callCommand(async (name, options) => { + await new DeleteAppCommand(program).execute(name, options); + })); +// Save Application + program + .command('save <definitions...>') + .description('Save application(s)') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--profile <profile>', 'The profile to use') + .option('--project <project>', 'The project to use') + .option('-k, --k8sResource <file...>', 'Additional kubernetes resources deployed and owned by the application') + // eslint-disable-next-line max-len + .option('--podspec <podspec>', 'A file containing either a JSON or YAML formatted pod spec to merge with the application definition, used for specifying resources (like memory, ephemeral storage, CPUs, and GPUs) and tolerations (like allowing pods to be scheduled on tainted nodes).') + .option('-y, --yaml', 'Use YAML for application definition format') + .option('--scaleCount <count>', 'Scale count, only used for daemon action types') + .action(callCommand(async (definitions, options) => { + await new SaveAppCommand(program).execute(definitions, options); + })); +// Undeploy Application + program + .command('undeploy <names...>') + .description('Undeploy the application resource from the cluster') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--profile <profile>', 'The profile to use') + .option('--project <project>', 'The project to use') + .action(callCommand((names, options) => new UndeployAppCommand(program).execute(names, options))); +// Get Application logs + program + .command('logs <name>') + .description('Get logs from an application\'s pod') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--profile <profile>', 'The profile to use') + .option('--project <project>', 'The project to use') + .option('--raw', 'Get raw logs as a stream') + .action(callCommand((name, options) => new AppLogsCommand(program).execute(name, options))); + return program; +} +if (esMain(import.meta)) { + create().showHelpAfterError().parseAsync(process.argv); +} +export default create(); diff --git a/bin/cortex-assessments.js b/bin/cortex-assessments.js new file mode 100755 index 00000000..2ff4a8f1 --- /dev/null +++ b/bin/cortex-assessments.js @@ -0,0 +1,191 @@ +#!/usr/bin/env node +import esMain from 'es-main'; +import { Command } from 'commander'; +import { callCommand } from '../src/compatibility.js'; +import { + CreateAssessmentCommand, + DeleteAssessmentCommand, + DependencyTreeCommand, + DescribeAssessmentCommand, + ExportAssessmentReportCommand, + GetAssessmentReportCommand, + ListAssessmentCommand, + ListAssessmentReportCommand, + ListResourceTypesCommand, + ListResourcesCommand, + RunAssessmentCommand, +} from '../src/commands/assessments.js'; +import { + DEFAULT_LIST_LIMIT_COUNT, + DEFAULT_LIST_SKIP_COUNT, + DEFAULT_LIST_SORT_PARAMS, + GET_DEFAULT_SORT_CLI_OPTION, + LIST_JSON_HELP_TEXT, + QUERY_JSON_HELP_TEXT, +} from '../src/constants.js'; +import { checkForEmptyArgs } from '../src/commands/utils.js'; + +export function create() { + const program = new Command(); + program.name('cortex assessments'); + program.description('Work with Cortex Assessments'); + program + .command('list-types') + .description('List Cortex resource types') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--profile <profile>', 'The profile to use') + .option('--json [searchQuery]', LIST_JSON_HELP_TEXT) + .option('--query <query>', `[DEPRECATION WARNING] ${QUERY_JSON_HELP_TEXT}`) + .action(callCommand((options) => { + new ListResourceTypesCommand(program).execute(options); + })); + program + .command('dependency-tree [dependencyFile]') + .description('Dependencies of a resource or find missing ones from dependencyFile') + .storeOptionsAsProperties(false) + .requiredOption('--scope <project>', 'project name of the resource') + .option('--name <Cortex component name>', 'Cortex component name', ' ') + .option('--type <Cortex component type>', 'Cortex resource type', ' ') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--profile <profile>', 'The profile to use') + .option('--json [searchQuery]', LIST_JSON_HELP_TEXT) + .option('--query <query>', `[DEPRECATION WARNING] ${QUERY_JSON_HELP_TEXT}`) + .option('--missing [boolean]', 'Filter only missing dependencies') + .action(callCommand((dependencyFile, options) => { + new DependencyTreeCommand(program).execute(dependencyFile, options); + })); + program + .command('list-resources') + .description('List matching resources') + .storeOptionsAsProperties(false) + .option('--scope <projects>', 'Assessment scope projects (comma separated values)') + .option('--name <Cortex component name>', 'Cortex component name (case insensitive regex/substring match)') + .option('--type <Cortex component types>', 'Assessment scope component types (comma separated values)') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--profile <profile>', 'The profile to use') + .option('--json [searchQuery]', LIST_JSON_HELP_TEXT) + .option('--query <query>', `[DEPRECATION WARNING] ${QUERY_JSON_HELP_TEXT}`) + .option('--filter <filter>', 'A Mongo style filter to use.') + .option('--limit <limit>', 'Limit number of records', DEFAULT_LIST_LIMIT_COUNT) + .option('--skip <skip>', 'Skip number of records', DEFAULT_LIST_SKIP_COUNT) + .option('--sort <sort>', 'A Mongo style sort statement to use in the query.') + .action(callCommand((options) => { + new ListResourcesCommand(program).execute(options); + })); + program + .command('create [assessmentDefinition]') + .alias('save') + .description('Create an assessment. Do `list-resources` and `list-types` to select component and type') + .storeOptionsAsProperties(false) + .option('--name <name>', 'Assessment name') + .option('--title <title>', 'Assessment title') + .option('--description <description>', 'Assessment description') + .option('--scope <projects>', 'Assessment scope projects (comma separated values)') + .option('--component <Cortex component name>', 'Cortex component name (case insensitive regex/substring match)') + .option('--type <Cortex component types>', 'Assessment scope component types (comma separated values)') + .option('--overwrite [boolean]', 'Overwrite existing assessment, if exists') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--profile <profile>', 'The profile to use') + .option('-y, --yaml', 'Use YAML format') + .action(callCommand((assessmentDefinition, options) => { + new CreateAssessmentCommand(program).execute(assessmentDefinition, options); + })); + program + .command('list') + .description('List assessments') + .alias('l') + .option('--skip <Skip records for pagination>', 'Skip records for pagination') + .option('--limit <Limit records for pagination>', 'Limit records for pagination') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--profile <profile>', 'The profile to use') + .option('--json [searchQuery]', LIST_JSON_HELP_TEXT) + .option('--query <query>', `[DEPRECATION WARNING] ${QUERY_JSON_HELP_TEXT}`) + .option('--filter <filter>', 'A Mongo style filter to use.') + .option('--limit <limit>', 'Limit number of records', DEFAULT_LIST_LIMIT_COUNT) + .option('--skip <skip>', 'Skip number of records', DEFAULT_LIST_SKIP_COUNT) + .option('--sort <sort>', 'A Mongo style sort statement to use in the query.', GET_DEFAULT_SORT_CLI_OPTION(DEFAULT_LIST_SORT_PARAMS._updatedAt)) + .action(callCommand((options) => { + new ListAssessmentCommand(program).execute(options); + })); + program + .command('describe <assessmentName>') + .alias('get') + .description('Describe the assessment') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--profile <profile>', 'The profile to use') + .action(callCommand((assessmentName, options) => { + checkForEmptyArgs({ assessmentName }); + new DescribeAssessmentCommand(program).execute(assessmentName, options); + })); + program + .command('delete <assessmentName>') + .description('Delete the assessment and its generated reports') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--profile <profile>', 'The profile to use') + .action(callCommand((assessmentName, options) => { + checkForEmptyArgs({ assessmentName }); + new DeleteAssessmentCommand(program).execute(assessmentName, options); + })); + program + .command('report <assessmentName>') + .description('Run report generation of the assessment') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--profile <profile>', 'The profile to use') + .action(callCommand((assessmentName, options) => { + checkForEmptyArgs({ assessmentName }); + new RunAssessmentCommand(program).execute(assessmentName, options); + })); + program + .command('list-report <assessmentName>') + .alias('report-list') + .description('Lists reports of the assessment') + .option('--skip <Skip records for pagination>', 'Skip records for pagination') + .option('--limit <Limit records for pagination>', 'Limit records for pagination') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--profile <profile>', 'The profile to use') + .option('--json [searchQuery]', LIST_JSON_HELP_TEXT) + .option('--query <query>', `[DEPRECATION WARNING] ${QUERY_JSON_HELP_TEXT}`) + .action(callCommand((assessmentName, options) => { + checkForEmptyArgs({ assessmentName }); + new ListAssessmentReportCommand(program).execute(assessmentName, options); + })); + program + .command('get-report <assessmentName> <reportId>') + .alias('report-get') + .description('Get report of the assessment') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--profile <profile>', 'The profile to use') + .option('--json [searchQuery]', LIST_JSON_HELP_TEXT) + .option('--query <query>', `[DEPRECATION WARNING] ${QUERY_JSON_HELP_TEXT}`) + .action(callCommand((assessmentName, reportId, options) => { + checkForEmptyArgs({ assessmentName, reportId }); + new GetAssessmentReportCommand(program).execute(assessmentName, reportId, options); + })); + program + .command('export-report <assessmentName> <reportId>') + .alias('report-export') + .description('Export report of the assessment') + .storeOptionsAsProperties(false) + .option('--type <Cortex component types>', 'Component types to export (comma separated values)') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--profile <profile>', 'The profile to use') + .action(callCommand((assessmentName, reportId, options) => { + checkForEmptyArgs({ assessmentName, reportId }); + new ExportAssessmentReportCommand(program).execute(assessmentName, reportId, options); + })); + return program; +} +if (esMain(import.meta)) { + create().showHelpAfterError().parseAsync(process.argv); +} +export default create(); diff --git a/bin/cortex-campaigns.js b/bin/cortex-campaigns.js new file mode 100755 index 00000000..f8b43cc5 --- /dev/null +++ b/bin/cortex-campaigns.js @@ -0,0 +1,103 @@ +#!/usr/bin/env node +import esMain from 'es-main'; +import { Command } from 'commander'; +import { callCommand } from '../src/compatibility.js'; +import { + DEFAULT_LIST_SKIP_COUNT, DEFAULT_LIST_LIMIT_COUNT, GET_DEFAULT_SORT_CLI_OPTION, DEFAULT_LIST_SORT_PARAMS, LIST_JSON_HELP_TEXT, QUERY_JSON_HELP_TEXT, +} from '../src/constants.js'; +import { + ListCampaignsCommand, DescribeCampaignCommand, ExportCampaignCommand, ImportCampaignCommand, DeployCampaignCommand, UndeployCampaignCommand, +} from '../src/commands/campaigns.js'; +import { checkForEmptyArgs } from '../src/commands/utils.js'; + +export function create() { + const program = new Command(); + program.name('cortex campaigns'); + program.description('Work with Cortex Campaigns'); +// List Campaigns + program + .command('list') + .description('List Campaigns') + .alias('l') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--profile <profile>', 'The profile to use') + .option('--project <project>', 'The project to use') + .option('--json [searchQuery]', LIST_JSON_HELP_TEXT) + .option('--query <query>', `[DEPRECATION WARNING] ${QUERY_JSON_HELP_TEXT}`) + .option('--filter <filter>', 'A Mongo style filter to use.') + .option('--limit <limit>', 'Limit number of records', DEFAULT_LIST_LIMIT_COUNT) + .option('--skip <skip>', 'Skip number of records', DEFAULT_LIST_SKIP_COUNT) + .option('--sort <sort>', 'A Mongo style sort statement to use in the query.', GET_DEFAULT_SORT_CLI_OPTION(DEFAULT_LIST_SORT_PARAMS._updatedAt)) + .action(callCommand((options) => { + new ListCampaignsCommand(program).execute(options); + })); +// Get|Describe Campaign + program + .command('describe <campaignName>') + .alias('get') + .description('Describe Campaign') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--profile <profile>', 'The profile to use') + .option('--project <project>', 'The project to use') + .option('--json [searchPath]', QUERY_JSON_HELP_TEXT) + .option('--query <query>', `[DEPRECATION WARNING] ${QUERY_JSON_HELP_TEXT}`) + .action(callCommand((campaignName, options) => { + checkForEmptyArgs({ campaignName }); + new DescribeCampaignCommand(program).execute(campaignName, options); + })); + program + .command('export <campaignName>') + .description('Export Campaign Archive') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--profile <profile>', 'The profile to use') + .option('--project <project>', 'The project to use') + .option('--deployable [boolean]', 'Export only deployable missions', true) + .option('--o <output>', 'Export file name') + .action(callCommand(async (campaignName, options) => { + checkForEmptyArgs({ campaignName }); + await new ExportCampaignCommand(program).execute(campaignName, options); + })); + program + .command('import <campaignExportFilepath>') + .description('Import Campaign Archive') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--profile <profile>', 'The profile to use') + .option('--project <project>', 'The project to use') + .option('--deploy [boolean]', 'Set missions status Ready To Deploy', true) + .option('--overwrite [boolean]', 'Overwrite existing deployed missions with the imported one', false) + .action(callCommand((campaignName, options) => { + checkForEmptyArgs({ campaignName }); + new ImportCampaignCommand(program).execute(campaignName, options); + })); + program + .command('deploy <campaignName>') + .description('Deploy Campaign') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--profile <profile>', 'The profile to use') + .option('--project <project>', 'The project to use') + .action(callCommand((campaignName, options) => { + checkForEmptyArgs({ campaignName }); + new DeployCampaignCommand(program).execute(campaignName, options); + })); + program + .command('undeploy <campaignName>') + .description('Undeploy Campaign') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--profile <profile>', 'The profile to use') + .option('--project <project>', 'The project to use') + .action(callCommand((campaignName, options) => { + checkForEmptyArgs({ campaignName }); + new UndeployCampaignCommand(program).execute(campaignName, options); + })); + return program; +} +if (esMain(import.meta)) { + create().showHelpAfterError().parseAsync(process.argv); +} +export default create(); diff --git a/bin/cortex-configure.js b/bin/cortex-configure.js index 7c98e075..0f379e9d 100755 --- a/bin/cortex-configure.js +++ b/bin/cortex-configure.js @@ -1,64 +1,65 @@ -#!/usr/bin/env node +import esMain from 'es-main'; +import { Command } from 'commander'; +import { + ConfigureCommand, DescribeProfileCommand, ListProfilesCommand, SetProfileCommand, GetAccessToken, PrintEnvVars, +} from '../src/commands/configure.js'; -/* - * Copyright 2018 Cognitive Scale, Inc. All Rights Reserved. - * - * 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. - */ - -const program = require('commander'); -const { ConfigureCommand, DescribeProfileCommand, ListProfilesCommand, SetProfileCommand } = require('../src/commands/configure'); -program.description('Configure the Cortex CLI'); - -let cmd = undefined; - -program - .option('--color [on/off]', 'Turn on/off colors for JSON output.', 'on') - .option('--profile [profile]', 'The profile to configure') - .option('--url [url]', 'Cortex URL') - .option('--account [account]', 'Account') - .option('--username [username]', 'Username') - .option('--password [password]', 'Password'); - -program - .command('list') - .description('List configured profiles') - .action((options) => { - cmd = 'list'; - new ListProfilesCommand(program).execute({color: program.color}); - }); - -program - .command('describe <profileName>') - .description('Describe a configured profile') - .action((profileName, options) => { - cmd = 'describe'; - new DescribeProfileCommand(program).execute({profile: profileName, color: program.color}); - }); - -program - .command('set-profile <profileName>') - .description('Sets the current profile.') - .action((profileName, options) => { - cmd = 'set-profile'; - new SetProfileCommand(program).execute(profileName, {color: program.color}); - }); - -process.env.DOC && require('../src/commands/utils').exportDoc(program); - - -program.parse(process.argv); - -if (cmd === undefined) { - new ConfigureCommand(program).execute({profile: program.profile, color: program.color}); +export function create() { + const program = new Command(); + program.name('cortex configure'); + program.description('Configure cortex connection profiles'); + program.command('create', { isDefault: true }) + .description('Authenticate to cortex (default command)') + .option('--file <file>', 'Personal access config file location') + .option('--profile <profile>', 'The profile to configure', 'default') + .option('--project <project>', 'The default project') + .action(async (options, command) => { + await new ConfigureCommand(command).execute(options); + }); + program + .command('token') + .description('Create access token') + .option('--profile <profile>', 'The profile to use') + .option('--project <project>', 'The project to use') + .option('--ttl <time>', 'The amount of time for this login to remain active, expressed as a number of hours, days, or weeks (e.g. 1h, 2d, 2w)') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .action(async (options) => { + await new GetAccessToken(program).execute(options); + }); + program + .command('list') + .description('List configured profiles') + .alias('l') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .action(async (options) => { + await new ListProfilesCommand(program).execute(options); + }); + program + .command('describe <profileName>') + .alias('get') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .description('Describe a configured profile') + .action(async (profileName, options) => { + await new DescribeProfileCommand(program).execute({ profile: profileName, ...options }); + }); + program + .command('env') + .option('--profile <profile>', 'The profile to use') + .option('--project <project>', 'The project to use') + .option('--ttl <time>', 'The amount of time for this login to remain active, expressed as a number of hours, days, or weeks (e.g. 1h, 2d, 2w)', '1d') + .description('Print cortex environment variables') + .action(async (options) => { + await new PrintEnvVars(program).execute(options); + }); + program + .command('set-profile <profileName>') + .description('Sets the current profile.') + .action(async (profileName, options) => { + await new SetProfileCommand(program).execute(profileName, options); + }); + return program; +} +if (esMain(import.meta)) { + create().showHelpAfterError().parseAsync(process.argv); } +export default create(); diff --git a/bin/cortex-connections.js b/bin/cortex-connections.js index 4c3cd20e..63df1453 100755 --- a/bin/cortex-connections.js +++ b/bin/cortex-connections.js @@ -1,136 +1,88 @@ #!/usr/bin/env node +import esMain from 'es-main'; +import { Command } from 'commander'; +import { callCommand } from '../src/compatibility.js'; +import { + DEFAULT_LIST_SKIP_COUNT, DEFAULT_LIST_LIMIT_COUNT, DEFAULT_LIST_SORT_PARAMS, GET_DEFAULT_SORT_CLI_OPTION, LIST_JSON_HELP_TEXT, QUERY_JSON_HELP_TEXT, +} from '../src/constants.js'; +import { + ListConnections, SaveConnectionCommand, DescribeConnectionCommand, ListConnectionsTypes, DeleteConnectionCommand, +} from '../src/commands/connections.js'; +import { checkForEmptyArgs } from '../src/commands/utils.js'; -/* - * Copyright 2018 Cognitive Scale, Inc. All Rights Reserved. - * - * 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. - */ - -const helper = require('./utils.js'); -const program = require('commander'); -const chalk = require('chalk'); -const { ListConnections, SaveConnectionCommand, DescribeConnectionCommand, TestConnectionCommand, - ListConnectionsTypes, GenerateConnectionCommand } = require('../src/commands/connections'); - -let processed = false; -program.description('Work with Cortex Connections'); - - +export function create() { + const program = new Command(); + program.name('cortex connections'); + program.description('Work with Cortex Connections'); // List Connections -program - .command('list') - .description('List connections definitions') - .option('--color [on/off]', 'Turn on/off colors for JSON output.', 'on') - .option('--profile [profile]', 'The profile to use') - .option('--json', 'Output results using JSON') - .option('--query [query]', 'A JMESPath query to use in filtering the response data. Ignored if output format is not JSON.') - .action((options) => { - try { - new ListConnections(program).execute(options); - processed = true; - } - catch (err) { - console.error(chalk.red(err.message)); - } - }); - + program + .command('list') + .description('List connections definitions') + .alias('l') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--profile <profile>', 'The profile to use') + .option('--project <project>', 'The project to use') + .option('--json [searchQuery]', LIST_JSON_HELP_TEXT) + .option('--query <query>', `[DEPRECATION WARNING] ${QUERY_JSON_HELP_TEXT}`) + .option('--limit <limit>', 'Limit number of records', DEFAULT_LIST_LIMIT_COUNT) + .option('--skip <skip>', 'Skip number of records', DEFAULT_LIST_SKIP_COUNT) + .option('--sort <sort>', 'A Mongo style sort statement to use in the query.', GET_DEFAULT_SORT_CLI_OPTION(DEFAULT_LIST_SORT_PARAMS.updatedAt)) + .action(callCommand((options) => new ListConnections(program).execute(options))); // Save Connections -program - .command('save <connectionDefinition>') - .description('Save a connections definition. Takes JSON file by default.') - .option('--color [on/off]', 'Turn on/off colors for JSON output.', 'on') - .option('--profile [profile]', 'The profile to use') - .option('-y, --yaml', 'Use YAML for agent definition format') - .action((connDefinition, options) => { - try { - new SaveConnectionCommand(program).execute(connDefinition, options); - processed = true; - } - catch (err) { - console.error(chalk.red(err.message)); - } - }); - + program + .command('save <connectionDefinition>') + .description('Save a connections definition. Takes JSON file by default.') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--profile <profile>', 'The profile to use') + .option('--project <project>', 'The project to use') + .option('-y, --yaml', 'Use YAML for agent definition format') + .action(callCommand((connDefinition, options) => new SaveConnectionCommand(program).execute(connDefinition, options))); // Describe Connection -program - .command('describe <connectionName>') - .description('Describe connection') - .option('--color [on/off]', 'Turn on/off colors for JSON output.', 'on') - .option('--profile [profile]', 'The profile to use') - .option('--query [query]', 'A JMESPath query to use in filtering the response data.') - .action((connectionName, options) => { - try { - new DescribeConnectionCommand(program).execute(connectionName, options); - processed = true; - } - catch (err) { - console.error(chalk.red(err.message)); - } - }); - -// Test Connections -program - .command('test <connectionDefinition>') - .description('Test a connections definition before saving. Takes JSON file by default.') - .option('--color [on/off]', 'Turn on/off colors for JSON output.', 'on') - .option('--profile [profile]', 'The profile to use') - .option('-y, --yaml', 'Use YAML for agent definition format') - .action((connDefinition, options) => { - try { - new TestConnectionCommand(program).execute(connDefinition, options); - processed = true; - } - catch (err) { - console.error(chalk.red(err.message)); - } - }); - + program + .command('describe <connectionName>') + .alias('get') + .description('Describe connection') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--profile <profile>', 'The profile to use') + .option('--project <project>', 'The project to use') + .option('--json [searchPath]', QUERY_JSON_HELP_TEXT) + .option('--query <query>', `[DEPRECATION WARNING] ${QUERY_JSON_HELP_TEXT}`) + .action(callCommand((connectionName, options) => { + checkForEmptyArgs({ connectionName }); + return new DescribeConnectionCommand(program).execute(connectionName, options); + })); // List Connections Types -program - .command('list-types') - .description('List connections types') - .option('--color [on/off]', 'Turn on/off colors for JSON output.', 'on') - .option('--profile [profile]', 'The profile to use') - .option('--json', 'Output results using JSON') - .option('--query [query]', 'A JMESPath query to use in filtering the response data. Ignored if output format is not JSON.') - .action((options) => { - try { - new ListConnectionsTypes(program).execute(options); - processed = true; - } - catch (err) { - console.error(chalk.red(err.message)); - } - }); - - -// Generate Connections -program - .command('generate') - .description('Generates the structure of the connection payload') - .option('--color [on/off]', 'Turn on/off colors for JSON output.', 'on') - .option('--profile [profile]', 'The profile to use', 'default') - .action((options) => { - try { - new GenerateConnectionCommand(program).execute(options); - processed = true; - } - catch (err) { - console.error(chalk.red(err.message)); - } - }); - -process.env.DOC && require('../src/commands/utils').exportDoc(program); -program.parse(process.argv); -if (!processed) - ['string', 'undefined'].includes(typeof program.args[0]) && helper.helpAndExit(program); + program + .command('list-types') + .description('List connections types') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--profile <profile>', 'The profile to use') + .option('--project <project>', 'The project to use') + .option('--json [searchQuery]', LIST_JSON_HELP_TEXT) + .option('--query <query>', `[DEPRECATION WARNING] ${QUERY_JSON_HELP_TEXT}`) + .option('--limit <limit>', 'Limit number of records', DEFAULT_LIST_LIMIT_COUNT) + .option('--skip <skip>', 'Skip number of records', DEFAULT_LIST_SKIP_COUNT) + .option('--sort <sort>', 'A Mongo style sort statement to use in the query.') + .action(callCommand((options) => new ListConnectionsTypes(program).execute(options))); +// Delete Connection + program + .command('delete <connectionName>') + .description('Delete a connection') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--profile <profile>', 'The profile to use') + .option('--project <project>', 'The project to use') + .action(callCommand((connectionName, options) => { + checkForEmptyArgs({ connectionName }); + return new DeleteConnectionCommand(program).execute(connectionName, options); + })); + return program; +} +if (esMain(import.meta)) { + create().showHelpAfterError().parseAsync(process.argv); +} +export default create(); diff --git a/bin/cortex-content.js b/bin/cortex-content.js index fcf54ee3..c59b3f48 100755 --- a/bin/cortex-content.js +++ b/bin/cortex-content.js @@ -1,100 +1,65 @@ #!/usr/bin/env node +import esMain from 'es-main'; +import { Command } from 'commander'; +import { callCommand } from '../src/compatibility.js'; +import { + ListContent, UploadContent, DeleteContent, DownloadContent, +} from '../src/commands/content.js'; +import { LIST_JSON_HELP_TEXT, QUERY_JSON_HELP_TEXT } from '../src/constants.js'; -/* - * Copyright 2018 Cognitive Scale, Inc. All Rights Reserved. - * - * 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. - */ - -const helper = require('./utils.js'); -const program = require('commander'); -const chalk = require('chalk'); -const { ListContent, UploadContent, DeleteContent, DownloadContent } = require('../src/commands/content'); - -let processed = false; -program.description('Work with Cortex Contents'); - - +export function create() { + const program = new Command(); + program.name('cortex content'); + program.description('Work with Cortex Managed Content'); // List Content -program - .command('list') - .description('List content') - .option('--color [on/off]', 'Turn on/off colors for JSON output.', 'on') - .option('--profile [profile]', 'The profile to use') - .option('--json', 'Output results using JSON') - .option('--query [query]', 'A JMESPath query to use in filtering the response data. Ignored if output format is not JSON.') - .action((options) => { - try { - new ListContent(program).execute(options); - processed = true; - } - catch (err) { - console.error(chalk.red(err.message)); - } - }); - + program + .command('list') + .description('List content') + .alias('l') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--profile <profile>', 'The profile to use') + .option('--project <project>', 'The project to use') + .option('--json [searchQuery]', LIST_JSON_HELP_TEXT) + .option('--query <query>', `[DEPRECATION WARNING] ${QUERY_JSON_HELP_TEXT}`) + .option('--prefix <prefix>', 'Filter contents with the given prefix.') + .action(callCommand((options) => new ListContent(program).execute(options))); // Upload Content -program - .command('upload <contentKey> <filePath>') - .description('Upload content') - .option('--color [on/off]', 'Turn on/off colors for JSON output.', 'on') - .option('--profile [profile]', 'The profile to use') - .option('--secure', 'Uploads the content securely to the Cortex Vault. Use this option for keytab files or content that contains sensitive information that is required during Runtime. Take note of the contentKey you give to this content for future reference.') - .action((contentKey, filePath, options) => { - try { - new UploadContent(program).execute(contentKey, filePath, options); - processed = true; - } - catch (err) { - console.error(chalk.red(err.message)); - } - }); - + program + .command('upload <contentKey> <filePath>') + .description('Upload content') + .option('--progress', 'Show upload progress') + .option('--test', 'Show what would be uploaded along with file size') + .option('-r, --recursive', 'Recursively walk <filePath> and prefix each path stored with <contentKey>') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--profile <profile>', 'The profile to use') + .option('--project <project>', 'The project to use') + .option('--content-type <MIME type>', 'Sets the `Content-Type` or MIME type of the content ( default: application/octet-stream )') + .option('--chunkSize <int>', 'Number of files to simultaneous upload', 10) + .action(callCommand((contentKey, filePath, options) => new UploadContent(program).execute(contentKey, filePath, options))); // Delete Content -program - .command('delete <contentKey>') - .description('Delete content') - .option('--color [on/off]', 'Turn on/off colors for JSON output.', 'on') - .option('--profile [profile]', 'The profile to use') - .action((contentKey, options) => { - try { - new DeleteContent(program).execute(contentKey, options); - processed = true; - } - catch (err) { - console.error(chalk.red(err.message)); - } - }); - + program + .command('delete <contentKey>') + .description('Delete content') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--profile <profile>', 'The profile to use') + .option('--project <project>', 'The project to use') + .action(callCommand((contentKey, options) => new DeleteContent(program).execute(contentKey, options))); // Download Content -program - .command('download <contentKey>') - .description('Download content') - .option('--color [on/off]', 'Turn on/off colors for JSON output.', 'on') - .option('--profile [profile]', 'The profile to use') - .option('--secure', 'Downloads the content securely from the Cortex Vault.') - .action((contentKey, options) => { - try { - new DownloadContent(program).execute(contentKey, options); - processed = true; - } - catch (err) { - console.error(chalk.red(err.message)); - } - }); - - -process.env.DOC && require('../src/commands/utils').exportDoc(program); -program.parse(process.argv); -if (!processed) - ['string', 'undefined'].includes(typeof program.args[0]) && helper.helpAndExit(program); + program + .command('download <contentKey>') + .description('Downloads content which can be piped to a file on disk. `cortex content download filename > filename.json`') + .option('--progress', 'Show download progress') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--profile <profile>', 'The profile to use') + .option('--project <project>', 'The project to use') + .action(callCommand((contentKey, options) => new DownloadContent(program).execute(contentKey, options))); + return program; +} +if (esMain(import.meta)) { + create().showHelpAfterError().parseAsync(process.argv); +} +export default create(); diff --git a/bin/cortex-datasets.js b/bin/cortex-datasets.js deleted file mode 100644 index 09199fb6..00000000 --- a/bin/cortex-datasets.js +++ /dev/null @@ -1,130 +0,0 @@ -#!/usr/bin/env node - -/* - * Copyright 2018 Cognitive Scale, Inc. All Rights Reserved. - * - * 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. - */ - -const helper = require('./utils.js'); -const program = require('commander'); -const chalk = require('chalk'); -const { ListDatasets, SaveDatasetsCommand, DescribeDatasetCommand, GetDataframeCommand, - StreamDatasetCommand, GenerateDatasetCommand } = require('../src/commands/datasets'); - -let processed = false; -program.description('Work with Cortex Connections'); - - -// List Dataset -program - .command('list') - .description('List Datasets') - .option('--color [on/off]', 'Turn on/off colors for JSON output.', 'on') - .option('--profile [profile]', 'The profile to use') - .option('--json', 'Output results using detailed JSON') - .option('--query [query]', 'A JMESPath query to use in filtering the response data. Ignored if output format is not JSON.') - .action((options) => { - try { - new ListDatasets(program).execute(options); - processed = true; - } - catch (err) { - console.error(chalk.red(err.message)); - } - }); - -// Save Dataset -program - .command('save <datasetDefinition>') - .description('Save a dataset definition. Takes JSON file by default.') - .option('--color [on/off]', 'Turn on/off colors for JSON output.', 'on') - .option('--profile [profile]', 'The profile to use') - .option('-y, --yaml', 'Use YAML for dataset file definition format') - .action((datasetDef, options) => { - try { - new SaveDatasetsCommand(program).execute(datasetDef, options); - processed = true; - } - catch (err) { - console.error(chalk.red(err.message)); - } - }); - -// Describe Dataset -program - .command('describe <datasetName>') - .description('Describe dataset') - .option('--color [on/off]', 'Turn on/off colors for JSON output.', 'on') - .option('--profile [profile]', 'The profile to use') - .option('--query [query]', 'A JMESPath query to use in filtering the response data.') - .action((datasetName, options) => { - try { - new DescribeDatasetCommand(program).execute(datasetName, options); - processed = true; - } - catch (err) { - console.error(chalk.red(err.message)); - } - }); - -// Get Dataframe -program - .command('get-dataframe <datasetName>') - .description('Get dataset in dataframe format') - .option('--color [on/off]', 'Turn on/off colors for JSON output.', 'on') - .option('--profile [profile]', 'The profile to use') - .action((datasetName, options) => { - try { - new GetDataframeCommand(program).execute(datasetName, options); - processed = true; - } - catch (err) { - console.error(chalk.red(err.message)); - } - }); - -// Get Stream -program - .command('get-stream <datasetName>') - .description('Stream dataset content') - .option('--color [on/off]', 'Turn on/off colors for JSON output.', 'on') - .option('--profile [profile]', 'The profile to use') - .action((datasetName, options) => { - try { - new StreamDatasetCommand(program).execute(datasetName, options); - processed = true; - } - catch (err) { - console.error(chalk.red(err.message)); - } - }); - -program - .command('generate') - .description('Generates the structure and top level build script for a dataset') - .option('--color [on/off]', 'Turn on/off colors for JSON output.', 'on') - .option('--profile [profile]', 'The profile to use', 'default') - .action((options) => { - try { - new GenerateDatasetCommand(program).execute(options); - } - catch (err) { - console.error(chalk.red(err.message)); - } - }); - -process.env.DOC && require('../src/commands/utils').exportDoc(program); -program.parse(process.argv); -if (!processed) - ['string', 'undefined'].includes(typeof program.args[0]) && helper.helpAndExit(program); diff --git a/bin/cortex-deploy.js b/bin/cortex-deploy.js new file mode 100755 index 00000000..539e6022 --- /dev/null +++ b/bin/cortex-deploy.js @@ -0,0 +1,76 @@ +#!/usr/bin/env node +import esMain from 'es-main'; + +import { Command } from 'commander'; +import { callCommand } from '../src/compatibility.js'; +import { + DeploySnapshotCommand, DeployCampaignCommand, DeployConnectionCommand, DeployExperimentCommand, DeploySkillCommand, +} from '../src/commands/deploy.js'; + +export function create() { + const program = new Command(); + program.name('cortex deploy'); + program.description('Export Cortex artifacts for deployment'); +// Export Agent Snapshot. Provide snapshotIds in quotes separated by space like "snapshotId1 snapshotId2". This is to support multiple agents in one deployment manifest file. + program + .command('snapshots <snapshotIds>') + .description('Export Agent(s) snapshots(s) for deployment. Provide snapshotIds separated by space, as <"snapshotId1 snapshotId2 ...">') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--profile <profile>', 'The profile to use') + .option('--project <project>', 'The project to use') + .option('-y, --yaml', 'Use YAML for snapshot export format') + .option('-f, --force', 'Force delete existing exported files') + .option('--latestRun [boolean]', 'Export latest run of Experiment(s) if not specified') + .action(callCommand((snapshotIds, options) => { + new DeploySnapshotCommand(program).execute(snapshotIds, options); + })); + program + .command('campaign <campaignName>') + .description('Export Campaigns for deployment') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--profile <profile>', 'The profile to use') + .option('--project <project>', 'The project to use') + .option('--deployable [boolean]', 'Export only ready to deploy Campaigns', true) + .action(callCommand((campaignName, options) => { + new DeployCampaignCommand(program).execute(campaignName, options); + })); + program + .command('connection <connectionName>') + .description('Export Connection for deployment') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--profile <profile>', 'The profile to use') + .option('--project <project>', 'The project to use') + .action(callCommand((connectionName, options) => { + new DeployConnectionCommand(program).execute(connectionName, options); + })); + program + .command('skill <skillName>') + .description('Export Skill for deployment') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--profile <profile>', 'The profile to use') + .option('--project <project>', 'The project to use') + .option('--latestRun [boolean]', 'Export latest run of experiment if not specified') + .action(callCommand((skillName, options) => { + new DeploySkillCommand(program).execute(skillName, options); + })); + program + .command('experiment <experimentName> [runId]') + .description('Export Experiment with Model and Run for deployment') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--profile <profile>', 'The profile to use') + .option('--project <project>', 'The project to use') + .option('--latestRun [boolean]', 'Export latest run of experiment if not specified') + .action(callCommand((experimentName, runId, options) => { + new DeployExperimentCommand(program).execute(experimentName, runId, options); + })); + return program; +} +if (esMain(import.meta)) { + create().showHelpAfterError().parseAsync(process.argv); +} +export default create(); diff --git a/bin/cortex-docker.js b/bin/cortex-docker.js new file mode 100755 index 00000000..b109dc89 --- /dev/null +++ b/bin/cortex-docker.js @@ -0,0 +1,29 @@ +#!/usr/bin/env node +import esMain from 'es-main'; +import { Command } from 'commander'; +import { callCommand } from '../src/compatibility.js'; +import DockerLoginCommand from '../src/commands/docker.js'; + +export function create() { + const program = new Command(); + program.name('cortex docker'); + program.description('Work with Docker'); +// Login + program + .command('login') + .description('Docker login to the Cortex registry with your jwt token') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--dockerCli <string>', 'Specify the docker client, the following are supported [docker, nerdctl, podman]. If not specified the cli will scan the PATH for available clients') + .option('--profile <profile>', 'The profile to use') + .option('--project <project>', 'The project to use') + .option('--ttl <time>', 'The amount of time for this login to remain active, expressed as a number of hours, days, or weeks (e.g. 1h, 2d, 2w)', '14d') + .action(callCommand((options) => { + new DockerLoginCommand(program).execute(options); + })); + return program; +} +if (esMain(import.meta)) { + create().showHelpAfterError().parseAsync(process.argv); +} +export default create(); diff --git a/bin/cortex-environments.js b/bin/cortex-environments.js deleted file mode 100755 index 40d504f7..00000000 --- a/bin/cortex-environments.js +++ /dev/null @@ -1,102 +0,0 @@ -#!/usr/bin/env node - -/* - * Copyright 2018 Cognitive Scale, Inc. All Rights Reserved. - * - * 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. - */ - -const helper = require('./utils.js'); -const program = require('commander'); -const chalk = require('chalk'); -const { ListEnvironments, SaveEnvironmentCommand, PromoteEnvironmentCommand, DescribeEnvironmentCommand } = require('../src/commands/environments'); - -let processed = false; -program.description('Work with Cortex Environments'); - - -// List Environments -program - .command('list') - .description('List environments') - .option('--color [on/off]', 'Turn on/off colors for JSON output.', 'on') - .option('--profile [profile]', 'The profile to use') - .option('--json', 'Output results using JSON') - .option('--query [query]', 'A JMESPath query to use in filtering the response data. Ignored if output format is not JSON.') - .action((options) => { - try { - new ListEnvironments(program).execute(options); - processed = true; - } - catch (err) { - console.error(chalk.red(err.message)); - } - }); - -// Save Environement -program - .command('save <environmentDefinition>') - .description('Create an environment. Takes JSON file by default.') - .option('--color [on/off]', 'Turn on/off colors for JSON output.', 'on') - .option('--profile [profile]', 'The profile to use') - .option('-y, --yaml', 'Use YAML for environment definition format') - .action((envDefinition, options) => { - try { - new SaveEnvironmentCommand(program).execute(envDefinition, options); - processed = true; - } - catch (err) { - console.error(chalk.red(err.message)); - } - }); - -// Promote Environment -program - .command('promote [promotionDefinition]') - .description('Promote a snapshot to an environment. Takes JSON file by default.') - .option('--color [on/off]', 'Turn on/off colors for JSON output.', 'on') - .option('--profile [profile]', 'The profile to use') - .option('-y, --yaml', 'Use YAML for environment definition format') - .option('--snapshotId [snapshotId]', 'SnapshotID to promote') - .option('--environmentName [environmentName]', 'Environment to promote snapshotId to') - .action((promotionDefinition, options) => { - try { - new PromoteEnvironmentCommand(program).execute(promotionDefinition, options); - processed = true; - } - catch (err) { - console.error(chalk.red(err.message)); - } - }); - -// Describe Environment -program - .command('describe <environmentName>') - .description('Describe environment') - .option('--color [on/off]', 'Turn on/off colors for JSON output.', 'on') - .option('--profile [profile]', 'The profile to use') - .option('--query [query]', 'A JMESPath query to use in filtering the response data.') - .action((environmentName, options) => { - try { - new DescribeEnvironmentCommand(program).execute(environmentName, options); - processed = true; - } - catch (err) { - console.error(chalk.red(err.message)); - } - }); - -process.env.DOC && require('../src/commands/utils').exportDoc(program); -program.parse(process.argv); -if (!processed) - ['string', 'undefined'].includes(typeof program.args[0]) && helper.helpAndExit(program); diff --git a/bin/cortex-experiments.js b/bin/cortex-experiments.js new file mode 100755 index 00000000..27301e2e --- /dev/null +++ b/bin/cortex-experiments.js @@ -0,0 +1,182 @@ +#!/usr/bin/env node +import esMain from 'es-main'; +import { Command } from 'commander'; +import { callCommand } from '../src/compatibility.js'; +import { + DEFAULT_LIST_LIMIT_COUNT, + DEFAULT_LIST_SKIP_COUNT, + DEFAULT_LIST_SORT_PARAMS, + GET_DEFAULT_SORT_CLI_OPTION, + LIST_JSON_HELP_TEXT, + QUERY_JSON_HELP_TEXT, +} from '../src/constants.js'; +import { + CreateRunCommand, + DeleteExperimentCommand, + DeleteRunCommand, + DescribeExperimentCommand, + DescribeRunCommand, + DownloadArtifactCommand, + ListExperiments, + ListRuns, + SaveExperimentCommand, + UploadArtifactCommand, +} from '../src/commands/experiments.js'; +import { checkForEmptyArgs } from '../src/commands/utils.js'; + +export function create() { + const program = new Command(); + program.name('cortex experiments'); + program.description('Work with Cortex Experiments'); +// List Experiments + program + .command('list') + .description('Find experiments') + .alias('l') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--profile <profile>', 'The profile to use') + .option('--project <project>', 'The project to use') + .option('--model <model>', 'The model to use') + .option('--json [searchQuery]', LIST_JSON_HELP_TEXT) + .option('--query <query>', `[DEPRECATION WARNING] ${QUERY_JSON_HELP_TEXT}`) + .option('--filter <filter>', 'A Mongo style filter to use.') + .option('--limit <limit>', 'Limit number of records', DEFAULT_LIST_LIMIT_COUNT) + .option('--skip <skip>', 'Skip number of records', DEFAULT_LIST_SKIP_COUNT) + .option('--sort <sort>', 'A Mongo style sort statement to use in the query.', GET_DEFAULT_SORT_CLI_OPTION(DEFAULT_LIST_SORT_PARAMS.updatedAt)) + .action(callCommand((options) => { + new ListExperiments(program).execute(options); + })); +// Describe Experiment + program + .command('describe <experimentName>') + .description('Describe experiment') + .alias('get') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--profile <profile>', 'The profile to use') + .option('--project <project>', 'The project to use') + .option('--json [searchPath]', QUERY_JSON_HELP_TEXT) + .option('--query <query>', `[DEPRECATION WARNING] ${QUERY_JSON_HELP_TEXT}`) + .action(callCommand((experimentName, options) => { + checkForEmptyArgs({ experimentName }); + new DescribeExperimentCommand(program).execute(experimentName, options); + })); +// Delete Experiment + program + .command('delete <experimentName>') + .description('Delete an experiment') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--profile <profile>', 'The profile to use') + .option('--project <project>', 'The project to use') + .action(callCommand((experimentName, options) => { + checkForEmptyArgs({ experimentName }); + new DeleteExperimentCommand(program).execute(experimentName, options); + })); +// List Runs + program + .command('list-runs <experimentName>') + .description('Find runs within an experiment') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--profile <profile>', 'The profile to use') + .option('--project <project>', 'The project to use') + .option('--json [searchQuery]', LIST_JSON_HELP_TEXT) + .option('--query <query>', `[DEPRECATION WARNING] ${QUERY_JSON_HELP_TEXT}`) + .option('--filter <filter>', 'A Mongo style filter to use.') + .option('--limit <limit>', 'Limit number of records', DEFAULT_LIST_LIMIT_COUNT) + .option('--skip <skip>', 'Skip number of records', DEFAULT_LIST_SKIP_COUNT) + .option('--sort <sort>', 'A Mongo style sort statement to use in the query.', GET_DEFAULT_SORT_CLI_OPTION(DEFAULT_LIST_SORT_PARAMS._updatedAt)) + .action(callCommand((experimentName, options) => { + checkForEmptyArgs({ experimentName }); + new ListRuns(program).execute(experimentName, options); + })); +// Describe Run + program + .command('describe-run <experimentName> <runId>') + .description('Describe run') + .alias('get-run') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--profile <profile>', 'The profile to use') + .option('--project <project>', 'The project to use') + .option('--json [searchPath]', QUERY_JSON_HELP_TEXT) + .option('--query <query>', `[DEPRECATION WARNING] ${QUERY_JSON_HELP_TEXT}`) + .action(callCommand((experimentName, runId, options) => { + checkForEmptyArgs({ experimentName, runId }); + new DescribeRunCommand(program).execute(experimentName, runId, options); + })); +// Delete Run + program + .command('delete-run <experimentName> <runId>') + .description('Delete run') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--profile <profile>', 'The profile to use') + .option('--project <project>', 'The project to use') + .action(callCommand((experimentName, runId, options) => { + checkForEmptyArgs({ experimentName, runId }); + new DeleteRunCommand(program).execute(experimentName, runId, options); + })); +// Download Artifact + program + .command('download-artifact <experimentName> <runId> <artifactName>') + .description('Download artifact from run') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--profile <profile>', 'The profile to use') + .option('--project <project>', 'The project to use') + .action(callCommand((experimentName, runId, artifactName, options) => { + checkForEmptyArgs({ experimentName, runId, artifactName }); + new DownloadArtifactCommand(program).execute(experimentName, runId, artifactName, options); + })); +// Save Experiment + program + .command('save <experimentDefinition>') + .description('Save an experiment definition') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--profile <profile>', 'The profile to use') + .option('--project <project>', 'The project to use') + .option('-y, --yaml', 'Use YAML for experiment definition format') + .action(callCommand((experimentDefinition, options) => { + new SaveExperimentCommand(program).execute(experimentDefinition, options); + })); +// Save Experiment + program + .command('create-run <runDefinition>') + .description('Create run for experiment') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--profile <profile>', 'The profile to use') + .option('--project <project>', 'The project to use') + .option('-y, --yaml', 'Use YAML for run definition format') + .action(callCommand((runDefinition, options) => { + new CreateRunCommand(program).execute(runDefinition, options); + })); +// Upload Artifact + program + .command('upload-artifact <experimentName> <runId> <filePath> <artifactKey>') + .description('Upload artifact for run') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--profile <profile>', 'The profile to use') + .option('--project <project>', 'The project to use') + .option('--content-type <MIME type>', 'Sets the `Content-Type` or MIME type of the content ( default: application/octet-stream )') + .option('--chunkSize <int>', 'Number of files to simultaneous upload', 10) + .action(callCommand((experimentName, runId, filePath, artifactKey, options) => { + checkForEmptyArgs({ + experimentName, + runId, + filePath, + artifactKey, + }); + new UploadArtifactCommand(program).execute(experimentName, runId, filePath, artifactKey, options); + })); + return program; +} +if (esMain(import.meta)) { + create().showHelpAfterError().parseAsync(process.argv); +} +export default create(); diff --git a/bin/cortex-jobs.js b/bin/cortex-jobs.js deleted file mode 100755 index af17c2f0..00000000 --- a/bin/cortex-jobs.js +++ /dev/null @@ -1,102 +0,0 @@ -#!/usr/bin/env node - -/* -* Copyright 2018 Cognitive Scale, Inc. All Rights Reserved. -* -* 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. -*/ - -const helper = require('./utils.js'); -const program = require('commander'); -const chalk = require('chalk'); -const { ListJobs, JobStatus, DescribeJob, SaveJob } = require('../src/commands/jobs'); - -let processed = false; -program.description('Work with Cortex Jobs'); - - -// List Jobs -program - .command('list') - .description('List job definitions') - .option('--color [on/off]', 'Turn on/off colors for JSON output.', 'on') - .option('--profile [profile]', 'The profile to use') - .option('--json', 'Output results using JSON') - .option('--query [query]', 'A JMESPath query to use in filtering the response data. Ignored if output format is not JSON.') - .action((options) => { - try { - new ListJobs(program).execute(options); - processed = true; - } - catch (err) { - console.error(chalk.red(err.message)); - } - }); - -// Describe Job -program - .command('describe <jobDefinition>') - .description('Describe job definition') - .option('--color [on/off]', 'Turn on/off colors for JSON output.', 'on') - .option('--profile [profile]', 'The profile to use') - .option('--json', 'Output results using JSON') - .action((jobDefinition, options) => { - try { - new DescribeJob(program).execute(jobDefinition, options); - processed = true; - } - catch (err) { - console.error(chalk.red(err.message)); - } - }); - -// Get Job Status -program - .command('status <jobDefinition>') - .description('Get job status') - .option('--color [on/off]', 'Turn on/off colors for JSON output.', 'on') - .option('--profile [profile]', 'The profile to use') - .option('--json', 'Output results using JSON') - .action((jobDefinition, options) => { - try { - new JobStatus(program).execute(jobDefinition, options); - processed = true; - } - catch (err) { - console.error(chalk.red(err.message)); - } - }); - - -// Save Job -program - .command('save <jobDefinition>') - .description('Save a job definition') - .option('--color [on/off]', 'Turn on/off colors for JSON output.', 'on') - .option('--profile [profile]', 'The profile to use') - .option('-y, --yaml', 'Use YAML for job definition format') - .action((jobDefinition, options) => { - try { - new SaveJob(program).execute(jobDefinition, options); - processed = true; - } - catch (err) { - console.error(chalk.red(err.message)); - } - }); - -process.env.DOC && require('../src/commands/utils').exportDoc(program); - -program.parse(process.argv); -if (!processed) - ['string', 'undefined'].includes(typeof program.args[0]) && helper.helpAndExit(program); diff --git a/bin/cortex-missions.js b/bin/cortex-missions.js new file mode 100755 index 00000000..8042ca5d --- /dev/null +++ b/bin/cortex-missions.js @@ -0,0 +1,128 @@ +#!/usr/bin/env node +import esMain from 'es-main'; +import fs from 'node:fs'; +import { Command } from 'commander'; +import { checkForEmptyArgs, parseObject, printError } from '../src/commands/utils.js'; +import { callCommand } from '../src/compatibility.js'; +import { + DEFAULT_LIST_SKIP_COUNT, DEFAULT_LIST_LIMIT_COUNT, GET_DEFAULT_SORT_CLI_OPTION, DEFAULT_LIST_SORT_PARAMS, LIST_JSON_HELP_TEXT, QUERY_JSON_HELP_TEXT, +} from '../src/constants.js'; +import { + ListMissionsCommand, DeployMissionCommand, DescribeMissionCommand, UndeployMissionCommand, +} from '../src/commands/campaigns.js'; +import { InvokeAgentServiceCommand } from '../src/commands/agents.js'; +import { DownloadContent } from '../src/commands/content.js'; + +export function create() { + const program = new Command(); + + program.name('cortex missions'); + program.description('Work with Cortex Missions'); + program + .command('list <campaignName>') + .alias('l') + .description('List Missions of the Campaign') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--profile <profile>', 'The profile to use') + .option('--project <project>', 'The project to use') + .option('--json [searchQuery]', LIST_JSON_HELP_TEXT) + .option('--query <query>', `[DEPRECATION WARNING] ${QUERY_JSON_HELP_TEXT}`) + .option('--filter <filter>', 'A Mongo style filter to use.') + .option('--limit <limit>', 'Limit number of records', DEFAULT_LIST_LIMIT_COUNT) + .option('--skip <skip>', 'Skip number of records', DEFAULT_LIST_SKIP_COUNT) + .option('--sort <sort>', 'A Mongo style sort statement to use in the query.', GET_DEFAULT_SORT_CLI_OPTION(DEFAULT_LIST_SORT_PARAMS._updatedAt)) + .action(callCommand((campaignName, options) => { + checkForEmptyArgs({ campaignName }); + new ListMissionsCommand(program).execute(campaignName, options); + })); + program + .command('describe <campaignName> <missionName>') + .alias('get') + .description('Describe the selected Missions of the Campaign') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--profile <profile>', 'The profile to use') + .option('--project <project>', 'The project to use') + .action(callCommand((campaignName, missionName, options) => { + checkForEmptyArgs({ campaignName, missionName }); + new DescribeMissionCommand(program).execute(campaignName, missionName, options); + })); + program + .command('deploy <campaignName> <missionName>') + .description('Deploy the selected Missions of the Campaign') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--profile <profile>', 'The profile to use') + .option('--project <project>', 'The project to use') + .action(callCommand((campaignName, missionName, options) => { + checkForEmptyArgs({ campaignName, missionName }); + new DeployMissionCommand(program).execute(campaignName, missionName, options); + })); + program + .command('invoke <campaignName> <missionName>') + .description('Invoke the mission') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--profile <profile>', 'The profile to use') + .option('--project <project>', 'The project to use') + .option('--params <params>', 'JSON params to send to the action') + .option('--params-file <paramsFile>', 'A file containing either JSON or YAML formatted params') + .action(callCommand((campaignName, missionName, options) => { + checkForEmptyArgs({ campaignName, missionName }); + let params = {}; + if (options.params) { + try { + params = parseObject(options.params, options); + } catch (e) { + printError(`Failed to parse params: ${options.params} Error: ${e}`, options); + } + } else if (options.paramsFile) { + const paramsStr = fs.readFileSync(options.paramsFile); + params = parseObject(paramsStr, options); + } + if (params.payload) { + // apply validations + if (!params.payload.campaign_name || !params.payload.mission_name || !params.payload.profile_schema) { + printError('payload must contain campaign_name, mission_name and profile_schema'); + throw new Error('Payload must contain campaign_name, mission_name and profile_schema'); + } + // batch size can't be empty, can't be negative + if (!params.payload.batch_size || params.payload.batch_size <= 0) { + printError(`batch_size cannot be "${params.payload.batch_size}"`); + throw new Error('Inappropriate Batch Size passed in params'); + } + } else { + printError('payload cannot be empty'); + throw new Error('Empty payload received in params'); + } + new InvokeAgentServiceCommand(program).execute(`${campaignName}-${missionName}`, 'mission_manager_service', options); + })); + program + .command('undeploy <campaignName> <missionName>') + .description('Undeploy the selected Missions of the Campaign') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--profile <profile>', 'The profile to use') + .option('--project <project>', 'The project to use') + .action(callCommand((campaignName, missionName, options) => { + checkForEmptyArgs({ campaignName, missionName }); + new UndeployMissionCommand(program).execute(campaignName, missionName, options); + })); + program + .command('status <activationId>') + .description('Status of Missions activation') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--profile <profile>', 'The profile to use') + .option('--project <project>', 'The project to use') + .action(callCommand((activationId, options) => { + checkForEmptyArgs({ activationId }); + new DownloadContent(program).execute(`mission_runtime/${activationId}/status.chk`, options); + })); + return program; +} +if (esMain(import.meta)) { + create().showHelpAfterError().parseAsync(process.argv); +} +export default create(); diff --git a/bin/cortex-models.js b/bin/cortex-models.js new file mode 100755 index 00000000..ce5f3c19 --- /dev/null +++ b/bin/cortex-models.js @@ -0,0 +1,128 @@ +#!/usr/bin/env node +import esMain from 'es-main'; +import { Command } from 'commander'; +import { callCommand } from '../src/compatibility.js'; +import { + DeleteModelCommand, SaveModelCommand, DescribeModelCommand, ListModelsCommand, UpdateModelStatusCommand, ListModelRunsCommand, +} from '../src/commands/models.js'; +import { + DEFAULT_LIST_SKIP_COUNT, DEFAULT_LIST_LIMIT_COUNT, GET_DEFAULT_SORT_CLI_OPTION, DEFAULT_LIST_SORT_PARAMS, LIST_JSON_HELP_TEXT, QUERY_JSON_HELP_TEXT, +} from '../src/constants.js'; +import { checkForEmptyArgs } from '../src/commands/utils.js'; + +export function create() { + const program = new Command(); + + program.name('cortex models'); + program.description('Work with Cortex Models'); +// List Models + program + .command('list') + .description('List model definitions') + .alias('l') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--profile <profile>', 'The profile to use') + .option('--project <project>', 'The project to use') + .option('--tags <tags>', 'The tags to use (comma separated values)') + .option('--json [searchQuery]', LIST_JSON_HELP_TEXT) + .option('--query <query>', `[DEPRECATION WARNING] ${QUERY_JSON_HELP_TEXT}`) + .option('--filter <filter>', 'A Mongo style filter to use.') + .option('--limit <limit>', 'Limit number of records', DEFAULT_LIST_LIMIT_COUNT) + .option('--skip <skip>', 'Skip number of records', DEFAULT_LIST_SKIP_COUNT) + .option('--sort <sort>', 'A Mongo style sort statement to use in the query.', GET_DEFAULT_SORT_CLI_OPTION(DEFAULT_LIST_SORT_PARAMS.updatedAt)) + .action(callCommand((options) => { + new ListModelsCommand(program).execute(options); + })); +// Describe Model + program + .command('describe <modelName>') + .description('Describe model') + .alias('get') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--profile <profile>', 'The profile to use') + .option('--project <project>', 'The project to use') + .option('--json [searchPath]', QUERY_JSON_HELP_TEXT) + .option('--query <query>', `[DEPRECATION WARNING] ${QUERY_JSON_HELP_TEXT}`) + .option('--verbose', 'Verbose output') + .action(callCommand((modelName, options) => { + checkForEmptyArgs({ modelName }); + new DescribeModelCommand(program).execute(modelName, options); + })); +// Delete Model + program + .command('delete <modelName>') + .description('Delete a model') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--profile <profile>', 'The profile to use') + .option('--project <project>', 'The project to use') + .action(callCommand((modelName, options) => { + checkForEmptyArgs({ modelName }); + new DeleteModelCommand(program).execute(modelName, options); + })); +// Save Model + program + .command('save <modelDefinition>') + .description('Save a model definition') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--profile <profile>', 'The profile to use') + .option('--project <project>', 'The project to use') + .option('-y, --yaml', 'Use YAML for model definition format') + .action(callCommand((modelDefinition, options) => { + new SaveModelCommand(program).execute(modelDefinition, options); + })); +// Publish a Model + program + .command('publish <modelName>') + .description('Publish a model') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--profile <profile>', 'The profile to use') + .option('--project <project>', 'The project to use') + .option('-y, --yaml', 'Use YAML for model definition format') + .option('--content-type <MIME type>', 'Sets the `Content-Type` or MIME type of the content ( default: application/octet-stream )') + .action(callCommand((modelName, options) => { + checkForEmptyArgs({ modelName }); + new UpdateModelStatusCommand(program).execute(modelName, options, 'publish'); + })); +// Unpublish a Model + program + .command('unpublish <modelName>') + .description('Unpublish a model') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--profile <profile>', 'The profile to use') + .option('--project <project>', 'The project to use') + .option('-y, --yaml', 'Use YAML for model definition format') + .option('--content-type <MIME type>', 'Sets the `Content-Type` or MIME type of the content ( default: application/octet-stream )') + .action(callCommand((modelName, options) => { + checkForEmptyArgs({ modelName }); + new UpdateModelStatusCommand(program).execute(modelName, options, 'unpublish'); + })); +// List model runs + program + .command('list-runs <modelName>') + .description('List model run') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--profile <profile>', 'The profile to use') + .option('--project <project>', 'The project to use') + .option('--json [searchQuery]', LIST_JSON_HELP_TEXT) + .option('--query <query>', `[DEPRECATION WARNING] ${QUERY_JSON_HELP_TEXT}`) + .option('--filter <filter>', 'A Mongo style filter to use.') + .option('--limit <limit>', 'Limit number of records', DEFAULT_LIST_LIMIT_COUNT) + .option('--skip <skip>', 'Skip number of records', DEFAULT_LIST_SKIP_COUNT) + .option('--sort <sort>', 'A Mongo style sort statement to use in the query.', GET_DEFAULT_SORT_CLI_OPTION(DEFAULT_LIST_SORT_PARAMS._updatedAt)) + .action(callCommand((modelName, options) => { + checkForEmptyArgs({ modelName }); + new ListModelRunsCommand(program).execute(modelName, options); + })); + return program; +} +if (esMain(import.meta)) { + create().showHelpAfterError().parseAsync(process.argv); +} +export default create(); diff --git a/bin/cortex-pipelines-repos.js b/bin/cortex-pipelines-repos.js new file mode 100755 index 00000000..210e5d80 --- /dev/null +++ b/bin/cortex-pipelines-repos.js @@ -0,0 +1,103 @@ +#!/usr/bin/env node +import esMain from 'es-main'; +import { Command } from 'commander'; +import { callCommand } from '../src/compatibility.js'; +import { + ListPipelineRepoCommand, + DescribePipelineRepoCommand, + DeletePipelineRepoCommand, + SavePipelineRepoCommand, + UpdateRepoPipelinesCommand, +} from '../src/commands/pipelineRepositories.js'; +import { + DEFAULT_LIST_SKIP_COUNT, DEFAULT_LIST_LIMIT_COUNT, DEFAULT_LIST_SORT_PARAMS, GET_DEFAULT_SORT_CLI_OPTION, LIST_JSON_HELP_TEXT, QUERY_JSON_HELP_TEXT, +} from '../src/constants.js'; +import { checkForEmptyArgs } from '../src/commands/utils.js'; + +export function create() { + const repos = new Command(); + repos.name('cortex pipelines repos'); + repos.description('Work with Cortex Pipeline Repositories'); + + // List + repos + .command('list') + .alias('l') + .description('List Pipeline Repositories') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--profile <profile>', 'The profile to use') + .option('--project <project>', 'The project to use') + .option('--json [searchQuery]', LIST_JSON_HELP_TEXT) + .option('--filter <filter>', 'A Mongo style filter to use.') + .option('--limit <limit>', 'Limit number of records', DEFAULT_LIST_LIMIT_COUNT) + .option('--skip <skip>', 'Skip number of records', DEFAULT_LIST_SKIP_COUNT) + .option('--sort <sort>', 'A Mongo style sort statement to use in the query.', GET_DEFAULT_SORT_CLI_OPTION(DEFAULT_LIST_SORT_PARAMS.updatedAt)) + .action(callCommand((options) => new ListPipelineRepoCommand(repos).execute(options))); + + // Describe + repos + .command('describe <pipelineRepoName>') + .alias('get') + .description('Describe a Pipeline Repository') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--profile <profile>', 'The profile to use') + .option('--project <project>', 'The project to use') + .option('--json [searchPath]', QUERY_JSON_HELP_TEXT) + .action(callCommand((pipelineRepoName, options) => { + checkForEmptyArgs({ pipelineRepoName }); + return new DescribePipelineRepoCommand(repos).execute(pipelineRepoName, options); + })); + + // Delete + repos + .command('delete <pipelineRepoName>') + .description('Delete a Pipeline Repository') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--profile <profile>', 'The profile to use') + .option('--project <project>', 'The project to use') + .action(callCommand((pipelineRepoName, options) => { + checkForEmptyArgs({ pipelineRepoName }); + return new DeletePipelineRepoCommand(repos).execute(pipelineRepoName, options); + })); + + // Save + repos + .command('save [pipelineRepoDefinition]') + .description('Save a Pipeline Repository definition. Takes JSON file by default.') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--profile <profile>', 'The profile to use') + .option('--project <project>', 'The project to use') + .option('--name <name>', 'Pipeline repository name') + .option('--repo <repo>', 'Repository URL or use "mc://<content key>" for a bundle.') + .option('--branch <branch>', 'Git branch to checkout') + .option('-y, --yaml', 'Use YAML for Pipeline Repository definition format') + // TODO: should this have CLI args for <repo> & <branch>?? + .action(callCommand((pipelineRepoDefinition, options) => new SavePipelineRepoCommand(repos).execute(pipelineRepoDefinition, options))); + + // Update pipelines + repos + .command('update-pipelines <pipelineRepoName>') + .description('Updates pipelines within Sensa catalog.') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--profile <profile>', 'The profile to use') + .option('--project <project>', 'The project to use') + .option('--json [searchPath]', QUERY_JSON_HELP_TEXT) + .option('--skill <skillName>', 'Name of the underlying Skill in the same Project to use for running the Pipeline') + .option('--force-recreate', 'Whether to force Pipelines to be recreated even if the Pipeline\'s state (git SHA) has not has not changed.') + .action(callCommand((pipelineRepoName, options) => { + checkForEmptyArgs({ pipelineRepoName }); + return new UpdateRepoPipelinesCommand(repos).execute(pipelineRepoName, options); + })); + + return repos; +} + +if (esMain(import.meta)) { + create().showHelpAfterError().parseAsync(process.argv); +} +export default create(); diff --git a/bin/cortex-pipelines.js b/bin/cortex-pipelines.js new file mode 100755 index 00000000..10eec48e --- /dev/null +++ b/bin/cortex-pipelines.js @@ -0,0 +1,119 @@ +#!/usr/bin/env node +import esMain from 'es-main'; +import { Command } from 'commander'; +import { + ListPipelineCommand, + DescribePipelineCommand, + RunPipelineCommand, + DescribePipelineRunCommand, + ListPipelineRunsCommand, +} from '../src/commands/pipelines.js'; +import { callCommand } from '../src/compatibility.js'; +import { + LIST_JSON_HELP_TEXT, + DEFAULT_LIST_LIMIT_COUNT, + DEFAULT_LIST_SKIP_COUNT, + GET_DEFAULT_SORT_CLI_OPTION, + DEFAULT_LIST_SORT_PARAMS, + QUERY_JSON_HELP_TEXT, +} from '../src/constants.js'; +import { checkForEmptyArgs } from '../src/commands/utils.js'; + +export function create() { + const pipelines = new Command(); + pipelines.name('cortex pipelines'); + pipelines.description('Work with Cortex Pipelines'); + pipelines.command('repos <cmd>', 'Work with Cortex Pipeline Repositories'); + + // List + pipelines + .command('list') + .alias('l') + .description('List Pipelines') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--profile <profile>', 'The profile to use') + .option('--project <project>', 'The project to use') + .option('--json [searchQuery]', LIST_JSON_HELP_TEXT) + .option('--filter <filter>', 'A Mongo style filter to use.') + .option('--repo <pipelineRepo>', 'Convenience flag for filtering by Pipeline Repository. Has a lower priority than --filter') + .option('--limit <limit>', 'Limit number of records', DEFAULT_LIST_LIMIT_COUNT) + .option('--skip <skip>', 'Skip number of records', DEFAULT_LIST_SKIP_COUNT) + .option('--sort <sort>', 'A Mongo style sort statement to use in the query.', GET_DEFAULT_SORT_CLI_OPTION(DEFAULT_LIST_SORT_PARAMS.updatedAt)) + .action(callCommand((options) => new ListPipelineCommand(pipelines).execute(options))); + + // Describe + pipelines + .command('describe <pipelineName> <gitRepoName>') + .alias('get') + .description('Describe a Pipeline') + .option('--sha [sha]', 'Filter by SHA - to find specific version of the pipeline') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--profile <profile>', 'The profile to use') + .option('--project <project>', 'The project to use') + .option('--json [searchPath]', QUERY_JSON_HELP_TEXT) + .action(callCommand((pipelineName, gitRepoName, options) => { + checkForEmptyArgs({ pipelineName, gitRepoName }); + return new DescribePipelineCommand(pipelines).execute(pipelineName, gitRepoName, options); + })); + + // Run Pipeline + pipelines + .command('run <pipelineName> <gitRepoName>') + .description('Run a Pipeline') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--profile <profile>', 'The profile to use') + .option('--project <project>', 'The project to use') + .option('--json [searchPath]', QUERY_JSON_HELP_TEXT) + .option('--params <params>', 'JSON params to send to the action') + .option('--params-file <paramsFile>', 'A file containing either JSON or YAML formatted params') + .option('--commit <commit>', 'Git SHA for pipeline repository') + .option('--block <block>', 'Block name to run a specific block') + .option('--scheduleName <name>', 'Identifier to uniquely identify a Pipelines schedule') + .option('--scheduleCron <cron>', 'Schedule Pipeline to run periodically using a cron schedule string for example: "0 0 * * *", @hourly, or @daily') + .action(callCommand((pipelineName, gitRepoName, options) => { + checkForEmptyArgs({ pipelineName, gitRepoName }); + return new RunPipelineCommand(pipelines).execute(pipelineName, gitRepoName, options); + })); + + // Describe Pipeline Run + pipelines + .command('describe-run <runId>') + .alias('get-run') + .description('Describe a Pipeline Run') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--profile <profile>', 'The profile to use') + .option('--project <project>', 'The project to use') + .option('--json [searchPath]', QUERY_JSON_HELP_TEXT) + .action(callCommand((runId, options) => { + checkForEmptyArgs({ runId }); + return new DescribePipelineRunCommand(pipelines).execute(runId, options); + })); + + // List Pipeline Run + pipelines + .command('list-runs <pipelineName> <gitRepoName>') + .alias('lr') + .description('List all Runs for a Pipeline') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--profile <profile>', 'The profile to use') + .option('--project <project>', 'The project to use') + .option('--json [searchPath]', QUERY_JSON_HELP_TEXT) + .option('--limit <limit>', 'Limit number of records', DEFAULT_LIST_LIMIT_COUNT) + .option('--skip <skip>', 'Skip number of records', DEFAULT_LIST_SKIP_COUNT) + .action(callCommand((pipelineName, gitRepoName, options) => { + checkForEmptyArgs({ pipelineName, gitRepoName }); + return new ListPipelineRunsCommand(pipelines).execute(pipelineName, gitRepoName, options); + })); + + return pipelines; +} + +if (esMain(import.meta)) { + create().showHelpAfterError().parseAsync(process.argv); +} +export default create(); diff --git a/bin/cortex-processors.js b/bin/cortex-processors.js deleted file mode 100755 index 6f7f2f16..00000000 --- a/bin/cortex-processors.js +++ /dev/null @@ -1,144 +0,0 @@ -#!/usr/bin/env node - -/* - * Copyright 2018 Cognitive Scale, Inc. All Rights Reserved. - * - * 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. - */ - -const helper = require('./utils.js'); -const program = require('commander'); -const chalk = require('chalk'); -const { - ListRuntimesCommand, - ListRuntimeTypesCommand, - ListActionsCommand, - DescribeRuntimeCommand, - DeleteRuntimeCommand, - InvokeActionCommand -} = require('../src/commands/processors'); - -let processed = false; -program.description('Work with the Cortex Processor Runtime'); - -// List Processor Runtime Types -program - .command('list-runtime-types') - .description('List available processor runtime types') - .option('--color [on/off]', 'Turn on/off colors for JSON output.', 'on') - .option('--profile [profile]', 'The profile to use') - .option('--json', 'Output results using JSON') - .option('--query [query]', 'A JMESPath query to use in filtering the response data.') - .action((options) => { - try { - new ListRuntimeTypesCommand(program).execute(options); - processed = true; - } - catch (err) { - console.error(chalk.red(err.message)); - } - }); - -// List Processor Runtimes -program - .command('list-runtimes') - .description('List configured processor runtimes') - .option('--color [on/off]', 'Turn on/off colors for JSON output.', 'on') - .option('--profile [profile]', 'The profile to use') - .option('--json', 'Output results using JSON') - .option('--query [query]', 'A JMESPath query to use in filtering the response data.') - .action((options) => { - try { - new ListRuntimesCommand(program).execute(options); - processed = true; - } - catch (err) { - console.error(chalk.red(err.message)); - } - }); - -// Describe Processor Runtime -program - .command('describe <runtimeName>') - .description('Describe a processor runtime') - .option('--color [on/off]', 'Turn on/off colors for JSON output.', 'on') - .option('--profile [profile]', 'The profile to use') - .option('--query [query]', 'A JMESPath query to use in filtering the response data.') - .action((runtimeName, options) => { - try { - new DescribeRuntimeCommand(program).execute(runtimeName, options); - processed = true; - } - catch (err) { - console.error(chalk.red(err.message)); - } - }); - -// Delete Processor Runtime -program - .command('delete <runtimeName>') - .description('Delete a processor runtime') - .option('--color [on/off]', 'Turn on/off colors for JSON output.', 'on') - .option('--profile [profile]', 'The profile to use') - .action((runtimeName, options) => { - try { - new DeleteRuntimeCommand(program).execute(runtimeName, options); - processed = true; - } - catch (err) { - console.error(chalk.red(err.message)); - } - }); - -// List Actions -program - .command('list-actions <runtimeName>') - .description('List the available processor runtime actions') - .option('--color [on/off]', 'Turn on/off colors for JSON output.', 'on') - .option('--profile [profile]', 'The profile to use') - .option('--json', 'Output results using JSON') - .option('--query [query]', 'A JMESPath query to use in filtering the response data.') - .action((runtimeName, options) => { - try { - new ListActionsCommand(program).execute(runtimeName, options); - processed = true; - } - catch (err) { - console.error(chalk.red(err.message)); - } - }); - -// Invoke Action -program - .command('invoke <runtimeName> <actionId>') - .description('Invoke a processor action') - .option('--color [on/off]', 'Turn on/off colors for JSON output.', 'on') - .option('--profile [profile]', 'The profile to use') - .option('--query [query]', 'A JMESPath query to use in filtering the response data.') - .option('--params [params]', 'JSON params to send to the action') - .option('--params-file [paramsFile]', 'A file containing either JSON or YAML formatted params') - .action((runtimeName, actionId, options) => { - try { - new InvokeActionCommand(program).execute(runtimeName, actionId, options); - processed = true; - } - catch (err) { - console.error(chalk.red(err.message)); - } - }); - -process.env.DOC && require('../src/commands/utils').exportDoc(program); - -program.parse(process.argv); -if (!processed) - ['string', 'undefined'].includes(typeof program.args[0]) && helper.helpAndExit(program); diff --git a/bin/cortex-project.js b/bin/cortex-project.js deleted file mode 100644 index d9b80d55..00000000 --- a/bin/cortex-project.js +++ /dev/null @@ -1,49 +0,0 @@ -#!/usr/bin/env node - -/* - * Copyright 2018 Cognitive Scale, Inc. All Rights Reserved. - * - * 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. - */ - -const helper = require('./utils.js'); -const program = require('commander'); -const chalk = require('chalk'); -const { - GenerateProjectCommand -} = require('../src/commands/project'); - -let processed = false; -program.description('Work with a related collection of Cortex contributions'); - -// Generate Project -program - .command('generate') - .description('Generates the structure and top level build script for a project') - .option('--color [on/off]', 'Turn on/off colors for JSON output.', 'on') - .option('--profile [profile]', 'The profile to use') - .action((options) => { - try { - new GenerateProjectCommand(program).execute(options); - processed = true; - } - catch (err) { - console.error(chalk.red(err.message)); - } - }); - -process.env.DOC && require('../src/commands/utils').exportDoc(program); - -program.parse(process.argv); -if (!processed) - ['string', 'undefined'].includes(typeof program.args[0]) && helper.helpAndExit(program); diff --git a/bin/cortex-projects.js b/bin/cortex-projects.js new file mode 100755 index 00000000..24744ae7 --- /dev/null +++ b/bin/cortex-projects.js @@ -0,0 +1,78 @@ +#!/usr/bin/env node +import esMain from 'es-main'; +import process from 'node:process'; +import { Command } from 'commander'; +import { callCommand } from '../src/compatibility.js'; +import { + CreateProjectCommand, ListProjectsCommand, DescribeProjectCommand, DeleteProjectCommand, +} from '../src/commands/projects.js'; +import { + DEFAULT_LIST_SKIP_COUNT, DEFAULT_LIST_LIMIT_COUNT, GET_DEFAULT_SORT_CLI_OPTION, DEFAULT_LIST_SORT_PARAMS, LIST_JSON_HELP_TEXT, QUERY_JSON_HELP_TEXT, +} from '../src/constants.js'; +import { checkForEmptyArgs } from '../src/commands/utils.js'; + +export function create() { + const program = new Command(); + + program.name('cortex projects'); + program.description('Work with Cortex Projects'); +// Create Project + program + .command('save [projectDefinition]') + .description('Save a project definition') + .storeOptionsAsProperties(false) + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--profile <profile>', 'The profile to use') + .option('-n, --name <string>', 'The project name') + .option('-t, --title <string>', 'The project title') + .option('-d, --description <string>', 'The project description') + .option('-y, --yaml', 'Use YAML for project definition format') + .action(callCommand((projectDefinition, options) => new CreateProjectCommand(program).execute(projectDefinition, options))); +// List Projects + program + .command('list') + .description('List project definitions') + .alias('l') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--profile <profile>', 'The profile to use') + .option('--json [searchQuery]', LIST_JSON_HELP_TEXT) + .option('--query <query>', `[DEPRECATION WARNING] ${QUERY_JSON_HELP_TEXT}`) + .option('--filter <filter>', 'A Mongo style filter to use.') + .option('--limit <limit>', 'Limit number of records', DEFAULT_LIST_LIMIT_COUNT) + .option('--skip <skip>', 'Skip number of records', DEFAULT_LIST_SKIP_COUNT) + .option('--sort <sort>', 'A Mongo style sort statement to use in the query.', GET_DEFAULT_SORT_CLI_OPTION(DEFAULT_LIST_SORT_PARAMS._updatedAt)) + .action(callCommand((options) => new ListProjectsCommand(program).execute(options))); +// Get|Describe Project + program + .command('describe <projectName>') + .alias('get') + .description('Describe project') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--profile <profile>', 'The profile to use') + .option('--json [searchPath]', QUERY_JSON_HELP_TEXT) + .option('--query <query>', `[DEPRECATION WARNING] ${QUERY_JSON_HELP_TEXT}`) + .action(callCommand((projectName, options) => { + checkForEmptyArgs({ projectName }); + return new DescribeProjectCommand(program).execute(projectName, options); + })); +// Delete Project + program + .command('delete <projectName>') + .alias('d') + .description('Delete project') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--profile <profile>', 'The profile to use') + .action(callCommand((projectName, options) => { + checkForEmptyArgs({ projectName }); + return new DeleteProjectCommand(program).execute(projectName, options); + })); + return program; +} +if (esMain(import.meta)) { + create().showHelpAfterError().parseAsync(process.argv); +} +export default create(); diff --git a/bin/cortex-roles.js b/bin/cortex-roles.js new file mode 100755 index 00000000..702e0736 --- /dev/null +++ b/bin/cortex-roles.js @@ -0,0 +1,140 @@ +#!/usr/bin/env node +import esMain from 'es-main'; +import process from 'node:process'; +import { Command } from 'commander'; +import { callCommand } from '../src/compatibility.js'; +import { nonEmptyStringParser } from '../src/parsers.js'; +import { + ExternalGroupAssignCommand, + ExternalGroupDeleteCommand, + ExternalGroupDescribeCommand, + ExternalGroupListCommand, + RoleAssignCommand, + RoleCreateCommand, + RoleDeleteCommand, + RoleDescribeCommand, + RoleGrantCommand, + RoleListCommand, + RoleProjectAssignCommand, +} from '../src/commands/roles.js'; +import { LIST_JSON_HELP_TEXT, QUERY_JSON_HELP_TEXT } from '../src/constants.js'; + +export function create() { + const program = new Command(); + + program.name('cortex roles'); + program.description('Work with Cortex Roles'); + program.command('list') + .description('List roles by project and user') + .alias('l') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--json [searchQuery]', LIST_JSON_HELP_TEXT) + .option('--query <query>', `[DEPRECATION WARNING] ${QUERY_JSON_HELP_TEXT}`) + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--profile <profile>', 'The profile to use') + .option('--project <project>', 'Project to list roles for') + .action(callCommand((options) => new RoleListCommand(program).execute(options))); + + program.command('describe <role>') + .alias('get') + .description('Describe role grants and users') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--profile <profile>', 'The profile to use') + .option('--users', 'List users associated with role') + .option('--no-grants', 'Ignore grants associated with role') + .action(callCommand((role, options) => new RoleDescribeCommand(program).execute(role, options))); + + program.command('delete <role>') + .description('Delete a role and remove grants/assignments') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--profile <profile>', 'The profile to use') + .action(callCommand((role, options) => new RoleDeleteCommand(program).execute(role, options))); + + program.command('create <role>') + .description('Create a role with the given grant') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--profile <profile>', 'The profile to use') + .requiredOption('--project <project>', 'Grant project') + .requiredOption('--resource <resource>', 'Grant resource') + .requiredOption('--actions <actions...>', 'Grant actions read | write | execute | *') + .option('--deny', 'Explicit deny of action(s)') + .action(callCommand((role, options) => new RoleCreateCommand(program).execute(role, options))); + + program.command('grant <role>') + .description('Manage grants on an existing role') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--profile <profile>', 'The profile to use') + .requiredOption('--project <project>', 'Grant project') + .requiredOption('--resource <resource>', 'Grant resource') + .requiredOption('--actions <actions...>', 'Grant actions read | write | execute | *') + .option('--delete', 'Remove grant from role') + .option('--deny', 'Explicit deny of action(s)') + .action(callCommand((role, options) => new RoleGrantCommand(program).execute(role, options))); + + program.command('assign <role>') + .description('Manage a list of user assignments on a role') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--profile <profile>', 'The profile to use') + .requiredOption('--users <users...>', 'Users to add/remove to/from a role', nonEmptyStringParser({ + message: 'user names must not be empty', + variadic: true, + })) + .option('--delete', 'Unassign users from role') + .action(callCommand((role, options) => new RoleAssignCommand(program).execute(role, options))); + + program.command('project <project>') + .description('Manage a list of role assignments on a project') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--profile <profile>', 'The profile to use') + .requiredOption('--roles <roles...>', 'Roles to add/remove on project') + .option('--delete', 'Unassign roles from project') + .action(callCommand((project, options) => new RoleProjectAssignCommand(program).execute(project, options))); + + program.command('list-external') + .description('List external groups') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--json [searchQuery]', LIST_JSON_HELP_TEXT) + .option('--query <query>', `[DEPRECATION WARNING] ${QUERY_JSON_HELP_TEXT}`) + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--profile <profile>', 'The profile to use') + .action(callCommand((options) => new ExternalGroupListCommand(program).execute(options))); + + program.command('describe-external <externalGroup>') + .alias('get-external') + .description('Describe external group roles and users') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--profile <profile>', 'The profile to use') + .option('--users', 'List users associated with external group') + .option('--roles', 'List roles associated with external group') + .action(callCommand((externalGroup, options) => new ExternalGroupDescribeCommand(program).execute(externalGroup, options))); + + program.command('delete-external <externalGroup>') + .description('Delete an external group and remove assignments') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--profile <profile>', 'The profile to use') + .action(callCommand((externalGroup, options) => new ExternalGroupDeleteCommand(program).execute(externalGroup, options))); + + program.command('assign-external') + .description('Manage an external group assignment on a role') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--profile <profile>', 'The profile to use') + .requiredOption('--role <role>', 'Role to add/remove assignment') + .requiredOption('--externalGroup <externalGroup>', 'External group to add/remove on role') + .option('--delete', 'Unassign external group from role') + .action(callCommand((options) => new ExternalGroupAssignCommand(program).execute(options))); + + return program; +} +if (esMain(import.meta)) { + create().showHelpAfterError().parseAsync(process.argv); +} +export default create(); diff --git a/bin/cortex-secrets.js b/bin/cortex-secrets.js new file mode 100755 index 00000000..74240efb --- /dev/null +++ b/bin/cortex-secrets.js @@ -0,0 +1,72 @@ +#!/usr/bin/env node +import esMain from 'es-main'; +import process from 'node:process'; +import { Command } from 'commander'; +import { callCommand } from '../src/compatibility.js'; +import { + ListSecretsCommand, ReadSecretsCommand, WriteSecretsCommand, DeleteSecretCommand, +} from '../src/commands/secrets.js'; +import { LIST_JSON_HELP_TEXT, QUERY_JSON_HELP_TEXT } from '../src/constants.js'; +import { checkForEmptyArgs } from '../src/commands/utils.js'; + +export function create() { + const program = new Command(); + + program.name('cortex secrets'); + program.description('Work with Cortex Secrets'); +// List Secure Keys + program + .command('list') + .description('List secure keys') + .alias('l') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--json [searchQuery]', LIST_JSON_HELP_TEXT) + .option('--query <query>', `[DEPRECATION WARNING] ${QUERY_JSON_HELP_TEXT}`) + .option('--profile <profile>', 'The profile to use') + .option('--project <project>', 'The project to use') + .action(callCommand((options) => new ListSecretsCommand(program).execute(options))); + +// Read Secure Value + program + .command('describe <keyName>') + .alias('get') + .description('Retrieve the value stored for the given key.') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--json', 'Output results using JSON') + .option('--profile <profile>', 'The profile to use') + .option('--project <project>', 'The project to use') + .action(callCommand((keyName, options) => { + checkForEmptyArgs({ keyName }); + return new ReadSecretsCommand(program).execute(keyName, options); + })); +// Write Secure Value + program + .command('save <keyName> [value]') + .description('Save or overwrite a secure value. By default values are stored as strings but can also be saved as JSON or YAML.') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--data <data>', 'JSON value to save') + .option('--data-file <dataFile>', 'A file containing either JSON or YAML formatted value to save') + .option('--profile <profile>', 'The profile to use') + .option('--project <project>', 'The project to use') + .action(callCommand((keyName, value, options) => new WriteSecretsCommand(program).execute(keyName, value, options))); +// Delete Secret + program + .command('delete <keyName>') + .description('Delete a secret') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--profile <profile>', 'The profile to use') + .option('--project <project>', 'The project to use') + .action(callCommand((keyName, options) => { + checkForEmptyArgs({ keyName }); + return new DeleteSecretCommand(program).execute(keyName, options); + })); + return program; +} +if (esMain(import.meta)) { + await create().showHelpAfterError().parseAsync(process.argv); +} +export default create(); diff --git a/bin/cortex-sessions.js b/bin/cortex-sessions.js new file mode 100755 index 00000000..68214087 --- /dev/null +++ b/bin/cortex-sessions.js @@ -0,0 +1,76 @@ +#!/usr/bin/env node +import esMain from 'es-main'; +import { Command } from 'commander'; +import { callCommand } from '../src/compatibility.js'; +import { + DeleteSessionCommand, SaveSessionCommand, DescribeSessionCommand, ListSessionsCommand, +} from '../src/commands/sessions.js'; +import { DEFAULT_LIST_LIMIT_COUNT, LIST_JSON_HELP_TEXT, QUERY_JSON_HELP_TEXT } from '../src/constants.js'; +import { checkForEmptyArgs } from '../src/commands/utils.js'; + +export function create() { + const program = new Command(); + + program.name('cortex sessions'); + program.description('Work with Cortex Sessions'); +// List Sessions + program + .command('list') + .description('List session definitions') + .alias('l') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--limit <limit>', 'Number of sessions to list', DEFAULT_LIST_LIMIT_COUNT) + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--profile <profile>', 'The profile to use') + .option('--project <project>', 'The project to use') + .option('--json [searchQuery]', LIST_JSON_HELP_TEXT) + .option('--query <query>', `[DEPRECATION WARNING] ${QUERY_JSON_HELP_TEXT}`) + .action(callCommand((options) => { + new ListSessionsCommand(program).execute(options); + })); +// Describe Session + program + .command('describe <sessionId>') + .description('Describe session') + .alias('get') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--profile <profile>', 'The profile to use') + .option('--project <project>', 'The project to use') + .option('--json [searchPath]', QUERY_JSON_HELP_TEXT) + .option('--query <query>', `[DEPRECATION WARNING] ${QUERY_JSON_HELP_TEXT}`) + .option('--verbose', 'Verbose output') + .action(callCommand((sessionName, options) => { + checkForEmptyArgs({ sessionName }); + new DescribeSessionCommand(program).execute(sessionName, options); + })); +// Delete Session + program + .command('delete <sessionId>') + .description('Delete a session') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--profile <profile>', 'The profile to use') + .option('--project <project>', 'The project to use') + .action(callCommand((sessionName, options) => { + checkForEmptyArgs({ sessionName }); + new DeleteSessionCommand(program).execute(sessionName, options); + })); +// Save Session + program + .command('save <sessionDefinition>') + .description('Save a session definition') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--profile <profile>', 'The profile to use') + .option('--project <project>', 'The project to use') + .option('-y, --yaml', 'Use YAML for session definition format') + .action(callCommand((sessionDefinition, options) => { + new SaveSessionCommand(program).execute(sessionDefinition, options); + })); + return program; +} +if (esMain(import.meta)) { + create().showHelpAfterError().parseAsync(process.argv); +} +export default create(); diff --git a/bin/cortex-skills.js b/bin/cortex-skills.js index d38307c2..7c7d4649 100755 --- a/bin/cortex-skills.js +++ b/bin/cortex-skills.js @@ -1,98 +1,155 @@ #!/usr/bin/env node +import esMain from 'es-main'; +import process from 'node:process'; +import { Command } from 'commander'; +import { callCommand } from '../src/compatibility.js'; +import { + DEFAULT_LIST_LIMIT_COUNT, + DEFAULT_LIST_SKIP_COUNT, + DEFAULT_LIST_SORT_PARAMS, + GET_DEFAULT_SORT_CLI_OPTION, + LIST_JSON_HELP_TEXT, + QUERY_JSON_HELP_TEXT, +} from '../src/constants.js'; +import { + DeleteSkillCommand, + DeploySkillCommand, + DescribeSkillCommand, + InvokeSkillCommand, + ListSkillsCommand, + SaveSkillCommand, + SkillLogsCommand, + UndeploySkillCommand, +} from '../src/commands/skills.js'; +import { checkForEmptyArgs } from '../src/commands/utils.js'; -/* - * Copyright 2018 Cognitive Scale, Inc. All Rights Reserved. - * - * 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. - */ - -const helper = require('./utils.js'); -const program = require('commander'); -const chalk = require('chalk'); -const { SaveSkillCommand, ListSkillsCommand, DescribeSkillCommand, GenerateSkillCommand } = require('../src/commands/skills'); - -let processed = false; -program.description('Work with Cortex Skills'); - -// Save Skill -program - .command('save <skillDefinition>') - .description('Save a skill definition') - .option('--color [on/off]', 'Turn on/off colors for JSON output.', 'on') - .option('--profile [profile]', 'The profile to use') - .option('-y, --yaml', 'Use YAML for skill definition format') - .action((skillDefinition, options) => { - try { - new SaveSkillCommand(program).execute(skillDefinition, options); - processed = true; - } - catch (err) { - console.error(chalk.red(err.message)); - } - }); - -// List Skills -program - .command('list') - .description('List skill definitions') - .option('--color [on/off]', 'Turn on/off colors for JSON output.', 'on') - .option('--profile [profile]', 'The profile to use') - .option('--json', 'Output results using JSON') - .option('--query [query]', 'A JMESPath query to use in filtering the response data.') - .action((options) => { - try { - new ListSkillsCommand(program).execute(options); - processed = true; - } - catch (err) { - console.error(chalk.red(err.message)); - } - }); - +export function create() { + const program = new Command(); + program.name('cortex skills'); + program.description('Work with Cortex Skills'); +// Deploy Skill + program + .command('deploy <skillNames...>') + .description('Deploy the skill resource to the cluster') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--profile <profile>', 'The profile to use') + .option('--project <project>', 'The project to use') + .option('--dryrun', 'Return the K8S skill resource and skip deployment to the cluster') + .option('--stereotypes <stereotypes...>', 'Stereotype(s) to apply during deployment: use "none" to remove ALL stereotypes.\nNOTE: These will not be persisted with the skill definition') + .action(callCommand(async (skillNames, options) => { + checkForEmptyArgs({ skillNames }); + await new DeploySkillCommand(program).execute(skillNames, options); + })); // Describe Skill -program - .command('describe <skillName>') - .description('Describe skill') - .option('--color [on/off]', 'Turn on/off colors for JSON output.', 'on') - .option('--profile [profile]', 'The profile to use') - .option('--query [query]', 'A JMESPath query to use in filtering the response data.') - .action((skillName, options) => { - try { - new DescribeSkillCommand(program).execute(skillName, options); - processed = true; - } - catch (err) { - console.error(chalk.red(err.message)); - } - }); - -program - .command('generate') - .description('Generates the structure and top level build script for a skill') - .option('--color [on/off]', 'Turn on/off colors for JSON output.', 'on') - .option('--profile [profile]', 'The profile to use', 'default') - .action((options) => { - try { - new GenerateSkillCommand(program).execute(options); - processed = true; - } - catch (err) { - console.error(chalk.red(err.message)); - } - }); - -process.env.DOC && require('../src/commands/utils').exportDoc(program); - -program.parse(process.argv); -if (!processed) - ['string', 'undefined'].includes(typeof program.args[0]) && helper.helpAndExit(program); \ No newline at end of file + program + .command('describe <skillName>') + .alias('get') + .description('Describe skill') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--profile <profile>', 'The profile to use') + .option('--project <project>', 'The project to use') + .option('-f --output-file <file>', 'Write output to file instead of stdout') + .option('-o, --output <json|yaml|k8s|openapi>', 'Format output as yaml or k8s resource', 'json') + .option('--json [searchPath]', QUERY_JSON_HELP_TEXT) + .option('--query <query>', `[DEPRECATION WARNING] ${QUERY_JSON_HELP_TEXT}`) + .option('--verbose', 'Verbose output', false) + .action(callCommand(async (skillName, options) => { + checkForEmptyArgs({ skillName }); + await new DescribeSkillCommand(program).execute(skillName, options); + })); +// Invoke Skill + program + .command('invoke <skillName> <inputName>') + .description('Invoke a skill') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', true) + .option('--profile <profile>', 'The profile to use') + .option('--project <project>', 'The project to use') + .option('--params <params>', 'JSON params to send to the action') + .option('--params-file <paramsFile>', 'A file containing either JSON or YAML formatted params') + .option('--sync', 'Invoke the skill synchronously', false) + .action(callCommand(async (skillName, inputName, options) => { + checkForEmptyArgs({ skillName, inputName }); + await new InvokeSkillCommand(program).execute(skillName, inputName, options); + })); +// List Skills + program + .command('list') + .description('List skill definitions') + .alias('l') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--profile <profile>', 'The profile to use') + .option('--project <project>', 'The project to use') + .option('--json [searchQuery]', LIST_JSON_HELP_TEXT) + .option('--query <query>', `[DEPRECATION WARNING] ${QUERY_JSON_HELP_TEXT}`) + .option('--nostatus', 'skip extra call for skill status') + .option('--noshared', 'do not list shared skills') + .option('--filter <filter>', 'A Mongo style filter to use.') + .option('--limit <limit>', 'Limit number of records', DEFAULT_LIST_LIMIT_COUNT) + .option('--skip <skip>', 'Skip number of records', DEFAULT_LIST_SKIP_COUNT) + .option('--sort <sort>', 'A Mongo style sort statement to use in the query.', GET_DEFAULT_SORT_CLI_OPTION(DEFAULT_LIST_SORT_PARAMS.updatedAt)) + .action(callCommand(async (options) => { + await new ListSkillsCommand(program).execute(options); + })); +// Delete Skill + program + .command('delete <skillName>') + .description('Delete a skill') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--json', 'Output results using JSON') + .option('--profile <profile>', 'The profile to use') + .option('--project <project>', 'The project to use') + .action(callCommand(async (skillName, options) => { + checkForEmptyArgs({ skillName }); + await new DeleteSkillCommand(program).execute(skillName, options); + })); +// Save Skill + program + .command('save <skillDefinitions...>') + .description('Save a skill definition') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--profile <profile>', 'The profile to use') + .option('--project <project>', 'The project to use') + .option('-k, --k8sResource <file...>', 'Additional kubernetes resources deployed and owned by the skill') + // eslint-disable-next-line max-len + .option('--podspec <podspec>', 'A file containing either a JSON or YAML formatted pod spec to merge with the skill definition, used for specifying resources (like memory, ephemeral storage, CPUs, and GPUs) and tolerations (like allowing pods to be scheduled on tainted nodes).') + .option('-y, --yaml', 'Use YAML for skill definition format') + .option('--scaleCount <count>', 'Scale count, only used for daemon action types') + .action(callCommand(async (skillDefinitions, options) => { + await new SaveSkillCommand(program).execute(skillDefinitions, options); + })); +// Undeploy Skill + program + .command('undeploy <skillNames...>') + .description('Undeploy the skill resource from the cluster') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--profile <profile>', 'The profile to use') + .option('--project <project>', 'The project to use') + .action(callCommand((skillNames, options) => { + checkForEmptyArgs({ skillNames }); + return new UndeploySkillCommand(program).execute(skillNames, options); + })); +// Get Skill/action logs + program + .command('logs <skillName> <actionName>') + .description('Get logs of a skill and action') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--profile <profile>', 'The profile to use') + .option('--project <project>', 'The project to use') + .option('--raw', 'Get raw logs as a stream') + // TODO enable when we want to support tasks + // .option('--type [type]', 'The type of action logs to fetch [skill|task]') + .action(callCommand((skillName, actionName, options) => { + checkForEmptyArgs({ skillName, actionName }); + return new SkillLogsCommand(program).execute(skillName, actionName, options); + })); + return program; +} +if (esMain(import.meta)) { + await create().showHelpAfterError().parseAsync(process.argv); +} +export default create(); diff --git a/bin/cortex-stereotypes.js b/bin/cortex-stereotypes.js new file mode 100755 index 00000000..27968588 --- /dev/null +++ b/bin/cortex-stereotypes.js @@ -0,0 +1,91 @@ +#!/usr/bin/env node +import esMain from 'es-main'; +import process from 'node:process'; +import { Command } from 'commander'; +import { callCommand } from '../src/compatibility.js'; +import { + DEFAULT_LIST_LIMIT_COUNT, + DEFAULT_LIST_SKIP_COUNT, + DEFAULT_LIST_SORT_PARAMS, + GET_DEFAULT_SORT_CLI_OPTION, + LIST_JSON_HELP_TEXT, + QUERY_JSON_HELP_TEXT, +} from '../src/constants.js'; +import { + DeleteStereotypeCommand, DescribeStereotypeCommand, ListStereotypesCommand, SaveStereotypeCommand, +} from '../src/commands/stereotype.js'; + +export function create() { + const program = new Command(); + program.name('cortex skill stereotypes'); + program.description('Work with Cortex Skill Stereotypes'); +// Describe Stereotype + program + .command('describe <Name>') + .alias('get') + .description('Describe skill stereotype') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--profile <profile>', 'The profile to use') + .option('--project <project>', 'The project to use') + .option('-f --output-file <file>', 'Write output to file instead of stdout') + .option('-o, --output <json|yaml>', 'Format output as yaml', 'json') + .option('--json [searchPath]', QUERY_JSON_HELP_TEXT) + .option('--query <query>', `[DEPRECATION WARNING] ${QUERY_JSON_HELP_TEXT}`) + .option('--verbose', 'Verbose output', false) + .action(callCommand(async (skillName, options) => { + await new DescribeStereotypeCommand(program).execute(skillName, options); + })); +// List Stereotypes + program + .command('list') + .description('List skill stereotypes') + .alias('l') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--profile <profile>', 'The profile to use') + .option('--project <project>', 'The project to use') + .option('--json [searchQuery]', LIST_JSON_HELP_TEXT) + .option('--query <query>', `[DEPRECATION WARNING] ${QUERY_JSON_HELP_TEXT}`) + .option('--noshared', 'do not list shared skills') + .option('--filter <filter>', 'A Mongo style filter to use.') + .option('--limit <limit>', 'Limit number of records', DEFAULT_LIST_LIMIT_COUNT) + .option('--skip <skip>', 'Skip number of records', DEFAULT_LIST_SKIP_COUNT) + .option('--sort <sort>', 'A Mongo style sort statement to use in the query.', GET_DEFAULT_SORT_CLI_OPTION(DEFAULT_LIST_SORT_PARAMS.updatedAt)) + .action(callCommand(async (options) => { + await new ListStereotypesCommand(program).execute(options); + })); +// Delete Stereotype + program + .command('delete <skillName>') + .description('Delete a skill') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--json', 'Output results using JSON') + .option('--profile <profile>', 'The profile to use') + .option('--project <project>', 'The project to use') + .action(callCommand(async (skillName, options) => { + await new DeleteStereotypeCommand(program).execute(skillName, options); + })); +// Save Stereotype + program + .command('save <skillDefinitions...>') + .description('Save a skill definition') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--profile <profile>', 'The profile to use') + .option('--project <project>', 'The project to use') + .option('-k, --k8sResource <file...>', 'Additional kubernetes resources deployed and owned by the skill') + // eslint-disable-next-line max-len + .option('--podspec <podspec>', 'A file containing either a JSON or YAML formatted pod spec to merge with the skill definition, used for specifying resources (like memory, ephemeral storage, CPUs, and GPUs) and tolerations (like allowing pods to be scheduled on tainted nodes).') + .option('-y, --yaml', 'Use YAML for skill definition format') + .option('--scaleCount <count>', 'Scale count, only used for daemon action types') + .action(callCommand(async (skillDefinitions, options) => { + await new SaveStereotypeCommand(program).execute(skillDefinitions, options); + })); + return program; +} +if (esMain(import.meta)) { + create().showHelpAfterError().parseAsync(process.argv); +} +export default create(); diff --git a/bin/cortex-tasks.js b/bin/cortex-tasks.js index b805cdac..97634364 100755 --- a/bin/cortex-tasks.js +++ b/bin/cortex-tasks.js @@ -1,104 +1,109 @@ -#!/usr/bin/env node +import esMain from 'es-main'; +import { Command } from 'commander'; +import { callCommand } from '../src/compatibility.js'; +import { + ListTasksCommand, DescribeTaskCommand, TaskLogsCommand, TaskDeleteCommand, TaskPauseCommand, TaskResumeCommand, +} from '../src/commands/tasks.js'; +import { + DEFAULT_LIST_LIMIT_COUNT, DEFAULT_LIST_SKIP_COUNT, LIST_JSON_HELP_TEXT, QUERY_JSON_HELP_TEXT, DEFAULT_LIST_SORT_PARAMS, GET_DEFAULT_SORT_CLI_OPTION, +} from '../src/constants.js'; +import { checkForEmptyArgs } from '../src/commands/utils.js'; -/* -* Copyright 2018 Cognitive Scale, Inc. All Rights Reserved. -* -* 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. -*/ - -const helper = require('./utils.js'); -const program = require('commander'); -const chalk = require('chalk'); -const { ListTasks, TaskLogs, CancelTask, DescribeTask } = require('../src/commands/tasks'); - -let processed = false; -program.description('Work with Cortex Jobs'); - - -// List Tasks -program - .command('list <jobId>') - .description('List task definitions within a job definition') - .option('--color [on/off]', 'Turn on/off colors for JSON output.', 'on') - .option('--profile [profile]', 'The profile to use') - .option('--json', 'Output results using JSON') - .option('--query [query]', 'A JMESPath query to use in filtering the response data. Ignored if output format is not JSON.') - .action((jobId, options) => { - try { - new ListTasks(program).execute(jobId, options); - processed = true; - } - catch (err) { - console.error(chalk.red(err.message)); - } - }); - -// Get Tasks logs -program - .command('logs <jobId> <taskId>') - .description('Get Tasks logs') - .option('--color [on/off]', 'Turn on/off colors for JSON output.', 'on') - .option('--profile [profile]', 'The profile to use') - .option('--json', 'Output results using JSON') - .option('--query [query]', 'A JMESPath query to use in filtering the response data. Ignored if output format is not JSON.') - .action((jobId, taskId, options) => { - try { - new TaskLogs(program).execute(jobId, taskId, options); - processed = true; - } - catch (err) { - console.error(chalk.red(err.message)); - } - }); - -// Cancel task -program - .command('cancel <jobId> <taskId>') - .description('Cancel a task') - .option('--color [on/off]', 'Turn on/off colors for JSON output.', 'on') - .option('--profile [profile]', 'The profile to use') - .option('--json', 'Output results using JSON') - .option('-m, --message <message>', 'Cancellation message') - .action((jobId, taskId, options) => { - try { - new CancelTask(program).execute(jobId, taskId, options); - processed = true; - } - catch (err) { - console.error(chalk.red(err.message)); - } - }); - -// Describe task -program - .command('describe <jobId> <taskId>') - .description('Describe a task definition') - .option('--color [on/off]', 'Turn on/off colors for JSON output.', 'on') - .option('--profile [profile]', 'The profile to use') - .option('--json', 'Output results using JSON') - .option('--query [query]', 'A JMESPath query to use in filtering the response data. Ignored if output format is not JSON.') - .action((jobId, taskId, options) => { - try { - new DescribeTask(program).execute(jobId, taskId, options); - processed = true; - } - catch (err) { - console.error(chalk.red(err.message)); - } - }); - -process.env.DOC && require('../src/commands/utils').exportDoc(program); - -program.parse(process.argv); -if (!processed) - ['string', 'undefined'].includes(typeof program.args[0]) && helper.helpAndExit(program); +export function create() { + const program = new Command(); + program.name('cortex tasks'); + program.description('Work with Cortex Tasks'); +// List tasks + program + .command('list') + .description('List tasks') + .alias('l') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--profile <profile>', 'The profile to use') + .option('--project <project>', 'The project to use') + .option('--scheduled', 'Show scheduled tasks only') + // TODO Should we remove and just use filter {} + .option('--actionName <string>', 'Filter tasks by action name') + .option('--activationId <string>', 'filter tasks by activation id') + .option('--agentName <string>', 'Filter tasks by agent name') + .option('--skillName <string>', 'Filter tasks by skill name') + .option('--username <string>', 'Filter tasks by username') + .option('--skillName <string>', 'Filter tasks by skill name') + .option('--json [searchQuery]', LIST_JSON_HELP_TEXT) + .option('--query <query>', `[DEPRECATION WARNING] ${QUERY_JSON_HELP_TEXT}`) + // This is a client-side sort + .option('--filter <filter>', 'Filter by actionName, activationId, agentName, or skillName only equality is supported') + .option('--limit <limit>', 'Limit number of records', DEFAULT_LIST_LIMIT_COUNT) + .option('--skip <skip>', 'Skip number of records', DEFAULT_LIST_SKIP_COUNT) + .option('--sort <sort>', 'A Mongo style sort statement to use in the query.', GET_DEFAULT_SORT_CLI_OPTION(DEFAULT_LIST_SORT_PARAMS.startTime)) + .action(callCommand(async (options) => { + await new ListTasksCommand(program).execute(options); + })); + program + .command('describe <taskName>') + .alias('get') + .description('Describe a task') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--profile <profile>', 'The profile to use') + .option('--project <project>', 'The project to use') + .option('--k8s', 'Return the full k8sResource in the response') + // .option('-o, --output <json|yaml|k8s>', 'Format output as yaml or k8s resources') + .action(callCommand((taskName, options) => { + checkForEmptyArgs({ taskName }); + new DescribeTaskCommand(program).execute(taskName, options); + })); +// Get task logs + program + .command('logs <taskName>') + .description('Get logs of a task') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--profile <profile>', 'The profile to use') + .option('--project <project>', 'The project to use') + .option('--follow [boolean]', 'Stream live logs', false) + // TODO enable when we want to support tasks + // .option('--type [type]', 'The type of action logs to fetch [skill|task]') + .action(callCommand((taskName, options) => { + checkForEmptyArgs({ taskName }); + new TaskLogsCommand(program).execute(taskName, options); + })); +// Delete a task + program + .command('delete <taskName...>') + .description('Delete a task, if it exists') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--profile <profile>', 'The profile to use') + .option('--project <project>', 'The project to use') + .action(callCommand(async (taskName, options) => { + checkForEmptyArgs({ taskName }); + await new TaskDeleteCommand(program).execute(taskName, options); + })); +// Pause a task + program + .command('pauseSchedule <taskNames...>') + .description('Pause scheduling for a scheduled task') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--profile <profile>', 'The profile to use') + .option('--project <project>', 'The project to use') + .action(callCommand(async (taskName, options) => { + checkForEmptyArgs({ taskName }); + await new TaskPauseCommand(program).execute(taskName, options); + })); +// Resume a task + program + .command('resumeSchedule <taskNames...>') + .description('Resume paused schedule for a scheduled task') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--profile <profile>', 'The profile to use') + .option('--project <project>', 'The project to use') + .action(callCommand(async (taskName, options) => { + checkForEmptyArgs({ taskName }); + await new TaskResumeCommand(program).execute(taskName, options); + })); + return program; +} +if (esMain(import.meta)) { + create().showHelpAfterError().parseAsync(process.argv); +} +export default create(); diff --git a/bin/cortex-types.js b/bin/cortex-types.js index 30212c3e..b3ed51f4 100755 --- a/bin/cortex-types.js +++ b/bin/cortex-types.js @@ -1,83 +1,79 @@ #!/usr/bin/env node +import esMain from 'es-main'; +import process from 'node:process'; +import { Command } from 'commander'; +import { callCommand } from '../src/compatibility.js'; +import { + DEFAULT_LIST_SKIP_COUNT, DEFAULT_LIST_LIMIT_COUNT, GET_DEFAULT_SORT_CLI_OPTION, DEFAULT_LIST_SORT_PARAMS, LIST_JSON_HELP_TEXT, QUERY_JSON_HELP_TEXT, +} from '../src/constants.js'; +import { + SaveTypeCommand, ListTypesCommand, DescribeTypeCommand, DeleteTypeCommand, +} from '../src/commands/types.js'; +import { checkForEmptyArgs } from '../src/commands/utils.js'; -/* - * Copyright 2018 Cognitive Scale, Inc. All Rights Reserved. - * - * 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. - */ - -const helper = require('./utils.js'); -const program = require('commander'); -const chalk = require('chalk'); -const { SaveTypeCommand, ListTypesCommand, DescribeTypeCommand } = require('../src/commands/types'); - -let processed = false; -program.description('Work with Cortex Types'); - +export function create() { + const program = new Command(); + program.name('cortex types'); + program.description('Work with Cortex Types'); // Save Type -program - .command('save <typeDefinition>') - .description('Save a type definition') - .option('--color [on/off]', 'Turn on/off colors for JSON output.', 'on') - .option('--profile [profile]', 'The profile to use') - .option('-y, --yaml', 'Use YAML for type definition format') - .action((typeDefinition, options) => { - try { - new SaveTypeCommand(program).execute(typeDefinition, options); - processed = true; - } - catch (err) { - console.error(chalk.red(err.message)); - } - }); - + program + .command('save <typeDefinition>') + .description('Save a type definition') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--profile <profile>', 'The profile to use') + .option('--project <project>', 'The project to use') + .option('-y, --yaml', 'Use YAML for type definition format') + .action(callCommand(async (typeDefinition, options) => { + await new SaveTypeCommand(program).execute(typeDefinition, options); + })); // List Types -program - .command('list') - .description('List type definitions') - .option('--color [on/off]', 'Turn on/off colors for JSON output.', 'on') - .option('--profile [profile]', 'The profile to use') - .option('--json', 'Output results using JSON') - .option('--query [query]', 'A JMESPath query to use in filtering the response data.') - .action((options) => { - try { - new ListTypesCommand(program).execute(options); - processed = true; - } - catch (err) { - console.error(chalk.red(err.message)); - } - }); - + program + .command('list') + .description('List type definitions') + .alias('l') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--profile <profile>', 'The profile to use') + .option('--project <project>', 'The project to use') + .option('--json [searchQuery]', LIST_JSON_HELP_TEXT) + .option('--query <query>', `[DEPRECATION WARNING] ${QUERY_JSON_HELP_TEXT}`) + .option('--limit <limit>', 'Limit number of records', DEFAULT_LIST_LIMIT_COUNT) + .option('--skip <skip>', 'Skip number of records', DEFAULT_LIST_SKIP_COUNT) + .option('--sort <sort>', 'A Mongo style sort statement to use in the query.', GET_DEFAULT_SORT_CLI_OPTION(DEFAULT_LIST_SORT_PARAMS.updatedAt)) + .action(callCommand(async (options) => { + await new ListTypesCommand(program).execute(options); + })); // Describe Type -program - .command('describe <typeName>') - .description('Describe type') - .option('--color [on/off]', 'Turn on/off colors for JSON output.', 'on') - .option('--profile [profile]', 'The profile to use') - .option('--query [query]', 'A JMESPath query to use in filtering the response data.') - .action((typeName, options) => { - try { - new DescribeTypeCommand(program).execute(typeName, options); - processed = true; - } - catch (err) { - console.error(chalk.red(err.message)); - } - }); - -process.env.DOC && require('../src/commands/utils').exportDoc(program); - -program.parse(process.argv); -if (!processed) - ['string', 'undefined'].includes(typeof program.args[0]) && helper.helpAndExit(program); + program + .command('describe <typeName>') + .alias('get') + .description('Describe type') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--profile <profile>', 'The profile to use') + .option('--project <project>', 'The project to use') + .option('--json [searchPath]', QUERY_JSON_HELP_TEXT) + .option('--query <query>', `[DEPRECATION WARNING] ${QUERY_JSON_HELP_TEXT}`) + .action(callCommand(async (typeName, options) => { + checkForEmptyArgs({ typeName }); + await new DescribeTypeCommand(program).execute(typeName, options); + })); + program + .command('delete <typeName>') + .description('Delete a type') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--json', 'Output results using JSON') + .option('--profile <profile>', 'The profile to use') + .option('--project <project>', 'The project to use') + .action(callCommand(async (typeName, options) => { + checkForEmptyArgs({ typeName }); + await new DeleteTypeCommand(program).execute(typeName, options); + })); + return program; +} +if (esMain(import.meta)) { + create().showHelpAfterError().parseAsync(process.argv); +} +export default create(); diff --git a/bin/cortex-users.js b/bin/cortex-users.js new file mode 100755 index 00000000..246a2b78 --- /dev/null +++ b/bin/cortex-users.js @@ -0,0 +1,99 @@ +#!/usr/bin/env node +import esMain from 'es-main'; +import { Command } from 'commander'; +import { callCommand } from '../src/compatibility.js'; +import { + UserProjectAssignCommand, UserDescribeCommand, UserGrantCommand, UserCreateCommand, UserListCommand, UserDeleteCommand, UserResetPATCommand, UserGetPATCommand, +} from '../src/commands/users.js'; +import { LIST_JSON_HELP_TEXT, QUERY_JSON_HELP_TEXT } from '../src/constants.js'; +import { checkForEmptyArgs } from '../src/commands/utils.js'; + +export function create() { + const program = new Command(); + + program.name('cortex users'); + program.description('Work with Cortex User'); + program.command('describe [user]') + .alias('get') + .description('Describe a users grants and roles') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--profile <profile>', 'The profile to use') + .option('--user <user>', 'The user to describe, self for default') + .option('--roles', 'Include grant inheritance from roles') + .action(callCommand((user, options) => { + checkForEmptyArgs({ user }); + return new UserDescribeCommand(program).execute(user, options); + })); + program.command('grant <user>') + .description('Manage user grants') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .requiredOption('--project <project>', 'Grant project') + .requiredOption('--resource <resource>', 'Grant resource') + .requiredOption('--actions <actions...>', 'Grant actions read | write | execute | *') + .option('--profile <profile>', 'The profile to use') + .option('--delete', 'Remove grant from user') + .option('--deny', 'Explicit deny of action(s)') + .action(callCommand((user, options) => { + checkForEmptyArgs({ user }); + return new UserGrantCommand(program).execute(user, options); + })); + program.command('project <project>') + .description('Manage a list of user assignments on a project') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--profile <profile>', 'The profile to use') + .requiredOption('--users <users...>', 'Users to add/remove on project') + .option('--delete', 'Unassign users from project') + .action(callCommand((project, options) => { + checkForEmptyArgs({ project }); + return new UserProjectAssignCommand(program).execute(project, options); + })); + program.command('delete <user>') + .description('Deletes a service user and disables any existing tokens created by the user') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--profile <profile>', 'The profile to use') + .action(callCommand((user, options) => { + checkForEmptyArgs({ user }); + new UserDeleteCommand(program).execute(user, options); + })); + program.command('create <user>') + .description('Creates a service user (with no assigned roles/grants) that can authenticate with and call Fabric API\'s') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--profile <profile>', 'The profile to use') + .action(callCommand((user, options) => { + checkForEmptyArgs({ user }); + return new UserCreateCommand(program).execute(user, options); + })); + program.command('list') + .description('Lists all service users created within Cortex') + .alias('l') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--json [searchQuery]', LIST_JSON_HELP_TEXT) + .option('--query <query>', `[DEPRECATION WARNING] ${QUERY_JSON_HELP_TEXT}`) + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--profile <profile>', 'The profile to use') + .action(callCommand((options) => new UserListCommand(program).execute(options))); + program.command('reset-pat') + .description('Invalidates personal access token (PAT) for a specified user within Cortex') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--profile <profile>', 'The profile to use') + .option('--user <user>', 'The user to reset personal access token') + .action(callCommand((options) => new UserResetPATCommand(program).execute(options))); + program.command('get-pat') + .description('Fetch personal access token (PAT) for a specified user within Cortex') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [boolean]', 'Turn on/off colors for JSON output.', 'true') + .option('--profile <profile>', 'The profile to use') + .option('--user <user>', 'The user to reset personal access token (admins only)') + .action(callCommand((options) => new UserGetPATCommand(program).execute(options))); + return program; +} +if (esMain(import.meta)) { + create().showHelpAfterError().parseAsync(process.argv); +} +export default create(); diff --git a/bin/cortex-workspaces.js b/bin/cortex-workspaces.js new file mode 100755 index 00000000..dcdd0310 --- /dev/null +++ b/bin/cortex-workspaces.js @@ -0,0 +1,90 @@ +import { Command } from 'commander'; +import process from 'node:process'; +import esMain from 'es-main'; +import WorkspaceConfigureCommand from '../src/commands/workspaces/configure.js'; +import WorkspaceGenerateCommand from '../src/commands/workspaces/generate.js'; +import WorkspaceBuildCommand from '../src/commands/workspaces/build.js'; +import WorkspacePublishCommand from '../src/commands/workspaces/publish.js'; +import { + WorkspaceAddRegistryCommand, WorkspaceRemoveRegistryCommand, WorkspaceActivateRegistryCommand, WorkspaceListRegistryCommand, +} from '../src/commands/workspaces/registry.js'; + +export function create() { + const program = new Command(); + + program.name('cortex workspaces'); + program.description('Scaffolding Cortex Components'); + program + .command('configure') + .option('--refresh', 'Refresh the Github access token') + .option('--color [boolean]', 'Turn on/off colors', 'true') + .description('Configures the Cortex Template System for generating skill templates') + .action((options) => (new WorkspaceConfigureCommand(program)).execute(options)); + + program + .command('generate [name] [destination]') + .option('--color [boolean]', 'Turn on/off colors', 'true') + .option('--notree [boolean]', 'Do not display generated file tree', 'false') + .option('--template <templateName>', 'Name of template to use') + .description('Generates a workspace based on a template from the template repository') + .action((name, destination, options) => new WorkspaceGenerateCommand(program).execute(name, destination, options)); + + program + .command('build [folder]') + .option('--profile <profile>', 'The profile to use') + .option('--color [boolean]', 'Turn on/off colors', 'true') + .option('--dockerCli <string>', 'Specify the docker client, the following are supported [docker, nerdctl, podman]. If not specified the cli will scan the PATH for available clients') + .option('--skill <skill name>', 'Build only the specified skill') + .description('Builds all skills in the workspace, or optionally only the specified skill') + .action((folder, options) => (new WorkspaceBuildCommand(program)).execute(folder, options)); + + program + .command('publish [folder]') + .option('--profile <profile>', 'The profile to use') + .option('--project <project name>', 'Publish to the specified project') + .option('--color [boolean]', 'Turn on/off colors', 'true') + .option('--dockerCli <string>', 'Specify the docker client, the following are supported [docker, nerdctl, podman]. If not specified the cli will scan the PATH for available clients') + .option('--skip-push', 'Don\'t push images to docker registry') + .option('--skill <skill name>', 'Publish only the specified skill') + .description('Publishes all skills and resources in the workspace') + .action(async (folder, options) => { + await (new WorkspacePublishCommand(program)).execute(folder, options); + }); + + const registry = program + .command('registry') + .description('Manage image repositories.'); + registry + .command('add [name]') + .option('--profile <profile>', 'The profile to use') + .option('--color [boolean]', 'Turn on/off colors', 'true') + .option('--url <registry url>', 'Registry URL') + .option('--namespace <registry namespace>', 'Registry Namespace') + .description('Adds an image registry to publish to.') + .action((name, options) => new WorkspaceAddRegistryCommand(program).execute(name, options)); + + registry + .command('remove [name]') + .option('--profile <profile>', 'The profile to use') + .option('--color [boolean]', 'Turn on/off colors', 'true') + .description('Removes the specified image registry.') + .action((name, options) => new WorkspaceRemoveRegistryCommand(program).execute(name, options)); + + registry + .command('activate [name]') + .option('--profile <profile>', 'The profile to use') + .option('--color [boolean]', 'Turn on/off colors', 'true') + .description('Activates the specified image registry.') + .action((name, options) => new WorkspaceActivateRegistryCommand(program).execute(name, options)); + registry + .command('list') + .option('--profile <profile>', 'The profile to use') + .option('--color [boolean]', 'Turn on/off colors', 'true') + .description('Lists all image registries') + .action((options) => new WorkspaceListRegistryCommand(program).execute(options)); + return program; +} +if (esMain(import.meta)) { + await create().showHelpAfterError().parseAsync(process.argv); +} +export default create(); diff --git a/bin/cortex.js b/bin/cortex.js index 88e29bc2..c9799424 100755 --- a/bin/cortex.js +++ b/bin/cortex.js @@ -1,45 +1,123 @@ #!/usr/bin/env node +import { Command } from 'commander'; +import debugSetup from 'debug'; +import esMain from 'es-main'; +import { readPackageJSON, printError } from '../src/commands/utils.js'; +import { loadProfileAllowingUnconfiguredCLI } from '../src/config.js'; +import { getAllSubcommands, FeatureController } from '../src/features.js'; -/* - * Copyright 2018 Cognitive Scale, Inc. All Rights Reserved. - * - * 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. - */ +// NOTE: The Commander CLI framework will executes this module multiple initializing +// CLI subcommands. This global variable stores the users profile, to avoid +// redundantly loading the Profile (avoid slow startup). +let profile; -const helper = require('./utils.js'); -const { version } = require('../package.json'); -const program = require('commander'); +// TODO: Investigate slow startup when using custom SSL certs (NODE_EXTRA_CA_CERTS) - might be related to timeouts/retrys or SSL3? -program - .version(version, '-v, --version') - .description('Cortex CLI') - .command('accounts [cmd]', 'Work with Cortex Accounts') - .command('actions [cmd]', 'Work with Cortex Actions') - .command('agents [cmd]', 'Work with Cortex Agents') - .command('configure', 'Configure the Cortex CLI') - .command('connections [cmd]', 'Work with Cortex Connections') - .command('content [cmd]', 'Work with Cortex Managed Content') - .command('environments [cmd]', 'Work with Cortex Environments') - .command('datasets [cmd]', 'Work with Cortex Datasets') - .command('jobs [cmd]', 'Work with Cortex Jobs') - .command('processors [cmd]', 'Work with the Cortex Processor Runtime') - .command('project [cmd]', 'Work with a related collection of Cortex contributions') - .command('skills [cmd]', 'Work with Cortex Skills') - .command('tasks [cmd]', 'Work with Cortex Tasks') - .command('types [cmd]', 'Work with Cortex Types'); +async function resolveAvailableSubcommands(profileName) { + const debug = debugSetup('cortex:cli'); + let supportedCommands; + if (process.env.CORTEX_SKIP_INIT_PROFILE) { + // Provide all subcommands - mostly intended as workaround for Docgen + // testing + debug('Skipping profile initialization, all subcommands will be available (CORTEX_SKIP_INIT_PROFILE=%s}).', process.env.CORTEX_SKIP_INIT_PROFILE); + supportedCommands = getAllSubcommands(); + } else { + // Load profile if it isn't already defined + if (profile === undefined || profile === null) { + // Alert the user that CORTEX_TOKEN is set, this has a higher + // priority than other methods of retrieving the token. The Alert + // happens at startup, rather than downstream when getting the value + // from the env, as to only alert the user once (avoid duplicate + // messages in cases where the variable is loaded multiple times). + if (process.env.CORTEX_TOKEN) { + printError('Using token from "CORTEX_TOKEN" environment variable', {}, false); + } + // Load profile, but support the "fresh install" scenario where the Profile doesn't exist + profile = await loadProfileAllowingUnconfiguredCLI(profileName); + } + if (profile) { + // if the profile was found, then side-load the token & feature + // flags to avoid future calls to the server + process.env.CORTEX_TOKEN_SILENT = profile.token; + process.env.CORTEX_FEATURE_FLAGS = JSON.stringify(profile.featureFlags); + // Filter subcommands - only include those that are supported by the server + const features = new FeatureController(profile); + supportedCommands = features.getSupportedSubCommands(); + } else { + // Failed to load Profile -> likely means CLI is not configured, so + // allow all subcommands. If a subcommand (API call) is going to be + // made, then we expect the CLI to try again & fail when the Profile + // is needed. If the subcommand doesn't need the users Profile, its + // likely just printing help messages. + debug('Unable to load CLI Profile, so making all subcommands available to the user (allowing failures to be handled downstream).'); + supportedCommands = getAllSubcommands(); + } + } + return supportedCommands; +} -process.env.DOC && require('../src/commands/utils').exportDoc(program); - -program.parse(process.argv); -!program.commands.map(cmd => cmd._name).includes(program.args[0]) && helper.helpAndExit(program); +export async function create(profileName) { + const program = new Command(); + program.version(readPackageJSON('../../package.json').version, '-v, --version', 'Outputs the installed version of the Cortex CLI'); + program.name('cortex'); + program + .description('Cortex CLI') + .option('--debug', 'Enables enhanced log output for debugging', false) + .on('option:debug', () => { + process.env.DEBUG = '*'; + }) + .option('--no-timeout', 'Disables all network timeouts for slower connections (not recommended)') + .on('option:no-timeout', () => { + process.env.CORTEX_TIMEOUT_IGNORE = '*'; + }); + const supportedCommands = await resolveAvailableSubcommands(profileName); + const _toObject = (nameAndArgs, description) => ({ nameAndArgs, description }); + const isCommandSupported = ({ nameAndArgs }) => (supportedCommands.includes(nameAndArgs.split(' ')[0].trim())); + const allCommands = [ + _toObject('actions <cmd>', 'Work with Cortex Actions'), + _toObject('agents <cmd>', 'Work with Cortex Agents'), + _toObject('applications <cmd>', 'Work with Cortex Applications'), + _toObject('assessments <cmd>', 'Work with Cortex Impact Assessments'), + _toObject('campaigns <cmd>', 'Work with Cortex Campaigns'), + _toObject('configure', 'Configure the Cortex CLI'), + _toObject('connections <cmd>', 'Work with Cortex Connections'), + _toObject('content <cmd>', 'Work with Cortex Managed Content'), + _toObject('deploy <cmd>', 'Work with Cortex Artifacts export for deployment'), + _toObject('docker <cmd>', 'Work with Docker'), + _toObject('experiments <cmd>', 'Work with Cortex Experiments'), + _toObject('missions <cmd>', 'Work with Cortex Missions'), + _toObject('models <cmd>', 'Work with Cortex Models'), + _toObject('pipelines <cmd>', 'Work with Cortex Pipelines'), + _toObject('projects <cmd>', 'Work with Cortex Projects'), + _toObject('roles <cmd>', 'Work with a Cortex Roles'), + _toObject('secrets <cmd>', 'Work with Cortex Secrets'), + _toObject('sessions <cmd>', 'Work with Cortex Sessions'), + _toObject('skills <cmd>', 'Work with Cortex Skills'), + _toObject('stereotypes <cmd>', 'Work with Skill Stereotypes'), + _toObject('tasks <cmd>', 'Work with Cortex Tasks'), + _toObject('types <cmd>', 'Work with Cortex Types'), + _toObject('users <cmd>', 'Work with a Cortex Users'), + _toObject('workspaces <cmd>', 'Scaffold Cortex Components'), + ]; + allCommands + .filter(isCommandSupported) + .forEach(({ nameAndArgs, description }) => { + program.command(nameAndArgs, description); + }); + return program; +} +if (esMain(import.meta)) { + // module was not imported but called directly + let _profile; + if (process.argv.includes('--profile')) { + // Pre-process arguments to extract user specified '--profile'. This + // allows the CLI to load the profile config, without requiring arg + // processing (chicken & egg problem). Additionally, the '--profile' flag + // means different things depending on the subcommand being run. + const idx = process.argv.indexOf('--profile') + 1; + _profile = process.argv[idx] ?? _profile; // possibly undefined + } + (await create(_profile)).showHelpAfterError().parseAsync(process.argv); +} +export default await create(); diff --git a/bin/utils.js b/bin/utils.js deleted file mode 100644 index b4f3ef36..00000000 --- a/bin/utils.js +++ /dev/null @@ -1,4 +0,0 @@ -exports.helpAndExit = function (theProgram) { - theProgram.outputHelp((txt) => {return txt}); - process.exit(1); -}; diff --git a/build.sh b/build.sh index 75331f17..9846d3c9 100755 --- a/build.sh +++ b/build.sh @@ -1,6 +1,10 @@ -#!/bin/bash -eu +#!/bin/bash -eux # Copied from: https://bitbucket.org/cognitivescale/cortex-console/src +if [ "$PWD" = "/work" ]; then + # Needed when using newer git + git config --global --add safe.directory /work +fi IMAGE_NAME=$(git remote -v | grep "(fetch)" | sed -E "s/.*git@.*:.*\/(.*)\.git.*/\1/") BRANCH=$(git symbolic-ref --short -q HEAD) @@ -26,23 +30,43 @@ function local_docker(){ # This runs inside a linux docker container function docker_build(){ - npm config set loglevel warn - npm cache clear --force - npm install -dd --verbose + export CI="script" + # libsecrets is needed for keytar node-gyp build + apt-get update + apt-get install -y apt-utils libsecret-1-dev + # Upgrade to NPM 9 to avoid permissions error when running npm as root. + # Must either be `<7,>8`, but the latest version of npm is NOT + # supported with Node 16 (node runtime for CLI). See: https://stackoverflow.com/questions/70952903/npm-error-eacces-permission-denied-scandir + npm install -g npm@9.9.2 + npm ci --unsafe-perm --userconfig=/root/.npmrc --ignore-scripts + npm rebuild + npm test + npm run scan echo ${VERSION} > version.txt - + ./generate_docs.sh BRANCH=$(git symbolic-ref --short -q HEAD) - if [[ ${BRANCH} = "master" ]]; then - rm -rf node_modules - npm install --silent --only=production -# npm publish -# elif [[ ${BRANCH} = "develop" ]]; then -# npm publish --tag develop + if [[ ${BRANCH} = "main" ]]; then +# TODO this should maybe be done as part of the gocd pipeline?.. or maybe get in the habit of pushing alpha versions to npm just like we do for cortex-python?.. + npm publish --registry=https://registry.npmjs.org/ + elif [[ ${BRANCH} = "develop" ]]; then + echo "skipping plublish to dev npm registry" +# npm publish --tag "${BRANCH}" --registry=https://cognitivescale.jfrog.io/artifactory/api/npm/npm-local/ fi + npm pack . + mv cortex-cli-*.tgz cortex-cli.tgz + # NOTE: This script running in a container on the gocd-agent. The coverage + # report belongs to the (root) user in the container. The pipeline uploads + # artifacts after the container exists, at which point the script is + # running as a different user. For some reason, one of the files in the + # coverage report (coverage/tmp/coverage-<SHA>.json), has 700 permissions, + # so the pipeline would fail to upload the artifact. Give 755 permissions to + # allow nested folders to be executable and to allow for artifact upload. + chmod -R 755 coverage/ } ## MAIN cd "$(dirname "$0")" + echo "##### BUILDING BRANCH[${BRANCH}],VERSION[${VERSION}] of IMAGE[${IMAGE_NAME}] ######" case ${1-local} in CI) diff --git a/c12e-ci.yml b/c12e-ci.yml index 2d5cdc73..f319e78d 100644 --- a/c12e-ci.yml +++ b/c12e-ci.yml @@ -1,3 +1,3 @@ builder: - image: c12e/nodejs:dev-8.11.0 + image: node:18.19-bookworm command: "./build.sh CI" diff --git a/client/index.d.ts b/client/index.d.ts new file mode 100644 index 00000000..f165c3f7 --- /dev/null +++ b/client/index.d.ts @@ -0,0 +1,713 @@ +declare module "cortex-cli/client" { + export interface InstanceEndpoint { + url: string; + description: string; + } + + export interface ActionStatus { + name: string; + state: string; + } + + export interface HealthStatus { + ok: boolean; + message: string; + } + + export interface InstanceHealth { + health: Record<string, HealthStatus>; + healthy: boolean; + name: string; + version: string; + } + + export interface InstanceStatus { + deployType: string; + endpoints: Record<string, InstanceEndpoint>; + healths: InstanceHealth[]; + version: string; + } + + export interface InputOutputParams { + name: string; + type: string; + description?: string; + required?: boolean; + } + + export interface RoutingRule { + match: string; + action: string; + runtime?: string; + output: string; + } + + export interface AllRouting { + action: string; + runtime?: string; + output: string; + } + + export interface PropertyRouting { + property: string; + default?: AllRouting; + rules: RoutingRule[]; + } + + export interface FieldRouting { + field: string; + default?: AllRouting; + rules: RoutingRule[]; + } + + export interface Input { + name: string; + title: string; + parameters?: InputOutputParams[] | Reference; + routing: { + field?: string; + property?: string; + all: AllRouting; + default?: RoutingRule; + rules?: RoutingRule[]; + }; + } + + export interface Output { + name: string; + title: string; + parameters?: InputOutputParams[] | Reference; + } + + export interface PropertyDefinition { + name: string; + title: string; + description: string; + required?: boolean; + type: string; + defaultValue?: any; + validValues?: string[]; + qualifiedBy?: string; + secure?: boolean; + } + + export interface PropertyValue { + name: string; + value: any; + } + + export interface Tag { + label: string; + value: string; + } + + export interface Parameter { + name: string; + type: string; + title?: string; + description?: string; + format?: string; + required?: boolean; + } + + export interface Reference { + $ref: string; + } + + export interface URLReference { + $url: string; + } + + export interface DatasetReference { + name: string; + title?: string; + description?: string; + parameters?: Parameter[] | Reference[]; + requiresWrite?: boolean; + } + + export interface ConnectionReference { + name: string; + type: string; + query?: PropertyValue; + environment?: string; + } + + export interface ConnectionConfig { + default?: ConnectionReference; + environments?: ConnectionReference; + } + + export interface Payload { + records?: any[]; + columns?: string; + values?: any; + $ref?: Reference; + } + + export interface Message { + payload: Payload; + parameters?: Parameter[]; + } + + export interface Dataset { + camel: string; + name: string; + title: string; + description?: string; + tags?: Tag[]; + parameters: Parameter[]; + connections: ConnectionConfig[]; + } + + export interface Type { + camel: string; + name: string; + title: string; + description?: string; + parameters: Parameter[]; + } + + export interface Action { + name: string; + description?: string; + image: string; + command: string[]; + port?: string; + type: string; + scaleCount: number; + environmentVariables?: string | null; + } + + export interface Skill { + camel: string; + name: string; + title: string; + description?: string; + deployStatus?: string; + actionStatuses?: ActionStatus[]; + createdAt: string; + createdBy: string; + tags?: Tag[]; + inputs: Input[]; + outputs: Output[]; + properties?: PropertyDefinition[]; + actions?: Action[]; + datasets?: Dataset[]; + updatedAt?: string; + } + + export interface Experiment { + name: string; + title?: string; + description?: string; + tags?: Tag[]; + properties?: PropertyDefinition[]; + } + + export interface Model { + name: string; + title?: string; + description?: string; + tags?: Tag[]; + properties?: PropertyDefinition[]; + type?: string; + mode?: string; + status?: string; + source?: string; + } + + export interface ExperimentRun { + name: string; + experimentName: string; + title?: string; + description?: string; + tags?: Tag[]; + params?: any; + metrics?: any; + meta?: any; + artifacts?: any; + runId: string; + } + + export interface ManagedContent { + name: string; + title?: string; + description?: string; + connectionType?: string; + allowWrite?: boolean; + params?: any[]; + } + + export interface Activation { + success: boolean; + activationId: string; + instanceId: string; + channelId: string; + sessionId: string; + tenantId: string; + username: string; + status: string; + start: number; + end: number; + type: string; + request?: any; + requestType?: string; + response?: any; + responseType?: string; + inputServiceName?: string; + outputServiceName?: string; + datasetName: string; + trigger: any; + } + + export interface AgentIO { + name: string; + title?: string; + description?: string; + signalType: string; + } + + export interface Agent { + name: string; + title?: string; + description?: string; + properties?: PropertyDefinition[]; + inputs?: AgentIO[]; + outputs?: AgentIO[]; + processors?: any[]; + } + + export interface Session { + description?: string; + sessionId: string; + ttl: number; + } + + export interface Log { + name: string; + log: string; + } + + export interface Artifact { + artifactId: string; + experimentName: string; + projectId: string; + runId: string; + } + + export interface User { + externalGroups: string[]; + grants: any[]; + projects: string[]; + roles: string[]; + user: string; + } + + export interface Project { + name: string; + title: string; + description?: string; + } + + export interface Connection { + name: string; + title: string; + description?: string; + connectionType: string; + allowWrite: boolean; + allowRead: boolean; + params: Parameter[]; + } + + export interface Secret { + key: string; + value: string; + } + + export class Catalog { + cortexUrl: any; + endpoints: { + skills: (projectId: string) => string; + agents: (projectId: string) => string; + types: (projectId: string) => string; + campaigns: (projectId: string) => string; + }; + constructor(cortexUrl: any); + saveSkill( + projectId: string, + token: string, + skillObj: Skill + ): Promise<string>; + listSkills( + projectId: string, + token: string, + query?: any, + filter?: any, + limit?: any, + skip?: any, + sort?: any + ): Promise<Skill[]>; + describeSkill( + projectId: string, + token: string, + skillName: string, + verbose?: boolean, + output?: string + ): Promise<Skill>; + deleteSkill(projectId: string, token: string, skillName: string): any; + deploySkill( + projectId: string, + token: string, + skillName: string, + verbose?: boolean + ): any; + unDeploySkill( + projectId: string, + token: string, + skillName: string, + verbose?: boolean + ): any; + skillLogs( + projectId: string, + token: string, + skillName: string, + actionName: any, + verbose?: boolean + ): any; + + saveType(projectId: string, token: string, types: any): any; + describeType(projectId: string, token: string, typeName: any): any; + listTypes( + projectId: string, + token: string, + filter?: any, + limit?: any, + skip?: any, + sort?: any + ): any; + deleteType(projectId: string, token: string, typeName: any): any; + + listAgents( + projectId: string, + token: string, + filter?: any, + limit?: any, + skip?: any, + sort?: any + ): any; + // listServices(projectId: string, token: string, agentName: string, filter: any, limit: any, skip: any, sort: any): any; + saveAgent(projectId: string, token: string, agentObj: Agent): any; + describeAgent( + projectId: string, + token: string, + agentName: string, + verbose?: any + ): Promise<Agent>; + // describeAgentVersions(projectId: string, token: string, agentName: string): any; + deployAgent( + projectId: string, + token: string, + agentName: string, + verbose?: boolean + ): any; + deleteAgent(projectId: string, token: string, agentName: string): any; + unDeployAgent( + projectId: string, + token: string, + agentName: string, + verbose?: boolean + ): any; + + // exportCampaign(projectId: string, token: string, campaignName: any, deployable: any, path: any): Promise<any>; + // importCampaign(projectId: string, token: string, filepath: any, deploy: any, overwrite: any): void; + // undeployCampaign(projectId: string, token: string, campaign: any): any; + // deployCampaign(projectId: string, token: string, campaign: any): any; + // undeployMission(projectId: string, token: string, campaign: any, mission: any): any; + // listMissions(projectId: string, token: string, campaign: any, filter: any, limit: any, skip: any, sort: any): any; + // deployMission(projectId: string, token: string, campaign: any, mission: any): any; + // getMission(projectId: string, token: string, campaign: any, mission: any): any; + } + + export class Agents { + constructor(cortexUrl: any); + cortexUrl: any; + endpointV4: (projectId: any) => string; + invokeAgentService( + projectId: any, + token: any, + agentName: string, + serviceName: any, + params: any + ): any; + invokeSkill( + projectId: any, + token: any, + skillName: any, + inputName: any, + params: any + ): any; + getActivation( + projectId: any, + token: any, + activationId: any, + verbose?: any, + report?: any + ): any; + listActivations(projectId: any, token: any, params?: any): any; + listAgentSnapshots( + projectId: any, + token: any, + agentName: string, + filter: any, + limit: any, + skip: any, + sort: any + ): any; + describeAgentSnapshot( + projectId: any, + token: any, + snapshotId: any, + output: any + ): any; + createAgentSnapshot(projectId: any, token: any, snapshot: any): any; + } + + export class Connections { + constructor(cortexUrl: any); + endpoint: (projectId: any) => string; + cortexUrl: any; + listConnections( + projectId: any, + token: any, + filter?: any, + limit?: any, + skip?: any, + sort?: any + ): Promise<Connection[]>; + saveConnection( + projectId: any, + token: any, + connObj: Connection + ): Promise<any>; + describeConnection(projectId: any, token: any, connectionName: any): any; + deleteConnection(projectId: any, token: any, connectionName: any): any; + listConnectionsTypes( + token: any, + filter: any, + limit: any, + skip: any, + sort: any + ): any; + } + + export class Content { + constructor(cortexUrl: any); + cortexUrl: any; + endpoint: (projectId: any) => string; + _sanitizeKey(key: any): any; + listContent(projectId: any, token: any): any; + uploadContentStreaming( + projectId: any, + token: any, + key: any, + content: any, + showProgress?: boolean, + contentType?: string + ): Promise<any>; + deleteContent(projectId: any, token: any, key: any): any; + downloadContent( + projectId: any, + token: any, + key: any, + showProgress?: boolean, + toFile?: any + ): any; + } + + export class Experiments { + constructor(cortexUrl: any); + cortexUrl: any; + endpoint: (projectId: string) => string; + listExperiments( + projectId: string, + modelId: any, + token: string, + filter?: any, + limit?: any, + skip?: any, + sort?: any + ): Promise<Experiment[]>; + describeExperiment( + projectId: string, + token: string, + name: any + ): Promise<Experiment>; + deleteExperiment(projectId: string, token: string, name: any): any; + listRuns( + projectId: string, + token: string, + experimentName: string, + filter?: any, + limit?: any, + sort?: any, + skip?: any + ): Promise<ExperimentRun[]>; + describeRun( + projectId: string, + token: string, + experimentName: string, + runId: string + ): any; + deleteRun( + projectId: string, + token: string, + experimentName: string, + runId: string + ): any; + _artifactKey(experimentName: string, runId: string, artifact: any): string; + downloadArtifact( + projectId: string, + token: string, + experimentName: string, + runId: string, + artifactName: any, + showProgress?: boolean + ): Promise<any>; + saveExperiment( + projectId: string, + token: string, + experimentObj: Experiment + ): any; + createRun(projectId: string, token: string, runObj: ExperimentRun): any; + updateRun(projectId: string, token: string, runObj: ExperimentRun): any; + uploadArtifact( + projectId: string, + token: string, + experimentName: string, + runId: string, + content: string, + artifact: string, + contentType?: string + ): Promise<any>; + } + + export class Models { + constructor(cortexUrl: any); + cortexUrl: any; + endpointV4: (projectId: any) => string; + saveModel(projectId: any, token: any, modelObj: any): Promise<any>; + updateModelStatus( + projectId: any, + token: any, + modelName: any, + status: any + ): Promise<any>; + deleteModel(projectId: any, token: any, modelName: any): Promise<any>; + describeModel( + projectId: any, + token: any, + modelName: any, + verbose?: any + ): any; + listModels( + projectId: any, + skip: any, + limit: any, + filter: any, + sort: any, + tags: any, + token: any + ): any; + listModelRuns( + projectId: any, + modelName: any, + token: any, + filter: any, + limit: any, + skip: any, + sort: any + ): any; + } + + export class Secrets { + constructor(cortexUrl: any); + cortexUrl: any; + endpoint: (projectId: any) => any; + listSecrets(projectId: any, token: any): any; + readSecret(projectId: any, token: any, keyName: any): any; + deleteSecret(projectId: any, token: any, keyName: any): any; + writeSecret(projectId: any, token: any, keyName: any, value: any): any; + } + + export class Sessions { + constructor(cortexUrl: any); + cortexUrl: any; + endpointV4: (projectId: any) => string; + saveSession(projectId: any, token: any, sessionObj: any): any; + deleteSession(projectId: any, token: any, sessionName: any): any; + describeSession( + projectId: any, + token: any, + sessionName: any, + verbose?: any + ): any; + listSessions(projectId: any, token: any, limit?: any): any; + } + + export class AmpServer { + constructor(cortexUrl: any); + cortexUrl: any; + endpoint: string; + _client(token: any): any; + getProject(token: any, projectId: any): Promise<Project>; + listProjects( + token: any, + filter?: any, + limit?: any, + skip?: any, + sort?: any + ): Promise<Project[]>; + createProject(token: any, projDef: Project): Promise<any>; + getCampaign(projectId: any, token: any, campaignId: any): Promise<any>; + listCampaigns( + projectId: any, + token: any, + filter?: any, + limit?: any, + skip?: any, + sort?: any + ): Promise<any>; + listModels( + projectId: any, + offset: any, + limit: any, + token: any + ): Promise<any>; + descModels(projectId: any, name: any, token: any): Promise<any>; + } + + export class Info { + constructor(cortexUrl: any); + endpoint: () => string; + cortexUrl: any; + getInfo(): any; + } + + export class Users { + constructor(cortexUrl: any, user?: any, flags?: any[]); + cortexUrl: any; + endpoint: string; + flags: string; + usersEndpoint: string; + usersProjectsEndpoint: string; + createServiceUser(token: string, user: User): any; + deleteServiceUser(token: string, user: User): any; + listServiceUsers(token: string): any; + describeUser(token: string): any; + createGrantForUser(token: string, body: any): any; + removeGrantFromUser(token: string, body: any): any; + addUsersToProject(token: string, project: any, users: any): any; + removeUsersFromProject(token: string, project: any, users: any): any; + } +} diff --git a/client/index.js b/client/index.js new file mode 100644 index 00000000..6d5c9702 --- /dev/null +++ b/client/index.js @@ -0,0 +1,23 @@ +import info from '../src/client/info.js'; +import users from '../src/client/users.js'; +import catalog from '../src/client/catalog.js'; +import agents from '../src/client/agents.js'; +import connections from '../src/client/connections.js'; +import content from '../src/client/content.js'; +import experiments from '../src/client/experiments.js'; +import models from '../src/client/models.js'; +import secrets from '../src/client/secrets.js'; +import sessions from '../src/client/sessions.js'; +import apiServerClient from '../src/client/apiServerClient.js'; + +export { info as Info }; +export { users as Users }; +export { catalog as Catalog }; +export { agents as Agents }; +export { connections as Connections }; +export { content as Content }; +export { experiments as Experiments }; +export { models as Models }; +export { secrets as Secrets }; +export { sessions as Sessions }; +export { apiServerClient as AmpServer }; diff --git a/cortex-cli.gocd.yaml b/cortex-cli.gocd.yaml new file mode 100644 index 00000000..5933f9b5 --- /dev/null +++ b/cortex-cli.gocd.yaml @@ -0,0 +1,127 @@ +format_version: 10 +pipelines: + cortex-cli: + group: fabric6 + materials: + cortex-cli: + git: git@github.com:CognitiveScale/cortex-cli.git + branch: develop + stages: + - build: + clean_workspace: yes + jobs: + Build: + elastic_profile_id: gocd-agent-dind-2023 + artifacts: + - build: + source: cruise-output/buildReport*.json + - build: + source: devBranchRevision.json + - build: + source: cortex-cli-docs.md + - build: + source: cortex-cli.tgz + tasks: + - script: | + c12e-common.sh dev + - publish: + clean_workspace: true + approval: + type: manual + jobs: + Promote: + elastic_profile_id: gocd-agent-dind-2023 + environment_variables: + PROMOTE_BRANCH: "main" + tasks: + - fetch: + stage: build + job: Build + source: devBranchRevision.json + is_file: yes + - script: | + set -eux + + COMMIT_SHA="$(cat devBranchRevision.json)" + git fetch + git checkout -b ${PROMOTE_BRANCH} origin/${PROMOTE_BRANCH} + git merge ${COMMIT_SHA} \ + --ff-only + git push origin ${PROMOTE_BRANCH} + cortex-cli-master: + group: fabric6-rc + environment_variables: + DOCKER_SECRET_FLAG: "id=npmrc,src=~/.npmrc" + materials: + cortex-cli: + git: git@github.com:CognitiveScale/cortex-cli.git + branch: main + stages: + - build: + clean_workspace: yes + jobs: + Build: + elastic_profile_id: gocd-agent-dind-2023 + artifacts: + - build: + source: cortex-cli-docs.md + - build: + source: cortex-cli.tgz + - test: + source: coverage + tabs: + coverage: testoutput/coverage/index.html + tasks: + - script: | + c12e-common.sh master + - publishDocs: + clean_workspace: yes + jobs: + commitDocs: + elastic_profile_id: gocd-agent-dind-2023 + tasks: + - fetch: + stage: build + job: Build + source: cortex-cli-docs.md + is_file: yes + - script: | + git clone --branch main --single-branch git@github.com:CognitiveScale/cortex-fabric.git + mv cortex-cli-docs.md cortex-fabric/docs/docs/reference-guides/cortex-cli.md + cd cortex-fabric + git commit -a -m "Publishing New CLI Docs" + git push origin main + cortex-cli-pr: + group: pull-requests + label_template: ${COUNT} + materials: + cortex-cli: + plugin_configuration: + id: github.pr + version: 1 + options: + url: git@github.com:CognitiveScale/cortex-cli.git + defaultBranch: develop + stages: + - build: + fetch_materials: true + clean_workspace: true + approval: + type: success + allow_only_on_success: false + jobs: + Build: + elastic_profile_id: gocd-agent-dind-2023 + artifacts: + - build: + source: cruise-output/buildReport*.json + - build: + source: devBranchRevision.json + - build: + source: cortex-cli-docs.md + - build: + source: cortex-cli.tgz + timeout: 0 + tasks: + - script: | + c12e-common.sh dev diff --git a/entrypoint.sh b/entrypoint.sh deleted file mode 100755 index 82efa85c..00000000 --- a/entrypoint.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/bin/bash - -function error_exit { - echo "$1" >&2 - exit "${2:-1}" ## Return a code specified by $2 or 1 by default. -} - -case $1 in - bootstrap) - VERSION=$(cat /from_ci_cd_build/version.txt) - >&2 echo "Boostrapping Version[${VERSION}] of Cortex Executable." - cat /from_ci_cd_build/scripts/cortex | sed "s/<<VERSION>>/${VERSION}/g" - ;; - *) - cortex $@ - ;; -esac \ No newline at end of file diff --git a/env.json b/env.json deleted file mode 100644 index 14eb65c4..00000000 --- a/env.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "name": "env1", - "title": "Environment 1", - "description": "This is my description", - "label": "env1", - "consoleUrl": "http://console.localhost:8000", - "apiUrl": "http://api.localhost:8000", - "whiteList": ["127.0.0.1"], - "blackList": ["10.255.255.255"], - "provisionToken": "token-token-token", - "type": "foo", - "properties": [{"name":"p1", "value":"v1"}] -} \ No newline at end of file diff --git a/generate_docs.js b/generate_docs.js new file mode 100644 index 00000000..5e88daa8 --- /dev/null +++ b/generate_docs.js @@ -0,0 +1,61 @@ +/** + * Opted to use pure JS versus adding handlebars as a dep, as handlebars always has vulns.. + */ +import process from 'node:process'; +import _ from 'lodash'; +import esMain from 'es-main'; + +function docObject(program) { + return program.commands.map((c) => ({ + name: c._name, + description: c._description, + usage: c.usage(), + options: c.options.map((o) => ({ + flags: o.flags, + defaultValue: o.defaultValue, + description: o.description, + })), + })); +} + +// replace /n/t with HTML equivs +const cleanString = (s) => s + .replace(/\n/g, '') + .replace(/\t/g, '') + .replace(/</g, '`<') + .replace(/>/g, '>`'); + +// print sub-command flags as string +const optionRow = (opts) => _.join( + _.sortBy(opts, ['flags']).map( + (opt) => `\`${opt.flags}\`${_.isEmpty(opt.defaultValue) ? '' : ` (*default:* \`${opt.defaultValue}\`)`} - ${cleanString(opt.description)}`, + ), + '<br />', +); + +const subcmdTable = (subcmd) => `| Command | Description | Options | +| ------- | ----------- | ------- |\n${ +_.join( + _.sortBy(subcmd, ['name']).map((cmd) => `| \`${cmd.name} ${cmd.usage}\` | ${cmd.description} | ${optionRow(cmd.options)} |`), + '\n', +)}`; + +const cmdHeading = (cmd) => ` +## ${_.capitalize(cmd.name())} +\`\`\` +${cmd.name()} ${cmd.usage()} +\`\`\` +${cmd.description()} +`; + +export default function markdownForCmd(program) { + const docObj = docObject(program); + return `${cmdHeading(program)}\n${subcmdTable(docObj)}`; +} + +if (esMain(import.meta)) { + const { default: program } = await import(process.argv[2]); + console.log(markdownForCmd(program)); +} + +// eslint-disable-next-line import/no-dynamic-require diff --git a/generate_docs.sh b/generate_docs.sh index 42ac3629..197b6f1a 100755 --- a/generate_docs.sh +++ b/generate_docs.sh @@ -1,6 +1,48 @@ -#!/bin/bash -export DOC=true -for F in bin/*.js; do - NAME=$(basename $F .js) - node $F > ${NAME}.json +#!/bin/bash -eu +OUTFILE=cortex-cli-docs.md + +header() { +cat << 'EOF' +--- +title: "Cortex CLI" +linkTitle: "Cortex CLI" +description: > + Provides a list of all commands available in the Cortex Command Line Interface. +--- +This reference guide provides a list of all commands available in the Cortex Command Line +Interface (Cortex CLI). See [CLI](getting-started/use-cli.md) for +help getting started using the CLI. +## Notation +The following table describes the common notation used in this reference guide. + +| Notation | Description | Example | +| -------- | ----------- | ------- | +| `\< \>` | Required | `<value>` denotes a required value. | +| `[ ]` | Optional | `[options]` denotes an optional value. | +## Syntax +```cli +cortex [command] [options] +``` +## Global options +| Option | Description | +| --------- | --------- | +| `-v`, `--version` | Outputs the version number. | +| `-h`, `--help` | Outputs usage information. | +| `--profile [profile]` | Run the CLI using the specified profile instead of the default config. | +## General notes +* The output results for most `list` commands display as a table. Add the +`--json` option to output results as JSON. When `--json` is used, you can use the +`--query` option to [filter the results](/getting-started/use-cli.md). +* The output results for `describe` commands display in the format of the document +it is describing. This can include YAML, JSON, or other formats. +EOF +} +# generating markdown +#node generate_docs.js ${OUTFILE} +#echo +#echo "Done -> ${OUTFILE}" +echo "$(header)" > ${OUTFILE} +for CMD in $(ls -1 ./bin/cortex-*.js | sort); do + echo $CMD + node generate_docs.js $CMD >> $OUTFILE done diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..bded87c7 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,7709 @@ +{ + "name": "cortex-cli", + "version": "2.4.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "cortex-cli", + "version": "2.4.1", + "license": "Apache-2.0", + "dependencies": { + "@supercharge/promise-pool": "3.1.0", + "boolean": "3.2.0", + "chalk": "5.3.0", + "cli-progress": "3.12.0", + "cli-table3": "0.6.3", + "commander": "11.1.0", + "dayjs": "1.11.10", + "debug": "4.3.4", + "es-main": "1.3.0", + "form-data": "4.0.0", + "get-stream": "8.0.1", + "gh-got": "10.0.0", + "glob": "10.3.10", + "got": "13.0.0", + "graphql": "16.8.1", + "graphql-request": "6.1.0", + "inquirer": "9.2.16", + "is-installed-globally": "1.0.0", + "istextorbinary": "6.0.0", + "jmespath": "0.16.0", + "joi": "17.11.0", + "jose": "5.2.3", + "js-yaml": "4.1.0", + "keytar": "7.9.0", + "listr2": "6.4.2", + "lodash": "4.17.21", + "minimatch": "9.0.3", + "npm-registry-fetch": "16.1.0", + "open": "10.1.0", + "prompts": "2.4.2", + "qs": "6.12.0", + "semver": "7.5.4", + "treeify": "1.1.0", + "url-join": "5.0.0", + "url-parse": "1.5.10", + "uuid": "9.0.1", + "which": "4.0.0", + "yauzl": "3.1.2" + }, + "bin": { + "cortex": "bin/cortex.js" + }, + "devDependencies": { + "audit-ci": "6.6.1", + "c8": "8.0.1", + "caxa": "3.0.1", + "chai": "4.3.10", + "chai-as-promised": "7.1.1", + "cross-env": "7.0.3", + "esbuild": "0.19.11", + "eslint": "8.56.0", + "eslint-config-airbnb-base": "15.0.0", + "eslint-plugin-import": "2.29.1", + "husky": "8.0.3", + "is-ci": "3.0.1", + "mocha": "10.2.0", + "mocked-env": "1.3.5", + "nock": "13.4.0", + "pkg": "5.8.1", + "sinon": "17.0.1", + "sonarqube-scanner": "3.3.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.22.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.22.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "dev": true, + "license": "MIT" + }, + "node_modules/@colors/colors": { + "version": "1.5.0", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.11.tgz", + "integrity": "sha512-FnzU0LyE3ySQk7UntJO4+qIiQgI7KoODnZg5xzXIrFJlKd2P2gwHsHY4927xj9y5PJmJSzULiUCWmv7iWnNa7g==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.11.tgz", + "integrity": "sha512-5OVapq0ClabvKvQ58Bws8+wkLCV+Rxg7tUVbo9xu034Nm536QTII4YzhaFriQ7rMrorfnFKUsArD2lqKbFY4vw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.11.tgz", + "integrity": "sha512-aiu7K/5JnLj//KOnOfEZ0D90obUkRzDMyqd/wNAUQ34m4YUPVhRZpnqKV9uqDGxT7cToSDnIHsGooyIczu9T+Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.11.tgz", + "integrity": "sha512-eccxjlfGw43WYoY9QgB82SgGgDbibcqyDTlk3l3C0jOVHKxrjdc9CTwDUQd0vkvYg5um0OH+GpxYvp39r+IPOg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.11.tgz", + "integrity": "sha512-ETp87DRWuSt9KdDVkqSoKoLFHYTrkyz2+65fj9nfXsaV3bMhTCjtQfw3y+um88vGRKRiF7erPrh/ZuIdLUIVxQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.11.tgz", + "integrity": "sha512-fkFUiS6IUK9WYUO/+22omwetaSNl5/A8giXvQlcinLIjVkxwTLSktbF5f/kJMftM2MJp9+fXqZ5ezS7+SALp4g==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.11.tgz", + "integrity": "sha512-lhoSp5K6bxKRNdXUtHoNc5HhbXVCS8V0iZmDvyWvYq9S5WSfTIHU2UGjcGt7UeS6iEYp9eeymIl5mJBn0yiuxA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.11.tgz", + "integrity": "sha512-JkUqn44AffGXitVI6/AbQdoYAq0TEullFdqcMY/PCUZ36xJ9ZJRtQabzMA+Vi7r78+25ZIBosLTOKnUXBSi1Kw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.11.tgz", + "integrity": "sha512-3CRkr9+vCV2XJbjwgzjPtO8T0SZUmRZla+UL1jw+XqHZPkPgZiyWvbDvl9rqAN8Zl7qJF0O/9ycMtjU67HN9/Q==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.11.tgz", + "integrity": "sha512-LneLg3ypEeveBSMuoa0kwMpCGmpu8XQUh+mL8XXwoYZ6Be2qBnVtcDI5azSvh7vioMDhoJFZzp9GWp9IWpYoUg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.11.tgz", + "integrity": "sha512-caHy++CsD8Bgq2V5CodbJjFPEiDPq8JJmBdeyZ8GWVQMjRD0sU548nNdwPNvKjVpamYYVL40AORekgfIubwHoA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.11.tgz", + "integrity": "sha512-ppZSSLVpPrwHccvC6nQVZaSHlFsvCQyjnvirnVjbKSHuE5N24Yl8F3UwYUUR1UEPaFObGD2tSvVKbvR+uT1Nrg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.11.tgz", + "integrity": "sha512-B5x9j0OgjG+v1dF2DkH34lr+7Gmv0kzX6/V0afF41FkPMMqaQ77pH7CrhWeR22aEeHKaeZVtZ6yFwlxOKPVFyg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.11.tgz", + "integrity": "sha512-MHrZYLeCG8vXblMetWyttkdVRjQlQUb/oMgBNurVEnhj4YWOr4G5lmBfZjHYQHHN0g6yDmCAQRR8MUHldvvRDA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.11.tgz", + "integrity": "sha512-f3DY++t94uVg141dozDu4CCUkYW+09rWtaWfnb3bqe4w5NqmZd6nPVBm+qbz7WaHZCoqXqHz5p6CM6qv3qnSSQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.11.tgz", + "integrity": "sha512-A5xdUoyWJHMMlcSMcPGVLzYzpcY8QP1RtYzX5/bS4dvjBGVxdhuiYyFwp7z74ocV7WDc0n1harxmpq2ePOjI0Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.11.tgz", + "integrity": "sha512-grbyMlVCvJSfxFQUndw5mCtWs5LO1gUlwP4CDi4iJBbVpZcqLVT29FxgGuBJGSzyOxotFG4LoO5X+M1350zmPA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.11.tgz", + "integrity": "sha512-13jvrQZJc3P230OhU8xgwUnDeuC/9egsjTkXN49b3GcS5BKvJqZn86aGM8W9pd14Kd+u7HuFBMVtrNGhh6fHEQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.11.tgz", + "integrity": "sha512-ysyOGZuTp6SNKPE11INDUeFVVQFrhcNDVUgSQVDzqsqX38DjhPEPATpid04LCoUr2WXhQTEZ8ct/EgJCUDpyNw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.11.tgz", + "integrity": "sha512-Hf+Sad9nVwvtxy4DXCZQqLpgmRTQqyFyhT3bZ4F2XlJCjxGmRFF0Shwn9rzhOYRB61w9VMXUkxlBy56dk9JJiQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.11.tgz", + "integrity": "sha512-0P58Sbi0LctOMOQbpEOvOL44Ne0sqbS0XWHMvvrg6NE5jQ1xguCSSw9jQeUk2lfrXYsKDdOe6K+oZiwKPilYPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.11.tgz", + "integrity": "sha512-6YOrWS+sDJDmshdBIQU+Uoyh7pQKrdykdefC1avn76ss5c+RN6gut3LZA4E2cH5xUEp5/cA0+YxRaVtRAb0xBg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.11.tgz", + "integrity": "sha512-vfkhltrjCAb603XaFhqhAF4LGDi2M4OrCRrFusyQ+iTLQ/o60QQXxc9cZC/FFpihBI9N1Grn6SMKVJ4KP7Fuiw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.9.1", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.56.0.tgz", + "integrity": "sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@graphql-typed-document-node/core": { + "version": "3.2.0", + "license": "MIT", + "peerDependencies": { + "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@hapi/hoek": { + "version": "9.3.0", + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/topo": { + "version": "5.1.0", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.13", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.1", + "debug": "^4.1.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { + "version": "3.1.2", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.1", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.0.1", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.1", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.1.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.14", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.17", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "3.1.0", + "@jridgewell/sourcemap-codec": "1.4.14" + } + }, + "node_modules/@ljharb/through": { + "version": "2.3.13", + "resolved": "https://registry.npmjs.org/@ljharb/through/-/through-2.3.13.tgz", + "integrity": "sha512-/gKJun8NNiWGZJkGzI/Ragc53cOdcLNdzjLaIa+GEjguQs0ulsurx8WN0jijdK9yPqDvziX995sMRLyLt1uZMQ==", + "dependencies": { + "call-bind": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@npmcli/agent": { + "version": "2.1.1", + "license": "ISC", + "dependencies": { + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.1", + "lru-cache": "^10.0.1", + "socks-proxy-agent": "^8.0.1" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/agent/node_modules/agent-base": { + "version": "7.1.0", + "license": "MIT", + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@npmcli/agent/node_modules/https-proxy-agent": { + "version": "7.0.2", + "license": "MIT", + "dependencies": { + "agent-base": "^7.0.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@npmcli/agent/node_modules/lru-cache": { + "version": "10.0.1", + "license": "ISC", + "engines": { + "node": "14 || >=16.14" + } + }, + "node_modules/@npmcli/fs": { + "version": "3.1.0", + "license": "ISC", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@sideway/address": { + "version": "4.1.4", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, + "node_modules/@sideway/formula": { + "version": "3.0.1", + "license": "BSD-3-Clause" + }, + "node_modules/@sideway/pinpoint": { + "version": "2.0.0", + "license": "BSD-3-Clause" + }, + "node_modules/@sindresorhus/is": { + "version": "5.3.0", + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@sinonjs/commons": { + "version": "2.0.0", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "11.2.2", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@sinonjs/fake-timers/node_modules/@sinonjs/commons": { + "version": "3.0.0", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/samsam": { + "version": "8.0.0", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^2.0.0", + "lodash.get": "^4.4.2", + "type-detect": "^4.0.8" + } + }, + "node_modules/@sinonjs/text-encoding": { + "version": "0.7.2", + "dev": true, + "license": "(Unlicense OR Apache-2.0)" + }, + "node_modules/@supercharge/promise-pool": { + "version": "3.1.0", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@szmarczak/http-timer": { + "version": "5.0.1", + "license": "MIT", + "dependencies": { + "defer-to-connect": "^2.0.1" + }, + "engines": { + "node": ">=14.16" + } + }, + "node_modules/@types/http-cache-semantics": { + "version": "4.0.1", + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.4", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true + }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "dev": true, + "license": "ISC" + }, + "node_modules/acorn": { + "version": "8.11.2", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/adm-zip": { + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.10.tgz", + "integrity": "sha512-x0HvcHqVJNTPk/Bw8JbLWlWoo6Wwnsug0fnYYro1HBrjxZ3G7/AZk7Ahv8JwDe1uIcz8eBqvu86FuF1POiG7vQ==", + "dev": true, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "license": "MIT", + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-escapes/node_modules/type-fest": { + "version": "0.21.3", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/archiver": { + "version": "5.3.1", + "dev": true, + "license": "MIT", + "dependencies": { + "archiver-utils": "^2.1.0", + "async": "^3.2.3", + "buffer-crc32": "^0.2.1", + "readable-stream": "^3.6.0", + "readdir-glob": "^1.0.0", + "tar-stream": "^2.2.0", + "zip-stream": "^4.1.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/archiver-utils": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "glob": "^7.1.4", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^2.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/archiver-utils/node_modules/glob": { + "version": "7.2.3", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/archiver-utils/node_modules/isarray": { + "version": "1.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/archiver-utils/node_modules/minimatch": { + "version": "3.1.2", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/archiver-utils/node_modules/readable-stream": { + "version": "2.3.8", + "dev": true, + "license": "MIT", + "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/archiver-utils/node_modules/safe-buffer": { + "version": "5.1.2", + "dev": true, + "license": "MIT" + }, + "node_modules/archiver-utils/node_modules/string_decoder": { + "version": "1.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "license": "Python-2.0" + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "is-array-buffer": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-includes": { + "version": "3.1.7", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1", + "is-string": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-union": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/array.prototype.findlastindex": { + "version": "1.2.3", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0", + "get-intrinsic": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.2", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.2", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.0", + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1", + "is-array-buffer": "^3.0.2", + "is-shared-array-buffer": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/assertion-error": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/async": { + "version": "3.2.4", + "dev": true, + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "license": "MIT" + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/audit-ci": { + "version": "6.6.1", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "cross-spawn": "^7.0.3", + "escape-string-regexp": "^4.0.0", + "event-stream": "4.0.1", + "jju": "^1.4.0", + "JSONStream": "^1.3.5", + "readline-transform": "1.0.0", + "semver": "^7.0.0", + "yargs": "^17.0.0" + }, + "bin": { + "audit-ci": "dist/bin.js" + }, + "engines": { + "node": ">=12.9.0" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/binaryextensions": { + "version": "4.18.0", + "license": "MIT", + "engines": { + "node": ">=0.8" + }, + "funding": { + "url": "https://bevry.me/fund" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/boolean": { + "version": "3.2.0", + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browser-stdout": { + "version": "1.3.1", + "dev": true, + "license": "ISC" + }, + "node_modules/buffer": { + "version": "5.7.1", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/builtins": { + "version": "5.0.1", + "license": "MIT", + "dependencies": { + "semver": "^7.0.0" + } + }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/c8": { + "version": "8.0.1", + "dev": true, + "license": "ISC", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@istanbuljs/schema": "^0.1.3", + "find-up": "^5.0.0", + "foreground-child": "^2.0.0", + "istanbul-lib-coverage": "^3.2.0", + "istanbul-lib-report": "^3.0.1", + "istanbul-reports": "^3.1.6", + "rimraf": "^3.0.2", + "test-exclude": "^6.0.0", + "v8-to-istanbul": "^9.0.0", + "yargs": "^17.7.2", + "yargs-parser": "^21.1.1" + }, + "bin": { + "c8": "bin/c8.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/c8/node_modules/yargs-parser": { + "version": "21.1.1", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/cacache": { + "version": "18.0.0", + "license": "ISC", + "dependencies": { + "@npmcli/fs": "^3.1.0", + "fs-minipass": "^3.0.0", + "glob": "^10.2.2", + "lru-cache": "^10.0.1", + "minipass": "^7.0.3", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "p-map": "^4.0.0", + "ssri": "^10.0.0", + "tar": "^6.1.11", + "unique-filename": "^3.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/cacache/node_modules/lru-cache": { + "version": "10.0.1", + "license": "ISC", + "engines": { + "node": "14 || >=16.14" + } + }, + "node_modules/cacache/node_modules/minipass": { + "version": "7.0.4", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/cacheable-lookup": { + "version": "7.0.0", + "license": "MIT", + "engines": { + "node": ">=14.16" + } + }, + "node_modules/cacheable-request": { + "version": "10.2.8", + "license": "MIT", + "dependencies": { + "@types/http-cache-semantics": "^4.0.1", + "get-stream": "^6.0.1", + "http-cache-semantics": "^4.1.1", + "keyv": "^4.5.2", + "mimic-response": "^4.0.0", + "normalize-url": "^8.0.0", + "responselike": "^3.0.0" + }, + "engines": { + "node": ">=14.16" + } + }, + "node_modules/cacheable-request/node_modules/get-stream": { + "version": "6.0.1", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/call-bind": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caxa": { + "version": "3.0.1", + "dev": true, + "funding": [ + "https://patreon.com/leafac", + "https://paypal.me/LeandroFacchinettiEU", + "https://github.com/sponsors/leafac", + "https://btc.com/34KJBgtaFYMtDqpSgMayw9qiKWg2GQXA9M" + ], + "license": "MIT", + "dependencies": { + "archiver": "^5.3.1", + "commander": "^9.4.1", + "crypto-random-string": "^5.0.0", + "dedent": "^0.7.0", + "execa": "^6.1.0", + "fs-extra": "^10.1.0", + "globby": "^13.1.2" + }, + "bin": { + "caxa": "build/index.mjs" + } + }, + "node_modules/caxa/node_modules/commander": { + "version": "9.5.0", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || >=14" + } + }, + "node_modules/caxa/node_modules/execa": { + "version": "6.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.1", + "human-signals": "^3.0.1", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^3.0.7", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/caxa/node_modules/fs-extra": { + "version": "10.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/caxa/node_modules/get-stream": { + "version": "6.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/caxa/node_modules/globby": { + "version": "13.2.2", + "dev": true, + "license": "MIT", + "dependencies": { + "dir-glob": "^3.0.1", + "fast-glob": "^3.3.0", + "ignore": "^5.2.4", + "merge2": "^1.4.1", + "slash": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/caxa/node_modules/human-signals": { + "version": "3.0.1", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/caxa/node_modules/mimic-fn": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/caxa/node_modules/onetime": { + "version": "6.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/caxa/node_modules/slash": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/chai": { + "version": "4.3.10", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.3", + "deep-eql": "^4.1.3", + "get-func-name": "^2.0.2", + "loupe": "^2.3.6", + "pathval": "^1.1.1", + "type-detect": "^4.0.8" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chai-as-promised": { + "version": "7.1.1", + "dev": true, + "license": "WTFPL", + "dependencies": { + "check-error": "^1.0.2" + }, + "peerDependencies": { + "chai": ">= 2.1.2 < 5" + } + }, + "node_modules/chalk": { + "version": "5.3.0", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chardet": { + "version": "0.7.0", + "license": "MIT" + }, + "node_modules/check-error": { + "version": "1.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "get-func-name": "^2.0.2" + }, + "engines": { + "node": "*" + } + }, + "node_modules/check-more-types": { + "version": "2.24.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/chokidar": { + "version": "3.5.3", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/ci-info": { + "version": "3.7.1", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/clean-stack": { + "version": "2.2.0", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "license": "MIT", + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-progress": { + "version": "3.12.0", + "license": "MIT", + "dependencies": { + "string-width": "^4.2.3" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.0", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-table3": { + "version": "0.6.3", + "license": "MIT", + "dependencies": { + "string-width": "^4.2.0" + }, + "engines": { + "node": "10.* || >= 12.*" + }, + "optionalDependencies": { + "@colors/colors": "1.5.0" + } + }, + "node_modules/cli-truncate": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-3.1.0.tgz", + "integrity": "sha512-wfOBkjXteqSnI59oPcJkcPl/ZmwvMMOj340qUIY1SKZCv0B9Cf4D4fAucRkIKQmsIuYK3x1rrgU7MeGRruiuiA==", + "dependencies": { + "slice-ansi": "^5.0.0", + "string-width": "^5.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/cli-truncate/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" + }, + "node_modules/cli-truncate/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/cli-width": { + "version": "4.1.0", + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/clone": { + "version": "1.0.4", + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "license": "MIT" + }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "dev": true, + "bin": { + "color-support": "bin.js" + } + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "11.1.0", + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/compress-commons": { + "version": "4.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-crc32": "^0.2.13", + "crc32-stream": "^4.0.2", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/confusing-browser-globals": { + "version": "1.0.11", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "1.9.0", + "dev": true, + "license": "MIT" + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "dev": true, + "license": "MIT" + }, + "node_modules/crc-32": { + "version": "1.2.2", + "dev": true, + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/crc32-stream": { + "version": "4.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "crc-32": "^1.2.0", + "readable-stream": "^3.4.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/cross-env": { + "version": "7.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "bin": { + "cross-env": "src/bin/cross-env.js", + "cross-env-shell": "src/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=10.14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/cross-fetch": { + "version": "3.1.5", + "license": "MIT", + "dependencies": { + "node-fetch": "2.6.7" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cross-spawn/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/crypto-random-string": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^2.12.2" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/crypto-random-string/node_modules/type-fest": { + "version": "2.19.0", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/dayjs": { + "version": "1.11.10", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.3.4", + "license": "MIT", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decompress-response/node_modules/mimic-response": { + "version": "3.1.0", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/dedent": { + "version": "0.7.0", + "dev": true, + "license": "MIT" + }, + "node_modules/deep-eql": { + "version": "4.1.3", + "dev": true, + "license": "MIT", + "dependencies": { + "type-detect": "^4.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "dev": true, + "license": "MIT" + }, + "node_modules/default-browser": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.2.1.tgz", + "integrity": "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==", + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.0.tgz", + "integrity": "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/defaults": { + "version": "1.0.4", + "license": "MIT", + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/defer-to-connect": { + "version": "2.0.1", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-libc": { + "version": "2.0.1", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/diff": { + "version": "5.0.0", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/duplexer": { + "version": "0.1.2", + "dev": true, + "license": "MIT" + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "license": "MIT" + }, + "node_modules/encoding": { + "version": "0.1.13", + "license": "MIT", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "license": "MIT", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "license": "MIT" + }, + "node_modules/es-abstract": { + "version": "1.22.2", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.0", + "arraybuffer.prototype.slice": "^1.0.2", + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "es-set-tostringtag": "^2.0.1", + "es-to-primitive": "^1.2.1", + "function.prototype.name": "^1.1.6", + "get-intrinsic": "^1.2.1", + "get-symbol-description": "^1.0.0", + "globalthis": "^1.0.3", + "gopd": "^1.0.1", + "has": "^1.0.3", + "has-property-descriptors": "^1.0.0", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.5", + "is-array-buffer": "^3.0.2", + "is-callable": "^1.2.7", + "is-negative-zero": "^2.0.2", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "is-string": "^1.0.7", + "is-typed-array": "^1.1.12", + "is-weakref": "^1.0.2", + "object-inspect": "^1.12.3", + "object-keys": "^1.1.1", + "object.assign": "^4.1.4", + "regexp.prototype.flags": "^1.5.1", + "safe-array-concat": "^1.0.1", + "safe-regex-test": "^1.0.0", + "string.prototype.trim": "^1.2.8", + "string.prototype.trimend": "^1.0.7", + "string.prototype.trimstart": "^1.0.7", + "typed-array-buffer": "^1.0.0", + "typed-array-byte-length": "^1.0.0", + "typed-array-byte-offset": "^1.0.0", + "typed-array-length": "^1.0.4", + "unbox-primitive": "^1.0.2", + "which-typed-array": "^1.1.11" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-main": { + "version": "1.3.0", + "license": "MIT" + }, + "node_modules/es-set-tostringtag": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.1.3", + "has": "^1.0.3", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has": "^1.0.3" + } + }, + "node_modules/es-to-primitive": { + "version": "1.2.1", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/esbuild": { + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.11.tgz", + "integrity": "sha512-HJ96Hev2hX/6i5cDVwcqiJBBtuo9+FeIJOtZ9W1kA5M6AMJRHUZlpYZ1/SbEwtO0ioNAW8rUooVpC/WehY2SfA==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.19.11", + "@esbuild/android-arm": "0.19.11", + "@esbuild/android-arm64": "0.19.11", + "@esbuild/android-x64": "0.19.11", + "@esbuild/darwin-arm64": "0.19.11", + "@esbuild/darwin-x64": "0.19.11", + "@esbuild/freebsd-arm64": "0.19.11", + "@esbuild/freebsd-x64": "0.19.11", + "@esbuild/linux-arm": "0.19.11", + "@esbuild/linux-arm64": "0.19.11", + "@esbuild/linux-ia32": "0.19.11", + "@esbuild/linux-loong64": "0.19.11", + "@esbuild/linux-mips64el": "0.19.11", + "@esbuild/linux-ppc64": "0.19.11", + "@esbuild/linux-riscv64": "0.19.11", + "@esbuild/linux-s390x": "0.19.11", + "@esbuild/linux-x64": "0.19.11", + "@esbuild/netbsd-x64": "0.19.11", + "@esbuild/openbsd-x64": "0.19.11", + "@esbuild/sunos-x64": "0.19.11", + "@esbuild/win32-arm64": "0.19.11", + "@esbuild/win32-ia32": "0.19.11", + "@esbuild/win32-x64": "0.19.11" + } + }, + "node_modules/escalade": { + "version": "3.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.56.0.tgz", + "integrity": "sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.56.0", + "@humanwhocodes/config-array": "^0.11.13", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-airbnb-base": { + "version": "15.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "confusing-browser-globals": "^1.0.10", + "object.assign": "^4.1.2", + "object.entries": "^1.1.5", + "semver": "^6.3.0" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + }, + "peerDependencies": { + "eslint": "^7.32.0 || ^8.2.0", + "eslint-plugin-import": "^2.25.2" + } + }, + "node_modules/eslint-config-airbnb-base/node_modules/semver": { + "version": "6.3.1", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.9", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.13.0", + "resolve": "^1.22.4" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-module-utils": { + "version": "2.8.0", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import": { + "version": "2.29.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.1.tgz", + "integrity": "sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==", + "dev": true, + "dependencies": { + "array-includes": "^3.1.7", + "array.prototype.findlastindex": "^1.2.3", + "array.prototype.flat": "^1.3.2", + "array.prototype.flatmap": "^1.3.2", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.8.0", + "hasown": "^2.0.0", + "is-core-module": "^2.13.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.7", + "object.groupby": "^1.0.1", + "object.values": "^1.1.7", + "semver": "^6.3.1", + "tsconfig-paths": "^3.15.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/doctrine": { + "version": "2.1.0", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-import/node_modules/minimatch": { + "version": "3.1.2", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint-plugin-import/node_modules/semver": { + "version": "6.3.1", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/chalk": { + "version": "4.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.2", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.5.0", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/event-stream": { + "version": "4.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "duplexer": "^0.1.1", + "from": "^0.1.7", + "map-stream": "0.0.7", + "pause-stream": "^0.0.11", + "split": "^1.0.1", + "stream-combiner": "^0.2.2", + "through": "^2.3.8" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==" + }, + "node_modules/expand-template": { + "version": "2.0.3", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, + "node_modules/external-editor": { + "version": "3.1.0", + "license": "MIT", + "dependencies": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/fancy-log": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fancy-log/-/fancy-log-2.0.0.tgz", + "integrity": "sha512-9CzxZbACXMUXW13tS0tI8XsGGmxWzO2DmYrGuBJOJ8k8q2K7hwfJA5qHjuPPe8wtsco33YR9wc+Rlr5wYFvhSA==", + "dev": true, + "dependencies": { + "color-support": "^1.1.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.15.0", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/figures/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/file-entry-cache": { + "version": "6.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "dev": true, + "license": "BSD-3-Clause", + "bin": { + "flat": "cli.js" + } + }, + "node_modules/flat-cache": { + "version": "3.0.4", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.1.0", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.2.7", + "dev": true, + "license": "ISC" + }, + "node_modules/for-each": { + "version": "0.3.3", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.1.3" + } + }, + "node_modules/foreground-child": { + "version": "2.0.0", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/form-data-encoder": { + "version": "2.1.4", + "license": "MIT", + "engines": { + "node": ">= 14.17" + } + }, + "node_modules/from": { + "version": "0.1.7", + "dev": true, + "license": "MIT" + }, + "node_modules/from2": { + "version": "2.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.1", + "readable-stream": "^2.0.0" + } + }, + "node_modules/from2/node_modules/isarray": { + "version": "1.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/from2/node_modules/readable-stream": { + "version": "2.3.7", + "dev": true, + "license": "MIT", + "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/from2/node_modules/safe-buffer": { + "version": "5.1.2", + "dev": true, + "license": "MIT" + }, + "node_modules/from2/node_modules/string_decoder": { + "version": "1.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "license": "MIT" + }, + "node_modules/fs-extra": { + "version": "9.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/fs-minipass": { + "version": "3.0.3", + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "7.0.4", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.2", + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.6", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "functions-have-names": "^1.2.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-func-name": { + "version": "2.0.2", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-stream": { + "version": "8.0.1", + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-symbol-description": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gh-got": { + "version": "10.0.0", + "license": "MIT", + "dependencies": { + "got": "^12.5.2" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, + "node_modules/gh-got/node_modules/get-stream": { + "version": "6.0.1", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gh-got/node_modules/got": { + "version": "12.6.1", + "license": "MIT", + "dependencies": { + "@sindresorhus/is": "^5.2.0", + "@szmarczak/http-timer": "^5.0.1", + "cacheable-lookup": "^7.0.0", + "cacheable-request": "^10.2.8", + "decompress-response": "^6.0.0", + "form-data-encoder": "^2.1.2", + "get-stream": "^6.0.1", + "http2-wrapper": "^2.1.10", + "lowercase-keys": "^3.0.0", + "p-cancelable": "^3.0.0", + "responselike": "^3.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "license": "MIT" + }, + "node_modules/glob": { + "version": "10.3.10", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^2.3.5", + "minimatch": "^9.0.1", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", + "path-scurry": "^1.10.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob/node_modules/foreground-child": { + "version": "3.1.1", + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob/node_modules/minipass": { + "version": "5.0.0", + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/glob/node_modules/signal-exit": { + "version": "4.0.2", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/global-directory": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/global-directory/-/global-directory-4.0.1.tgz", + "integrity": "sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==", + "dependencies": { + "ini": "4.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globals": { + "version": "13.23.0", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalthis": { + "version": "1.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/got": { + "version": "13.0.0", + "license": "MIT", + "dependencies": { + "@sindresorhus/is": "^5.2.0", + "@szmarczak/http-timer": "^5.0.1", + "cacheable-lookup": "^7.0.0", + "cacheable-request": "^10.2.8", + "decompress-response": "^6.0.0", + "form-data-encoder": "^2.1.2", + "get-stream": "^6.0.1", + "http2-wrapper": "^2.1.10", + "lowercase-keys": "^3.0.0", + "p-cancelable": "^3.0.0", + "responselike": "^3.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, + "node_modules/got/node_modules/get-stream": { + "version": "6.0.1", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.10", + "dev": true, + "license": "ISC" + }, + "node_modules/graphemer": { + "version": "1.4.0", + "dev": true, + "license": "MIT" + }, + "node_modules/graphql": { + "version": "16.8.1", + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" + } + }, + "node_modules/graphql-request": { + "version": "6.1.0", + "license": "MIT", + "dependencies": { + "@graphql-typed-document-node/core": "^3.2.0", + "cross-fetch": "^3.1.5" + }, + "peerDependencies": { + "graphql": "14 - 16" + } + }, + "node_modules/has": { + "version": "1.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-bigints": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.1", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.0", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/he": { + "version": "1.2.0", + "dev": true, + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "node_modules/hosted-git-info": { + "version": "7.0.1", + "license": "ISC", + "dependencies": { + "lru-cache": "^10.0.1" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/hosted-git-info/node_modules/lru-cache": { + "version": "10.0.1", + "license": "ISC", + "engines": { + "node": "14 || >=16.14" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/http-cache-semantics": { + "version": "4.1.1", + "license": "BSD-2-Clause" + }, + "node_modules/http-proxy-agent": { + "version": "7.0.0", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/http-proxy-agent/node_modules/agent-base": { + "version": "7.1.0", + "license": "MIT", + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/http2-wrapper": { + "version": "2.2.0", + "license": "MIT", + "dependencies": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.2.0" + }, + "engines": { + "node": ">=10.19.0" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/husky": { + "version": "8.0.3", + "dev": true, + "license": "MIT", + "bin": { + "husky": "lib/bin.js" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/ignore": { + "version": "5.2.4", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "license": "ISC" + }, + "node_modules/ini": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.1.tgz", + "integrity": "sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/inquirer": { + "version": "9.2.16", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-9.2.16.tgz", + "integrity": "sha512-qzgbB+yNjgSzk2omeqMDtO9IgJet/UL67luT1MaaggRpGK73DBQct5Q4pipwFQcIKK1GbMODYd4UfsRCkSP1DA==", + "dependencies": { + "@ljharb/through": "^2.3.13", + "ansi-escapes": "^4.3.2", + "chalk": "^5.3.0", + "cli-cursor": "^3.1.0", + "cli-width": "^4.1.0", + "external-editor": "^3.1.0", + "figures": "^3.2.0", + "lodash": "^4.17.21", + "mute-stream": "1.0.0", + "ora": "^5.4.1", + "run-async": "^3.0.0", + "rxjs": "^7.8.1", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^6.2.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/inquirer/node_modules/wrap-ansi": { + "version": "6.2.0", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/internal-slot": { + "version": "1.0.5", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.0", + "has": "^1.0.3", + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/into-stream": { + "version": "6.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "from2": "^2.3.0", + "p-is-promise": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ip-address": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", + "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", + "dependencies": { + "jsbn": "1.1.0", + "sprintf-js": "^1.1.3" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.0", + "is-typed-array": "^1.1.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.0.4", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-boolean-object": { + "version": "1.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-ci": { + "version": "3.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "ci-info": "^3.2.0" + }, + "bin": { + "is-ci": "bin.js" + } + }, + "node_modules/is-core-module": { + "version": "2.13.1", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.0.5", + "dev": true, + "license": "MIT", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-inside-container/node_modules/is-docker": { + "version": "3.0.0", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-installed-globally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-1.0.0.tgz", + "integrity": "sha512-K55T22lfpQ63N4KEN57jZUAaAYqYHEe8veb/TycJRk9DdSCLLcovXz/mL6mOnhQaZsQGwPhuFopdQIlqGSEjiQ==", + "dependencies": { + "global-directory": "^4.0.1", + "is-path-inside": "^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-installed-globally/node_modules/is-path-inside": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-4.0.0.tgz", + "integrity": "sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-interactive": { + "version": "1.0.0", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-lambda": { + "version": "1.0.1", + "license": "MIT" + }, + "node_modules/is-negative-zero": { + "version": "2.0.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.0.7", + "dev": true, + "license": "MIT", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-obj": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-regex": { + "version": "1.1.4", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-stream": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-string": { + "version": "1.0.7", + "dev": true, + "license": "MIT", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.0.4", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.12", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.11" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-weakref": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-wsl": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", + "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isarray": { + "version": "0.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.0", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.1.6", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istextorbinary": { + "version": "6.0.0", + "license": "MIT", + "dependencies": { + "binaryextensions": "^4.18.0", + "textextensions": "^5.14.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://bevry.me/fund" + } + }, + "node_modules/jackspeak": { + "version": "2.3.6", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jest-sonar-reporter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jest-sonar-reporter/-/jest-sonar-reporter-2.0.0.tgz", + "integrity": "sha512-ZervDCgEX5gdUbdtWsjdipLN3bKJwpxbvhkYNXTAYvAckCihobSLr9OT/IuyNIRT1EZMDDwR6DroWtrq+IL64w==", + "dev": true, + "dependencies": { + "xml": "^1.0.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/jju": { + "version": "1.4.0", + "dev": true, + "license": "MIT" + }, + "node_modules/jmespath": { + "version": "0.16.0", + "license": "Apache-2.0", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/joi": { + "version": "17.11.0", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^9.0.0", + "@hapi/topo": "^5.0.0", + "@sideway/address": "^4.1.3", + "@sideway/formula": "^3.0.1", + "@sideway/pinpoint": "^2.0.0" + } + }, + "node_modules/jose": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-5.2.3.tgz", + "integrity": "sha512-KUXdbctm1uHVL8BYhnyHkgp3zDX5KW8ZhAKVFEfUbU2P8Alpzjb+48hHvjOdQIyPshoblhzsuqOwEEAbtHVirA==", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsbn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==" + }, + "node_modules/jsesc": { + "version": "2.5.2", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "dev": true, + "license": "ISC" + }, + "node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/jsonfile": { + "version": "6.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonparse": { + "version": "1.3.1", + "engines": [ + "node >= 0.2.0" + ], + "license": "MIT" + }, + "node_modules/JSONStream": { + "version": "1.3.5", + "dev": true, + "license": "(MIT OR Apache-2.0)", + "dependencies": { + "jsonparse": "^1.2.0", + "through": ">=2.2.7 <3" + }, + "bin": { + "JSONStream": "bin.js" + }, + "engines": { + "node": "*" + } + }, + "node_modules/just-extend": { + "version": "4.2.1", + "dev": true, + "license": "MIT" + }, + "node_modules/keytar": { + "version": "7.9.0", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^4.3.0", + "prebuild-install": "^7.0.1" + } + }, + "node_modules/keyv": { + "version": "4.5.2", + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/lazy-ass": { + "version": "1.6.0", + "dev": true, + "license": "MIT", + "engines": { + "node": "> 0.8" + } + }, + "node_modules/lazystream": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "readable-stream": "^2.0.5" + }, + "engines": { + "node": ">= 0.6.3" + } + }, + "node_modules/lazystream/node_modules/isarray": { + "version": "1.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/lazystream/node_modules/readable-stream": { + "version": "2.3.8", + "dev": true, + "license": "MIT", + "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/lazystream/node_modules/safe-buffer": { + "version": "5.1.2", + "dev": true, + "license": "MIT" + }, + "node_modules/lazystream/node_modules/string_decoder": { + "version": "1.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/listr2": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-6.4.2.tgz", + "integrity": "sha512-v55SFIDP7SiPEYFeIFGbKW44B4NPpqGEklbAc1EKacMxIqFVXpDlc93e/Q6hE3IgIGRu5870rh5yJc+ESwGUpQ==", + "dependencies": { + "cli-truncate": "^3.1.0", + "colorette": "^2.0.20", + "eventemitter3": "^5.0.1", + "log-update": "^5.0.1", + "rfdc": "^1.3.0", + "wrap-ansi": "^8.1.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "enquirer": ">= 2.3.0 < 3" + }, + "peerDependenciesMeta": { + "enquirer": { + "optional": true + } + } + }, + "node_modules/listr2/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/listr2/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/listr2/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" + }, + "node_modules/listr2/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/listr2/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/listr2/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "license": "MIT" + }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.difference": { + "version": "4.5.0", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.flatten": { + "version": "4.4.0", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.get": { + "version": "4.4.2", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.union": { + "version": "4.6.0", + "dev": true, + "license": "MIT" + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-symbols/node_modules/chalk": { + "version": "4.1.2", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/log-update": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-5.0.1.tgz", + "integrity": "sha512-5UtUDQ/6edw4ofyljDNcOVJQ4c7OjDro4h3y8e1GQL5iYElYclVHJ3zeWchylvMaKnDbDilC8irOVyexnA/Slw==", + "dependencies": { + "ansi-escapes": "^5.0.0", + "cli-cursor": "^4.0.0", + "slice-ansi": "^5.0.0", + "strip-ansi": "^7.0.1", + "wrap-ansi": "^8.0.1" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-escapes": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-5.0.0.tgz", + "integrity": "sha512-5GFMVX8HqE/TB+FuBJGuO5XG0WrsA6ptUqoODaT/n9mmUaZFkqnBueB4leqGBCmrUHnCnC4PCZTCd0E7QQ83bA==", + "dependencies": { + "type-fest": "^1.0.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/log-update/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/log-update/node_modules/cli-cursor": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-4.0.0.tgz", + "integrity": "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==", + "dependencies": { + "restore-cursor": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" + }, + "node_modules/log-update/node_modules/restore-cursor": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz", + "integrity": "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/log-update/node_modules/type-fest": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", + "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/loupe": { + "version": "2.3.6", + "dev": true, + "license": "MIT", + "dependencies": { + "get-func-name": "^2.0.0" + } + }, + "node_modules/lowercase-keys": { + "version": "3.0.0", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-fetch-happen": { + "version": "13.0.0", + "license": "ISC", + "dependencies": { + "@npmcli/agent": "^2.0.0", + "cacache": "^18.0.0", + "http-cache-semantics": "^4.1.1", + "is-lambda": "^1.0.1", + "minipass": "^7.0.2", + "minipass-fetch": "^3.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.3", + "promise-retry": "^2.0.1", + "ssri": "^10.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/make-fetch-happen/node_modules/minipass": { + "version": "7.0.4", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/map-stream": { + "version": "0.0.7", + "dev": true, + "license": "MIT" + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.5", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/mimic-response": { + "version": "4.0.0", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "9.0.3", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimatch/node_modules/brace-expansion": { + "version": "2.0.1", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/minimist": { + "version": "1.2.7", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "3.3.6", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-collect": { + "version": "1.0.2", + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-fetch": { + "version": "3.0.4", + "license": "MIT", + "dependencies": { + "minipass": "^7.0.3", + "minipass-sized": "^1.0.3", + "minizlib": "^2.1.2" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + }, + "optionalDependencies": { + "encoding": "^0.1.13" + } + }, + "node_modules/minipass-fetch/node_modules/minipass": { + "version": "7.0.4", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minipass-flush": { + "version": "1.0.5", + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-json-stream": { + "version": "1.0.1", + "license": "MIT", + "dependencies": { + "jsonparse": "^1.3.1", + "minipass": "^3.0.0" + } + }, + "node_modules/minipass-pipeline": { + "version": "1.2.4", + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized": { + "version": "1.0.3", + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "license": "MIT" + }, + "node_modules/mocha": { + "version": "10.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-colors": "4.1.1", + "browser-stdout": "1.3.1", + "chokidar": "3.5.3", + "debug": "4.3.4", + "diff": "5.0.0", + "escape-string-regexp": "4.0.0", + "find-up": "5.0.0", + "glob": "7.2.0", + "he": "1.2.0", + "js-yaml": "4.1.0", + "log-symbols": "4.1.0", + "minimatch": "5.0.1", + "ms": "2.1.3", + "nanoid": "3.3.3", + "serialize-javascript": "6.0.0", + "strip-json-comments": "3.1.1", + "supports-color": "8.1.1", + "workerpool": "6.2.1", + "yargs": "16.2.0", + "yargs-parser": "20.2.4", + "yargs-unparser": "2.0.0" + }, + "bin": { + "_mocha": "bin/_mocha", + "mocha": "bin/mocha.js" + }, + "engines": { + "node": ">= 14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mochajs" + } + }, + "node_modules/mocha/node_modules/cliui": { + "version": "7.0.4", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/mocha/node_modules/glob": { + "version": "7.2.0", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/mocha/node_modules/glob/node_modules/minimatch": { + "version": "3.1.2", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/mocha/node_modules/minimatch": { + "version": "5.0.1", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mocha/node_modules/minimatch/node_modules/brace-expansion": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/mocha/node_modules/ms": { + "version": "2.1.3", + "dev": true, + "license": "MIT" + }, + "node_modules/mocha/node_modules/supports-color": { + "version": "8.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/mocha/node_modules/yargs": { + "version": "16.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mocked-env": { + "version": "1.3.5", + "dev": true, + "license": "MIT", + "dependencies": { + "check-more-types": "2.24.0", + "debug": "4.3.2", + "lazy-ass": "1.6.0", + "ramda": "0.27.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/mocked-env/node_modules/debug": { + "version": "4.3.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/ms": { + "version": "2.1.2", + "license": "MIT" + }, + "node_modules/multistream": { + "version": "4.1.0", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "once": "^1.4.0", + "readable-stream": "^3.6.0" + } + }, + "node_modules/mute-stream": { + "version": "1.0.0", + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.3", + "dev": true, + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/napi-build-utils": { + "version": "1.0.2", + "license": "MIT" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "dev": true, + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/nise": { + "version": "5.1.5", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^2.0.0", + "@sinonjs/fake-timers": "^10.0.2", + "@sinonjs/text-encoding": "^0.7.1", + "just-extend": "^4.0.2", + "path-to-regexp": "^1.7.0" + } + }, + "node_modules/nise/node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/nise/node_modules/@sinonjs/fake-timers/node_modules/@sinonjs/commons": { + "version": "3.0.0", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/nock": { + "version": "13.4.0", + "resolved": "https://registry.npmjs.org/nock/-/nock-13.4.0.tgz", + "integrity": "sha512-W8NVHjO/LCTNA64yxAPHV/K47LpGYcVzgKd3Q0n6owhwvD0Dgoterc25R4rnZbckJEb6Loxz1f5QMuJpJnbSyQ==", + "dev": true, + "dependencies": { + "debug": "^4.1.0", + "json-stringify-safe": "^5.0.1", + "propagate": "^2.0.0" + }, + "engines": { + "node": ">= 10.13" + } + }, + "node_modules/node-abi": { + "version": "3.31.0", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-addon-api": { + "version": "4.3.0", + "license": "MIT" + }, + "node_modules/node-downloader-helper": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/node-downloader-helper/-/node-downloader-helper-2.1.9.tgz", + "integrity": "sha512-FSvAol2Z8UP191sZtsUZwHIN0eGoGue3uEXGdWIH5228e9KH1YHXT7fN8Oa33UGf+FbqGTQg3sJfrRGzmVCaJA==", + "dev": true, + "bin": { + "ndh": "bin/ndh" + }, + "engines": { + "node": ">=14.18" + } + }, + "node_modules/node-fetch": { + "version": "2.6.7", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-url": { + "version": "8.0.0", + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-package-arg": { + "version": "11.0.1", + "license": "ISC", + "dependencies": { + "hosted-git-info": "^7.0.0", + "proc-log": "^3.0.0", + "semver": "^7.3.5", + "validate-npm-package-name": "^5.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm-registry-fetch": { + "version": "16.1.0", + "license": "ISC", + "dependencies": { + "make-fetch-happen": "^13.0.0", + "minipass": "^7.0.2", + "minipass-fetch": "^3.0.0", + "minipass-json-stream": "^1.0.1", + "minizlib": "^2.1.2", + "npm-package-arg": "^11.0.0", + "proc-log": "^3.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm-registry-fetch/node_modules/minipass": { + "version": "7.0.4", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/npm-run-path": { + "version": "5.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/object-inspect": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", + "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.4", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "has-symbols": "^1.0.3", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.entries": { + "version": "1.1.6", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.7", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.groupby": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1" + } + }, + "node_modules/object.values": { + "version": "1.1.7", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/once": { + "version": "1.4.0", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.1.0.tgz", + "integrity": "sha512-mnkeQ1qP5Ue2wd+aivTD3NHd/lZ96Lu0jgf0pwktLPtx6cTZiH7tyeGRRHs0zX0rbrahXPnXlUnbeXyaBBuIaw==", + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@aashutoshrathi/word-wrap": "^1.2.3", + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/ora": { + "version": "5.4.1", + "license": "MIT", + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/chalk": { + "version": "4.1.2", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/p-cancelable": { + "version": "3.0.0", + "license": "MIT", + "engines": { + "node": ">=12.20" + } + }, + "node_modules/p-is-promise": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-map": { + "version": "4.0.0", + "license": "MIT", + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "dev": true, + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "1.10.1", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^9.1.1 || ^10.0.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.0.1", + "license": "ISC", + "engines": { + "node": "14 || >=16.14" + } + }, + "node_modules/path-scurry/node_modules/minipass": { + "version": "7.0.4", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/path-to-regexp": { + "version": "1.8.0", + "dev": true, + "license": "MIT", + "dependencies": { + "isarray": "0.0.1" + } + }, + "node_modules/path-type": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pathval": { + "version": "1.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/pause-stream": { + "version": "0.0.11", + "dev": true, + "license": [ + "MIT", + "Apache2" + ], + "dependencies": { + "through": "~2.3" + } + }, + "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/picomatch": { + "version": "2.3.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pkg": { + "version": "5.8.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/generator": "7.18.2", + "@babel/parser": "7.18.4", + "@babel/types": "7.19.0", + "chalk": "^4.1.2", + "fs-extra": "^9.1.0", + "globby": "^11.1.0", + "into-stream": "^6.0.0", + "is-core-module": "2.9.0", + "minimist": "^1.2.6", + "multistream": "^4.1.0", + "pkg-fetch": "3.4.2", + "prebuild-install": "7.1.1", + "resolve": "^1.22.0", + "stream-meter": "^1.0.4" + }, + "bin": { + "pkg": "lib-es5/bin.js" + }, + "peerDependencies": { + "node-notifier": ">=9.0.1" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/pkg-fetch": { + "version": "3.4.2", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2", + "fs-extra": "^9.1.0", + "https-proxy-agent": "^5.0.0", + "node-fetch": "^2.6.6", + "progress": "^2.0.3", + "semver": "^7.3.5", + "tar-fs": "^2.1.1", + "yargs": "^16.2.0" + }, + "bin": { + "pkg-fetch": "lib-es5/bin.js" + } + }, + "node_modules/pkg-fetch/node_modules/chalk": { + "version": "4.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/pkg-fetch/node_modules/chownr": { + "version": "1.1.4", + "dev": true, + "license": "ISC" + }, + "node_modules/pkg-fetch/node_modules/cliui": { + "version": "7.0.4", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/pkg-fetch/node_modules/tar-fs": { + "version": "2.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/pkg-fetch/node_modules/yargs": { + "version": "16.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/pkg/node_modules/@babel/generator": { + "version": "7.18.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.18.2", + "@jridgewell/gen-mapping": "^0.3.0", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/pkg/node_modules/@babel/parser": { + "version": "7.18.4", + "dev": true, + "license": "MIT", + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/pkg/node_modules/@babel/types": { + "version": "7.19.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.18.10", + "@babel/helper-validator-identifier": "^7.18.6", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/pkg/node_modules/@jridgewell/gen-mapping": { + "version": "0.3.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/pkg/node_modules/chalk": { + "version": "4.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/pkg/node_modules/is-core-module": { + "version": "2.9.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/prebuild-install": { + "version": "7.1.1", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^1.0.1", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/proc-log": { + "version": "3.0.0", + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/progress": { + "version": "2.0.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/promise-retry": { + "version": "2.0.1", + "license": "MIT", + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/propagate": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/pump": { + "version": "3.0.0", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.12.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.12.0.tgz", + "integrity": "sha512-trVZiI6RMOkO476zLGaBIzszOdFPnCCXHPG9kn0yuS1uz6xdVxPfZdB3vUig9pxPFDM9BRAgz/YUIVQ1/vuiUg==", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/querystringify": { + "version": "2.2.0", + "license": "MIT" + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/quick-lru": { + "version": "5.1.1", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ramda": { + "version": "0.27.1", + "dev": true, + "license": "MIT" + }, + "node_modules/randombytes": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/rc/node_modules/ini": { + "version": "1.3.8", + "license": "ISC" + }, + "node_modules/rc/node_modules/strip-json-comments": { + "version": "2.0.1", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/readable-stream": { + "version": "3.6.0", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdir-glob": { + "version": "1.1.3", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.1.0" + } + }, + "node_modules/readdir-glob/node_modules/brace-expansion": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/readdir-glob/node_modules/minimatch": { + "version": "5.1.6", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/readline-transform": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.1", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "set-function-name": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "license": "MIT" + }, + "node_modules/resolve": { + "version": "1.22.8", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-alpn": { + "version": "1.2.1", + "license": "MIT" + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/responselike": { + "version": "3.0.0", + "license": "MIT", + "dependencies": { + "lowercase-keys": "^3.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/retry": { + "version": "0.12.0", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rfdc": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.1.tgz", + "integrity": "sha512-r5a3l5HzYlIC68TpmYKlxWjmOP6wiPJ1vWv2HeLhNsRZMrCkxeqxiHlQ21oXmQ4F3SiryXBHhAD7JZqvOJjFmg==" + }, + "node_modules/rimraf": { + "version": "3.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "7.2.3", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/minimatch": { + "version": "3.1.2", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/run-applescript": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.0.0.tgz", + "integrity": "sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/run-async": { + "version": "3.0.0", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/rxjs": { + "version": "7.8.1", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-array-concat": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.1", + "has-symbols": "^1.0.3", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-array-concat/node_modules/isarray": { + "version": "2.0.5", + "dev": true, + "license": "MIT" + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safe-regex-test": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.3", + "is-regex": "^1.1.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.5.4", + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.0", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/shell-quote": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.1.tgz", + "integrity": "sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "license": "ISC" + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/sinon": { + "version": "17.0.1", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.0", + "@sinonjs/fake-timers": "^11.2.2", + "@sinonjs/samsam": "^8.0.0", + "diff": "^5.1.0", + "nise": "^5.1.5", + "supports-color": "^7.2.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/sinon" + } + }, + "node_modules/sinon/node_modules/@sinonjs/commons": { + "version": "3.0.0", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/sinon/node_modules/diff": { + "version": "5.1.0", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "license": "MIT" + }, + "node_modules/slash": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/slice-ansi": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", + "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", + "dependencies": { + "ansi-styles": "^6.0.0", + "is-fullwidth-code-point": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/is-fullwidth-code-point": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", + "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/slugify": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/slugify/-/slugify-1.6.6.tgz", + "integrity": "sha512-h+z7HKHYXj6wJU+AnS/+IH8Uh9fdcX1Lrhg1/VMdf9PwoBQXFcXiAdsy2tSK0P6gKwJLXp02r90ahUCqHk9rrw==", + "dev": true, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.7.3.tgz", + "integrity": "sha512-vfuYK48HXCTFD03G/1/zkIls3Ebr2YNa4qU9gHDZdblHLiqhJrJGkY3+0Nx0JpN9qBhJbVObc1CNciT1bIZJxw==", + "dependencies": { + "ip-address": "^9.0.5", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.2", + "license": "MIT", + "dependencies": { + "agent-base": "^7.0.2", + "debug": "^4.3.4", + "socks": "^2.7.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/socks-proxy-agent/node_modules/agent-base": { + "version": "7.1.0", + "license": "MIT", + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/sonarqube-scanner": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/sonarqube-scanner/-/sonarqube-scanner-3.3.0.tgz", + "integrity": "sha512-G1A6nXT3GwoK5eRwHfFrR/7ThiDfaRefWPIFQ+ifwFOQ/V9OwziLpZBdWZgmZp21kBRnzAMvjcTzgZMqGBXQKA==", + "dev": true, + "dependencies": { + "adm-zip": "^0.5.10", + "fancy-log": "^2.0.0", + "https-proxy-agent": "^7.0.1", + "jest-sonar-reporter": "^2.0.0", + "mkdirp": "^3.0.1", + "node-downloader-helper": "^2.1.9", + "progress": "^2.0.3", + "shell-quote": "^1.8.1", + "slugify": "^1.6.6" + }, + "bin": { + "sonar-scanner": "src/bin/sonar-scanner" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/sonarqube-scanner/node_modules/agent-base": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.0.tgz", + "integrity": "sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==", + "dev": true, + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/sonarqube-scanner/node_modules/https-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.2.tgz", + "integrity": "sha512-NmLNjm6ucYwtcUmL7JQC1ZQ57LmHP4lT15FQ8D61nak1rO6DH+fz5qNK2Ap5UN4ZapYICE3/0KodcLYSPsPbaA==", + "dev": true, + "dependencies": { + "agent-base": "^7.0.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/sonarqube-scanner/node_modules/mkdirp": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", + "dev": true, + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/split": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "through": "2" + }, + "engines": { + "node": "*" + } + }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==" + }, + "node_modules/ssri": { + "version": "10.0.5", + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/ssri/node_modules/minipass": { + "version": "7.0.4", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/stream-combiner": { + "version": "0.2.2", + "dev": true, + "license": "MIT", + "dependencies": { + "duplexer": "~0.1.1", + "through": "~2.3.4" + } + }, + "node_modules/stream-meter": { + "version": "1.0.4", + "dev": true, + "license": "MIT", + "dependencies": { + "readable-stream": "^2.1.4" + } + }, + "node_modules/stream-meter/node_modules/isarray": { + "version": "1.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/stream-meter/node_modules/readable-stream": { + "version": "2.3.7", + "dev": true, + "license": "MIT", + "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/stream-meter/node_modules/safe-buffer": { + "version": "5.1.2", + "dev": true, + "license": "MIT" + }, + "node_modules/stream-meter/node_modules/string_decoder": { + "version": "1.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.8", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.7", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.7", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "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==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-final-newline": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tar": { + "version": "6.2.0", + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar-fs": { + "version": "2.0.1", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.0.0" + } + }, + "node_modules/tar-fs/node_modules/chownr": { + "version": "1.1.4", + "license": "ISC" + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tar/node_modules/fs-minipass": { + "version": "2.1.0", + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/tar/node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude/node_modules/glob": { + "version": "7.2.3", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "3.1.2", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "dev": true, + "license": "MIT" + }, + "node_modules/textextensions": { + "version": "5.15.0", + "license": "MIT", + "engines": { + "node": ">=0.8" + }, + "funding": { + "url": "https://bevry.me/fund" + } + }, + "node_modules/through": { + "version": "2.3.8", + "dev": true, + "license": "MIT" + }, + "node_modules/tmp": { + "version": "0.0.33", + "license": "MIT", + "dependencies": { + "os-tmpdir": "~1.0.2" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "license": "MIT" + }, + "node_modules/treeify": { + "version": "1.1.0", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tsconfig-paths": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", + "dev": true, + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "node_modules/tslib": { + "version": "2.5.3", + "license": "0BSD" + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.1", + "is-typed-array": "^1.1.10" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "has-proto": "^1.0.1", + "is-typed-array": "^1.1.10" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "has-proto": "^1.0.1", + "is-typed-array": "^1.1.10" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.4", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "is-typed-array": "^1.1.9" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/unbox-primitive": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "has-bigints": "^1.0.2", + "has-symbols": "^1.0.3", + "which-boxed-primitive": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/unique-filename": { + "version": "3.0.0", + "license": "ISC", + "dependencies": { + "unique-slug": "^4.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/unique-slug": { + "version": "4.0.0", + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/universalify": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/url-join": { + "version": "5.0.0", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/url-parse": { + "version": "1.5.10", + "license": "MIT", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "license": "MIT" + }, + "node_modules/uuid": { + "version": "9.0.1", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/v8-to-istanbul": { + "version": "9.1.0", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^1.6.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/validate-npm-package-name": { + "version": "5.0.0", + "license": "ISC", + "dependencies": { + "builtins": "^5.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "license": "MIT", + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", + "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^16.13.0 || >=18.0.0" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.11", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which/node_modules/isexe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", + "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "engines": { + "node": ">=16" + } + }, + "node_modules/workerpool": { + "version": "6.2.1", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "license": "ISC" + }, + "node_modules/xml": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz", + "integrity": "sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==", + "dev": true + }, + "node_modules/y18n": { + "version": "5.0.8", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.2", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "20.2.4", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-unparser": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-unparser/node_modules/camelcase": { + "version": "6.3.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yargs-unparser/node_modules/decamelize": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yargs/node_modules/yargs-parser": { + "version": "21.1.1", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yauzl": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-3.1.2.tgz", + "integrity": "sha512-621iCPgEG1wXViDZS/L3h9F8TgrdQV1eayJlJ8j5A2SZg8OdY/1DLf+VxNeD+q5QbMFEAbjjR8nITj7g4nKa0Q==", + "dependencies": { + "buffer-crc32": "~0.2.3", + "pend": "~1.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zip-stream": { + "version": "4.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "archiver-utils": "^2.1.0", + "compress-commons": "^4.1.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + } + } +} diff --git a/package.json b/package.json index 94a9bf0b..b7432d84 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,32 @@ { "name": "cortex-cli", - "version": "0.6.23", + "version": "2.4.1", "description": "Command line tool for CognitiveScale Cortex.", "main": "./bin/cortex.js", + "type": "module", + "types": "./client/index.d.ts", "scripts": { - "test": "CORTEX_CONFIG_DIR=./test/cortex mocha" + "package-binary": "caxa --input . --output 'dist/cortex' '{{caxa}}/node_modules/.bin/node' '{{caxa}}/bin/cortex.js' --exclude '*' '.*' '!bin' '!src' '!client' '!package.json'", + "package-binary-windows": "caxa --input . --output \"dist/cortex.exe\" \"{{caxa}}/node_modules/.bin/node\" \"{{caxa}}/bin/cortex.js\" --exclude \"*\" \".*\" \"!bin\" \"!src\" \"!client\" \"!package.json\"", + "prepare": "is-ci || husky install", + "prepack": "cp package-lock.json npm-shrinkwrap.json", + "postpack": "rm npm-shrinkwrap.json", + "pretest": "eslint bin/**/*.js src/**/*.js test/**/*.js && audit-ci -h", + "test": "cross-env NODE_ENV=test c8 mocha", + "test:workspaces": "cross-env NODE_ENV=test mocha --grep='Workspaces'", + "clean": "rm -rf dist/ coverage/ .nyc_output/ node_modules/ cortex-cli.tgz cortex-cli-docs.md version.txt", + "scan": "sonar-scanner" + }, + "files": [ + "bin/*", + "src/*", + "client/*", + "README.md", + "npm-shrinkwrap.json", + "LICENSE" + ], + "engines": { + "node": ">=16.0.0" }, "repository": { "type": "git", @@ -25,23 +47,78 @@ "license": "Apache-2.0", "homepage": "https://github.com/CognitiveScale/cortex-cli", "dependencies": { - "@c12e/generator-cortex": "0.3.9", - "chalk": "2.3.0", - "cli-table": "0.3.1", - "co": "4.6.0", - "co-prompt": "1.0.0", - "commander": "2.12.2", - "debug": "3.1.0", - "event-stream": "3.3.4", - "lodash": "^4.17.5", - "jmespath": "0.15.0", - "joi": "13.1.0", - "js-yaml": "3.10.0", - "superagent": "3.8.2", - "uuid": "3.2.1", - "yeoman-environment": "2.0.5" + "@supercharge/promise-pool": "3.1.0", + "boolean": "3.2.0", + "chalk": "5.3.0", + "cli-progress": "3.12.0", + "cli-table3": "0.6.3", + "commander": "11.1.0", + "dayjs": "1.11.10", + "debug": "4.3.4", + "es-main": "1.3.0", + "form-data": "4.0.0", + "get-stream": "8.0.1", + "gh-got": "10.0.0", + "glob": "10.3.10", + "got": "13.0.0", + "graphql": "16.8.1", + "graphql-request": "6.1.0", + "inquirer": "9.2.16", + "is-installed-globally": "1.0.0", + "istextorbinary": "6.0.0", + "jmespath": "0.16.0", + "joi": "17.11.0", + "jose": "5.2.3", + "js-yaml": "4.1.0", + "keytar": "7.9.0", + "listr2": "6.4.2", + "lodash": "4.17.21", + "minimatch": "9.0.3", + "npm-registry-fetch": "16.1.0", + "open": "10.1.0", + "prompts": "2.4.2", + "qs": "6.12.0", + "semver": "7.5.4", + "treeify": "1.1.0", + "url-join": "5.0.0", + "url-parse": "1.5.10", + "uuid": "9.0.1", + "which": "4.0.0", + "yauzl": "3.1.2" }, "devDependencies": { - "mocha": "5.0.0" + "audit-ci": "6.6.1", + "c8": "8.0.1", + "caxa": "3.0.1", + "chai": "4.3.10", + "chai-as-promised": "7.1.1", + "cross-env": "7.0.3", + "esbuild": "0.19.11", + "eslint": "8.56.0", + "eslint-config-airbnb-base": "15.0.0", + "eslint-plugin-import": "2.29.1", + "husky": "8.0.3", + "is-ci": "3.0.1", + "mocha": "10.2.0", + "mocked-env": "1.3.5", + "nock": "13.4.0", + "pkg": "5.8.1", + "sinon": "17.0.1", + "sonarqube-scanner": "3.3.0" + }, + "c8": { + "check-coverage": true, + "statements": 40, + "branches": 71, + "functions": 17, + "lines": 40, + "include": [ + "bin", + "src" + ], + "reporter": [ + "text", + "lcov" + ] } } diff --git a/pull_request_template.md b/pull_request_template.md new file mode 100644 index 00000000..8246c506 --- /dev/null +++ b/pull_request_template.md @@ -0,0 +1,10 @@ +# Checklist: +Please check you fulfill ALL of the relevant checkboxes +- [ ] Notified docs of any potential USER-facing changes +- [ ] Added short description of the change - with relevant motivation and context. +- [ ] Branch has the ticket number in its name (along with a ticket summary) +- [ ] Commented the code, particularly in hard-to-understand areas +- [ ] Added tests that prove my fix is effective or that my feature works +- [ ] Ran `npm test` and it passes +- [ ] Changes generate no new warnings +- [ ] Changes are backward compatible with older clusters diff --git a/scripts/cortex b/scripts/cortex index 4c9800db..1cfd5707 100755 --- a/scripts/cortex +++ b/scripts/cortex @@ -1,6 +1,6 @@ #!/bin/bash -# Copyright 2018 Cognitive Scale, Inc. All Rights Reserved. +# Copyright 2023 Cognitive Scale, Inc. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the “License”); # you may not use this file except in compliance with the License. diff --git a/scripts/dockerTag.sh b/scripts/dockerTag.sh new file mode 100755 index 00000000..d4fabfa3 --- /dev/null +++ b/scripts/dockerTag.sh @@ -0,0 +1,33 @@ +#!/bin/bash -eu + +# PREREQS +# - Login against the secured docker registry with JWT as the password, docker login private-registry.${CORTEX_ENV}.insights.ai + +### +# +# USAGE: ./dockerTag.sh <dockerhubOrg> <dockerImageNameAndTag> <cortexTenant> [<cortexDeployment>] +# Ex. ./dockerTag.sh c12e ansible:latest colttest cortex-dev +# +### + + +# Organization/User Namespace in DockerHub +DOCKERHUB_ORG=${1:-c12e} +# Image name published to DockerHub +# The image name and tag that exists in DockerHub +IMAGE=${2:-ansible} +# The Cortex account/tenant name +TENANT_NAME=${3:-company} +# The Cortex deployment that the tenant exists in, should be not needed for PROD +CORTEX_ENV=${4:-cortex} +SECURED_REGISTRY=private-registry.${CORTEX_ENV}.insights.ai + +echo "Attempting to pull '${DOCKERHUB_ORG}/${IMAGE}' from hub.docker.com" +docker pull ${DOCKERHUB_ORG}/${IMAGE} || (echo "Unable to pull '${DOCKERHUB_ORG}/${IMAGE}'" && exit 1) + +echo "Retagging '${DOCKERHUB_ORG}/${IMAGE}' as '${SECURED_REGISTRY}/${TENANT_NAME}/${IMAGE}'" +docker tag ${DOCKERHUB_ORG}/${IMAGE} ${SECURED_REGISTRY}/${TENANT_NAME}/${IMAGE} + +echo "Pushing '${SECURED_REGISTRY}/${TENANT_NAME}/${IMAGE}'" +docker push ${SECURED_REGISTRY}/${TENANT_NAME}/${IMAGE} || (echo "Unable to push '${SECURED_REGISTRY}/${TENANT_NAME}/${IMAGE}', Are you sure you logged in?" && exit 1) + diff --git a/scripts/entrypoint.sh b/scripts/entrypoint.sh new file mode 100755 index 00000000..1386c6c8 --- /dev/null +++ b/scripts/entrypoint.sh @@ -0,0 +1,2 @@ +#!/bin/bash +/usr/local/bin/cortex $@ diff --git a/sonar-project.properties b/sonar-project.properties new file mode 100644 index 00000000..f7160179 --- /dev/null +++ b/sonar-project.properties @@ -0,0 +1,14 @@ +# Required metadata +sonar.projectKey=cortex-cli +sonar.projectName=cortex-cli +sonar.links.homepage=https://github.com/CognitiveScale/cortex-cli +sonar.links.issue=https://cognitivescale.atlassian.net/jira/your-work + +sonar.sources=bin,client,src +sonar.tests=test +sonar.exclusions=node_modules/*,Docker* +sonar.language=js +sonar.profile=node +# Encoding of the source code +sonar.sourceEncoding=UTF-8 +sonar.javascript.lcov.reportPaths=coverage/lcov.info diff --git a/src/client/accounts.js b/src/client/accounts.js deleted file mode 100644 index ca41c39e..00000000 --- a/src/client/accounts.js +++ /dev/null @@ -1,234 +0,0 @@ -/* - * Copyright 2018 Cognitive Scale, Inc. All Rights Reserved. - * - * 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. - */ - -const request = require('superagent'); -const debug = require('debug')('cortex:cli'); -const { constructError } = require('../commands/utils'); - -module.exports = class Accounts { - - constructor(cortexUrl) { - this.cortexUrl = cortexUrl; - this.endpoint = `${cortexUrl}/v2/accounts`; - this.groupsEndpoint = `${this.endpoint}/groups`; - this.resourcesEndpoint = `${this.endpoint}/resources`; - } - - listGroups(token) { - const endpoint = this.groupsEndpoint; - debug('listGroups => %s', endpoint); - return request - .get(endpoint) - .set('Authorization', `Bearer ${token}`) - .then((res) => { - if (res.ok) { - return {success: true, result: res.body}; - } - return {success: false, status: res.status, message: res.body}; - }) - .catch((err) => { - return constructError(err); - }); - } - - describeGroup(token, groupName) { - const endpoint = `${this.groupsEndpoint}/${groupName}`; - debug('describeGroup($s) => %s', groupName, endpoint); - return request - .get(endpoint) - .set('Authorization', `Bearer ${token}`) - .then((res) => { - if (res.ok) { - return {success: true, result: res.body}; - } - return {success: false, status: res.status, message: res.body}; - }) - .catch((err) => { - return constructError(err); - }); - } - - createGroup(token, groupName, description) { - const endpoint = this.groupsEndpoint; - debug('createGroup(%s) => %s', groupName, endpoint); - let body = { - name: groupName - }; - if (description) - body.description = description; - return request - .post(endpoint) - .set('Authorization', `Bearer ${token}`) - .send(body) - .then((res) => { - if (res.ok) { - return {success: true, result: res.body}; - } - return {success: false, status: res.status, message: res.body}; - }) - .catch((err) => { - return constructError(err); - }); - } - - addMembersToGroup(token, groupName, members) { - const endpoint = `${this.groupsEndpoint}/${groupName}/members`; - debug('addMembersToGroup => %s', endpoint); - return request - .put(endpoint) - .set('Authorization', `Bearer ${token}`) - .send({ - members: members - }) - .then((res) => { - if (res.ok) { - return {success: true, result: res.body}; - } - return {success: false, status: res.status, message: res.body}; - }) - .catch((err) => { - return constructError(err); - }); - } - - deleteGroup(token, groupName) { - const endpoint = `${this.groupsEndpoint}/${groupName}`; - debug('deleteGroup($s) => %s', groupName, endpoint); - return request - .delete(endpoint) - .set('Authorization', `Bearer ${token}`) - .then((res) => { - if (res.ok) { - return {success: true, result: res.body}; - } - return {success: false, status: res.status, message: res.body}; - }) - .catch((err) => { - return constructError(err); - }); - } - - removeMembersFromGroup(token, groupName, members) { - const endpoint = `${this.groupsEndpoint}/${groupName}/members`; - debug('removeMembersFromGroup($s) => %s', groupName, endpoint); - return request - .delete(endpoint) - .set('Authorization', `Bearer ${token}`) - .send({ - members: members - }) - .then((res) => { - if (res.ok) { - return {success: true, result: res.body}; - } - return {success: false, status: res.status, message: res.body}; - }) - .catch((err) => { - return constructError(err); - }); - } - - registerResource(token, resourceName, description, access) { - const endpoint = this.resourcesEndpoint; - debug('registerResource(%s) => %s', resourceName, endpoint); - let body = { - name: resourceName - }; - if (description) - body.description = description; - if (access) - body.access = access; - return request - .post(endpoint) - .set('Authorization', `Bearer ${token}`) - .send(body) - .then((res) => { - if (res.ok) { - return {success: true, result: res.body}; - } - return {success: false, status: res.status, message: res.body}; - }) - .catch((err) => { - return constructError(err); - }); - } - - grantGroupAccessToResource(token, resourceId, groupName) { - const endpoint = `${this.resourcesEndpoint}/${resourceId}/access`; - debug('grantGroupAccessToResource(%s) => %s', resourceId, endpoint); - return request - .put(endpoint) - .set('Authorization', `Bearer ${token}`) - .send({ - groupId: groupName - }) - .then((res) => { - if (res.ok) { - return {success: true, result: res.body}; - } - return {success: false, status: res.status, message: res.body}; - }) - .catch((err) => { - return constructError(err); - }); - } - - // TODO: To be implemented - // listResources(token) { - // const endpoint = this.resourcesEndpoint; - // debug('listResources => %s', endpoint); - // return request - // .get(endpoint) - // .set('Authorization', `Bearer ${token}`) - // .then((res) => { - // if (res.ok) { - // return {success: true, result: res.body}; - // } - // return {success: false, status: res.status, message: res.body}; - // }); - // } - - // TODO: To be implemented - // unregisterResource(token, resourceName) { - // const endpoint = `${this.resourcesEndpoint}/${resourceName}`; - // debug('unregisterResource(%s) => %s', resourceName, endpoint); - // return request - // .delete(endpoint) - // .set('Authorization', `Bearer ${token}`) - // .then((res) => { - // if (res.ok) { - // return {success: true, result: res.body}; - // } - // return {success: false, status: res.status, message: res.body}; - // }); - // } - - // TODO: To be implemented - // revokeGroupAccessToResource(token, resourceId) { - // const endpoint = `${this.resourcesEndpoint}/${resourceId}/access`; - // debug('revokeGroupAccessToResource(%s) => %s', resourceId, endpoint); - // return request - // .delete(endpoint) - // .set('Authorization', `Bearer ${token}`) - // .then((res) => { - // if (res.ok) { - // return {success: true, result: res.body}; - // } - // return {success: false, status: res.status, message: res.body}; - // }); - // } - -}; diff --git a/src/client/actions.js b/src/client/actions.js index 0bdc9c09..c3b1a23a 100644 --- a/src/client/actions.js +++ b/src/client/actions.js @@ -1,142 +1,134 @@ -/* - * Copyright 2018 Cognitive Scale, Inc. All Rights Reserved. - * - * 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. - */ - -const request = require('superagent'); -const debug = require('debug')('cortex:cli'); -const { constructError } = require('../commands/utils'); - -module.exports = class Actions { +import _ from 'lodash'; +import debugSetup from 'debug'; +import { got, defaultHeaders } from './apiutils.js'; +import { checkProject, printWarning } from '../commands/utils.js'; +import dockerCli from './dockerClient.js'; +const debug = debugSetup('cortex:cli'); +export default class Actions { constructor(cortexUrl) { this.cortexUrl = cortexUrl; - this.endpoint = `${cortexUrl}/v2/actions`; - this.endpointV3 = `${cortexUrl}/v3/actions`; + this.endpointV4 = (projectId) => `${cortexUrl}/fabric/v4/projects/${projectId}/actions`; } - invokeAction(token, actionName, path, params, actionType, method) { - let endpoint = `${this.endpointV3}/${actionName}/invoke`; - if (path) { - endpoint = `${endpoint}/${path}` + async deployAction(projectId, token, actionInst) { + checkProject(projectId); + let endpoint = this.endpointV4(projectId); + if (actionInst.actionType) { + endpoint = `${endpoint}?actionType=${actionInst.actionType}`; } - if (actionType) { - endpoint = `${endpoint}?actionType=${actionType}` + debug('deployAction(%s, docker=%s, ttl=%s) => %s', actionInst.name, actionInst.image, actionInst.ttl, endpoint); + const body = { ...actionInst }; + // image & docker floating around fixup just in case. + if (body.docker) { + body.image = body.docker; + delete body.docker; } - debug('invokeAction(%s) => %s', actionName, endpoint); - - const req = request(method || 'POST', endpoint) - .set('Authorization', `Bearer ${token}`) - .send(params); - + try { + body.docker = await this._maybePushDockerImage(actionInst.image, token, actionInst.pushDocker); + } catch (error) { + return { success: false, status: 400, message: error.message || error }; + } + return got + .post(endpoint, { + headers: defaultHeaders(token), + json: body, + }).json(); + } - return req.then((res) => { - if (res.ok) { - return {success: true, result: res.body}; - } - return {success: false, status: res.status, message: res.body}; - }) - .catch((err) => { - return constructError(err); - }); + listActions(projectId, token, filter, limit, skip, sort) { + checkProject(projectId); + debug('listActions() => %s', this.endpointV4(projectId)); + const query = {}; + if (filter) query.filter = filter; + if (limit) query.limit = limit; + if (sort) query.sort = sort; + if (skip) query.skip = skip; + return got + .get(this.endpointV4(projectId), { + headers: defaultHeaders(token), + searchParams: query, + }).json(); } - deployAction(token, actionName, docker, kind, code, memory, timeout, actionType, command, ports, environment) { - let endpoint = `${this.endpointV3}`; - if (actionType) { - endpoint = `${endpoint}?actionType=${actionType}`; - } - debug('deployAction(%s, docker=%s, kind=%s, code=%s, memory=%s, timeout=%s) => %s', - actionName, docker, kind, code, memory, timeout, endpoint); + describeAction(projectId, token, actionName) { + checkProject(projectId); + const endpoint = `${this.endpointV4(projectId)}/${encodeURIComponent(actionName)}`; + debug('describeAction(%s) => %s', actionName, endpoint); + return got + .get(endpoint, { headers: defaultHeaders(token) }) + .json(); + } - const req = request - .post(endpoint) - .set('Authorization', `Bearer ${token}`) - .field('name', actionName); + getLogsAction(projectId, token, actionName) { + checkProject(projectId); + const endpoint = `${this.endpointV4(projectId)}/${encodeURIComponent(actionName)}/logs`; + debug('getLogsAction(%s) => %s', actionName, endpoint); + return got + .get(endpoint, { headers: defaultHeaders(token) }) + .json(); + } - if (docker) req.field('docker', docker); - if (kind) req.field('kind', kind); - if (memory) req.field('memory', memory); - if (timeout) req.field('timeout', timeout); - if (code) req.attach('code', code); - if (command) req.field('command', command); - if (ports) req.field('ports', ports); - if (environment) req.field('environment', environment); + deleteAction(projectId, token, actionName) { + checkProject(projectId); + const endpoint = `${this.endpointV4(projectId)}/${encodeURIComponent(actionName)}`; + debug('deleteAction(%s) => %s', actionName, endpoint); + return got + .delete(endpoint, { + headers: defaultHeaders(token), + }) + .json(); + } - return req.then((res) => { - if (res.ok) { - return {success: true, message: res.body}; - } - return {success: false, status: res.status, message: res.body}; + getConfig(projectId, token) { + checkProject(projectId); + const endpoint = _.join([this.endpointV4(projectId), '_config'], '/'); + debug('getConfig() => %s', endpoint); + return got + .get(endpoint, { + headers: defaultHeaders(token), }) - .catch((err) => { - return constructError(err); - }); + .json(); } - listActions(token) { - debug('listActions() => %s', this.endpointV3); - return request - .get(this.endpointV3) - .set('Authorization', `Bearer ${token}`) - .then((res) => { - if (res.ok) { - return {success: true, actions: res.body.functions}; - } - return {success: false, status: res.status, message: res.body}; - }) - .catch((err) => { - return constructError(err); - }); + static getCanonicalJobId(jobId) { + let canonicalJobId = jobId; + const namespaceProvided = /\w\/\w/.test(jobId); + if (namespaceProvided) { + canonicalJobId = encodeURIComponent(jobId); + } + return canonicalJobId; + } + async _cortexRegistryUrl(token) { + const res = await this.getConfig(token); + if (res.success) return res.config.dockerPrivateRegistryUrl; + throw res; } - describeAction(token, actionName) { - const endpoint = `${this.endpointV3}/${actionName}`; - debug('describeAction(%s) => %s', actionName, endpoint); - return request - .get(endpoint) - .set('Authorization', `Bearer ${token}`) - .then((res) => { - if (res.ok) { - return {success: true, action: res.body.function}; - } - return {success: false, status: res.status, message: res.body}; - }) - .catch((err) => { - return constructError(err); - }); + _cortexRegistryImagePath(registryUrl, imageRepo, tenant) { + const imageName = _.split(imageRepo, '/').slice(-1)[0]; + return _.join([registryUrl, tenant, imageName], '/'); } - deleteAction(token, actionName, actionType) { - let endpoint = `${this.endpointV3}/${actionName}`; - if (actionType) { - endpoint = `${endpoint}?actionType=${actionType}` + async _maybePushDockerImage(image, token, pushDocker) { + // This is a helper to push a local or remote image to a private registry + if (!image || !pushDocker) { + return image; + } + const registryUrl = await this._cortexRegistryUrl(token); + const imageName = image.replace(/.+\..+\//, ''); + const cortexImageUrl = this._cortexRegistryImagePath(registryUrl, imageName, ''); + // TODO support non cortex registry !! + await dockerCli().login(registryUrl, 'cli', token); + try { + await dockerCli() + .pull(image); + } catch (err) { + printWarning(`Docker pull failed using local image: ${err.message}`); } - debug('deleteAction(%s, %s) => %s', actionName, actionType, endpoint); - return request - .delete(endpoint) - .set('Authorization', `Bearer ${token}`) - .accept('application/json') - .then((res) => { - if (res.ok) { - return {success: true, action: res.body.action}; - } - return {success: false, status: res.status, message: res.body}; - }) - .catch((err) => { - return constructError(err); - }); + await dockerCli().tag(image, cortexImageUrl); + await dockerCli().push(cortexImageUrl); + return cortexImageUrl; } -}; +} diff --git a/src/client/agents.js b/src/client/agents.js index 09a54ad0..46e81cde 100644 --- a/src/client/agents.js +++ b/src/client/agents.js @@ -1,5 +1,5 @@ /* - * Copyright 2018 Cognitive Scale, Inc. All Rights Reserved. + * Copyright 2023 Cognitive Scale, Inc. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the “License”); * you may not use this file except in compliance with the License. @@ -13,191 +13,121 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import debugSetup from 'debug'; +import { got, defaultHeaders } from './apiutils.js'; +import { checkProject } from '../commands/utils.js'; -const request = require('superagent'); -const debug = require('debug')('cortex:cli'); -const { constructError } = require('../commands/utils'); - -module.exports = class Agents { - +const debug = debugSetup('cortex:cli'); +export default (class Agents { constructor(cortexUrl) { this.cortexUrl = cortexUrl; - this.endpoint = `${cortexUrl}/v2/agents`; - this.endpointV3 = `${cortexUrl}/v3/agents`; + this.endpointV4 = (projectId) => `${cortexUrl}/fabric/v4/projects/${projectId}`; } - invokeAgentService(token, agentName, serviceName, params) { - const endpoint = `${this.endpointV3}/${agentName}/services/${serviceName}`; + invokeAgentService(projectId, token, agentName, serviceName, params, options) { + checkProject(projectId); + const endpoint = `${this.endpointV4(projectId)}/agentinvoke/${encodeURIComponent(agentName)}/services/${serviceName}`; debug('invokeAgentService(%s, %s) => %s', agentName, serviceName, endpoint); - return request - .post(endpoint) - .set('Authorization', `Bearer ${token}`) - .send(params) - .then((res) => { - if (res.ok) { - return {success: true, result: res.body}; - } - return {success: false, status: res.status, message: res.body}; - }) - .catch((err) => { - return constructError(err); - }); - } - - getServiceActivation(token, activationId) { - const endpoint = `${this.endpointV3}/services/activations/${activationId}`; - debug('getServiceActivation(%s) => %s', activationId, endpoint); - return request - .get(endpoint) - .set('Authorization', `Bearer ${token}`) - .then((res) => { - if (res.ok) { - return {success: true, result: res.body}; - } - return {success: false, status: res.status, message: res.body}; - }) - .catch((err) => { - return constructError(err); - }); + return got + .post(endpoint, { + headers: defaultHeaders(token), + json: params, + searchParams: { sync: options.sync, scheduleName: options.scheduleName, scheduleCron: options.scheduleCron }, + }).json(); } - listAgentSnapshots(token, agentName) { - const endpoint = `${this.endpoint}/snapshots/${agentName}`; - debug('listAgentSnapshots(%s) => %s', agentName, endpoint); - return request - .get(endpoint) - .set('Authorization', `Bearer ${token}`) - .then((res) => { - if (res.ok) { - return {success: true, result: res.body}; - } - return {success: false, status: res.status, message: res.body}; - }) - .catch((err) => { - return constructError(err); - }); - } - - createAgentSnapshot(token, snapshot) { - const agentName = snapshot.agentName; - const endpoint = `${this.endpoint}/snapshots`; - debug('getAgentSnapshot=> %s', endpoint); - return request - .post(endpoint) - .set('Authorization', `Bearer ${token}`) - .send(snapshot) - .then((res) => { - if (res.ok) { - return {success: true, result: res.body}; - } - return {success: false, status: res.status, message: res.body}; - }) - .catch((err) => { - return constructError(err); - }); + invokeSkill(projectId, token, skillName, inputName, params, sync) { + checkProject(projectId); + const endpoint = `${this.endpointV4(projectId)}/skillinvoke/${encodeURIComponent(skillName)}/inputs/${inputName}`; + debug('invokeSkill(%s, %s) => %s', skillName, inputName, endpoint); + return got + .post(endpoint, { + headers: defaultHeaders(token), + json: params, + searchParams: { sync }, + }).json(); } - listAgentInstances(token, agentName) { - const endpoint = `${this.endpoint}/instances/${agentName}`; - debug('getAgentInstances(%s) => %s', agentName, endpoint); - return request - .get(endpoint) - .set('Authorization', `Bearer ${token}`) - .then((res) => { - if (res.ok) { - return {success: true, result: res.body}; - } - return {success: false, status: res.status, message: res.body}; - }) - .catch((err) => { - return constructError(err); - }); + getActivation(projectId, token, activationId, verbose, report) { + checkProject(projectId); + const endpoint = `${this.endpointV4(projectId)}/activations/${activationId}`; + debug('getActivation(%s, %s, %s) => %s', activationId, verbose, report, endpoint); + const opts = { + headers: defaultHeaders(token), + }; + if (verbose) { + opts.searchParams = { verbose }; + } + if (report) { + opts.searchParams = { report }; + } + return got + .get(endpoint, opts).json(); } - createAgentInstance(token, instance) { - const endpoint = `${this.endpoint}/instances`; - debug('createAgentInstance => %s', endpoint); - return request - .post(endpoint) - .set('Authorization', `Bearer ${token}`) - .send(instance) - .type('json') - .then((res) => { - if (res.ok) { - return {success: true, result: res.body}; - } - return {success: false, status: res.status, message: res.body}; - }) - .catch((err) => { - return constructError(err); - }); + cancelActivation(projectId, token, activationId, inFlight) { + checkProject(projectId); + const endpoint = `${this.endpointV4(projectId)}/activations/${activationId}/cancel`; + debug('cancelActivation(%s, %b) => %s', activationId, inFlight, endpoint); + const opts = { + headers: defaultHeaders(token), + }; + if (inFlight) { + opts.searchParams = { inFlight }; + } + return got.post(endpoint, opts).json(); } - getAgentInstance(token, instanceId) { - const endpoint = `${this.endpoint}/instances/${instanceId}`; - debug('getAgentInstance => %s', endpoint); - return request - .get(endpoint) - .set('Authorization', `Bearer ${token}`) - .then((res) => { - if (res.ok) { - return {success: true, result: res.body}; - } - return {success: false, status: res.status, message: res.body}; - }) - .catch((err) => { - return constructError(err); - }); + listActivations(projectId, token, params) { + checkProject(projectId); + const endpoint = `${this.endpointV4(projectId)}/activations`; + debug('listActivations(%s) => %s', params.agentName, endpoint); + const opts = { headers: defaultHeaders(token) }; + if (params) { + opts.searchParams = params; + } + return got + .get(endpoint, opts).json(); } - deleteAgentInstance(token, instanceId) { - const endpoint = `${this.endpoint}/instances/${instanceId}`; - debug('deleteAgentInstance => %s', endpoint); - return request - .delete(endpoint) - .set('Authorization', `Bearer ${token}`) - .then((res) => { - if (res.ok) { - return {success: true, result: res.body}; - } - return {success: false, status: res.status, message: res.body}; - }) - .catch((err) => { - return constructError(err); - }); + listAgentSnapshots(projectId, token, agentName, filter, limit, skip, sort) { + checkProject(projectId); + const endpoint = `${this.endpointV4(projectId)}/agents/${encodeURIComponent(agentName)}/snapshots`; + debug('listAgentSnapshots(%s) => %s', agentName, endpoint); + const query = {}; + if (filter) query.filter = filter; + if (limit) query.limit = limit; + if (sort) { + query.sort = sort; + query.mongoRawSort = true; + } + if (skip) query.skip = skip; + return got + .get(endpoint, { + headers: defaultHeaders(token), + searchParams: query, + }).json(); } - stopAgentInstance(token, instanceId) { - const endpoint = `${this.endpoint}/instances/${instanceId}/stop`; - debug('stopAgentInstance => %s', endpoint); - return request - .post(endpoint) - .set('Authorization', `Bearer ${token}`) - .then((res) => { - if (res.ok) { - return {success: true, result: res.body}; - } - return {success: false, status: res.status, message: res.body}; - }) - .catch((err) => { - return constructError(err); - }); + describeAgentSnapshot(projectId, token, snapshotId, output) { + checkProject(projectId); + const endpoint = `${this.endpointV4(projectId)}/snapshots/${snapshotId}?deps=true`; + debug('describeAgentSnapshot(%s) => %s', snapshotId, endpoint); + return got + .get(endpoint, { + headers: defaultHeaders(token), + searchParams: { output, deps: true }, + }).text(); } - listTriggers(token) { - const endpoint = `${this.endpoint}/triggers`; - debug('listTriggers => %s', endpoint); - return request - .get(endpoint) - .set('Authorization', `Bearer ${token}`) - .then((res) => { - if (res.ok) { - return {success: true, result: res.body}; - } - return {success: false, status: res.status, message: res.body}; - }) - .catch((err) => { - return constructError(err); - }); + createAgentSnapshot(projectId, token, snapshot) { + checkProject(projectId); + const endpoint = `${this.endpointV4(projectId)}/agents/${encodeURIComponent(snapshot.agentName)}/snapshots`; + debug('createAgentSnapshot(%s)=> %s', snapshot, endpoint); + return got + .post(endpoint, { + headers: defaultHeaders(token), + json: snapshot, + }).json(); } -}; \ No newline at end of file +}); diff --git a/src/client/apiServerClient.js b/src/client/apiServerClient.js new file mode 100644 index 00000000..92934816 --- /dev/null +++ b/src/client/apiServerClient.js @@ -0,0 +1,119 @@ +import _ from 'lodash'; +// eslint-disable-next-line import/no-unresolved +import { GraphQLClient, gql } from 'graphql-request'; +import { defaultHeaders } from './apiutils.js'; + +export default (class ApiServerClient { + constructor(cortexUrl) { + this.cortexUrl = cortexUrl; + this.endpoint = `${cortexUrl}/fabric/v4/graphql`; + } + + _client(token) { + return new GraphQLClient(this.endpoint, { + headers: defaultHeaders(token), + redirect: 'error', + }); + } + + /** + * Query project using graphql + * @param token + * @param projectId + * @return {Promise<any>} + */ + async getProject(token, projectId) { + const fetched = await this._client(token) + .request(gql`query { projectByName( name: "${projectId}" ){name, title, description}}`); + return _.get(fetched, 'projectByName'); + } + + /** + * Query project using graphql + * @param token + * @return {Promise<any>} + */ + async listProjects(token, filter, limit, skip, sort) { + const fetched = await this._client(token) + .request(gql`{ + projects ( filter: "${filter}", limit: "${limit}", skip: "${skip}", sort: "${sort}" ) { name, title, description} } + `); + return _.get(fetched, 'projects', []); + } + + /** + * Query project using graphql + * @param token + * @param projDef + * @return {Promise<any>} + */ + async createProject(token, projDef) { + const fetched = await this._client(token) + .request(gql`mutation NewProject($input: CreateProjectInput!) { createProject(input: $input){name}}`, { input: projDef }); + return _.get(fetched, 'createProject', {}); + } + + /** + * Delete project using graphql + * @param token + * @param projectId + * @return {Promise<any>} + */ + async deleteProject(token, projectId) { + const fetched = await this._client(token) + .request(gql`mutation { deleteProject(name: "${projectId}") }`); + return _.get(fetched, 'deleteProject', {}); + } + + /** + * Fetch campaign using graphql + * @param projectId + * @param token + * @param campaignId + * @return {Promise<any>} + */ + async getCampaign(projectId, token, campaignId) { + const fetched = await this._client(token) + .request(gql`query { campaignByName( project: "${projectId}", name: "${campaignId}" ){name, title, description}}`); + return _.get(fetched, 'campaignByName'); + } + + /** + * Query campaign using graphql + * @param projectId + * @param token + * @return {Promise<any>} + */ + async listCampaigns(projectId, token, filter, limit, skip, sort) { + const fetched = await this._client(token) + .request(gql`{ campaigns ( project: "${projectId}", filter: "${filter}", limit: "${limit}", skip: "${skip}", sort: "${sort}" ) { name, title, description} }`); + return _.get(fetched, 'campaigns', []); + } + + /** + * Query model list using graphql + * @param projectId + * @param offset + * @param limit + * @param token + * @return {Promise<any>} + */ + async listModels(projectId, offset, limit, token) { + const fetched = await this._client(token) + .request(gql`{ models ( project: "${projectId}", offset: ${offset}, limit: ${limit} ) { name, title, description } }`); + return _.get(fetched, 'models', []); + } + + /** + * Query model description using graphql + * @param projectId + * @param name + * @param token + * @return {Promise<any>} + */ + async descModels(projectId, name, token) { + const fetched = await this._client(token) + .request(gql`{ modelByName ( project: "${projectId}", name: "${name}" ) { name, description, model_mode, source, status, title, type } }`); + return _.get(fetched, 'modelByName', []); + } +}); diff --git a/src/client/apiutils.js b/src/client/apiutils.js new file mode 100644 index 00000000..a8287834 --- /dev/null +++ b/src/client/apiutils.js @@ -0,0 +1,179 @@ +import _ from 'lodash'; +// eslint-disable-next-line import/no-unresolved +import got from 'got'; +import debugSetup from 'debug'; +import os from 'os'; +import { readPackageJSON } from '../commands/utils.js'; + +const pkg = readPackageJSON('../../package.json'); + +const debug = debugSetup('cortex:cli'); +function getUserAgent() { + return `${pkg.name}/${pkg.version} (${os.platform()}; ${os.arch()}; ${os.release()}; ${os.platform()})`; +} + +const gotOpts = { + followRedirect: false, + // Put a reasonable timeout, see: https://github.com/sindresorhus/got/blob/main/documentation/6-timeout.md + timeout: { + lookup: 75, // greater than 100 not recommended + connect: 400, + secureConnect: 400, + socket: 1000, + // send: 10000, // Not "SAFE" with large uploads + response: 2000, + }, + retry: { + limit: 3, // 3 retries + }, + headers: { + 'user-agent': getUserAgent(), + }, + hooks: { + beforeRequest: [ + (options) => { + if (debug.enabled) { + debug(`request url: ${options.url}`); + debug(`request methods: ${options.method}`); + debug(`request headers: ${JSON.stringify(options.headers)}`); + debug(`request json: ${JSON.stringify(options.json)}`); + } + }, + ], + afterResponse: [ + (res) => { + if (res.headers['x-auth-error']) { + res.statusCode = 401; + throw new Error(`Auth Error: ${res.headers['x-auth-error']}`); + } + if (debug.enabled) { + debug(`response http code: ${res.statusCode}`); + debug(`response headers: ${JSON.stringify(res.headers)}`); + debug(`response body: ${res.body}`); + debug(`response time(ms): ${JSON.stringify(_.get(res, 'timings.phases'))}`); + } + return res; + }, + ], + }, +}; + +// TODO: convert to ES6 class (use getters, etc.) or Typescript? +function gotEnvValues() { + return { + timeout: [ + { + envVar: 'CORTEX_TIMEOUT_LOOKUP', + envValue: process.env.CORTEX_TIMEOUT_LOOKUP, + defaultValue: gotOpts.timeout.lookup, + key: 'lookup', + }, + { + envVar: 'CORTEX_TIMEOUT_CONNECT', + envValue: process.env.CORTEX_TIMEOUT_CONNECT, + defaultValue: gotOpts.timeout.connect, + key: 'connect', + }, + { + envVar: 'CORTEX_TIMEOUT_SECURE_CONNECT', + envValue: process.env.CORTEX_TIMEOUT_SECURE_CONNECT, + defaultValue: gotOpts.timeout.secureConnect, + key: 'secureConnect', + }, + { + envVar: 'CORTEX_TIMEOUT_SOCKET', + envValue: process.env.CORTEX_TIMEOUT_SOCKET, + defaultValue: gotOpts.timeout.socket, + key: 'socket', + }, + // { + // envVar: 'CORTEX_TIMEOUT_SEND', + // envValue: process.env.CORTEX_TIMEOUT_SEND, + // defaultValue: gotOpts.timeout.send, + // key: 'send', + // }, + { + envVar: 'CORTEX_TIMEOUT_RESPONSE', + envValue: process.env.CORTEX_TIMEOUT_RESPONSE, + defaultValue: gotOpts.timeout.response, + key: 'response', + }, + + ], + retry: [ + { + envVar: 'CORTEX_API_RETRY', + envValue: process.env.CORTEX_API_RETRY, + defaultValue: gotOpts.retry.limit, + key: 'limit', + }, + ], + }; +} + +export function getGotEnvOverrides() { + function resolveNumericValue(v) { + // if its nullish -> keep the default + // if its a non-negative number -> parse as an int, then apply to k + // if its falsey or not a number -> use no timeout + if (v == null) { + return null; // use default + } + const value = parseInt(v, 10); + if (value >= 0) { + return value; // user defined int + } + return false; // use no timeout + } + + // NOTE: 'parsedValue' & 'userDefined' fields are resolved dynamically + const values = gotEnvValues(); + Object.keys(values.timeout).forEach((k) => { + // Parse each environment variable override. If result is (bool) false, + // then the key should NOT be set. If it's nullish, then the user didn't + // set a value. + const entry = values.timeout[k]; + entry.parsedValue = resolveNumericValue(entry.envValue); + entry.skipAssignment = (entry.parsedValue === false); + entry.userDefined = (entry.parsedValue != null); + }); + Object.keys(values.retry).forEach((k) => { + // Ditto (from above). + const entry = values.retry[k]; + entry.parsedValue = resolveNumericValue(entry.envValue); + entry.skipAssignment = (entry.parsedValue === false); + entry.userDefined = (entry.parsedValue != null); + }); + debug('got Environment Variables: %s', JSON.stringify(values)); + return values; +} + +export function getTimeoutUnit() { + return 'ms'; +} + +function getEnvOptions() { + const retry = {}; + const timeout = {}; + if (process.env.CORTEX_TIMEOUT_IGNORE) { + debug('Ignoring all timeout configuration from env variables'); + } else { + debug('Loading timeout configuration from env variables'); + const { timeout: timeoutValues, retry: retryValues } = getGotEnvOverrides(); + timeoutValues.forEach((t) => { + if (!t.skipAssignment) { + timeout[t.key] = t.parsedValue ?? t.defaultValue; // could use 'userDefined', but ?? is shorter + } + }); + retryValues.forEach((v) => { + if (!v.skipAssignment) { + retry[v.key] = v.parsedValue ?? v.defaultValue; // could use 'userDefined', but ?? is shorter + } + }); + } + return { ...gotOpts, timeout, retry }; +} + +const gotExt = got.extend(getEnvOptions()); +export const defaultHeaders = (token, otherHeaders = {}) => ({ Authorization: `Bearer ${token}`, ...otherHeaders }); +export { gotExt as got }; diff --git a/src/client/assessments.js b/src/client/assessments.js new file mode 100644 index 00000000..8ad804f4 --- /dev/null +++ b/src/client/assessments.js @@ -0,0 +1,140 @@ +import querystring from 'querystring'; +import stream from 'node:stream'; +import { createWriteStream } from 'node:fs'; +import debugSetup from 'debug'; +import { got, defaultHeaders } from './apiutils.js'; +import { printSuccess, printError, constructError } from '../commands/utils.js'; + +const { pipeline } = stream; +const debug = debugSetup('cortex:cli'); +export default (class Assessments { + constructor(cortexUrl) { + this.endpointV4 = `${cortexUrl}/fabric/v4/impactassessment`; + this.endpointApiV4 = `${cortexUrl}/fabric/v4/dependencies`; + } + + getDependenciesOfResource(token, project, type, name, json = null, missing = false) { + const url = `${this.endpointApiV4}/tree/${project}/${type}/${encodeURIComponent(name)}?${querystring.stringify({ missing })}`; + debug('dependencyTree => %s', url); + const body = { headers: defaultHeaders(token) }; + if (json) { + body.json = json; + } + return got + .post(url, body) + .json(); + } + + queryResources(token, name, projectId, type, skip, limit, filter, sort) { + const url = `${this.endpointV4}/resources?${querystring.stringify({ + projectId, + name, + type, + skip, + limit, + filter, + sort, + })}`; + debug('queryResources => %s', url); + return got + .get(url, { headers: defaultHeaders(token) }) + .json(); + } + + listResourceTypes(token) { + const url = `${this.endpointV4}/types`; + debug('listResourceTypes => %s', url); + return got + .get(url, { headers: defaultHeaders(token) }) + .json(); + } + + createAssessment(token, name, title, description, scope, componentName, componentTypes, overwrite) { + const url = `${this.endpointV4}/assessments`; + debug('createAssessment => %s', url); + return got + .post(url, { + headers: defaultHeaders(token, { 'Content-Type': 'application/json' }), + body: JSON.stringify({ + name, + title, + description, + scope, + componentName, + componentTypes, + overwrite, + }), + }).json(); + } + + listAssessment(token, skip, limit, filter, sort) { + const url = `${this.endpointV4}/assessments?${querystring.stringify({ + skip, limit, filter, sort, + })}`; + debug('listAssessment => %s', url); + return got + .get(url, { headers: defaultHeaders(token) }).json(); + } + + getAssessment(token, name) { + const url = `${this.endpointV4}/assessments/${encodeURIComponent(name)}`; + debug('getAssessment => %s', url); + return got + .get(url, { + headers: defaultHeaders(token), + }).json(); + } + + deleteAssessment(token, name) { + const url = `${this.endpointV4}/assessments/${encodeURIComponent(name)}`; + debug('deleteAssessment => %s', url); + return got + .delete(url, { + headers: defaultHeaders(token), + }).json(); + } + + runAssessment(token, name) { + const url = `${this.endpointV4}/assessments/${encodeURIComponent(name)}/run`; + debug('runAssessment => %s', url); + return got + .post(url, { + headers: defaultHeaders(token), + }).json(); + } + + listAssessmentReports(token, name) { + const url = `${this.endpointV4}/assessments/${encodeURIComponent(name)}/reports`; + debug('listAssessmentReports => %s', url); + return got + .get(url, { + headers: defaultHeaders(token), + }).json(); + } + + getAssessmentReport(token, name, reportId) { + const url = `${this.endpointV4}/assessments/${encodeURIComponent(name)}/reports/${encodeURIComponent(reportId)}`; + debug('getAssessmentReport => %s', url); + return got + .get(url, { + headers: defaultHeaders(token), + }).json(); + } + + exportAssessmentReport(token, name, reportId, types) { + const options = { color: 'true' }; + const file = `${reportId.split('/').pop()}.csv`; + const filter = types && types.trim() ? `?components=${types.trim()}` : ''; + const url = `${this.endpointV4}/assessments/${encodeURIComponent(name)}/reports/${encodeURIComponent(reportId)}/export${filter}`; + debug('exportAssessmentReport => %s', url); + const rs = got.stream(url, { headers: defaultHeaders(token) }); + pipeline(rs, createWriteStream(file), (e) => { + if (e) { + const err = constructError(e); + printError(`Failed to export assessment "${name}" report "${reportId}": ${err.status} ${err.message}`, options); + } else { + printSuccess(`Report exported to file "${file}"`, options); + } + }); + } +}); diff --git a/src/client/auth.js b/src/client/auth.js deleted file mode 100644 index b3de4ea0..00000000 --- a/src/client/auth.js +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright 2018 Cognitive Scale, Inc. All Rights Reserved. - * - * 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. - */ - -const request = require('superagent'); -const debug = require('debug')('cortex:cli'); -const { constructError } = require('../commands/utils'); - -module.exports = class Auth { - - constructor(cortexUrl) { - this.cortexUrl = cortexUrl; - } - - login(tenantId, username, password) { - const endpoint = `${this.cortexUrl}/v2/admin/${tenantId}/users/authenticate`; - - debug('login(%s) => %s', username, endpoint); - - return request - .post(endpoint) - .send({ - username: username, - password: password - }) - .then((response) => { - if (response.ok) { - debug('login response: %o', response.body); - return response.body.jwt; - } - else { - throw new Error('Authentication failed'); - } - }) - .catch((err) => { - return constructError(err); - }); - } -}; \ No newline at end of file diff --git a/src/client/catalog.js b/src/client/catalog.js index 1be81eb3..3c281c45 100644 --- a/src/client/catalog.js +++ b/src/client/catalog.js @@ -1,5 +1,5 @@ /* - * Copyright 2018 Cognitive Scale, Inc. All Rights Reserved. + * Copyright 2023 Cognitive Scale, Inc. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the “License”); * you may not use this file except in compliance with the License. @@ -14,201 +14,542 @@ * limitations under the License. */ -const request = require('superagent'); -const debug = require('debug')('cortex:cli'); -const { constructError } = require('../commands/utils'); - -const createEndpoints = (baseUri) => { - return { - catalog: `${baseUri}/v2/catalog`, - skills: `${baseUri}/v3/catalog/skills`, - agents: `${baseUri}/v3/catalog/agents`, - types: `${baseUri}/v3/catalog/types`, - agentVersions: `${baseUri}/v3/agents/versions` - } -}; - -module.exports = class Catalog { +import debugSetup from 'debug'; +import FormData from 'form-data'; +import http from 'https'; +import qs from 'qs'; +import fs from 'node:fs'; +import { got, defaultHeaders } from './apiutils.js'; +import { + constructError, formatAllServiceInputParameters, checkProject, printSuccess, printError, printTable, +} from '../commands/utils.js'; +const debug = debugSetup('cortex:cli'); +const createEndpoints = (baseUri) => ({ + skills: (projectId) => `${baseUri}/fabric/v4/projects/${projectId}/skills`, + applications: (projectId) => `${baseUri}/fabric/v4/projects/${projectId}/applications`, + skillStereotype: (projectId) => `${baseUri}/fabric/v4/projects/${projectId}/skillStereotypes`, + agents: (projectId) => `${baseUri}/fabric/v4/projects/${projectId}/agents`, + agentinvoke: (projectId) => `${baseUri}/fabric/v4/projects/${projectId}/agentinvoke`, + types: (projectId) => `${baseUri}/fabric/v4/projects/${projectId}/types`, + campaigns: (projectId) => `${baseUri}/fabric/v4/projects/${projectId}/campaigns/`, +}); +export default class Catalog { constructor(cortexUrl) { this.cortexUrl = cortexUrl; this.endpoints = createEndpoints(cortexUrl); } - saveSkill(token, skillObj) { - debug('saveSkill(%s) => %s', skillObj.name, this.endpoints.skills); - return request - .post(this.endpoints.skills) - .set('Authorization', `Bearer ${token}`) - .send(skillObj) - .then((res) => { - if (res.ok) { - return {success: true, message: res.body}; - } - return {success: false, message: res.body, status: res.status}; + // Skill + deleteSkill(projectId, token, skillName) { + checkProject(projectId); + const endpoint = `${this.endpoints.skills(projectId)}/${encodeURIComponent(skillName)}`; + debug('deleteSkill(%s) => %s', skillName, endpoint); + return got + .delete(endpoint, { + headers: defaultHeaders(token), }) - .catch((err) => { - return constructError(err); - }); + .json(); } - listSkills(token) { - debug('listSkills() => %s', this.endpoints.skills); - return request - .get(this.endpoints.skills) - .set('Authorization', `Bearer ${token}`) - .then((res) => { - if (res.ok) { - return {success: true, skills: res.body.skills || res.body.processors}; - } - return {success: false, status: res.status, message: res.body}; - }) - .catch((err) => { - return constructError(err); - }); + saveSkill(projectId, token, skillObj) { + checkProject(projectId); + debug('saveSkill(%s) => %s', skillObj.name, this.endpoints.skills(projectId)); + return got + .post(this.endpoints.skills(projectId), { + headers: defaultHeaders(token), + json: skillObj, + }).json(); + } + + listSkills(projectId, token, query, filter, limit, skip, sort) { + checkProject(projectId); + debug('listSkills() => %s', this.endpoints.skills(projectId)); + if (filter) query.filter = filter; + if (limit) query.limit = limit; + if (sort) query.sort = sort; + if (skip) query.skip = skip; + return got + .get(this.endpoints.skills(projectId), { + headers: defaultHeaders(token), + searchParams: query, + }).json(); } - describeSkill(token, skillName) { - const endpoint = `${this.endpoints.skills}/${skillName}`; + describeSkill(projectId, token, skillName, verbose = false, output = 'json') { + checkProject(projectId); + const endpoint = `${this.endpoints.skills(projectId)}/${encodeURIComponent(skillName)}`; debug('describeSkill(%s) => %s', skillName, endpoint); - return request - .get(endpoint) - .set('Authorization', `Bearer ${token}`) - .then((res) => { - if (res.ok) { - return {success: true, skill: res.body}; - } - else { - return {success: false, message: res.body, status: res.status}; - } - }) - .catch((err) => { - return constructError(err); - }); + const searchParams = { output }; + if (verbose) { + searchParams.verbose = verbose; + searchParams.status = verbose; + } + const res = got.get(endpoint, { + headers: defaultHeaders(token), + searchParams, + }); + if (output.toLocaleLowerCase() === 'json') return res.json(); + return res.text(); } - listAgents(token) { - debug('listAgents() => %s', this.endpoints.agents); - return request - .get(this.endpoints.agents) - .set('Authorization', `Bearer ${token}`) - .then((res) => { - if (res.ok) { - return {success: true, agents: res.body.agents}; - } - return {success: false, status: res.status, message: res.body}; - }) - .catch((err) => { - return constructError(err); - }); + deploySkill(projectId, token, skillName, stereotypes, dryrun = false, verbose = false) { + checkProject(projectId); + const endpoint = `${this.endpoints.skills(projectId)}/${encodeURIComponent(skillName)}/deploy`; + debug('deploySkill(%s) => %s', skillName, endpoint); + return got + .get(endpoint, { + headers: defaultHeaders(token), + // Using qs as got doesn't handle serializing arrays well and this works with TSOA OOB + searchParams: qs.stringify({ verbose, dryrun, stereotypes }), + }).json(); } - saveAgent(token, agentObj) { - debug('saveAgent(%s) => %s', agentObj.name, this.endpoints.agents); - return request - .post(this.endpoints.agents) - .set('Authorization', `Bearer ${token}`) - .send(agentObj) - .then((res) => { - if (res.ok) { - return {success: true, message: res.body}; - } - return {success: false, message: res.body, status: res.status}; + unDeploySkill(projectId, token, skillName, verbose = false) { + checkProject(projectId); + const endpoint = `${this.endpoints.skills(projectId)}/${encodeURIComponent(skillName)}/undeploy`; + debug('undeploySkill(%s) => %s', skillName, endpoint); + return got + .get(endpoint, { + headers: defaultHeaders(token), + searchParams: { verbose }, + }).json(); + } + + skillLogs(projectId, token, skillName, actionName, raw = false) { + checkProject(projectId); + const endpoint = `${this.endpoints.skills(projectId)}/${encodeURIComponent(skillName)}/action/${encodeURIComponent(actionName)}/logs`; + debug('skillLogs(%s, %s) => %s', skillName, actionName, endpoint); + const gotOptions = { + headers: defaultHeaders(token), + searchParams: { raw }, + }; + return raw ? got.get(endpoint, gotOptions).text() : got.get(endpoint, gotOptions).json(); + } + + applicationLogs(projectId, token, name, raw = false) { + checkProject(projectId); + const endpoint = `${this.endpoints.applications(projectId)}/${encodeURIComponent(name)}/logs`; + debug('applicationLogs(%s, %s) => %s', name, endpoint); + const gotOptions = { + headers: defaultHeaders(token), + searchParams: { raw }, + }; + return raw ? got.get(endpoint, gotOptions).text() : got.get(endpoint, gotOptions).json(); + } + + // Agent + deleteAgent(projectId, token, agentName) { + checkProject(projectId); + const endpoint = `${this.endpoints.agents(projectId)}/${encodeURIComponent(agentName)}`; + debug('deleteAgent(%s) => %s', agentName, endpoint); + return got + .delete(endpoint, { + headers: defaultHeaders(token), }) - .catch((err) => { - return constructError(err); - }); + .json(); } - describeAgent(token, agentName) { - const endpoint = `${this.endpoints.agents}/${agentName}`; + listAgents(projectId, token, filter, limit, skip, sort) { + checkProject(projectId); + const endpoint = this.endpoints.agents(projectId); + debug('listAgents() => %s', endpoint); + const query = {}; + if (filter) query.filter = filter; + if (limit) query.limit = limit; + if (sort) query.sort = sort; + if (skip) query.skip = skip; + return got + .get(endpoint, { + headers: defaultHeaders(token), + searchParams: query, + }) + .json(); + } + + async listServices(projectId, token, agentName) { + // TODO removed profile should I use that as the URL ?? + checkProject(projectId); + debug('listServices() using describeAgent'); + const agent = await this.describeAgent(projectId, token, agentName); + const urlBase = `${this.endpoints.agentinvoke(projectId)}/${encodeURIComponent(agentName)}/services`; + debug('listServices(%s) => %s', agentName, urlBase); + const servicesList = agent.inputs + .filter((i) => i.signalType === 'Service') + .map((i) => ({ ...i, url: `${urlBase}/${i.name}` })) + .map((i) => ({ ...i, formatted_types: formatAllServiceInputParameters(i.parameters) })); + return servicesList; + } + + saveAgent(projectId, token, agentObj) { + checkProject(projectId); + const endpoint = this.endpoints.agents(projectId); + debug('saveAgent(%s) => %s', agentObj.name, endpoint); + return got + .post(endpoint, { + headers: defaultHeaders(token), + json: agentObj, + }).json(); + } + + describeAgent(projectId, token, agentName, verbose = false, output = 'json') { + checkProject(projectId); + const endpoint = `${this.endpoints.agents(projectId)}/${encodeURIComponent(agentName)}`; debug('describeAgent(%s) => %s', agentName, endpoint); - return request - .get(endpoint) - .set('Authorization', `Bearer ${token}`) - .then((res) => { - if (res.ok) { - return {success: true, agent: res.body}; - } - else { - return {success: false, message: res.body, status: res.status}; - } - }) - .catch((err) => { - return constructError(err); + const res = got + .get(endpoint, { + headers: defaultHeaders(token), + searchParams: { verbose, output }, }); + if (output?.toLocaleLowerCase() === 'json') return res.json(); + return res.text(); } - describeAgentVersions(token, agentName) { - const endpoint = `${this.endpoints.agentVersions}/${agentName}`; + async describeAgentVersions(projectId, token, agentName) { + checkProject(projectId); + const endpoint = `${this.endpoints.agents(projectId)}/${encodeURIComponent(agentName)}/versions`; debug('describeAgentVersions(%s) => %s', agentName, endpoint); - - return request - .get(endpoint) - .set('Authorization', `Bearer ${token}`) - .then((res) => { - if (res.ok) { - return {success: true, agent: res.body}; - } - else { - return {success: false, message: res.body, status: res.status}; - } - }) - .catch((err) => { - return constructError(err); + try { + const agent = await got + .get(endpoint, { + headers: defaultHeaders(token), + }) + .json(); + return ({ + success: true, + agent, }); + } catch (err) { + return constructError(err); + } } - saveType(token, types) { + deployAgent(projectId, token, agentName, verbose = false) { + checkProject(projectId); + const endpoint = `${this.endpoints.agents(projectId)}/${encodeURIComponent(agentName)}/deploy`; + debug('deployAgent(%s) => %s', agentName, endpoint); + return got + .get(endpoint, { + headers: defaultHeaders(token), + searchParams: { verbose }, + }).json(); + } + + unDeployAgent(projectId, token, agentName, verbose = false) { + checkProject(projectId); + const endpoint = `${this.endpoints.agents(projectId)}/${encodeURIComponent(agentName)}/undeploy`; + debug('undeployAgent(%s) => %s', agentName, endpoint); + return got + .get(endpoint, { + headers: defaultHeaders(token), + searchParams: { verbose }, + }).json(); + } + + // Type + saveType(projectId, token, types) { + checkProject(projectId); + const endpoint = `${this.endpoints.types(projectId)}`; const names = types.types.map((t) => t.name); - debug('saveType(%s) => %s', JSON.stringify(names), this.endpoints.types); - return request - .post(this.endpoints.types) - .set('Authorization', `Bearer ${token}`) - .send(types) - .then((res) => { - if (res.ok) { - return {success: true, message: res.body}; - } - return {success: false, message: res.body, status: res.status}; - }) - .catch((err) => { - return constructError(err); - }); + debug('saveType(%s) => %s', JSON.stringify(names), this.endpoints.types(projectId)); + return got + .post(endpoint, { + headers: defaultHeaders(token), + json: types, + }) + .json(); } - describeType(token, typeName) { - const endpoint = `${this.endpoints.types}/${typeName}`; + describeType(projectId, token, typeName) { + checkProject(projectId); + const endpoint = `${this.endpoints.types(projectId)}/${encodeURIComponent(typeName)}`; debug('describeType(%s) => %s', typeName, endpoint); - return request - .get(endpoint) - .set('Authorization', `Bearer ${token}`) - .then((res) => { - if (res.ok) { - return {success: true, type: res.body}; - } - else { - return {success: false, message: res.body, status: res.status}; - } - }) - .catch((err) => { - return constructError(err); + return got + .get(endpoint, { + headers: defaultHeaders(token), + }) + .json(); + } + + listTypes(projectId, token, limit, skip, sort) { + checkProject(projectId); + const endpoint = `${this.endpoints.types(projectId)}`; + debug('listTypes() => %s', endpoint); + const query = {}; + if (limit) query.limit = limit; + if (sort) query.sort = sort; + if (skip) query.skip = skip; + return got + .get(endpoint, { + headers: defaultHeaders(token), + searchParams: query, + }) + .json(); + } + + deleteType(projectId, token, typeName) { + checkProject(projectId); + const endpoint = `${this.endpoints.types(projectId)}/${encodeURIComponent(typeName)}`; + debug('deleteType(%s) => %s', typeName, endpoint); + return got + .delete(endpoint, { + headers: defaultHeaders(token), + }) + .json(); + } + + // Campaign + exportCampaign(projectId, token, campaignName, deployable, path) { + checkProject(projectId); + const url = `${this.endpoints.campaigns(projectId)}${campaignName}/export?deployable=${deployable}`; + debug('exportCampaign(%s) => %s', campaignName, url); + return new Promise((resolve, reject) => { + const download = got.stream(url, { + headers: defaultHeaders(token), }); + download.on('error', (error) => reject(constructError(error).message)); + const fileOutput = fs.createWriteStream(path); + fileOutput.on('error', (error) => reject(constructError(error).message)); + fileOutput.on('finish', resolve); + download.pipe(fileOutput); + }); + } + + importCampaign(projectId, token, filepath, deploy, overwrite) { + checkProject(projectId); + debug('importCampaign(%s)', this.endpoints.campaigns); + const importUrl = `${this.endpoints.campaigns(projectId)}import?deployable=${deploy}&overwrite=${overwrite}`; + debug('importCampaign(%s) => %s', filepath, importUrl); + if (!fs.existsSync(filepath) || !fs.lstatSync(filepath).isFile()) { + printError(`Campaign export file ${filepath} doesn't exists or not a valid export file`); + } + const readStream = fs.createReadStream(filepath); + const form = new FormData(); + form.append('file', readStream); + const headers = form.getHeaders(); + headers.Authorization = `Bearer ${token}`; + const req = http.request(importUrl, { + method: 'POST', + headers, + }); + form.pipe(req); + req.on('response', (response) => { + if (response.statusCode === 200 || response.statusCode === 201) { + response.setEncoding('utf-8'); + response.on('data', (data) => { + const importReport = JSON.parse(data); + if (!importReport.warnings || importReport.warnings.length === 0) { + printSuccess('Campaign imported successfully'); + } else { + printSuccess('Campaign imported with warnings'); + printTable([{ column: 'Warnings', field: 'message' }], importReport.warnings.map((w) => ({ message: w }))); + } + }); + } else { + printError(`Campaign file ${filepath} import failed with error: [${response.statusCode}] ${response.statusMessage}`); + } + }).on('error', (err) => { + printError(err); + }); } - listTypes(token) { - debug('listTypes() => %s', this.endpoints.types); - return request - .get(this.endpoints.types) - .set('Authorization', `Bearer ${token}`) - .then((res) => { - if (res.ok) { - return {success: true, types: res.body.types}; - } - return {success: false, status: res.status, message: res.body}; + undeployCampaign(projectId, token, campaign) { + checkProject(projectId); + const endpoint = `${this.endpoints.campaigns(projectId)}${campaign}/undeploy`; + debug('undeployCampaign() => %s', endpoint); + return got + .get(endpoint, { + headers: defaultHeaders(token), + }) + .json(); + } + + deployCampaign(projectId, token, campaign) { + checkProject(projectId); + const endpoint = `${this.endpoints.campaigns(projectId)}${campaign}/deploy`; + debug('deployCampaign() => %s', endpoint); + return got + .get(endpoint, { + headers: defaultHeaders(token), + }) + .json(); + } + + // Mission + undeployMission(projectId, token, campaign, mission) { + checkProject(projectId); + const endpoint = `${this.endpoints.campaigns(projectId)}${campaign}/missions/${mission}/undeploy`; + debug('undeployMissions() => %s', endpoint); + return got + .get(endpoint, { + headers: defaultHeaders(token), + }) + .json(); + } + + listMissions(projectId, token, campaign, filter, limit, skip, sort) { + checkProject(projectId); + const endpoint = `${this.endpoints.campaigns(projectId)}${campaign}/missions`; + debug('listMissions() => %s', endpoint); + const query = {}; + if (filter) query.filter = filter; + if (limit) query.limit = limit; + if (sort) query.sort = sort; + if (skip) query.skip = skip; + return got + .get(endpoint, { + headers: defaultHeaders(token), + searchParams: query, + }) + .json(); + } + + deployMission(projectId, token, campaign, mission) { + checkProject(projectId); + const endpoint = `${this.endpoints.campaigns(projectId)}${campaign}/missions/${mission}/deploy`; + debug('deployMissions() => %s', endpoint); + return got + .get(endpoint, { + headers: defaultHeaders(token), + }) + .json(); + } + + getMission(projectId, token, campaign, mission) { + checkProject(projectId); + const endpoint = `${this.endpoints.campaigns(projectId)}${campaign}/missions/${mission}`; + debug('getMissions() => %s', endpoint); + return got + .get(endpoint, { + headers: defaultHeaders(token), + }) + .json(); + } + + // Application + deleteApplication(projectId, token, name) { + checkProject(projectId); + const endpoint = `${this.endpoints.applications(projectId)}/${encodeURIComponent(name)}`; + debug('deleteApplication(%s) => %s', name, endpoint); + return got + .delete(endpoint, { + headers: defaultHeaders(token), }) - .catch((err) => { - return constructError(err); - }); + .json(); + } + + saveApplication(projectId, token, appObj) { + checkProject(projectId); + debug('saveApplication(%s) => %s', appObj.name, this.endpoints.applications(projectId)); + return got + .post(this.endpoints.applications(projectId), { + headers: defaultHeaders(token), + json: appObj, + }).json(); + } + + listApplications(projectId, token, query, filter, limit, skip, sort) { + checkProject(projectId); + debug('listApplications() => %s', this.endpoints.applications(projectId)); + if (filter) query.filter = filter; + if (limit) query.limit = limit; + if (sort) query.sort = sort; + if (skip) query.skip = skip; + return got + .get(this.endpoints.applications(projectId), { + headers: defaultHeaders(token), + searchParams: query, + }).json(); + } + + describeApplication(projectId, token, name, verbose = false, output = 'json') { + checkProject(projectId); + const endpoint = `${this.endpoints.applications(projectId)}/${encodeURIComponent(name)}`; + debug('describeApplication(%s) => %s', name, endpoint); + const searchParams = { output }; + if (verbose) { + searchParams.verbose = verbose; + searchParams.status = verbose; + } + const res = got.get(endpoint, { + headers: defaultHeaders(token), + searchParams, + }); + if (['json', 'pretty'].includes(output.toLocaleLowerCase())) return res.json(); + return res.text(); + } + + deployApplication(projectId, token, name) { + checkProject(projectId); + const endpoint = `${this.endpoints.applications(projectId)}/${encodeURIComponent(name)}/deploy`; + debug('deployApplication(%s) => %s', name, endpoint); + return got + .get(endpoint, { + headers: defaultHeaders(token), + }) + .json(); + } + + unDeployApplication(projectId, token, name) { + checkProject(projectId); + const endpoint = `${this.endpoints.applications(projectId)}/${encodeURIComponent(name)}/undeploy`; + debug('undeployApplication(%s) => %s', name, endpoint); + return got + .get(endpoint, { + headers: defaultHeaders(token), + }).json(); + } + + // Stereotype + deleteStereotype(projectId, token, name) { + checkProject(projectId); + const endpoint = `${this.endpoints.skillStereotype(projectId)}/${encodeURIComponent(name)}`; + debug('deleteStereotype(%s) => %s', name, endpoint); + return got + .delete(endpoint, { + headers: defaultHeaders(token), + }) + .json(); + } + + saveStereotype(projectId, token, appObj) { + checkProject(projectId); + debug('saveStereotype(%s) => %s', appObj.name, this.endpoints.skillStereotype(projectId)); + return got + .post(this.endpoints.skillStereotype(projectId), { + headers: defaultHeaders(token), + json: appObj, + }).json(); + } + + listStereotypes(projectId, token, query, filter, limit, skip, sort) { + checkProject(projectId); + debug('listStereotypes() => %s', this.endpoints.skillStereotype(projectId)); + if (filter) query.filter = filter; + if (limit) query.limit = limit; + if (sort) query.sort = sort; + if (skip) query.skip = skip; + return got + .get(this.endpoints.skillStereotype(projectId), { + headers: defaultHeaders(token), + searchParams: query, + }).json(); + } + + describeStereotype(projectId, token, name, verbose = false, output = 'json') { + checkProject(projectId); + const endpoint = `${this.endpoints.skillStereotype(projectId)}/${encodeURIComponent(name)}`; + debug('describeStereotype(%s) => %s', name, endpoint); + const searchParams = { output }; + if (verbose) { + searchParams.verbose = verbose; + searchParams.status = verbose; + } + const res = got.get(endpoint, { + headers: defaultHeaders(token), + searchParams, + }); + if (output.toLocaleLowerCase() === 'json') return res.json(); + return res.text(); } -}; +} diff --git a/src/client/connections.js b/src/client/connections.js index 1ead0609..c16e10de 100644 --- a/src/client/connections.js +++ b/src/client/connections.js @@ -1,5 +1,5 @@ /* - * Copyright 2018 Cognitive Scale, Inc. All Rights Reserved. + * Copyright 2023 Cognitive Scale, Inc. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the “License”); * you may not use this file except in compliance with the License. @@ -13,103 +13,75 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import debugSetup from 'debug'; +import { got, defaultHeaders } from './apiutils.js'; +import { checkProject } from '../commands/utils.js'; -const request = require('superagent'); -const debug = require('debug')('cortex:cli'); -const { constructError } = require('../commands/utils'); - -module.exports = class Connections { - +const debug = debugSetup('cortex:cli'); +export default (class Connections { constructor(cortexUrl) { + this.endpoint = (projectId) => `${cortexUrl}/fabric/v4/projects/${projectId}/connections`; this.cortexUrl = cortexUrl; - this.endpoint = `${cortexUrl}/v2/connections`; } - listConnections(token) { - const endpoint = `${this.endpoint}`; - return request - .get(endpoint) - .set('Authorization', `Bearer ${token}`) - .then((res) => { - if (res.ok) { - return {success: true, result: res.body}; - } - return {success: false, status: res.status, message: res.body}; - }) - .catch((err) => { - return constructError(err); - }); + listConnections(projectId, token, filter, limit, skip, sort) { + checkProject(projectId); + const endpoint = `${this.endpoint(projectId)}`; + debug('listConnections() => %s', endpoint); + const query = {}; + if (filter) query.filter = filter; + if (limit) query.limit = limit; + if (sort) query.sort = sort; + if (skip) query.skip = skip; + return got + .get(endpoint, { + headers: defaultHeaders(token), + searchParams: query, + }).json(); } - saveConnection(token, connObj) { - debug('saveConnection(%s) => %s', connObj.name, this.endpoint); - return request - .post(this.endpoint) - .set('Authorization', `Bearer ${token}`) - .send(connObj) - .then((res) => { - if (res.ok) { - return {success: true, message: res.body}; - } - return {success: false, message: res.body, status: res.status}; - }) - .catch((err) => { - return constructError(err); - }); + saveConnection(projectId, token, connObj) { + checkProject(projectId); + const endpoint = `${this.endpoint(projectId)}`; + debug('saveConnection(%s) => %s', connObj.name, endpoint); + return got + .post(endpoint, { + headers: defaultHeaders(token), + json: connObj, + }).json(); } - describeConnection(token, connectionName) { - const endpoint = `${this.endpoint}/${connectionName}`; + describeConnection(projectId, token, connectionName) { + checkProject(projectId); + const endpoint = `${this.endpoint(projectId)}/${encodeURIComponent(connectionName)}`; debug('describeConnection(%s) => %s', connectionName, endpoint); - return request - .get(endpoint) - .set('Authorization', `Bearer ${token}`) - .then((res) => { - if (res.ok) { - return {success: true, result: res.body}; - } - else { - return {success: false, message: res.body, status: res.status}; - } - }) - .catch((err) => { - return constructError(err); - }); + return got + .get(endpoint, { + headers: defaultHeaders(token), + }).json(); } - testConnection(token, {name, title, description, connectionType, allowWrite, tags, params}) { - const url = this.endpoint + '/test'; - debug('saveConnection(%s) => %s', name, url); - return request - .post(url) - .set('Authorization', `Bearer ${token}`) - .send({name, title, description, connectionType, allowWrite, tags, params}) - .then((res) => { - if (res.ok) { - return {success: true, message: res.body}; - } - return {success: false, message: res.message, status: res.status}; - }) - .catch((err) => { - return constructError(err); - }); + deleteConnection(projectId, token, connectionName) { + checkProject(projectId); + const endpoint = `${this.endpoint(projectId)}/${encodeURIComponent(connectionName)}`; + debug('deleteConnection(%s) => %s', connectionName, endpoint); + return got + .delete(endpoint, { + headers: defaultHeaders(token), + }).json(); } - listConnectionsTypes(token) { - const endpoint = `${this.endpoint}/types`; - return request - .get(endpoint) - .set('Authorization', `Bearer ${token}`) - .then((res) => { - if (res.ok) { - return {success: true, result: res.body}; - } - return {success: false, status: res.status, message: res.body}; - }) - .catch((err) => { - return constructError(err); - }); + listConnectionsTypes(token, limit, skip, sort) { + const endpoint = `${this.cortexUrl}/fabric/v4/connectiontypes`; + debug('listConnectionsTypes() => %s', endpoint); + const query = {}; + if (limit) query.limit = limit; + if (sort) query.sort = sort; + if (skip) query.skip = skip; + return got + .get(endpoint, { + headers: defaultHeaders(token), + searchParams: query, + }).json(); } - -}; - +}); diff --git a/src/client/content.js b/src/client/content.js index 162c2565..23ad28c4 100644 --- a/src/client/content.js +++ b/src/client/content.js @@ -1,141 +1,85 @@ -/* - * Copyright 2018 Cognitive Scale, Inc. All Rights Reserved. - * - * 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. - */ - -const request = require('superagent'); -const debug = require('debug')('cortex:cli'); -const { constructError } = require('../commands/utils'); - -module.exports = class Content { +import fs from 'node:fs'; +import process from 'node:process'; +import { PassThrough } from 'node:stream'; +import { pipeline } from 'node:stream/promises'; +import debugSetup from 'debug'; +import { defaultHeaders, got } from './apiutils.js'; +import { createFileStream, constructError, checkProject } from '../commands/utils.js'; +const debug = debugSetup('cortex:cli'); +export default (class Content { constructor(cortexUrl) { this.cortexUrl = cortexUrl; - this.endpoint = `${cortexUrl}/v2/content`; - } - - listContent(token) { - debug('listContent %s', this.endpoint); - return request - .get(this.endpoint) - .set('Authorization', `Bearer ${token}`) - .accept('application/json') - .then((res) => { - if (res.ok) { - return {success: true, message: res.body}; - } - return {success: false, message: res.body, status: res.status}; - }) - .catch((err) => { - return constructError(err); - }); + this.endpoint = (projectId) => `${cortexUrl}/fabric/v4/projects/${projectId}/content`; } - uploadContent(token, {content, key}) { - debug('saveContent(%s, %s) => %s', key, content, this.endpoint); - return request - .post(this.endpoint) - .set('Authorization', `Bearer ${token}`) - .accept('application/json') - .field('key', key) - .attach('content', content) - .then((res) => { - if (res.ok) { - return {success: true, message: res.body}; - } - return {success: false, message: res.body, status: res.status}; - }) - .catch((err) => { - return constructError(err); - }); + _sanitizeKey(key) { + // strip leading slash, if any + return key.replace(/^\//, ''); } - uploadSecureContent(token, key, content) { - const url = `${this.cortexUrl}/v2/tenants/secrets/${key}`; - debug('uploadSecureContent(%s => %s', content, url); - return request - .post(url) - .set('Authorization', `Bearer ${token}`) - .send(content) - .accept('application/json') - .type('json') - .then((res) => { - if (res.ok) { - return {success: true, message: res.body}; - } - return {success: false, message: res.body, status: res.status}; - }) - .catch((err) => { - return constructError(err); - }); + listContent(projectId, token, prefix) { + checkProject(projectId); + const endpoint = this.endpoint(projectId); + debug('listContent() => %s', endpoint); + const query = {}; + if (prefix) query.filter = prefix; + return got + .get(endpoint, { + headers: defaultHeaders(token), + searchParams: query, + }).json(); } - - deleteContent(token, contentKey) { - debug('deleteContent(%s) => %s', contentKey, this.endpoint); - const url = `${this.endpoint}/${contentKey}`; - return request - .delete(url) - .set('Authorization', `Bearer ${token}`) - .accept('application/json') - .then((res) => { - if (res.ok) { - return {success: true, message: res.body}; - } - return {success: false, message: res.body, status: res.status}; - }) - .catch((err) => { - return constructError(err); - }); + // eslint-disable-next-line no-unused-vars + async uploadContentStreaming(projectId, token, key, content, showProgress = false, contentType = 'application/octet-stream') { + checkProject(projectId); + const contentKey = this._sanitizeKey(key); + const endpoint = `${this.endpoint(projectId)}/${contentKey}`; + // todo show progress.. + debug('uploadContentStreaming(%s, %s) => %s', key, content, endpoint); + try { + const message = await pipeline(fs.createReadStream(content), got.stream.post(endpoint, { + headers: defaultHeaders(token, { 'Content-Type': contentType }), + }), + // https://github.com/sindresorhus/got/blob/HEAD/documentation/3-streams.md#stream-api + // new stream.PassThrough() is required to catch errors. + new PassThrough()); + return { success: true, message }; + } catch (err) { + // TODO this should just throw + return constructError(err); + } } - downloadContent(token, contentKey) { - debug('downloadContent(%s) => %s', contentKey, this.endpoint); - const url = `${this.endpoint}/${contentKey}`; - return request - .get(url) - .set('Authorization', `Bearer ${token}`) - .accept('application/json') - .then((res) => { - if (res.ok) { - return {success: true, message: res.text}; - } - return {success: false, message: res.body, status: res.status}; - }) - .catch((err) => { - return constructError(err); - }); + deleteContent(projectId, token, key) { + checkProject(projectId); + const contentKey = this._sanitizeKey(key); + const endpoint = `${this.endpoint(projectId)}/${contentKey}`; + debug('deleteContent() => %s', endpoint); + return got + .delete(endpoint, { + headers: defaultHeaders(token), + }).json(); } - - downloadSecureContent(token, contentKey) { - const url = `${this.cortexUrl}/v2/tenants/secrets/${contentKey}`; - debug('downloadContent(%s) => %s', contentKey); - return request - .get(url) - .set('Authorization', `Bearer ${token}`) - .accept('application/json') - .then((res) => { - if (res.ok) { - return {success: true, message: res.text}; - } - return {success: false, message: res.body, status: res.status}; - }) - .catch((err) => { - return constructError(err); - }); + // TODO progress + // eslint-disable-next-line no-unused-vars + downloadContent(projectId, token, key, showProgress = false, toFile = null) { + checkProject(projectId); + const contentKey = this._sanitizeKey(key); + const endpoint = `${this.endpoint(projectId)}/${contentKey}`; + debug('downloadContent() => %s', endpoint); + try { + let toFileStream; + if (toFile) { + toFileStream = createFileStream(toFile); + } else { + toFileStream = process.stdout; + } + return pipeline(got.stream(endpoint, { headers: defaultHeaders(token) }), toFileStream); + } catch (err) { + return constructError(err); + } } - -}; - +}); diff --git a/src/client/datasets.js b/src/client/datasets.js deleted file mode 100644 index a28cc27e..00000000 --- a/src/client/datasets.js +++ /dev/null @@ -1,118 +0,0 @@ -/* - * Copyright 2018 Cognitive Scale, Inc. All Rights Reserved. - * - * 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. - */ - -const request = require('superagent'); -const debug = require('debug')('cortex:cli'); -const { constructError } = require('../commands/utils'); - -module.exports = class Datasets { - - constructor(cortexUrl) { - this.cortexUrl = cortexUrl; - this.endpoint = `${cortexUrl}/v3/datasets`; - } - - listDatasets(token) { - const endpoint = `${this.endpoint}`; - return request - .get(endpoint) - .set('Authorization', `Bearer ${token}`) - .then((res) => { - if (res.ok) { - return {success: true, result: res.body}; - } - return {success: false, status: res.status, message: res.body}; - }) - .catch((err) => { - return constructError(err); - }); - } - - saveDatasets(token,dsObject) { - debug('saveConnection(%s) => %s', dsObject.name, this.endpoint); - return request - .post(this.endpoint) - .set('Authorization', `Bearer ${token}`) - .send(dsObject) - .then((res) => { - if (res.ok) { - return {success: true, message: res.body}; - } - return {success: false, message: res.body, status: res.status}; // don't think we ever hit this - }) - .catch((err) => { - return constructError(err); - }); - } - - describeDataset(token, datasetName) { - const endpoint = `${this.endpoint}/${datasetName}`; - debug('describeConnection(%s) => %s', datasetName, endpoint); - return request - .get(endpoint) - .set('Authorization', `Bearer ${token}`) - .then((res) => { - if (res.ok) { - return {success: true, result: res.body}; - } - else { - return {success: false, message: res.body, status: res.status}; - } - }) - .catch((err) => { - return constructError(err); - }); - } - - getDataframe (token, datasetName) { - const endpoint = `${this.endpoint}/${datasetName}/dataframe`; - debug('describeConnection(%s) => %s', datasetName, endpoint); - return request - .get(endpoint) - .set('Authorization', `Bearer ${token}`) - .then((res) => { - if (res.ok) { - return {success: true, result: res.body}; - } - else { - return {success: false, message: res.body, status: res.status}; - } - }) - .catch((err) => { - return constructError(err); - }); - } - - streamDataset(token, datasetName) { - const endpoint = `${this.endpoint}/${datasetName}/stream`; - debug('describeConnection(%s) => %s', datasetName, endpoint); - return request - .get(endpoint) - .set('Authorization', `Bearer ${token}`) - .buffer() - .then((res) => { - if (res.ok) { - return {success: true, result: res}; - } - else { - return {success: false, message: res.body, status: res.status}; - } - }) - .catch((err) => { - return constructError(err); - }); - } -}; diff --git a/src/client/dockerClient.js b/src/client/dockerClient.js new file mode 100644 index 00000000..454c28f7 --- /dev/null +++ b/src/client/dockerClient.js @@ -0,0 +1,208 @@ +import debugSetup from 'debug'; +import which from 'which'; +import { callMe, printError } from '../commands/utils.js'; +import { generateJwt, loadProfile } from '../config.js'; +import { getCurrentRegistry } from '../commands/workspaces/workspace-utils.js'; + +const debug = debugSetup('cortex:cli'); + +/** + * Interface for docker clients + */ +let currentClient; +/* + Will likely remove this in favor of third party clients as this is missing many features + */ + +class DockerCli { + async login(registryUrl, user, password) { + const command = `docker login -u ${user} --password ${password} ${registryUrl}`; + debug('%s', command); + await callMe(command, () => {}); // suppress warnings + return 'Login Succeeded'; + } + + build(opts) { + const { + imageTag, dockerFile, contextPath, stdoutHandler, + } = opts; + // TODO --platform=amd64,arm64,darwin/arm64 + const command = `docker build --progress plain --tag ${imageTag} -f ${dockerFile} ${contextPath}`; + debug('%s', command); + return callMe(command, stdoutHandler); + } + + pull(image) { + const command = `docker pull ${image}`; + debug('%s', command); + return callMe(command); + } + + tag(source, target) { + const command = `docker tag ${source} ${target}`; + debug('%s', command); + return callMe(command); + } + + push(image, stdOutHandler = () => {}) { + const command = `docker push ${image}`; + debug('%s', command); + return callMe(command, stdOutHandler); + } + + async listImages(image = undefined) { + const command = `docker images ${image ?? ''} --format "{{json .}}"`; + const lines = []; + await callMe(command, (s) => { + if (s.startsWith('{')) lines.push(s); + }); + const str = lines.join(); + const images = []; + str.split('\n').forEach((l) => { if (l.startsWith('{')) images.push(JSON.parse(l)); }); + return images; + } +} + +class NerdCtl { + async login(registryUrl, user, password) { + const command = `nerdctl login -u ${user} --password ${password} ${registryUrl}`; + debug('%s', command); + await callMe(command, () => {}); // suppress warnings + return 'Login Succeeded'; + } + + build(opts) { + const { imageTag, dockerFile, contextPath } = opts; + // TODO namespace + // TODO --platform=amd64,arm64,darwin/arm64 + const command = `nerdctl build --output type=image,name=${imageTag} -f ${dockerFile} ${contextPath}`; + debug('%s', command); + return callMe(command); + } + + tag(source, target) { + const command = `nerdctl tag ${source} ${target}`; + debug('%s', command); + return callMe(command); + } + + pull(image) { + const command = `nerdctl pull ${image}`; + debug('%s', command); + return callMe(command); + } + + push(image, stdOutHandler = () => {}) { + const command = `nerdctl push ${image}`; + debug('%s', command); + return callMe(command, stdOutHandler); + } + + async listImages(image = undefined) { + const command = `nerdctl images ${image ?? ''} --format "{{json .}}"`; + const lines = []; + await callMe(command, (s) => { + if (s.startsWith('{')) lines.push(s); + }); + const str = lines.join(); + const images = []; + str.split('\n').forEach((l) => { if (l.startsWith('{')) images.push(JSON.parse(l)); }); + return images; + } +} + +class PodMan { + async login(registryUrl, user, password) { + // TODO validate this + const command = `podman login -u ${user} --password ${password} ${registryUrl}`; + debug('%s', command); + await callMe(command, () => {}); // suppress warnings + return 'Login Succeeded'; + } + + build() {} + + tag(source, target) { + const command = `podman tag ${source} ${target}`; + debug('%s', command); + return callMe(command); + } + + pull(image) { + const command = `podman pull ${image}`; + debug('%s', command); + return callMe(command); + } + + push(image, stdOutHandler = () => {}) { + const command = `podman push ${image}`; + debug('%s', command); + return callMe(command, stdOutHandler); + } + + async listImages(image = undefined) { + const command = `podman images ${image ?? ''} --format "{{json .}}"`; + const lines = []; + await callMe(command, (s) => { + if (s.startsWith('{')) lines.push(s); + }); + const str = lines.join(); + const images = []; + str.split('\n').forEach((l) => { if (l.startsWith('{')) images.push(JSON.parse(l)); }); + return images; + } +} + +const SUPPORTED_CLIS = { docker: DockerCli, nerdctl: NerdCtl, podman: PodMan }; +export default function getClient(opts = {}) { + const { dockerCli } = opts; + if (currentClient) return currentClient; + // TODO add client to profile || support environment variable + // TODO add namespace support ( podman & nerdctl ) + if (dockerCli) { + debug(`User specified docker client ${dockerCli}`); + const found = Object.keys(SUPPORTED_CLIS).find((e) => e === dockerCli?.toLowerCase()); + if (!found) { + printError(`Unsupported docker client "${dockerCli}" use: ${Object.keys(SUPPORTED_CLIS).join(', ')}`); + } + const resolved = which.sync(found, { nothrow: true }); + if (!resolved) { + printError(`Docker client executable "${dockerCli}" not found in PATH`); + } + currentClient = new SUPPORTED_CLIS[found](opts); + return currentClient; + } + debug('No docker client specified, checking available ones'); + // If not specified check for supported OCI clients + Object.entries(SUPPORTED_CLIS).every(([exe, ClientClass]) => { + const resolved = which.sync(exe, { nothrow: true }); + if (resolved) { + debug(`Found ${exe} in PATH ${resolved} using ${ClientClass.name} client`); + currentClient = new ClientClass(opts); + return false; // break + } + return true; + }); + if (currentClient === undefined) { + return printError(`No docker client found in PATH, checked ${Object.keys(SUPPORTED_CLIS).join(',')}`); + } + return currentClient; +} + +export async function privateRegLogin(options) { + const profile = await loadProfile(options.profile); + const ttl = options?.ttl ?? '14d'; + // First try to see if we have of registries in out profile + const registry = await getCurrentRegistry(profile); + const registryUrl = registry?.url; + if (registryUrl === undefined) { + throw Error(`No docker registry configured in current profile ${profile.name}`); + } + if (!registry.isCortex) { + return getClient(options).login(registryUrl); // Login with registry URI and use existing credentials.. + } + // Sensa private registries are configured with JWT authentication ... + const jwt = await generateJwt(profile, ttl); + return getClient(options) + .login(registryUrl, 'cli', jwt); +} diff --git a/src/client/environments.js b/src/client/environments.js deleted file mode 100644 index 7b12e410..00000000 --- a/src/client/environments.js +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Copyright 2018 Cognitive Scale, Inc. All Rights Reserved. - * - * 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. - */ - -const request = require('superagent'); -const debug = require('debug')('cortex:cli'); -const { constructError } = require('../commands/utils'); - -module.exports = class Environments { - - constructor(cortexUrl) { - this.cortexUrl = cortexUrl; - this.endpoint = `${cortexUrl}/v2/agents/environments`; - } - - listEnvironments(token) { - const endpoint = `${this.endpoint}`; - return request - .get(endpoint) - .set('Authorization', `Bearer ${token}`) - .then((res) => { - if (res.ok) { - return {success: true, result: res.body}; - } - return {success: false, status: res.status, message: res.body}; - }) - .catch((err) => { - return constructError(err); - }); - } - - saveEnvironment(token, env) { - debug('saveEnvironment(%s) => %s', env.name, this.endpoint); - return request - .post(this.endpoint) - .set('Authorization', `Bearer ${token}`) - .send(env) - .then((res) => { - if (res.ok) { - return {success: true, message: res.body}; - } - return {success: false, message: res.body, status: res.status}; - }) - .catch((err) => { - return constructError(err); - }); - } - promoteEnvironment(token, promoteDef) { - debug('promoteEnvironment(%s) => %s', promoteDef.environmentName, this.endpoint); - return request - .post(`${this.endpoint}/promote`) - .set('Authorization', `Bearer ${token}`) - .send(promoteDef) - .then((res) => { - if (res.ok) { - return {success: true, message: res.body}; - } - return {success: false, message: res.body, status: res.status}; - }) - .catch((err) => { - return constructError(err); - }); - } - - describeEnvironment(token, environmentName) { - const endpoint = `${this.endpoint}/${environmentName}`; - debug('describeEnvironment(%s) => %s', environmentName, endpoint); - return request - .get(endpoint) - .set('Authorization', `Bearer ${token}`) - .then((res) => { - if (res.ok) { - return {success: true, result: res.body}; - } - else { - return {success: false, message: res.body, status: res.status}; - } - }) - .catch((err) => { - return constructError(err); - }); - } -}; - diff --git a/src/client/experiments.js b/src/client/experiments.js new file mode 100644 index 00000000..4c976848 --- /dev/null +++ b/src/client/experiments.js @@ -0,0 +1,143 @@ +import fs from 'node:fs'; +import { pipeline } from 'node:stream/promises'; +import debugSetup from 'debug'; +import { got, defaultHeaders } from './apiutils.js'; +import { checkProject } from '../commands/utils.js'; +import Content from './content.js'; + +const debug = debugSetup('cortex:cli'); +export default (class Experiments { + constructor(cortexUrl) { + this.cortexUrl = cortexUrl; + this.endpoint = (projectId) => `${cortexUrl}/fabric/v4/projects/${projectId}/experiments`; + } + + listExperiments(projectId, modelId, token, filter, limit, skip, sort) { + checkProject(projectId); + const endpoint = `${this.endpoint(projectId)}?${modelId ? `modelId=${modelId}` : ''}`; + debug('listExperiments() => %s', endpoint); + const query = {}; + if (filter) query.filter = filter; + if (limit) query.limit = limit; + if (sort) query.sort = sort; + if (skip) query.skip = skip; + return got + .get(endpoint, { + headers: defaultHeaders(token), + searchParams: query, + }).json(); + } + + describeExperiment(projectId, token, name) { + checkProject(projectId); + const endpoint = `${this.endpoint(projectId)}/${encodeURIComponent(name)}`; + debug('describeExperiment(%s) => %s', name, endpoint); + return got + .get(endpoint, { + headers: defaultHeaders(token), + }).json(); + } + + deleteExperiment(projectId, token, name) { + checkProject(projectId); + const endpoint = `${this.endpoint(projectId)}/${encodeURIComponent(name)}`; + debug('deleteExperiment(%s) => %s', name, endpoint); + return got + .delete(endpoint, { + headers: defaultHeaders(token), + }).json(); + } + + listRuns(projectId, token, experimentName, filter, limit, sort, skip) { + checkProject(projectId); + const endpoint = `${this.endpoint(projectId)}/${encodeURIComponent(experimentName)}/runs`; + debug('listRuns(%s) => %s', experimentName, endpoint); + const query = {}; + if (filter) query.filter = filter; + if (limit) query.limit = limit; + if (sort) query.sort = sort; + if (skip) query.skip = skip; + return got + .get(endpoint, { + headers: defaultHeaders(token), + searchParams: query, + }).json(); + } + + describeRun(projectId, token, experimentName, runId) { + checkProject(projectId); + const endpoint = `${this.endpoint(projectId)}/${encodeURIComponent(experimentName)}/runs/${runId}`; + debug('describeRun(%s) => %s', runId, endpoint); + return got + .get(endpoint, { + headers: defaultHeaders(token), + }).json(); + } + + deleteRun(projectId, token, experimentName, runId) { + checkProject(projectId); + const endpoint = `${this.endpoint(projectId)}/${encodeURIComponent(experimentName)}/runs/${runId}`; + debug('deleteRun(%s, %s) => %s', experimentName, runId, endpoint); + return got + .delete(endpoint, { + headers: defaultHeaders(token), + }).json(); + } + + _artifactKey(experimentName, runId, artifact) { + return `experiments/${experimentName}/${runId}/artifacts/${artifact}`; + } + + // eslint-disable-next-line no-unused-vars + async downloadArtifact(projectId, token, experimentName, runId, artifactName, showProgress = false) { + checkProject(projectId); + // Check if run exists.. + await this.describeRun(projectId, token, experimentName, runId); + // just generate the key and avoid hop to managed content.. + const key = this._artifactKey(experimentName, runId, artifactName); + const cont = new Content(this.cortexUrl); + return cont.downloadContent(projectId, token, key, showProgress); + } + + saveExperiment(projectId, token, experimentObj) { + checkProject(projectId); + const endpoint = `${this.endpoint(projectId)}`; + debug('saveExperiment(%s) => %s', experimentObj.name, endpoint); + return got + .post(endpoint, { + headers: defaultHeaders(token), + json: experimentObj, + }).json(); + } + + createRun(projectId, token, runObj) { + checkProject(projectId); + const endpoint = `${this.endpoint(projectId)}/${encodeURIComponent(runObj.experimentName)}/runs`; + debug('createRun(%s) => %s', runObj.experimentName, endpoint); + return got + .post(endpoint, { + headers: defaultHeaders(token), + json: runObj, + }).json(); + } + + updateRun(projectId, token, runObj) { + checkProject(projectId); + const endpoint = `${this.endpoint(projectId)}/${encodeURIComponent(runObj.experimentName)}/runs/${runObj.runId}`; + debug('updateRun(%s) => %s', runObj.experimentName, endpoint); + return got + .post(endpoint, { + headers: defaultHeaders(token), + json: runObj, + }).json(); + } + + uploadArtifact(projectId, token, experimentName, runId, content, artifact, contentType = 'application/octet-stream') { + checkProject(projectId); + const endpoint = `${this.endpoint(projectId)}/${encodeURIComponent(experimentName)}/runs/${encodeURIComponent(runId)}/artifacts/${artifact}`; + debug('uploadArtifact(%s, %s) => %s', artifact, content, endpoint); + return pipeline(fs.createReadStream(content), got.stream.put(endpoint, { + headers: defaultHeaders(token, { 'Content-Type': contentType }), + })); + } +}); diff --git a/src/client/info.js b/src/client/info.js new file mode 100644 index 00000000..833a8fc3 --- /dev/null +++ b/src/client/info.js @@ -0,0 +1,42 @@ +/* + * Copyright 2023 Cognitive Scale, Inc. All Rights Reserved. + * + * 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 debugSetup from 'debug'; +import { got } from './apiutils.js'; +import { constructError } from '../commands/utils.js'; + +const debug = debugSetup('cortex:cli'); +export default (class Info { + constructor(cortexUrl) { + this.endpoint = () => `${cortexUrl}`; + this.cortexUrl = cortexUrl; + } + + async getInfo() { + const endpoint = `${this.cortexUrl}/fabric/v4/info`; + debug('getInfo() => %s', endpoint); + try { + return await got + .get({ url: endpoint }) + .json(); + } catch (err) { + // Return error object and let caller deal with the failure, however alert the user + const e = constructError(err); + // printError('Failed to communicate with Cortex server. This may limit available CLI features!\n', null, false); + debug(`Failed to communicate with Cortex server for getInfo() call. Cluster "%s" - ${e.message}`, this.cortexUrl); + return e; + } + } +}); diff --git a/src/client/jobs.js b/src/client/jobs.js deleted file mode 100644 index 48e90827..00000000 --- a/src/client/jobs.js +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright 2018 Cognitive Scale, Inc. All Rights Reserved. - * - * 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. - */ - -const request = require('superagent'); -const debug = require('debug')('cortex:cli'); -const { constructError } = require('../commands/utils'); - -module.exports = class Jobs { - - constructor(cortexUrl) { - this.cortexUrl = cortexUrl; - this.endpoint = `${cortexUrl}/v2/jobs`; - } - - listJobs(token) { - const endpoint = `${this.endpoint}`; - return request - .get(endpoint) - .set('Authorization', `Bearer ${token}`) - .then((res) => { - if (res.ok) { - return { success: true, result: res.body }; - } - return { success: false, status: res.status, message: res.body }; - }) - .catch((err) => { - return constructError(err); - }); - } - - describeJob(token, name) { - const endpoint = `${this.endpoint}/${name}`; - return request - .get(endpoint) - .set('Authorization', `Bearer ${token}`) - .then((res) => { - if (res.ok) { - return { success: true, result: res.body }; - } - return { success: false, status: res.status, message: res.body }; - }) - .catch((err) => { - return constructError(err); - }); - } - - jobStatus(token, jobDefinition) { - const endpoint = `${this.endpoint}/${jobDefinition}/stats`; - return request - .get(endpoint) - .set('Authorization', `Bearer ${token}`) - .then((res) => { - if (res.ok) { - return { success: true, result: res.body }; - } - return { success: false, status: res.status, message: res.body }; - }) - .catch((err) => { - return constructError(err); - }); - } - - saveJob(token, {name, image, command, memory, vcpus, environment}) { - const endpoint = `${this.endpoint}`; - return request - .post(endpoint) - .set('Authorization', `Bearer ${token}`) - .send({name, image, command, memory, vcpus, environment}) - .then((res) => { - if (res.ok) { - return { success: true, result: res.body }; - } - return { success: false, status: res.status, message: res.body }; - }) - .catch((err) => { - return constructError(err); - }); - } -}; \ No newline at end of file diff --git a/src/client/models.js b/src/client/models.js new file mode 100644 index 00000000..ea50aaf3 --- /dev/null +++ b/src/client/models.js @@ -0,0 +1,102 @@ +/* + * Copyright 2023 Cognitive Scale, Inc. All Rights Reserved. + * + * 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 debugSetup from 'debug'; +import { got, defaultHeaders } from './apiutils.js'; +import { checkProject } from '../commands/utils.js'; + +const debug = debugSetup('cortex:cli'); +export default (class Models { + constructor(cortexUrl) { + this.cortexUrl = cortexUrl; + this.endpointV4 = (projectId) => `${cortexUrl}/fabric/v4/projects/${projectId}/models`; + } + + saveModel(projectId, token, modelObj) { + checkProject(projectId); + const endpoint = this.endpointV4(projectId); + debug('saveModel(%s) => %s', modelObj.name, endpoint); + return got + .post(endpoint, { + headers: defaultHeaders(token), + json: modelObj, + }).json(); + } + + updateModelStatus(projectId, token, modelName, status) { + checkProject(projectId); + const endpoint = `${this.endpointV4(projectId)}/${modelName}/${status}`; + debug('updateModelStatus(%s) => %s', modelName, status, endpoint); + return got + .post(endpoint, { + headers: defaultHeaders(token), + json: {}, + }).json(); + } + + deleteModel(projectId, token, modelName) { + checkProject(projectId); + const endpoint = `${this.endpointV4(projectId)}/${encodeURIComponent(modelName)}`; + debug('deleteAction(%s) => %s', modelName, endpoint); + return got + .delete(endpoint, { + headers: defaultHeaders(token), + }).json(); + } + + describeModel(projectId, token, modelName, verbose) { + checkProject(projectId); + const endpoint = `${this.endpointV4(projectId)}/${encodeURIComponent(modelName)}`; + debug('describeModel(%s) => %s', modelName, endpoint); + return got + .get(endpoint, { + headers: defaultHeaders(token), + searchParams: { verbose }, + }).json(); + } + + listModels(projectId, skip, limit, filter, sort, tags, token) { + checkProject(projectId); + const endpoint = `${this.endpointV4(projectId)}`; + debug('listModels() => %s', endpoint); + const query = {}; + if (filter) query.filter = filter; + if (limit) query.limit = limit; + if (sort) query.sort = sort; + if (skip) query.skip = skip; + if (tags) query.tags = tags; + return got + .get(endpoint, { + headers: defaultHeaders(token), + searchParams: query, + }).json(); + } + + listModelRuns(projectId, modelName, token, filter, limit, skip, sort) { + checkProject(projectId); + const endpoint = `${this.endpointV4(projectId)}/${modelName}/experiments/runs`; + debug('listModels() => %s', endpoint); + const query = {}; + if (filter) query.filter = filter; + if (limit) query.limit = limit; + if (sort) query.sort = sort; + if (skip) query.skip = skip; + return got + .get(endpoint, { + headers: defaultHeaders(token), + searchParams: query, + }).json(); + } +}); diff --git a/src/client/pipelineRepositories.js b/src/client/pipelineRepositories.js new file mode 100644 index 00000000..811cc929 --- /dev/null +++ b/src/client/pipelineRepositories.js @@ -0,0 +1,93 @@ +import debugSetup from 'debug'; +import { got, defaultHeaders } from './apiutils.js'; +import { constructError, checkProject } from '../commands/utils.js'; + +const PIPELINE_REPO_API = 'pipeline-repositories'; +const debug = debugSetup('cortex:cli'); + +export default class PipelineRepos { + constructor(cortexUrl) { + this.cortexUrl = cortexUrl; + this.endpoint = (projectId) => `${cortexUrl}/fabric/v4/projects/${projectId}/${PIPELINE_REPO_API}`; + } + + async listPipelineRepo(projectId, token, filter, limit, skip, sort) { + checkProject(projectId); + const endpoint = `${this.endpoint(projectId)}`; + debug('listPipelineRepo() => %s', endpoint); + const query = {}; + if (filter) query.filter = filter; + if (limit) query.limit = limit; + if (sort) query.sort = sort; + if (skip) query.skip = skip; + try { + return await got.get(endpoint, { + headers: defaultHeaders(token), + searchParams: query, + }).json(); + } catch (err) { + return constructError(err); + } + } + + async savePipelineRepo(projectId, token, repoObj) { + checkProject(projectId); + const endpoint = `${this.endpoint(projectId)}`; + debug('savePipelineRepo(%s) => %s', repoObj.name, endpoint); + try { + return await got.post(endpoint, { + headers: defaultHeaders(token), + json: repoObj, + }).json(); + } catch (err) { + return constructError(err); + } + } + + async describePipelineRepo(projectId, token, pipelineRepoName) { + checkProject(projectId); + const endpoint = `${this.endpoint(projectId)}/${encodeURIComponent(pipelineRepoName)}`; + debug('describePipelineRepo(%s) => %s', pipelineRepoName, endpoint); + try { + return await got.get(endpoint, { + headers: defaultHeaders(token), + }).json(); + } catch (err) { + return constructError(err); + } + } + + async deletePipelineRepo(projectId, token, pipelineRepoName) { + checkProject(projectId); + const endpoint = `${this.endpoint(projectId)}/${encodeURIComponent(pipelineRepoName)}`; + debug('deletePipelineRepo(%s) => %s', pipelineRepoName, endpoint); + try { + return await got.delete(endpoint, { + headers: defaultHeaders(token), + }).json(); + } catch (err) { + return constructError(err); + } + } + + async updateRepoPipelines(projectId, token, pipelineRepoName, skill, forceRecreate) { + checkProject(projectId); + const params = {}; + const endpoint = `${this.endpoint(projectId)}/${encodeURIComponent(pipelineRepoName)}/update`; + if (skill) { + params.skillName = skill; + } + if (forceRecreate) { + params.forceRecreate = true; + } + debug('updateRepoPipelines(%s) => %s', pipelineRepoName, endpoint); + try { + return await got.post(endpoint, { + headers: defaultHeaders(token), + searchParams: params, + }).json(); + } catch (err) { + return constructError(err); + } + } +} diff --git a/src/client/pipelines.js b/src/client/pipelines.js new file mode 100644 index 00000000..deabed15 --- /dev/null +++ b/src/client/pipelines.js @@ -0,0 +1,98 @@ +import debugSetup from 'debug'; +import { checkProject } from '../commands/utils.js'; +import { got, defaultHeaders } from './apiutils.js'; +import PipelineRepos from './pipelineRepositories.js'; + +const PIPELINE_API = 'pipelines'; +const debug = debugSetup('cortex:cli'); + +export default class Pipelines { + constructor(cortexUrl) { + this.cortexUrl = cortexUrl; + this.endpointV4 = (projectId) => `${cortexUrl}/fabric/v4/projects/${projectId}/${PIPELINE_API}`; + } + + listPipelines(projectId, token, filter, limit, skip, sort) { + checkProject(projectId); + const endpoint = this.endpointV4(projectId); + debug('listPipelines() => %s', endpoint); + const query = {}; + if (filter) query.filter = filter; + if (limit) query.limit = limit; + if (sort) query.sort = sort; + if (skip) query.skip = skip; + return got.get(endpoint, { + headers: defaultHeaders(token), + searchParams: query, + }).json(); + } + + describePipeline(projectId, token, name, gitRepoName, sha) { + checkProject(projectId); + const endpoint = `${this.endpointV4(projectId)}/${encodeURIComponent(name)}`; + debug('describePipeline(%s, %s) => %s', name, gitRepoName, endpoint); + const searchParams = { gitRepoName }; + if (sha) { + searchParams.sha = sha; + } + return got.get(endpoint, { + headers: defaultHeaders(token), + searchParams: { ...searchParams }, + }).json(); + } + + runPipeline(projectId, token, name, gitRepoName, params, options) { + checkProject(projectId); + const endpoint = `${this.endpointV4(projectId)}/${encodeURIComponent(name)}/run`; + debug('runPipeline(%s, %s) => %s', name, gitRepoName, endpoint); + const { commit, block } = options; + const query = { }; + if (options?.scheduleName) { + query.scheduleName = options.scheduleName; + } + if (options?.scheduleCron) { + query.scheduleCron = options.scheduleCron; + } + + const body = { gitRepoName, ...params }; + if (commit) { + body.commit = commit; + } + if (block) { + body.block = block; + } + return got.post(endpoint, { + headers: defaultHeaders(token), + json: body, + searchParams: query, + }).json(); + } + + describePipelineRun(projectId, token, runId) { + checkProject(projectId); + const endpoint = `${this.endpointV4(projectId)}/run/${runId}`; + debug('describePipeline(%s) => %s', runId, endpoint); + return got.get(endpoint, { + headers: defaultHeaders(token), + }).json(); + } + + listPipelineRuns(projectId, token, name, gitRepoName, limit, skip, sort, filter) { + checkProject(projectId); + const endpoint = `${this.endpointV4(projectId)}/${encodeURIComponent(name)}/run`; + debug('listPipelineRuns(%s, %s) => %s', name, gitRepoName, endpoint); + const query = { gitRepoName }; + if (filter) query.filter = filter; + if (limit) query.limit = limit; + if (sort) query.sort = sort; + if (skip) query.skip = skip; + return got.get(endpoint, { + headers: defaultHeaders(token), + searchParams: query, + }).json(); + } + + repos() { + return new PipelineRepos(this.cortexUrl); + } + } diff --git a/src/client/processors.js b/src/client/processors.js deleted file mode 100644 index 040593e2..00000000 --- a/src/client/processors.js +++ /dev/null @@ -1,148 +0,0 @@ -/* - * Copyright 2018 Cognitive Scale, Inc. All Rights Reserved. - * - * 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. - */ - -const request = require('superagent'); -const debug = require('debug')('cortex:cli'); -const { constructError } = require('../commands/utils'); - -module.exports = class Processors { - - constructor(cortexUrl) { - this.cortexUrl = cortexUrl; - this.endpoint = `${cortexUrl}/v2/processors`; - } - - listRuntimeTypes(token) { - const endpoint = `${this.endpoint}/runtime-types`; - debug('listRuntimeTypes() => %s', endpoint); - return request - .get(endpoint) - .set('Authorization', `Bearer ${token}`) - .then((res) => { - if (res.ok) { - return {success: true, runtimeTypes: res.body.runtimeTypes}; - } - return {success: false, status: res.status, message: res.body}; - }) - .catch((err) => { - return constructError(err); - }); - } - - listRuntimes(token) { - const endpoint = `${this.endpoint}/runtimes`; - debug('listRuntimes() => %s', endpoint); - return request - .get(endpoint) - .set('Authorization', `Bearer ${token}`) - .then((res) => { - if (res.ok) { - return {success: true, runtimes: res.body.runtimes}; - } - return {success: false, status: res.status, message: res.body}; - }) - .catch((err) => { - return constructError(err); - }); - } - - describeRuntime(token, runtimeName) { - const endpoint = `${this.endpoint}/runtimes/${runtimeName}`; - debug('describeRuntime(%s) => %s', runtimeName, endpoint); - return request - .get(endpoint) - .set('Authorization', `Bearer ${token}`) - .then((res) => { - if (res.ok) { - return {success: true, runtime: res.body.runtime}; - } - return {success: false, status: res.status, message: res.body}; - }) - .catch((err) => { - return constructError(err); - }); - } - - saveRuntime(token, {name, title, description, runtimeType, params}) { - const endpoint = `${this.endpoint}/runtimes`; - debug('saveRuntime(%s) => %s', name, endpoint); - return request - .post(endpoint) - .set('Authorization', `Bearer ${token}`) - .send({name, title, description, runtimeType, params}) - .then((res) => { - if (res.ok) { - return {success: true, version: res.body.version}; - } - return {success: false, status: res.status, message: res.body}; - }) - .catch((err) => { - return constructError(err); - }); - } - - deleteRuntime(token, runtimeName) { - const endpoint = `${this.endpoint}/runtimes/${runtimeName}`; - debug('deleteRuntime(%s) => %s', runtimeName, endpoint); - return request - .delete(endpoint) - .set('Authorization', `Bearer ${token}`) - .then((res) => { - if (res.ok) { - return {success: true}; - } - return {success: false, status: res.status, message: res.body}; - }) - .catch((err) => { - return constructError(err); - }); - } - - listRuntimeActions(token, runtimeName) { - const endpoint = `${this.endpoint}/runtimes/${runtimeName}/actions`; - debug('listRuntimeActions() => %s', endpoint); - return request - .get(endpoint) - .set('Authorization', `Bearer ${token}`) - .then((res) => { - if (res.ok) { - return {success: true, actions: res.body.actions}; - } - return {success: false, status: res.status, message: res.body}; - }) - .catch((err) => { - return constructError(err); - }); - } - - invokeRuntimeAction(token, runtimeName, actionId, params) { - const endpoint = `${this.endpoint}/runtimes/${runtimeName}/actions/invoke`; - debug('invokeRuntimeAction(%s, %s) => %s', runtimeName, actionId, endpoint); - return request - .post(endpoint) - .set('Authorization', `Bearer ${token}`) - .send({actionId: actionId, params: params}) - .then((res) => { - if (res.ok) { - return {success: true, result: res.body}; - } - return {success: false, status: res.status, message: res.body}; - }) - .catch((err) => { - return constructError(err); - }); - } -}; \ No newline at end of file diff --git a/src/client/roles.js b/src/client/roles.js new file mode 100644 index 00000000..2a077ea2 --- /dev/null +++ b/src/client/roles.js @@ -0,0 +1,191 @@ +/* + * Copyright 2023 Cognitive Scale, Inc. All Rights Reserved. + * + * 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 debugSetup from 'debug'; +import { got, defaultHeaders } from './apiutils.js'; + +const debug = debugSetup('cortex:cli'); +export default class Roles { + constructor(cortexUrl, role, flags = []) { + this.cortexUrl = cortexUrl; + this.endpoint = `${cortexUrl}/fabric/v4`; + this.flags = ''; + if (role) { + this.rolesEndpoint = `${this.endpoint}/roles/${role}`; + } else { + this.rolesEndpoint = `${this.endpoint}/roles`; + } + this.rolesUsersEndpoint = `${this.rolesEndpoint}/users`; + this.rolesGrantsEndpoint = `${this.rolesEndpoint}/grants`; + this.rolesProjectsEndpoint = `${this.rolesEndpoint}/projects`; + this.rolesMappingsEndpoint = `${this.rolesEndpoint}/mappings`; + if (flags.length > 0) { + this.flags = `${flags.reduce((flagString, flag) => { + if (flagString) { + flagString += `&${flag}=true`; + } else { + flagString += `?${flag}=true`; + } + return flagString; + }, '')}`; + } + } + + describeRole(token) { + const endpoint = this.rolesEndpoint + this.flags; + debug('describeRole => %s', endpoint); + return got + .get(endpoint, { + headers: defaultHeaders(token), + }).json(); + } + + deleteRole(token) { + const endpoint = this.rolesEndpoint; + debug('deleteRole => %s', endpoint); + return got + .delete(endpoint, { + headers: defaultHeaders(token), + }).json(); + } + + createRole(token, body) { + const endpoint = this.rolesEndpoint; + debug('createRole => %s', endpoint); + return got + .post(endpoint, { + headers: defaultHeaders(token), + json: body, + }).json(); + } + + addUsersToRole(token, users) { + const endpoint = `${this.rolesUsersEndpoint}`; + debug('addUsersToRole => %s', endpoint); + return got + .post(endpoint, { + headers: defaultHeaders(token), + json: { users }, + }).json(); + } + + removeUsersFromRole(token, users) { + const endpoint = `${this.rolesUsersEndpoint}`; + debug('removeUsersFromRole => %s', endpoint); + return got + .delete(endpoint, { + headers: defaultHeaders(token), + json: { users }, + }).json(); + } + + createGrantForRole(token, body) { + const endpoint = `${this.rolesGrantsEndpoint}`; + debug('createGrantForRole => %s', endpoint); + return got + .post(endpoint, { + headers: defaultHeaders(token), + json: body, + }).json(); + } + + removeGrantFromRole(token, body) { + const endpoint = `${this.rolesGrantsEndpoint}`; + debug('removeGrantFromRole => %s', endpoint); + return got + .delete(endpoint, { + headers: defaultHeaders(token), + json: body, + }).json(); + } + + listRoles(token, project) { + let endpoint = this.rolesEndpoint; + if (project) { + endpoint += `?project=${project}`; + } + debug('listRoles => %s', endpoint); + return got + .get(endpoint, { + headers: defaultHeaders(token), + }).json(); + } + + addRolesToProject(token, project, roles) { + const endpoint = `${this.rolesProjectsEndpoint}`; + debug('addRolesToProject(%s) => %s', roles, endpoint); + return got + .post(endpoint, { + headers: defaultHeaders(token), + json: { project, roles }, + }).json(); + } + + removeRolesFromProject(token, project, roles) { + const endpoint = `${this.rolesProjectsEndpoint}`; + debug('removeRolesFromProject => %s', endpoint); + return got + .delete(endpoint, { + headers: defaultHeaders(token), + json: { project, roles }, + }).json(); + } + + listExternalGroups(token) { + const endpoint = this.rolesMappingsEndpoint; + debug('listExternalGroups => %s', endpoint); + return got + .get(endpoint, { + headers: defaultHeaders(token), + }).json(); + } + + describeExternalGroup(token, externalGroup) { + const endpoint = `${this.rolesMappingsEndpoint}/${externalGroup}${this.flags}`; + debug('describeExternalGroup => %s', endpoint); + return got + .get(endpoint, { + headers: defaultHeaders(token), + }).json(); + } + + deleteExternalGroup(token, externalGroup) { + const endpoint = `${this.rolesMappingsEndpoint}/${externalGroup}`; + debug('deleteExternalGroup => %s', endpoint); + return got + .delete(endpoint, { + headers: defaultHeaders(token), + }).json(); + } + + addExternalGroupToRole(token, externalGroup) { + const endpoint = `${this.rolesMappingsEndpoint}`; + debug('addExternalGroupToRole(%s) => %s', externalGroup, endpoint); + return got + .post(endpoint, { + headers: defaultHeaders(token), + json: { externalGroup }, + }).json(); + } + + removeExternalGroupFromRole(token, externalGroup) { + const endpoint = `${this.rolesMappingsEndpoint}/${externalGroup}`; + debug('removeExternalGroupFromRole => %s', endpoint); + return got + .delete(endpoint, { + headers: defaultHeaders(token), + }).json(); + } +} diff --git a/src/client/secrets.js b/src/client/secrets.js new file mode 100644 index 00000000..6be6627a --- /dev/null +++ b/src/client/secrets.js @@ -0,0 +1,69 @@ +/* + * Copyright 2023 Cognitive Scale, Inc. All Rights Reserved. + * + * 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 debugSetup from 'debug'; +import urljoin from 'url-join'; +import { got, defaultHeaders } from './apiutils.js'; +import { checkProject } from '../commands/utils.js'; + +const debug = debugSetup('cortex:cli'); +export default (class Secrets { + constructor(cortexUrl) { + this.cortexUrl = cortexUrl; + this.endpoint = (projectId) => urljoin(cortexUrl, 'fabric/v4/projects', projectId, 'secrets'); + } + + listSecrets(projectId, token) { + checkProject(projectId); + const endpoint = urljoin(this.endpoint(projectId), '?list=true'); + debug('listSecrets => %s', endpoint); + return got + .get(endpoint, { + headers: defaultHeaders(token), + }).json(); + } + + readSecret(projectId, token, keyName) { + checkProject(projectId); + const endpoint = urljoin(this.endpoint(projectId), keyName); + debug('readSecret(%s) => %s', keyName, endpoint); + return got + .get(endpoint, { + headers: defaultHeaders(token), + }).json(); + } + + deleteSecret(projectId, token, keyName) { + checkProject(projectId); + const endpoint = urljoin(this.endpoint(projectId), keyName); + debug('deleteSecret(%s) => %s', keyName, endpoint); + return got + .delete(endpoint, { + headers: defaultHeaders(token), + }).json(); + } + + writeSecret(projectId, token, keyName, value) { + checkProject(projectId); + const endpoint = urljoin(this.endpoint(projectId), keyName); + debug('writeSecret(%s) => %s', keyName, endpoint); + const body = { value }; + return got + .post(endpoint, { + headers: defaultHeaders(token), + json: body, + }).json(); + } +}); diff --git a/src/client/sessions.js b/src/client/sessions.js new file mode 100644 index 00000000..98980b01 --- /dev/null +++ b/src/client/sessions.js @@ -0,0 +1,70 @@ +/* + * Copyright 2023 Cognitive Scale, Inc. All Rights Reserved. + * + * 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 debugSetup from 'debug'; +import { got, defaultHeaders } from './apiutils.js'; +import { checkProject } from '../commands/utils.js'; + +const debug = debugSetup('cortex:cli'); +export default (class Sessions { + constructor(cortexUrl) { + this.cortexUrl = cortexUrl; + this.endpointV4 = (projectId) => `${cortexUrl}/fabric/v4/projects/${projectId}/sessions`; + } + + saveSession(projectId, token, sessionObj) { + checkProject(projectId); + const endpoint = this.endpointV4(projectId); + debug('saveSession(%s) => %s', sessionObj.name, endpoint); + return got + .post(endpoint, { + headers: defaultHeaders(token), + json: sessionObj, + }).json(); + } + + deleteSession(projectId, token, sessionName) { + checkProject(projectId); + const endpoint = `${this.endpointV4(projectId)}/${encodeURIComponent(sessionName)}`; + debug('deleteSession(%s) => %s', sessionName, endpoint); + return got + .delete(endpoint, { + headers: defaultHeaders(token), + }) + .json(); + } + + describeSession(projectId, token, sessionName, verbose) { + checkProject(projectId); + const endpoint = `${this.endpointV4(projectId)}/${encodeURIComponent(sessionName)}`; + debug('describeSession(%s) => %s', sessionName, endpoint); + return got + .get(endpoint, { + headers: defaultHeaders(token), + searchParams: { verbose }, + }).json(); + } + + listSessions(projectId, token, limit) { + checkProject(projectId); + const endpoint = `${this.endpointV4(projectId)}?limit=${limit}`; + debug('listSessions() => %s', endpoint); + return got + .get(endpoint, { + headers: defaultHeaders(token), + }) + .json(); + } +}); diff --git a/src/client/tasks.js b/src/client/tasks.js index ccf75613..38ccb410 100644 --- a/src/client/tasks.js +++ b/src/client/tasks.js @@ -1,5 +1,5 @@ /* - * Copyright 2018 Cognitive Scale, Inc. All Rights Reserved. + * Copyright 2023 Cognitive Scale, Inc. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the “License”); * you may not use this file except in compliance with the License. @@ -13,80 +13,97 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import debugSetup from 'debug'; +// eslint-disable-next-line import/no-unresolved +import got from 'got'; +import { got as gotAPIUtils, defaultHeaders } from './apiutils.js'; +import { constructError, checkProject } from '../commands/utils.js'; -const request = require('superagent'); -const debug = require('debug')('cortex:cli'); -const { constructError } = require('../commands/utils'); +const debug = debugSetup('cortex:cli'); +export default (class Tasks { + constructor(cortexUrl) { + this.cortexUrl = cortexUrl; + this.endpointV4 = (projectId) => `${cortexUrl}/fabric/v4/projects/${projectId}`; + } -module.exports = class Tasks { + listTasks(projectId, token, params) { + checkProject(projectId); + const endpoint = `${this.endpointV4(projectId)}/tasks`; + debug('listTasks(%s) => %s', projectId, endpoint); + const opts = { + headers: defaultHeaders(token), + }; + if (params) { + opts.searchParams = params; + } + return gotAPIUtils.get(endpoint, opts).json(); + } - constructor(cortexUrl, jobId) { - this.cortexUrl = cortexUrl; - this.endpoint = `${cortexUrl}/v2/jobs/${jobId}/tasks`; + getTask(projectId, taskName, token, params) { + checkProject(projectId); + const endpoint = `${this.endpointV4(projectId)}/tasks/${taskName}`; + debug('getTask(%s, %s) => %s', projectId, taskName, endpoint); + const opts = { + headers: defaultHeaders(token), + }; + if (params) { + opts.searchParams = params; + } + return got + .get(endpoint, opts).json(); } - listTasks(token) { - const endpoint = `${this.endpoint}`; - return request - .get(endpoint) - .set('Authorization', `Bearer ${token}`) - .then((res) => { - if (res.ok) { - return { success: true, result: res.body }; - } - return { success: false, status: res.status, message: res.body }; - }) - .catch((err) => { - return constructError(err); + taskLogs(projectId, token, taskName, follow = false, verbose = false) { + checkProject(projectId); + const endpoint = `${this.endpointV4(projectId)}/tasks/${taskName}/logs`; + debug('taskLogs(%s) => %s', taskName, endpoint); + if (follow) { + return got.stream(endpoint, { + headers: defaultHeaders(token), + searchParams: { follow, verbose }, }); + // eslint-disable-next-line no-else-return + } else { + return gotAPIUtils + .get(endpoint, { + headers: defaultHeaders(token), + searchParams: { follow, verbose }, + }).json() + .then((res) => ({ ...res })) + .catch((err) => constructError(err)); + } } - taskLogs(token, taskId) { - const endpoint = `${this.endpoint}/${taskId}/logs`; - return request - .get(endpoint) - .set('Authorization', `Bearer ${token}`) - .then((res) => { - if (res.ok) { - return { success: true, result: res.body }; - } - return { success: false, status: res.status, message: res.body }; - }) - .catch((err) => { - return constructError(err); - }); + deleteTask(projectId, token, taskName, verbose = false) { + checkProject(projectId); + const endpoint = `${this.endpointV4(projectId)}/tasks/${taskName}`; + debug('deleteTask(%s) => %s', taskName, endpoint); + return gotAPIUtils + .delete(endpoint, { + headers: defaultHeaders(token), + searchParams: { verbose }, + }).json(); } - cancelTask(token, taskId, message) { - const endpoint = `${this.endpoint}/${taskId}/cancel`; - return request - .post(endpoint) - .set('Authorization', `Bearer ${token}`) - .send({message}) - .then((res) => { - if (res.ok) { - return { success: true, result: res.body }; - } - return { success: false, status: res.status, message: res.body }; - }) - .catch((err) => { - return constructError(err); - }); + pauseTask(projectId, token, taskName, verbose = false) { + checkProject(projectId); + const endpoint = `${this.endpointV4(projectId)}/tasks/${taskName}/pause`; + debug('pauseTask(%s) => %s', taskName, endpoint); + return gotAPIUtils + .post(endpoint, { + headers: defaultHeaders(token), + searchParams: { verbose }, + }).json(); } - describeTask(token, taskId) { - const endpoint = `${this.endpoint}/${taskId}`; - return request - .get(endpoint) - .set('Authorization', `Bearer ${token}`) - .then((res) => { - if (res.ok) { - return { success: true, result: res.body }; - } - return { success: false, status: res.status, message: res.body }; - }) - .catch((err) => { - return constructError(err); - }); + resumeTask(projectId, token, taskName, verbose = false) { + checkProject(projectId); + const endpoint = `${this.endpointV4(projectId)}/tasks/${taskName}/resume`; + debug('resumeTask(%s) => %s', taskName, endpoint); + return gotAPIUtils + .post(endpoint, { + headers: defaultHeaders(token), + searchParams: { verbose }, + }).json(); } -}; \ No newline at end of file +}); diff --git a/src/client/users.js b/src/client/users.js new file mode 100644 index 00000000..da5422fb --- /dev/null +++ b/src/client/users.js @@ -0,0 +1,142 @@ +/* + * Copyright 2023 Cognitive Scale, Inc. All Rights Reserved. + * + * 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 debugSetup from 'debug'; +import _ from 'lodash'; +import { got, defaultHeaders } from './apiutils.js'; + +const debug = debugSetup('cortex:cli'); +export default class Users { + constructor(cortexUrl, user, flags = []) { + this.cortexUrl = cortexUrl; + this.endpoint = `${cortexUrl}/fabric/v4`; + this.flags = ''; + if (user) { + this.usersEndpoint = `${this.endpoint}/usergrants/${user}`; + } else { + this.usersEndpoint = `${this.endpoint}/usergrants`; + } + this.usersProjectsEndpoint = `${this.usersEndpoint}/projects`; + if (flags.length > 0) { + this.flags = `${flags.reduce((flagString, flag) => { + if (flagString) { + flagString += `&${flag}=true`; + } else { + flagString += `?${flag}=true`; + } + return flagString; + }, '')}`; + } + } + + createServiceUser(token, user) { + const endpoint = `${this.endpoint}/accounts/service/${user}`; + debug('createServiceUser => %s', endpoint); + return got + .post(endpoint, { + headers: defaultHeaders(token), + }).json(); + } + + deleteServiceUser(token, user) { + const endpoint = `${this.endpoint}/accounts/service/${user}`; + debug('deleteServiceUser => %s', endpoint); + return got + .delete(endpoint, { + headers: defaultHeaders(token), + }).json(); + } + + listServiceUsers(token) { + const endpoint = `${this.endpoint}/accounts/service`; + debug('listServiceUser => %s', endpoint); + return got + .get(endpoint, { + headers: defaultHeaders(token), + }).json(); + } + + describeUser(token) { + const endpoint = `${this.usersEndpoint}/${this.flags}`; + debug('describeUser => %s', endpoint); + return got + .get(endpoint, { + headers: defaultHeaders(token), + }).json(); + } + + createGrantForUser(token, body) { + const endpoint = this.usersEndpoint; + debug('createGrantForUser => %s', endpoint); + return got + .post(endpoint, { + headers: defaultHeaders(token), + json: body, + }).json(); + } + + removeGrantFromUser(token, body) { + const endpoint = this.usersEndpoint; + debug('removeGrantFromUser => %s', endpoint); + return got + .delete(endpoint, { + headers: defaultHeaders(token), + json: body, + }).json(); + } + + addUsersToProject(token, project, users) { + const endpoint = `${this.usersProjectsEndpoint}`; + debug('addUsersToProject => %s', endpoint); + return got + .post(endpoint, { + headers: defaultHeaders(token), + json: { project, users }, + }).json(); + } + + removeUsersFromProject(token, project, users) { + const endpoint = `${this.usersProjectsEndpoint}`; + debug('removeUsersFromProject => %s', endpoint); + return got + .delete(endpoint, { + headers: defaultHeaders(token), + json: { project, users }, + }).json(); + } + + resetUserPAT(token, user) { + const endpoint = `${this.endpoint}/accounts/pat/invalidate`; + debug('resetUserPAT => %s', endpoint); + return got + .post(endpoint, { + headers: defaultHeaders(token), + json: { user }, + }).json(); + } + + getUserPAT(token, user) { + const endpoint = `${this.endpoint}/accounts/pat`; + debug('resetUserPAT => %s', endpoint); + const requestOptions = { + headers: defaultHeaders(token), + }; + if (!_.isEmpty(user)) { + requestOptions.searchParams = { user }; + } + return got + .get(endpoint, requestOptions).json(); + } +} diff --git a/src/commands/accounts.js b/src/commands/accounts.js deleted file mode 100644 index dc63b9a9..00000000 --- a/src/commands/accounts.js +++ /dev/null @@ -1,339 +0,0 @@ -/* - * Copyright 2018 Cognitive Scale, Inc. All Rights Reserved. - * - * 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. - */ - -const fs = require('fs'); -const debug = require('debug')('cortex:cli'); -const { loadProfile } = require('../config'); -const Accounts = require('../client/accounts'); -const { printSuccess, printError, filterObject, parseObject, printTable } = require('./utils'); - -module.exports.CreateGroupCommand = class { - - constructor(program) { - this.program = program; - } - - execute(groupName, options) { - const profile = loadProfile(options.profile); - debug('%s.createGroup(%s)', profile.name, groupName); - - const accounts = new Accounts(profile.url); - const description = options.description; - accounts.createGroup(profile.token, groupName, description).then((response) => { - if (response.success) { - let result = filterObject(response.result, options); - printSuccess(JSON.stringify(result, null, 2), options); - } - else { - printError(`Failed to create group : ${response.message}`, options); - } - }) - .catch((err) => { - printError(`Failed to create group : ${err.status} ${err.message}`, options); - }); - } -}; - -module.exports.AddMembersToGroupCommand = class { - - constructor(program) { - this.program = program; - } - - execute(groupName, members, options) { - const profile = loadProfile(options.profile); - debug('%s.addMembersToGroup(%s)', profile.name, groupName); - - const accounts = new Accounts(profile.url); - accounts.addMembersToGroup(profile.token, groupName, members).then((response) => { - if (response.success) { - let result = filterObject(response.result, options); - printSuccess(JSON.stringify(result, null, 2), options); - } - else { - printError(`Failed to add members to group : ${response.message}`, options); - } - }) - .catch((err) => { - printError(`Failed to add members to group : ${err.status} ${err.message}`, options); - }); - } -}; - -module.exports.ListGroupsCommand = class { - - constructor(program) { - this.program = program; - } - - execute(options) { - const profile = loadProfile(options.profile); - debug('%s.listGroups', profile.name); - - const accounts = new Accounts(profile.url); - accounts.listGroups(profile.token).then((response) => { - if (response.success) { - let result = filterObject(response.result, options); - if (options.json) - printSuccess(JSON.stringify(result, null, 2), options); - else { - const tableSpec = [ - { column: 'Name', field: 'name', width: 50 }, - { column: 'Owner', field: 'owner', width: 25 }, - { column: 'Description', field: 'description', width: 50 }, - { column: 'Tenant', field: 'tenant', width: 25 }, - { column: 'Members', field: 'members', width: 30 }, - { column: 'Upsert Date', field: 'upsert_date', width: 26 } - ]; - - printTable(tableSpec, result); - } - } - else { - printError(`Failed to list groups : ${response.message}`, options); - } - }) - .catch((err) => { - printError(`Failed to list groups : ${err.status} ${err.message}`, options); - }); - } -}; - -module.exports.DescribeGroupCommand = class { - - constructor(program) { - this.program = program; - } - - execute(groupName, options) { - const profile = loadProfile(options.profile); - debug('%s.describeGroup(%s)', profile.name, groupName); - - const accounts = new Accounts(profile.url); - accounts.describeGroup(profile.token, groupName).then((response) => { - if (response.success) { - let result = filterObject(response.result, options); - printSuccess(JSON.stringify(result, null, 2), options); - } - else { - printError(`Failed to describe group : ${response.message}`, options); - } - }) - .catch((err) => { - printError(`Failed to describe group : ${err.status} ${err.message}`, options); - }); - } -}; - -module.exports.DeleteGroupCommand = class { - - constructor(program) { - this.program = program; - } - - execute(groupName, options) { - const profile = loadProfile(options.profile); - debug('%s.deleteGroup(%s)', profile.name, groupName); - - const accounts = new Accounts(profile.url); - accounts.deleteGroup(profile.token, groupName).then((response) => { - if (response.success) { - let result = filterObject(response.result, options); - printSuccess(JSON.stringify(result, null, 2), options); - } - else { - printError(`Failed to delete group : ${response.message}`, options); - } - }) - .catch((err) => { - printError(`Failed to delete group : ${err.status} ${err.message}`, options); - }); - } -}; - -module.exports.RemoveMembersFromGroupCommand = class { - - constructor(program) { - this.program = program; - } - - execute(groupName, members, options) { - const profile = loadProfile(options.profile); - debug('%s.removeMembersFromGroup(%s)', profile.name, groupName); - - const accounts = new Accounts(profile.url); - accounts.removeMembersFromGroup(profile.token, groupName, members).then((response) => { - if (response.success) { - let result = filterObject(response.result, options); - printSuccess(JSON.stringify(result, null, 2), options); - } - else { - printError(`Failed to remove members from group : ${response.message}`, options); - } - }) - .catch((err) => { - printError(`Failed to remove members from group : ${err.status} ${err.message}`, options); - }); - } -}; - -module.exports.RegisterResourceCommand = class { - - constructor(program) { - this.program = program; - } - - execute(resourceName, options) { - const profile = loadProfile(options.profile); - debug('%s.registerResource(%s)', profile.name, resourceName); - - const accounts = new Accounts(profile.url); - const description = options.description; - const access = options.access; - accounts.registerResource(profile.token, resourceName, description, access).then((response) => { - if (response.success) { - let result = filterObject(response.result, options); - printSuccess(JSON.stringify(result, null, 2), options); - } - else { - printError(`Failed to register resource : ${response.message}`, options); - } - }) - .catch((err) => { - printError(`Failed to register resource : ${err.status} ${err.message}`, options); - }); - } -}; - -module.exports.GrantGroupAccessToResourceCommand = class { - - constructor(program) { - this.program = program; - } - - execute(resourceId, groupName, options) { - const profile = loadProfile(options.profile); - debug('%s.grantGroupAccessToResource(resourceId: %s, groupName: %s)', profile.name, resourceId, groupName); - - const accounts = new Accounts(profile.url); - accounts.grantGroupAccessToResource(profile.token, resourceId, groupName).then((response) => { - if (response.success) { - let result = filterObject(response.result, options); - printSuccess(JSON.stringify(result, null, 2), options); - } - else { - printError(`Failed to grant group access to resource : ${response.message}`, options); - } - }) - .catch((err) => { - printError(`Failed to grant group access to resource : ${err.status} ${err.message}`, options); - }); - } -}; - -// TODO: To be implemented -// module.exports.ListResourcesCommand = class { -// -// constructor(program) { -// this.program = program; -// } -// -// execute(options) { -// const profile = loadProfile(options.profile); -// debug('%s.listGroups', profile.name); -// -// const accounts = new Accounts(profile.url); -// accounts.listResources(profile.token).then((response) => { -// if (response.success) { -// let result = filterObject(response.result, options); -// if (options.json) -// printSuccess(JSON.stringify(result, null, 2), options); -// else { -// const tableSpec = [ -// { column: 'Name', field: 'name', width: 50 }, -// { column: 'Owner', field: 'owner', width: 25 }, -// { column: 'Description', field: 'description', width: 50 }, -// { column: 'Tenant', field: 'tenant', width: 25 }, -// { column: 'Members', field: 'members', width: 30 }, -// { column: 'Upsert Date', field: 'upsert_date', width: 26 } -// ]; -// -// printTable(tableSpec, result); -// } -// } -// else { -// printError(`Failed to add members to group : ${response.message}`, options); -// } -// }) -// .catch((err) => { -// printError(`Failed to add members to group : ${err.status} ${err.message}`, options); -// }); -// } -// }; - -// TODO: To be implemented -// module.exports.UnregisterResourceCommand = class { -// -// constructor(program) { -// this.program = program; -// } -// -// execute(resourceName, options) { -// const profile = loadProfile(options.profile); -// debug('%s.deleteGroup(%s)', profile.name, resourceName); -// -// const accounts = new Accounts(profile.url); -// accounts.unregisterResource(profile.token, resourceName).then((response) => { -// if (response.success) { -// let result = filterObject(response.result, options); -// printSuccess(JSON.stringify(result, null, 2), options); -// } -// else { -// printError(`Failed to delete group : ${response.message}`, options); -// } -// }) -// .catch((err) => { -// printError(`Failed to delete group : ${err.status} ${err.message}`, options); -// }); -// } -// }; - -// TODO: To be implemented -// module.exports.RevokeGroupAccessToResourceCommand = class { -// -// constructor(program) { -// this.program = program; -// } -// -// execute(resourceId, options) { -// const profile = loadProfile(options.profile); -// debug('%s.revokeGroupAccessToResource(%s)', profile.name, resourceId); -// -// const accounts = new Accounts(profile.url); -// accounts.revokeGroupAccessToResource(profile.token, resourceId).then((response) => { -// if (response.success) { -// let result = filterObject(response.result, options); -// printSuccess(JSON.stringify(result, null, 2), options); -// } -// else { -// printError(`Failed to delete group : ${response.message}`, options); -// } -// }) -// .catch((err) => { -// printError(`Failed to delete group : ${err.status} ${err.message}`, options); -// }); -// } -// }; \ No newline at end of file diff --git a/src/commands/actions.js b/src/commands/actions.js index f365847e..a1e167ef 100644 --- a/src/commands/actions.js +++ b/src/commands/actions.js @@ -1,194 +1,195 @@ -/* - * Copyright 2018 Cognitive Scale, Inc. All Rights Reserved. - * - * 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. - */ - -const fs = require('fs'); -const debug = require('debug')('cortex:cli'); -const {loadProfile} = require('../config'); -const Actions = require('../client/actions'); -const {printSuccess, printError, filterObject, parseObject, printTable} = require('./utils'); - -module.exports.ListActionsCommand = class { - +import _ from 'lodash'; +import fs from 'node:fs'; +import debugSetup from 'debug'; +import dayjs from 'dayjs'; +import relativeTime from 'dayjs/plugin/relativeTime.js'; +import { loadProfile } from '../config.js'; +import Actions from '../client/actions.js'; +import { + printSuccess, printError, filterObject, parseObject, isNumeric, handleTable, printExtendedLogs, getFilteredOutput, handleError, +} from './utils.js'; + +const debug = debugSetup('cortex:cli'); +dayjs.extend(relativeTime); +export const ListActionsCommand = class { constructor(program) { this.program = program; } - execute(options) { - const profile = loadProfile(options.profile); + // eslint-disable-next-line consistent-return + async execute(options) { + const profile = await loadProfile(options.profile); debug('%s.executeListActions()', profile.name); - const actions = new Actions(profile.url); - actions.listActions(profile.token) - .then((response) => { - if (response.success) { - let result = response.actions; - if (options.query) - result = filterObject(result, options); - - if (options.json) { - printSuccess(JSON.stringify(result, null, 2), options); - } - else { - const tableSpec = [ - {column: 'Name', field: 'name', width: 50}, - {column: 'Image', field: 'image', width: 50}, - {column: 'Kind', field: 'kind', width: 25}, - {column: 'Created On', field: 'createdAt', width: 26} - ]; - - printTable(tableSpec, result); - } - } - else { - printError(`Failed to list actions: ${response.status} ${response.message}`, options); - } - }) - .catch((err) => { - printError(`Failed to list actions: ${err.status} ${err.message}`, options); - }); + try { + const response = await actions.listActions(options.project || profile.project, profile.token, options.filter, options.limit, options.skip, options.sort); + // eslint-disable-next-line consistent-return + const result = response.actions; + // TODO remove --query on deprecation + if (options.json || options.query) { + getFilteredOutput(result, options); + } else { + printExtendedLogs(result, options); + const tableSpec = [ + { + column: 'Name', + field: 'name', + width: 30, + }, + { + column: 'Type', + field: 'type', + width: 8, + }, + { + column: 'Image', + field: 'image', + width: 50, + }, + { + column: 'Modified', + field: 'updatedAt', + width: 26, + }, + { + column: 'Author', + field: 'createdBy', + width: 26, + }, + ]; + handleTable(tableSpec, result, (o) => ({ + ...o, + updatedAt: o.updatedAt ? dayjs(o.updatedAt) + .fromNow() : '-', + }), 'No actions found'); + } + } catch (err) { + handleError(err, options, 'Failed to list actions'); + } } }; - -module.exports.DescribeActionCommand = class { - +export const DescribeActionCommand = class { constructor(program) { this.program = program; } - execute(actionName, options) { - const profile = loadProfile(options.profile); + async execute(actionName, options) { + const profile = await loadProfile(options.profile); debug('%s.executeDescribeAction(%s)', profile.name, actionName); - const actions = new Actions(profile.url); - actions.describeAction(profile.token, actionName) - .then((response) => { - if (response.success) { - let result = filterObject(response.action, options); - printSuccess(JSON.stringify(result, null, 2), options); - } - else { - printError(`Failed to describe action: ${response.status} ${response.message}`, options); - } - }) - .catch((err) => { - printError(`Failed to describe action: ${err.status} ${err.message}`, options); - }); + try { + const response = await actions.describeAction(options.project || profile.project, profile.token, actionName); + getFilteredOutput(response.action, options); + } catch (err) { + handleError(err, options, `Failed to describe action "${actionName}"`); + } } }; - -module.exports.DeployActionCommand = class { - +export const DeployActionCommand = class { constructor(program) { this.program = program; } - execute(actionName, options) { - const profile = loadProfile(options.profile); - debug('%s.deployAction(%s)', profile.name, actionName); - - const kind = options.kind; - const dockerImage = options.docker; - const code = options.code; - const memory = parseInt(options.memory); - const timeout = parseInt(options.timeout); - const actionType = options.actionType; - const cmd = options.cmd; - const ports = options.ports; - const environment = options.environment; - - const actions = new Actions(profile.url); - actions.deployAction(profile.token, actionName, dockerImage, kind, code, memory, timeout, actionType, cmd, ports, environment) - .then((response) => { - if (response.success) { - printSuccess(JSON.stringify(response.message, null, 2), options); + async execute(actionDefinition, options) { + try { + const profile = await loadProfile(options.profile); + let actionInst = {}; + if (actionDefinition) { + debug('%s.deployActionCommand(%s)', profile.name, actionDefinition); + try { + const actionDefStr = fs.readFileSync(actionDefinition); + actionInst = parseObject(actionDefStr, options); + } catch (err) { + printError(`Failed to deploy action: ${err.message}`, options); } - else { - printError(`Action deployment failed: ${response.status} ${response.message}`, options); + } + if (options.actionName) { + actionInst.name = options.actionName; + } + if (options.name) { + actionInst.name = options.name; + } + if (options.podspec) { + const paramsStr = fs.readFileSync(options.podspec); + actionInst.podSpec = parseObject(paramsStr, options); + } + if (!_.isEmpty(options.k8sResource)) { + const k8sResources = options.k8sResource.map((f) => parseObject(fs.readFileSync(f), options)); + actionInst.k8sResources = k8sResources; + } + if (options.docker) { + actionInst.image = options.docker; + } + if (options.image) { + actionInst.image = options.image; + } + if (options.type) { + actionInst.type = options.type; + } + if (options.actionType) { + actionInst.type = options.actionType; + } + if (options.cmd) { + actionInst.command = options.cmd; + } + if (options.port) { + if (!isNumeric(options.port)) { + printError('--port must be a number', options); } - }) - .catch((err) => { - printError(`Failed to deploy action: ${err.status} ${err.message}`, options); - }); - } -}; - -module.exports.InvokeActionCommand = class { - - constructor(program) { - this.program = program; - } - - execute(actionName, options) { - const profile = loadProfile(options.profile); - debug('%s.executeInvokeAction(%s)', profile.name, actionName); - - let params = {}; - if (options.params) { - params = parseObject(options.params, options); - } - else if (options.paramsFile) { - const paramsStr = fs.readFileSync(options.paramsFile); - params = parseObject(paramsStr, options); - } - - debug('params: %o', params); - const actionType = options.actionType; - const path = options.path; - const method = options.method; - const actions = new Actions(profile.url); - actions.invokeAction(profile.token, actionName, path, params, actionType, method) - .then((response) => { - if (response.success) { - let result = filterObject(response.result, options); - printSuccess(JSON.stringify(result, null, 2), options); + actionInst.port = options.port; + } + if (options.environmentVariables) { + actionInst.environmentVariables = options.environmentVariables; + } + if (options.pushDocker) { + actionInst.pushDocker = options.pushDocker; + } + if (options.scaleCount) { + if (!isNumeric(options.scaleCount)) { + printError('--scaleCount must be a number', options); } - else { - printError(`Action invocation failed: ${response.status} ${response.message}`, options); + actionInst.scaleCount = parseInt(options.scaleCount, 10); + } + if (options.jobTimeout) { + if (!isNumeric(options.jobTimeout)) { + printError('--jobTimeout must be a number', options); } - }) - .catch((err) => { - printError(`Failed to invoke action: ${err.status} ${err.message}`, options); - }); + actionInst.jobTimeout = parseInt(options.jobTimeout, 10); + } + // handle mutually exclusive options + if (actionInst.type === 'job' && _.has(actionInst, 'port')) { + printError('Option port not valid on job action types'); + } + if (actionInst.type === 'daemon' && _.has(actionInst, 'jobTimeout')) { + printError('Option jobTimeout not valid on daemon action types'); + } + if (_.has(actionInst, 'environmentVariables') && _.isEmpty(actionInst.environmentVariables)) { + // README: allowing user to ignore/skip environmentVariables by keeping it empty/null ("", [], null) + actionInst = _.omit(actionInst, 'environmentVariables'); + } + const actions = new Actions(profile.url); + const response = await actions.deployAction(options.project || profile.project, profile.token, actionInst); + printSuccess(JSON.stringify(response.message, null, 2), options); + } catch (err) { + printError(err, options, 'Failed to deploy action'); + } } }; - - -module.exports.DeleteActionCommand = class { - +export const DeleteActionCommand = class { constructor(program) { this.program = program; } - execute(actionName, options) { - const profile = loadProfile(options.profile); + async execute(actionName, options) { + const profile = await loadProfile(options.profile); debug('%s.executeDeleteAction(%s)', profile.name, actionName); - const actionType = options.actionType; const actions = new Actions(profile.url); - actions.deleteAction(profile.token, actionName, actionType) - .then((response) => { - if (response.success) { - let result = filterObject(response, options); - printSuccess(JSON.stringify(result, null, 2), options); - } - else { - printError(`Action deletion failed: ${response.status} ${response.message}`, options); - } - }) - .catch((err) => { - printError(`Failed to delete action: ${err.status} ${err.message}`, options); - }); + try { + const response = await actions.deleteAction(options.project || profile.project, profile.token, actionName); + const result = filterObject(response, options); + return printSuccess(JSON.stringify(result, null, 2), options); + } catch (err) { + return handleError(err, { ...options, tableformat: 'DEPENDENCYTABLEFORMAT' }, `Failed to delete action "${actionName}"`, options); + } } -}; \ No newline at end of file +}; diff --git a/src/commands/agents.js b/src/commands/agents.js index 7b1ce146..01bc2db5 100644 --- a/src/commands/agents.js +++ b/src/commands/agents.js @@ -1,454 +1,508 @@ -/* - * Copyright 2018 Cognitive Scale, Inc. All Rights Reserved. - * - * 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. - */ - -const fs = require('fs'); -const debug = require('debug')('cortex:cli'); -const { loadProfile } = require('../config'); -const Catalog = require('../client/catalog'); -const Agents = require('../client/agents'); -const _ = require('lodash/object'); -const { printSuccess, printError, filterObject, parseObject, printTable } = require('./utils'); - -module.exports.SaveAgentCommand = class SaveAgentCommand { - - constructor(program) { - this.program = program; - } - - execute(agentDefinition, options) { - const profile = loadProfile(options.profile); - debug('%s.executeSaveAgent(%s)', profile.name, agentDefinition); - - const agentDefStr = fs.readFileSync(agentDefinition); - const agent = parseObject(agentDefStr, options); - debug('%o', agent); - - const catalog = new Catalog(profile.url); - catalog.saveAgent(profile.token, agent).then((response) => { - if (response.success) { - printSuccess(`Agent saved`, options); - } - else { - printError(`Failed to save agent: ${response.status} ${response.message}`, options); - } - }) - .catch((err) => { - printError(`Failed to save agent: ${err.status} ${err.message}`, options); - }); - } -}; - -module.exports.ListAgentsCommand = class ListAgentsCommand { - +import fs from 'node:fs'; +import debugSetup from 'debug'; +import _ from 'lodash'; +import dayjs from 'dayjs'; +import relativeTime from 'dayjs/plugin/relativeTime.js'; +import { loadProfile } from '../config.js'; +// eslint-disable-next-line import/no-named-as-default +import Catalog from '../client/catalog.js'; +import Agents from '../client/agents.js'; +import { + LISTTABLEFORMAT, + filterObject, + getFilteredOutput, + getQueryOptions, + handleDeleteFailure, + handleTable, + parseObject, + printError, + printExtendedLogs, + printSuccess, + printTable, + writeOutput, + printErrorDetails, + handleError, +} from './utils.js'; + +const debug = debugSetup('cortex:cli'); +dayjs.extend(relativeTime); +export class ListAgentsCommand { constructor(program) { this.program = program; } - execute(options) { - const profile = loadProfile(options.profile); + // eslint-disable-next-line consistent-return + async execute(options) { + const profile = await loadProfile(options.profile); debug('%s.executeListAgents()', profile.name); - const catalog = new Catalog(profile.url); - catalog.listAgents(profile.token).then((response) => { - if (response.success) { - let result = response.agents; - if (options.query) - result = filterObject(result, options); - - if (options.json) { - printSuccess(JSON.stringify(result, null, 2), options); - } - else { - const tableSpec = [ - { column: 'Name', field: 'name', width: 50 }, - { column: 'Title', field: 'title', width: 25 }, - { column: 'Description', field: 'description', width: 50 }, - { column: 'Created On', field: 'createdAt', width: 26 } - ]; - - printTable(tableSpec, result); - } - } - else { - printError(`Failed to list agents: ${response.status} ${response.message}`, options); + // eslint-disable-next-line consistent-return + try { + const { agents } = await catalog.listAgents(options.project || profile.project, profile.token, options.filter, options.limit, options.skip, options.sort); + if (options.json || options.query) { + getFilteredOutput(agents, options); + } else { + printExtendedLogs(agents, options); + handleTable(LISTTABLEFORMAT, agents, (o) => ({ ...o, updatedAt: o.updatedAt ? dayjs(o.updatedAt).fromNow() : '-' }), 'No agents found'); } - }) - .catch((err) => { - debug(err); - printError(`Failed to list agents: ${err.status} ${err.message}`, options); - }); + } catch (err) { + handleError(err, options, 'Failed to list agents'); + } } -}; - -module.exports.DescribeAgentCommand = class DescribeAgentCommand { - +} +export class DescribeAgentCommand { constructor(program) { this.program = program; } - execute(agentName, options) { - const profile = loadProfile(options.profile); + async execute(agentName, options) { + const profile = await loadProfile(options.profile); const catalog = new Catalog(profile.url); - if (options.versions) { - debug('%s.executeDescribeAgentVersions(%s)', profile.name, agentName); - catalog.describeAgentVersions(profile.token, agentName).then((response) => { - if (response.success) { - let result = filterObject(response.agent, options); - printSuccess(JSON.stringify(result, null, 2), options); - } - else { - printError(`Failed to describe agent versions ${agentName}: ${response.message}`, options); - } - }) - .catch((err) => { - printError(`Failed to describe agent versions ${agentName}: ${err.status} ${err.message}`, options); - }); - } - - else { + const { + versions, verbose, output, project, + } = options; + if (versions) { + try { + debug('%s.executeDescribeAgentVersions(%s)', profile.name, agentName); + const response = await catalog.describeAgentVersions(options.project || profile.project, profile.token, agentName); + return writeOutput(getFilteredOutput(response.agent, options), options); + } catch (err) { + return handleError(err, options, `Failed to describe agent versions ${agentName}`); + } + } else { debug('%s.executeDescribeAgent(%s)', profile.name, agentName); - catalog.describeAgent(profile.token, agentName).then((response) => { - if (response.success) { - let result = filterObject(response.agent, options); - printSuccess(JSON.stringify(result, null, 2), options); - } - else { - printError(`Failed to describe agent ${agentName}: ${response.message}`, options); - } - }) - .catch((err) => { - printError(`Failed to describe agent ${agentName}: ${err.status} ${err.message}`, options); - }); + try { + const response = await catalog.describeAgent(project || profile.project, profile.token, agentName, verbose, output); + if ((output ?? 'json').toLowerCase() === 'json') return getFilteredOutput(response, options); + return writeOutput(response, options); + } catch (err) { + return handleError(err, options, `Failed to describe agent ${agentName}`); + } } } -}; - -module.exports.InvokeAgentServiceCommand = class { - +} +export class InvokeAgentServiceCommand { constructor(program) { this.program = program; } - execute(agentName, serviceName, options) { - const profile = loadProfile(options.profile); + async execute(agentName, serviceName, options) { + const profile = await loadProfile(options.profile); debug('%s.invokeAgentService(%s, %s)', profile.name, agentName, serviceName); - let params = {}; if (options.params) { - params = parseObject(options.params, options); - } - else if (options.paramsFile) { + try { + params = parseObject(options.params, options); + } catch (e) { + printError(`Failed to parse params: ${options.params} Error: ${e}`, options); + } + } else if (options.paramsFile) { + if (!fs.existsSync(options.paramsFile)) { + printError(`File does not exist at: ${options.paramsFile}`); + } const paramsStr = fs.readFileSync(options.paramsFile); params = parseObject(paramsStr, options); } - debug('params: %o', params); - const agents = new Agents(profile.url); - agents.invokeAgentService(profile.token, agentName, serviceName, params).then((response) => { - if (response.success) { - let result = filterObject(response.result, options); - printSuccess(JSON.stringify(result, null, 2), options); + try { + const response = await agents.invokeAgentService(options.project || profile.project, profile.token, agentName, serviceName, params, options); + const result = filterObject(response, options); + printSuccess(JSON.stringify(result, null, 2), options); + } catch (err) { + let message = `Failed to invoke agent service (${err.message})`; + if (err?.response?.body) { + message = `${message}: ${err.response.body}`; } - else { - printError(`Invocation failed: ${response.status} ${response.message}`, options); - } - }) - .catch((err) => { - if (err.response && err.response.body) { - debug('Raw error response: %o', err.response.body); - printError(`Failed to invoke agent service (${err.status} ${err.message}): ${err.response.body.error}`, options); - } - else { - printError(`Failed to invoke agent service: ${err.status} ${err.message}`, options); - } - }); + printError(message, options, false); + printErrorDetails(err, options); + } } -}; - -module.exports.GetServiceActivationCommand = class { - +} +export class GetActivationCommand { constructor(program) { this.program = program; } - execute(activationId, options) { - const profile = loadProfile(options.profile); - debug('%s.getServiceActivation(%s)', profile.name, activationId); - + async execute(activationId, options) { + const profile = await loadProfile(options.profile); + debug('%s.getActivation(%s, %s)', profile.name, activationId, JSON.stringify(options)); + const { + report, + verbose, + project, + } = options; const agents = new Agents(profile.url); - agents.getServiceActivation(profile.token, activationId).then((response) => { - if (response.success) { - let result = filterObject(response.result.activation, options); - printSuccess(JSON.stringify(result, null, 2), options); - } - else { - printError(`Failed to get service activation ${activationId}: ${response.message}`, options); + try { + const activation = await agents.getActivation(project || profile.project, profile.token, activationId, verbose, report); + if (options.report && !options.json) { + const result = filterObject(activation, getQueryOptions(options)); + const tableSpec = [ + { + column: 'Name', + field: 'name', + width: 40, + }, + { + column: 'Title', + field: 'title', + width: 40, + }, + { + column: 'Type', + field: 'type', + width: 20, + }, + { + column: 'Status', + field: 'status', + width: 20, + }, + { + column: 'Elapsed (ms)', + field: 'elapsed', + width: 30, + }, + ]; + printSuccess(`Status: ${_.get(result, 'status')}`); + printSuccess(`Elapsed Time (ms): ${_.get(result, 'elapsed')}`); + printTable(tableSpec, _.sortBy(_.get(result, 'transits'), ['start', 'end'])); + } else { + getFilteredOutput(activation, options); } - }) - .catch((err) => { - printError(`Failed to get service activation ${activationId}: ${err.status} ${err.message}`, options); - }); + } catch (err) { + handleError(err, options, `Failed to get activation ${activationId}`); + } } -}; - - -module.exports.ListAgentInstancesCommand = class { +} +export class CancelActivationCommand { constructor(program) { this.program = program; } - execute(agentName, options) { - const profile = loadProfile(options.profile); - debug('%s.listAgentInstances(%s)', profile.name, agentName); - + async execute(activationId, options) { + const profile = await loadProfile(options.profile); + debug('%s.cancelActivation(%s, %s)', profile.name, activationId, JSON.stringify(options)); + const { inFlight, project } = options; const agents = new Agents(profile.url); - agents.listAgentInstances(profile.token, agentName).then((response) => { - if (response.success) { - let result = filterObject(response.result, options); - printSuccess(JSON.stringify(result, null, 2), options); - } - else { - printError(`Failed to list agent instances ${agentName}: ${response.message}`, options); - } - }) - .catch((err) => { - printError(`Failed to list agent instances ${agentName}: ${err.status} ${err.message}`, options); - }); + try { + const response = await agents.cancelActivation(project || profile.project, profile.token, activationId, inFlight); + printSuccess(response.message); + } catch (err) { + handleError(err, options, `Failed to cancel activation ${activationId}`); + } } -}; +} -module.exports.ListAgentSnapshotsCommand = class { +export class ListActivationsCommand { constructor(program) { this.program = program; } - execute(agentName, options) { - const profile = loadProfile(options.profile); - debug('%s.listAgentSnapshots(%s)', profile.name, agentName); - + async execute(options) { + if (_.isEmpty(options.agentName) + && _.isEmpty(options.skillName) + && _.isEmpty(options.correlationId) + && _.isEmpty(options.status)) { + printError('Either --agentName, --skillName, --correlationId, or --status must be provided', options); + } + const profile = await loadProfile(options.profile); + debug('%s.listActivations(%s)', profile.name); const agents = new Agents(profile.url); - agents.listAgentSnapshots(profile.token, agentName).then((response) => { - if (response.success) { - let result = filterObject(response.result.snapshots, options); - if (options.json) { - printSuccess(JSON.stringify(result, null, 2), options); + // TODO validate param types? + const queryParams = {}; + if (options.startBefore) queryParams.startBefore = options.startBefore; + if (options.startAfter) queryParams.startAfter = options.startAfter; + if (options.endBefore) queryParams.endBefore = options.endBefore; + if (options.endAfter) queryParams.endAfter = options.endAfter; + if (options.status) queryParams.status = _.toUpper(options.status); + if (options.correlationId) queryParams.correlationId = options.correlationId; + if (options.agentName) queryParams.agentName = options.agentName; + if (options.skillName) queryParams.skillName = options.skillName; + if (options.limit) queryParams.limit = options.limit; + if (options.skip) queryParams.skip = options.skip; + if (options.sort) queryParams.sort = _.toLower(options.sort); + if (options.filter) queryParams.filter = options.filter; + try { + // eslint-disable-next-line consistent-return + const response = await agents.listActivations(options.project || profile.project, profile.token, queryParams); + const result = response.activations; + // TODO remove --query on deprecation + if (options.json || options.query) { + getFilteredOutput(result, options); + } else { + printExtendedLogs(result, options); + const tableSpec = [ + { + column: 'Name', + field: 'name', + width: 30, + }, + { + column: 'Activation Id', + field: 'activationId', + width: 40, + }, + { + column: 'Status', + field: 'status', + width: 20, + }, + { + column: 'Started', + field: 'start', + width: 65, + }, + ]; + const genName = (o) => { + if (o.agentName) { + return `${o.agentName} (Agent)`; } - else { - const tableSpec = [ - { column: 'Snapshot ID', field: 'id', width: 30 }, - { column: 'Title', field: 'title', width: 30 }, - { column: 'Description', field: 'description', width: 50 }, - { column: 'Created On', field: 'createdAt', width: 26 } - ]; - printTable(tableSpec, result); + if (o.skillName) { + return `${o.skillName} (Skill)`; } - } - else { - printError(`Failed to list agent snapshots ${agentName}: ${response.message}`, options); - } - }) + return '-'; + }; + handleTable(tableSpec, _.map(result, (o) => ({ + ...o, + name: genName(o), + start: o.start ? dayjs(o.start) + .fromNow() : '-', + })), null, 'No activations found'); + } + } catch (err) { + handleError(err, options, 'Failed to list activations'); + } + } +} +export class ListServicesCommand { + constructor(program) { + this.program = program; + } - .catch((err) => { - printError(`Failed to list agent snapshots ${agentName}: ${err.status} ${err.message}`, options); - }); + async execute(agentName, options) { + const profile = await loadProfile(options.profile); + debug('%s.listServices(%s)', profile.name, agentName); + const catalog = new Catalog(profile.url); + try { + const result = await catalog.listServices(options.project || profile.project, profile.token, agentName, profile); + if (options.json || options.query) { + getFilteredOutput(result, options); + } else { + printExtendedLogs(result, options); + const tableSpec = [ + { column: 'Service Name', field: 'name', width: 25 }, + { column: 'Service Endpoint URL', field: 'url', width: 115 }, + { column: 'Parameters', field: 'formatted_types', width: 65 }, + ]; + handleTable(tableSpec, result, null, 'No services found'); + } + } catch (err) { + printError(`Failed to return agent service information: ${agentName}: ${err.status} ${err.message}`, options); + } + } +} +export class ListAgentSnapshotsCommand { + constructor(program) { + this.program = program; } -}; -module.exports.CreateAgentSnapshotCommand = class { + // eslint-disable-next-line consistent-return + async execute(agentName, options) { + const profile = await loadProfile(options.profile); + debug('%s.listAgentSnapshots(%s)', profile.name, agentName); + const agents = new Agents(profile.url); + try { + const response = await agents.listAgentSnapshots(options.project || profile.project, profile.token, agentName, options.filter, options.limit, options.skip, options.sort); + // eslint-disable-next-line consistent-return + const result = response.result.snapshots; + // TODO remove --query on deprecation + if (options.json || options.query) { + getFilteredOutput(result, options); + } else { + printExtendedLogs(result, options); + const tableSpec = [ + { + column: 'Snapshot ID', + field: 'snapshotId', + width: 40, + }, + { + column: 'Title', + field: 'title', + width: 40, + }, + // Removed as this is confusing for end users, agent version may not change + // { column: 'Agent Version', field: 'agentVersion', width: 15 }, + { + column: 'Created', + field: 'createdAt', + width: 26, + }, + { + column: 'Author', + field: 'createdBy', + width: 26, + }, + ]; + handleTable(tableSpec, result, (o) => ({ + ...o, + createdAt: o.createdAt ? dayjs(o.createdAt) + .fromNow() : '-', + }), 'No snapshots found'); + } + } catch (err) { + handleError(err, options, `Failed to list agent snapshots ${agentName}`); + } + } +} +export class DescribeAgentSnapshotCommand { + constructor(program) { + this.program = program; + } + async execute(snapshotId, options) { + const profile = await loadProfile(options.profile); + debug('%s.describeAgentSnapshot(%s)', profile.name, snapshotId); + const agents = new Agents(profile.url); + const output = options?.output?.toLowerCase() ?? 'json'; + try { + const response = await agents.describeAgentSnapshot(options.project || profile.project, profile.token, snapshotId, output); + if (output === 'json') { + const result = filterObject(JSON.parse(response), options); + return printSuccess(JSON.stringify(result, null, 2), options); + } + return printSuccess(response); + } catch (err) { + return printError(`Failed to describe agent snapshot ${snapshotId}: ${err.status} ${err.message}`, options); + } + } +} +export class CreateAgentSnapshotCommand { constructor(program) { this.program = program; } - execute(snapshotDefinition, options) { - const profile = loadProfile(options.profile); + async execute(snapshotDefinition, options) { + const profile = await loadProfile(options.profile); let snapshot; - if (snapshotDefinition) { + if (fs.existsSync(snapshotDefinition)) { debug('%s.createAgentSnapshot(%s)', profile.name, snapshotDefinition); const snapshotDefStr = fs.readFileSync(snapshotDefinition); snapshot = parseObject(snapshotDefStr, options); - } else if ( _.get(options,'title','').length > 0 && _.get(options,'agentName','').length > 0) { - snapshot = { agentName: options.agentName, title: options.title } + } else if (_.get(options, 'title', '').length > 0 && _.get(options, 'agentName', '').length > 0) { + snapshot = { agentName: options.agentName, title: options.title }; } else { - printError(`Either --title <..> and --agentName <..> or a snapshot definition file must be provided`, options); + printError('Either --title <..> and --agentName <..> or a snapshot definition file must be provided', options); return; } - const agentName = snapshot.agentName; + const { agentName } = snapshot; const agents = new Agents(profile.url); - agents.createAgentSnapshot(profile.token, snapshot).then((response) => { + agents.createAgentSnapshot(options.project || profile.project, profile.token, snapshot).then((response) => { if (response.success) { - let result = filterObject(response.result, options); + const result = filterObject(response.result, options); printSuccess(JSON.stringify(result, null, 2), options); - } - else { + } else { printError(`Failed to create agent snapshot ${agentName}: ${response.message}`, options); } }) .catch((err) => { - printError(`Failed to create agent snapshot ${agentName}: ${err.status} ${err.message}`, options); - }); - } - -}; - -module.exports.CreateAgentInstanceCommand = class { - - constructor(program) { - this.program = program; - } - - execute(instanceDefinition, options) { - const profile = loadProfile(options.profile); - debug('%s.createAgentInstance', profile.name); - - const agents = new Agents(profile.url); - const instanceDefStr = fs.readFileSync(instanceDefinition); - const instance = parseObject(instanceDefStr, options); - agents.createAgentInstance(profile.token, instance).then((response) => { - if (response.success) { - let result = filterObject(response.result, options); - printSuccess(JSON.stringify(result, null, 2), options); - } - else { - printError(`Failed to create agent instance : ${response.message}`, options); - } - }) - .catch((err) => { - printError(`Failed to create agent instance : ${err.status} ${err.message}`, options); + printError(`Failed to create agent snapshot ${agentName}: ${err.status} ${err.message}`, options); }); } -}; - -module.exports.GetAgentInstanceCommand = class { - +} +export class DeleteAgentCommand { constructor(program) { this.program = program; } - execute(instanceId, options) { - const profile = loadProfile(options.profile); - debug('%s.getAgentInstance(%s)', profile.name, instanceId); - - const agents = new Agents(profile.url); - agents.getAgentInstance(profile.token, instanceId).then((response) => { + async execute(agentName, options) { + const profile = await loadProfile(options.profile); + debug('%s.executeDeleteAgent(%s)', profile.name, agentName); + const catalog = new Catalog(profile.url); + catalog.deleteAgent(options.project || profile.project, profile.token, agentName) + .then((response) => { if (response.success) { - let result = filterObject(response.result, options); - if (options.json) { - printSuccess(JSON.stringify(result, null, 2), options); - } - else { - const tableSpec = [ - { column: 'Instance Id', field: '_id', width: 26 }, - { column: 'Status', field: 'status', width: 15 }, - { column: 'Tenant Id', field: 'tenantId', width: 20 }, - { column: 'Snapshot Id', field: 'snapshotId', width: 26 }, - { column: 'Environment Id', field: 'environmentId', width: 26 }, - { column: 'Created At', field: 'createdAt', width: 26 }, - { column: 'Updated At', field: 'updatedAt', width: 26 } - ]; - printTable(tableSpec, [result.instance]); - } - - } - else { - printError(`Failed to get agent instance ${instanceId}: ${response.message}`, options); + const result = filterObject(response, options); + return printSuccess(JSON.stringify(result, null, 2), options); } + return handleDeleteFailure(response, options, 'Agent'); }) .catch((err) => { - printError(`Failed to get agent instance ${instanceId}: ${err.status} ${err.message}`, options); - }); + printError(`Failed to delete agent: ${err.status} ${err.message}`, options); + }); } -}; - -module.exports.DeleteAgentInstanceCommand = class { - +} +export class DeployAgentCommand { constructor(program) { this.program = program; } - execute(instanceId, options) { - const profile = loadProfile(options.profile); - debug('%s.deleteAgentInstance(%s)', profile.name, instanceId); - - const agents = new Agents(profile.url); - agents.deleteAgentInstance(profile.token, instanceId).then((response) => { - if (response.success) { - let result = filterObject(response.result, options); - printSuccess(JSON.stringify(result, null, 2), options); - } - else { - printError(`Failed to delete agent instance ${instanceId}: ${response.message}`, options); + async execute(agentNames, options) { + const profile = await loadProfile(options.profile); + const catalog = new Catalog(profile.url); + await Promise.all(agentNames.map(async (agentName) => { + debug('%s.executeDeployAgent(%s)', profile.name, agentName); + try { + const response = await catalog.deployAgent(options.project || profile.project, profile.token, agentName, options.verbose); + if (response.success) { + printSuccess(`Deployed Agent ${agentName}: ${response.message}`, options); + } else { + printError(`Failed to deploy Agent ${agentName}: ${response.message}`, options); + } + } catch (err) { + printError(`Failed to deploy Agent ${agentName}: ${err.status} ${err.message}`, options); } - }) - .catch((err) => { - printError(`Failed to delete agent instance ${instanceId} : ${err.status} ${err.message}`, options); - }); + })); } -}; - -module.exports.StopAgentInstanceCommand = class { - +} +export class UndeployAgentCommand { constructor(program) { this.program = program; } - execute(instanceId, options) { - const profile = loadProfile(options.profile); - debug('%s.stopAgentInstance(%s)', profile.name, instanceId); - - const agents = new Agents(profile.url); - agents.stopAgentInstance(profile.token, instanceId).then((response) => { - if (response.success) { - let result = filterObject(response.result, options); - printSuccess(JSON.stringify(result, null, 2), options); - } - else { - printError(`Failed to stop agent instance ${instanceId}: ${response.message}`, options); + async execute(agentNames, options) { + const profile = await loadProfile(options.profile); + const catalog = new Catalog(profile.url); + await Promise.all(agentNames.map(async (agentName) => { + debug('%s.executeUndeployAgent(%s)', profile.name, agentName); + try { + const response = await catalog.unDeployAgent(options.project || profile.project, profile.token, agentName, options.verbose); + if (response.success) { + printSuccess(`Undeploy agent ${agentName}: ${response.message}`, options); + } else { + printError(`Failed to Undeploy agent ${agentName}: ${response.message}`, options); + } + } catch (err) { + printError(`Failed to Undeploy agent ${agentName}: ${err.status} ${err.message}`, options); } - }) - .catch((err) => { - printError(`Failed to stop agent instance ${instanceId} : ${err.status} ${err.message}`, options); - }); + })); } -}; - -module.exports.ListTriggersCommand = class { - +} +export class SaveAgentCommand { constructor(program) { this.program = program; } - execute(options) { - const profile = loadProfile(options.profile); - debug('%s.listTriggers', profile.name); - - const agents = new Agents(profile.url); - agents.listTriggers(profile.token).then((response) => { - if (response.success) { - let result = filterObject(response.result, options); - printSuccess(JSON.stringify(result, null, 2), options); + async execute(agentDefinitions, options) { + const profile = await loadProfile(options.profile); + await Promise.all(agentDefinitions.map(async (agentDefinition) => { + debug('%s.executeSaveAgent(%s)', profile.name, agentDefinition); + if (!fs.existsSync(agentDefinition)) { + printError(`File does not exist at: ${agentDefinition}`); } - else { - printError(`Failed to list triggers ${instanceId}: ${response.message}`, options); + const agentDefStr = fs.readFileSync(agentDefinition); + const agent = parseObject(agentDefStr, options); + debug('%o', agent); + const catalog = new Catalog(profile.url); + const project = options.project || profile.project; + try { + await catalog.saveAgent(project, profile.token, agent); + printSuccess(`Agent "${agent.name}" saved in project "${project}"`, options); + } catch (err) { + handleError(err, options, 'Failed to save agent'); } - }) - .catch((err) => { - printError(`Failed to list triggers ${instanceId} : ${err.status} ${err.message}`, options); - }); + })); } -}; \ No newline at end of file +} diff --git a/src/commands/applications.js b/src/commands/applications.js new file mode 100644 index 00000000..b3528fe7 --- /dev/null +++ b/src/commands/applications.js @@ -0,0 +1,266 @@ +import _ from 'lodash'; +import fs from 'node:fs'; +import debugSetup from 'debug'; +import dayjs from 'dayjs'; +import Table from 'cli-table3'; +import relativeTime from 'dayjs/plugin/relativeTime.js'; +import { loadProfile } from '../config.js'; +import Catalog from '../client/catalog.js'; +import { + printSuccess, + printError, + printWarning, + filterObject, + parseObject, + LISTTABLEFORMAT, + isNumeric, + handleTable, + printExtendedLogs, + getFilteredOutput, + writeOutput, printErrorDetails, handleError, +} from './utils.js'; + +const debug = debugSetup('cortex:cli'); +dayjs.extend(relativeTime); +export class SaveAppCommand { + constructor(program) { + this.program = program; + } + + async execute(defns, options) { + try { + const profile = await loadProfile(options.profile); + await Promise.all(defns.map(async (defn) => { + const defnStr = fs.readFileSync(defn); + const app = parseObject(defnStr, options); + debug('%s.executeSaveApplication(%s)', profile.name, defnStr); + const catalog = new Catalog(profile.url); + if (!_.isEmpty(options.k8sResource)) { + const k8sResources = options.k8sResource.map((f) => JSON.stringify(parseObject(fs.readFileSync(f), options))); + if (_.isEmpty(_.get(app, 'actions', []))) { + printError('Application must contain an action to apply kubernetes resources', options, true); + } + if (app.actions.length > 1) { + printWarning('Applying kubernetes resources to all actions'); + } + app.actions.map((a) => a.k8sResources = k8sResources); + } + if (options.scaleCount) { + if (!isNumeric(options.scaleCount)) { + printError('--scaleCount must be a number', options); + } + if (app.actions.length > 1) { + printWarning('Applying kubernetes resources to all actions'); + } + const scaleCount = parseInt(options.scaleCount, 10); + app.actions.map((a) => a.scaleCount = scaleCount); + } + if (options.podspec) { + const paramsStr = fs.readFileSync(options.podspec); + const podSpec = parseObject(paramsStr, options); + if (_.isEmpty(_.get(app, 'actions', []))) { + printError('Application must contain an action', options, true); + } + app.actions.map((a) => a.podSpec = podSpec); + } + const response = await catalog.saveApplication(options.project || profile.project, profile.token, app); + printSuccess(`Application saved: ${JSON.stringify(response.message)}`, options); + })); + } catch (err) { + handleError(err, options, 'Failed to save application'); + } + } +} +export class ListAppsCommand { + constructor(program) { + this.program = program; + } + + async execute(options) { + const profile = await loadProfile(options.profile); + debug('%s.executeListApplications()', profile.name); + const catalog = new Catalog(profile.url); + try { + const status = !_.get(options, 'nostatus', false); // default show status, if nostatus==true status == false + const shared = !_.get(options, 'noshared', false); + const response = await catalog.listApplications(options.project || profile.project, profile.token, { status, shared }, options.filter, options.limit, options.skip, options.sort); + let result = response.applications; + const tableFormat = LISTTABLEFORMAT; + if (options.nostatus === undefined) { + result = result.map((appStat) => { + const statuses = _.isEmpty(appStat.actionStatuses) ? appStat.deployStatus : appStat.actionStatuses.map((s) => `${s.name}: ${s.state}`).join(' '); + return { + ...appStat, + status: statuses, + }; + }); + tableFormat.push({ column: 'Status', field: 'status', width: 30 }); + } + // TODO remove --query on deprecation + if (options.json || options.query) { + return getFilteredOutput(result, options); + } + printExtendedLogs(result, options); + return handleTable(tableFormat, _.sortBy(result, options.sort ? [] : ['name']), (o) => ({ ...o, updatedAt: o.updatedAt ? dayjs(o.updatedAt).fromNow() : '-' }), 'No applications found'); + } catch (err) { + printError(`Failed to list applications: (${err.message}): ${err.response?.body ?? ''}`, options, false); + return printErrorDetails(err.response, options, true); + } + } +} + +function parseMatch(m) { + const { uri } = m; + const matcher = Object.keys(uri).pop(); + const path = Object.values(uri).pop(); + return [matcher, path]; +} +export class DescribeAppCommand { + constructor(program) { + this.program = program; + } + + + async execute(name, options) { + const profile = await loadProfile(options.profile); + const { + verbose, output, project, json, + } = options; + let computedOutput = output; + // If I specify json/verbose/output=json json ensure output is json. + if (output.toLowerCase() === 'json' || verbose || json !== undefined) { + computedOutput = 'json'; + } else { + computedOutput = output; + } + const isPretty = (options?.output ?? 'pretty').toLowerCase() === 'pretty' && computedOutput !== 'json'; + debug('%s.executeDescribeApplication(%s)', profile.name, name); + const catalog = new Catalog(profile.url); + try { + const app = await catalog.describeApplication( + project || profile.project, + profile.token, name, + verbose || isPretty, + computedOutput, + ); // if --json use output JSON + if (isPretty) { +// Strange spacing needed for output formatting + printSuccess(`Name: ${app.name} +Description: ${app.description} +Image: ${app?.action?.image} +Created By: ${app._createdBy} +Updated At: ${app._updatedAt}`, options); + const hTable = new Table({ head: ['Hosts'] }); + hTable.push(...(app?.hosts?.length > 0 ? app.hosts.map((h) => [`https://${h}`]) : [[profile.url]])); + console.log(hTable.toString()); + printSuccess('Route(s):'); + const rTable = new Table({ head: ['Matcher', 'Path'] }); + rTable.push(...app?.match?.map(parseMatch) ?? []); + return console.log(rTable.toString()); + } + if (options?.output?.toLowerCase() === 'json' || json !== undefined) { + return getFilteredOutput(app, options); + } + // format output if json + return writeOutput(computedOutput === 'json' ? JSON.stringify(app, null, 2) : app, options); + } catch (err) { + return printError(`Failed to describe application ${name}: ${err.status ?? ''} ${err.message}`, options); + } + } +} +export class UndeployAppCommand { + constructor(program) { + this.program = program; + } + + async execute(names, options) { + const profile = await loadProfile(options.profile); + const catalog = new Catalog(profile.url); + await Promise.all(names.map(async (name) => { + debug('%s.executeUndeployApplication(%s)', profile.name, name); + try { + const response = await catalog.unDeployApplication(options.project || profile.project, profile.token, name); + printSuccess(`Undeploy Application ${name}: ${response.message}`, options); + } catch (err) { + printError(`Failed to Undeploy Application ${name}: ${err.message}`, options); + } + })); + } +} +export class AppLogsCommand { + constructor(program) { + this.program = program; + } + + async execute(name, options) { + try { + const profile = await loadProfile(options.profile); + debug('%s.executeApplicationLogs(%s,%s)', profile.name, name); + + const catalog = new Catalog(profile.url); + const response = await catalog.applicationLogs(options.project || profile.project, profile.token, name, options.raw); + + if (options.raw) { + try { + const respJSON = JSON.parse(response); // happens with errors + if (respJSON?.body) { + printError(`Failed to List Application Logs ${name}}: ${respJSON.body.message}`, options); + } + } catch (err) { + printSuccess(response, options); + } + } else if (response.success) { + printSuccess(JSON.stringify(response.logs), options); + } else { + printError(`Failed to List Application Logs ${name}: ${response.body.message}`, options); + } + } catch (err) { + printError(`Failed to List Application Logs ${name}: ${err.message}`, options); + } + } +} +export class DeployAppCommand { + constructor(program) { + this.program = program; + } + + async execute(names, options) { + const profile = await loadProfile(options.profile); + const catalog = new Catalog(profile.url); + await Promise.all(names.map(async (name) => { + debug('%s.executeDeployApplication(%s)', profile.name, name); + try { + const response = await catalog.deployApplication(options.project || profile.project, profile.token, name); + if (response.success) { + printSuccess(`Deployed Application ${name}: ${response.message}`, options); + } else { + printError(`Failed to deploy Application ${name}: ${response.message}`, options); + } + } catch (err) { + printError(`Failed to deploy Application ${name}: ${err.message}`, options); + } + })); + } +} + +export class DeleteAppCommand { + constructor(program) { + this.program = program; + } + + async execute(name, options) { + try { + const profile = await loadProfile(options.profile); + debug('%s.executeDeleteApplication(%s)', profile.name, name); + const catalog = new Catalog(profile.url); + const response = await catalog.deleteApplication(options.project || profile.project, profile.token, name); + const result = filterObject(response, options); + if (options.json) { + return printSuccess(JSON.stringify(result, null, 2), options); + } + return printSuccess(result.message); + } catch (err) { + return handleError(err, options, 'Failed to delete application'); + } + } +} diff --git a/src/commands/assessments.js b/src/commands/assessments.js new file mode 100644 index 00000000..caa8603e --- /dev/null +++ b/src/commands/assessments.js @@ -0,0 +1,352 @@ +import fs from 'node:fs'; +import uniqBy from 'lodash/uniqBy.js'; +import sortBy from 'lodash/sortBy.js'; +import flatten from 'lodash/flatten.js'; +import compact from 'lodash/compact.js'; +import startsWith from 'lodash/startsWith.js'; +import debugSetup from 'debug'; +import dayjs from 'dayjs'; +import relativeTime from 'dayjs/plugin/relativeTime.js'; +import { loadProfile } from '../config.js'; +import Assessments from '../client/assessments.js'; +import { + printSuccess, printError, parseObject, handleTable, printExtendedLogs, getFilteredOutput, handleError, +} from './utils.js'; + +const _ = { + uniqBy, + sortBy, + flatten, + compact, + startsWith, +}; +const debug = debugSetup('cortex:cli'); +dayjs.extend(relativeTime); +export const ListResourcesCommand = class { + constructor(program) { + this.program = program; + } + + // eslint-disable-next-line consistent-return + async execute(command) { + const options = command; + const profile = await loadProfile(options.profile); + debug('%s.ListResourcesCommand()', profile.name); + const client = new Assessments(profile.url); + try { + const response = await client.queryResources(profile.token, options.name, options.scope, options.type, options.skip, options.limit, options.filter, options.sort); + // eslint-disable-next-line consistent-return + // TODO remove --query on deprecation + if (options.json || options.query) { + getFilteredOutput(response, options); + } else { + printExtendedLogs(response.data, options); + const tableSpec = [ + { column: 'Name', field: 'resourceName' }, + { column: 'Title', field: 'resourceTitle' }, + { column: 'Resource Type', field: 'resourceType' }, + { column: 'Project', field: '_projectId' }, + ]; + handleTable(tableSpec, response.data, null, 'No matching resources found'); + } + } catch (err) { + handleError(err, options, 'Failed to list Cortex resources'); + } + } +}; +export const ListResourceTypesCommand = class { + constructor(program) { + this.program = program; + } + + async execute(command) { + const options = command; + const profile = await loadProfile(options.profile); + debug('%s.ListResourceTypesCommand()', profile.name); + const client = new Assessments(profile.url); + try { + const data = await client.listResourceTypes(profile.token); + // TODO remove --query on deprecation + if (options.json || options.query) { + getFilteredOutput(data, options); + } else { + const types = data.map((t) => ({ type: t })); + const tableSpec = [ + { column: 'Resource Type', field: 'type' }, + ]; + handleTable(tableSpec, types, null, 'Resource Types not available'); + } + } catch (err) { + handleError(err, options, 'Failed to list Cortex resource types'); + } + } +}; +export const DependencyTreeCommand = class { + constructor(program) { + this.program = program; + } + + async execute(dependencyFile, command) { + const options = command; + const profile = await loadProfile(options.profile); + debug('%s.DependencyTreeCommand()', profile.name); + const client = new Assessments(profile.url); + let body; + if (dependencyFile) { + body = JSON.parse(fs.readFileSync(dependencyFile).toString()); + } + try { + const response = await client.getDependenciesOfResource(profile.token, options.scope, options.type, options.name, body, options.missing); + + // TODO remove --query on deprecation + if (options.json || options.query) { + getFilteredOutput(response, options); + } else { + const tableSpec = [ + { column: 'Name', field: 'name' }, + { column: 'Title', field: 'title' }, + { column: 'Resource Type', field: 'type' }, + ]; + handleTable(tableSpec, response.data, null, 'No downstream dependency found'); + } + } catch (err) { + handleError(err, options, 'Failed to list dependency Cortex resource'); + } + } +}; + +export const CreateAssessmentCommand = class { + constructor(program) { + this.program = program; + } + + async execute(assessmentDef, command) { + const options = command; + const profile = await loadProfile(options.profile); + debug('%s.CreateAssessmentCommand()', profile.name); + const client = new Assessments(profile.url); + let assessment = {}; + if (assessmentDef) { + try { + const assessmentDefStr = fs.readFileSync(assessmentDef); + assessment = parseObject(assessmentDefStr, options); + } catch (err) { + printError(`Failed to read assessment definition "${assessmentDef}": ${err.message}`, options); + } + } + try { + await client.createAssessment(profile.token, + options.name || assessment.name, + options.title || assessment.title, + options.description || assessment.description, + options.scope || assessment.scope, + options.component || assessment.component, + options.type || assessment.type, + options.overwrite || assessment.overwrite); + printSuccess(`Assessment ${options.name} saved successfully`, options); + } catch (err) { + handleError(err, options, 'Failed to save assessment'); + } + } +}; + +export const ListAssessmentCommand = class { + constructor(program) { + this.program = program; + } + + // eslint-disable-next-line consistent-return + async execute(options) { + const profile = await loadProfile(options.profile); + debug('%s.ListAssessmentCommand()', profile.name); + const client = new Assessments(profile.url); + try { + const response = await client.listAssessment(profile.token, options.skip, options.limit, options.filter, options.sort); + const result = response.data; + // TODO remove --query on deprecation + if (options.json || options.query) { + getFilteredOutput(result, options); + } else { + printExtendedLogs(result, options); + const tableSpec = [ + { + column: 'Name', + field: 'name', + }, + { + column: 'Title', + field: 'title', + }, + { + column: 'Description', + field: 'description', + }, + { + column: 'Projects', + field: 'scope', + }, + { + column: '# Reports', + field: 'reportCount', + }, + { + column: 'Modified', + field: '_updatedAt', + }, + { + column: 'Author', + field: '_createdBy', + }, + ]; + handleTable(tableSpec, result, (o) => ({ + ...o, + _updatedAt: o._updatedAt ? dayjs(o._updatedAt) + .fromNow() : '-', + }), 'No Assessments found'); + } + } catch (err) { + handleError(err, options, 'Failed to list assessment'); + } + } +}; +export const DescribeAssessmentCommand = class { + constructor(program) { + this.program = program; + } + + async execute(name, options) { + const profile = await loadProfile(options.profile); + debug('%s.DescribeAssessmentCommand()', profile.name); + const client = new Assessments(profile.url); + client.getAssessment(profile.token, name) + .then((response) => { + if (response.success === false) throw response; + printSuccess(JSON.stringify(response, null, 2), options); + }) + .catch((err) => { + printError(`Failed to get assessment "${name}": ${err.status} ${err.message}`, options); + }); + } +}; +export const DeleteAssessmentCommand = class { + constructor(program) { + this.program = program; + } + + async execute(name, options) { + const profile = await loadProfile(options.profile); + debug('%s.DeleteAssessmentCommand()', profile.name); + const client = new Assessments(profile.url); + client.deleteAssessment(profile.token, name) + .then((response) => { + if (response.success === false) throw response; + printSuccess(JSON.stringify(response, null, 2), options); + }) + .catch((err) => { + printError(`Failed to delete assessment "${name}": ${err.status} ${err.message}`, options); + }); + } +}; +export const RunAssessmentCommand = class { + constructor(program) { + this.program = program; + } + + async execute(name, options) { + const profile = await loadProfile(options.profile); + debug('%s.RunAssessmentCommand()', profile.name); + const client = new Assessments(profile.url); + client.runAssessment(profile.token, name) + .then((response) => { + if (response.success === false) throw response; + printSuccess(JSON.stringify(response, null, 2), options); + }) + .catch((err) => { + printError(`Failed to run assessment "${name}": ${err.status} ${err.message}`, options); + }); + } +}; +export const ListAssessmentReportCommand = class { + constructor(program) { + this.program = program; + } + + async execute(name, options) { + const profile = await loadProfile(options.profile); + debug('%s.ListAssessmentReportCommand()', profile.name); + const client = new Assessments(profile.url); + client.listAssessmentReports(profile.token, name) + .then((response) => { + if (response.success === false) throw response; + // TODO remove --query on deprecation + if (options.json || options.query) { + getFilteredOutput(response, options); + } else { + const tableSpec = [ + { column: 'Assessment Name', field: 'assessmentId' }, + { column: 'Report Id', field: 'reportId' }, + { column: 'Impact Summary', field: 'summary' }, + { column: 'Modified', field: '_updatedAt' }, + { column: 'Author', field: '_createdBy' }, + ]; + response.data.forEach((r) => r.summary = JSON.stringify(Object.fromEntries(r.summary.map((item) => [item.type, item.count])))); + handleTable(tableSpec, response.data, (o) => ({ ...o, _updatedAt: o._updatedAt ? dayjs(o._updatedAt).fromNow() : '-' }), `No report found for the Assessment ${name}`); + } + }) + .catch((err) => { + printError(`Failed to list assessment "${name}" reports: ${err.status} ${err.message}`, options); + }); + } +}; +export const GetAssessmentReportCommand = class { + constructor(program) { + this.program = program; + } + + async execute(name, reportId, options) { + const profile = await loadProfile(options.profile); + debug('%s.GetAssessmentReportCommand()', profile.name); + const client = new Assessments(profile.url); + client.getAssessmentReport(profile.token, name, reportId) + .then((response) => { + if (response.success === false) throw response; + // TODO remove --query on deprecation + if (options.json || options.query) { + const output = { + name: response.reportId, + assessment: response.assessmentId, + summary: Object.fromEntries(response.summary.map((item) => [item.type, item.count])), + report: response.detail, + _createdAt: response._createdAt, + _createdBy: response._createdBy, + }; + getFilteredOutput(output, options); + } else { + const flattenRefs = _.uniqBy(_.flatten(response.detail.map((ref) => ref.sourcePath.map((s) => ({ ...s, projectId: ref._projectId })))), (r) => `${r.name}-${r.type}-${r.projectId}`); + const tableSpec = [ + { column: 'Name', field: 'name' }, + { column: 'Title', field: 'title' }, + { column: 'Resource Type', field: 'type' }, + { column: 'Project', field: 'projectId' }, + ]; + handleTable(tableSpec, _.sortBy(flattenRefs, ['projectId', 'type']), null, 'No upstream dependencies found'); + } + }) + .catch((err) => { + printError(`Failed to get assessment "${name}" report "${reportId}": ${err.status} ${err.message}`, options); + }); + } +}; +export const ExportAssessmentReportCommand = class { + constructor(program) { + this.program = program; + } + + async execute(name, reportId, command) { + const options = command; + const profile = await loadProfile(options.profile); + debug('%s.ExportAssessmentReportCommand()', profile.name); + const client = new Assessments(profile.url); + client.exportAssessmentReport(profile.token, name, reportId, options.type); + } +}; diff --git a/src/commands/campaigns.js b/src/commands/campaigns.js new file mode 100644 index 00000000..456bf4e7 --- /dev/null +++ b/src/commands/campaigns.js @@ -0,0 +1,250 @@ +import debugSetup from 'debug'; +import get from 'lodash/get.js'; +import { loadProfile } from '../config.js'; +import ApiServerClient from '../client/apiServerClient.js'; +import Catalog from '../client/catalog.js'; +import { + printSuccess, printError, printTable, handleTable, printExtendedLogs, handleListFailure, getFilteredOutput, handleError, +} from './utils.js'; +/* + * Copyright 2023 Cognitive Scale, Inc. All Rights Reserved. + * + * 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. + */ +const debug = debugSetup('cortex:cli'); +const _ = { get }; +export class ListCampaignsCommand { + constructor(program) { + this.program = program; + } + + // eslint-disable-next-line consistent-return + async execute(options) { + const profile = await loadProfile(options.profile); + debug('%s.executeListCampaigns()', profile.name); + const cli = new ApiServerClient(profile.url); + try { + const sortParam = (options.sort || '{}').replace(/"/g, '\''); + const filterParam = (options.filter || '{}').replace(/"/g, '\''); + const result = await cli.listCampaigns(options.project || profile.project, profile.token, filterParam, options.limit, options.skip, sortParam); + // TODO remove --query on deprecation + if (options.json || options.query) { + getFilteredOutput(result, options); + } else { + printExtendedLogs(result, options); + const tableSpec = [ + { column: 'Name', field: 'name', width: 30 }, + { column: 'Title', field: 'title', width: 50 }, + { column: 'Description', field: 'description', width: 80 }, + ]; + handleTable(tableSpec, result, null, 'No campaigns found'); + } + } catch (err) { + handleListFailure(_.get(err, 'response.errors[0]', err), options, 'Campaigns'); + } + } +} +export class DescribeCampaignCommand { + constructor(program) { + this.program = program; + } + + async execute(campaignName, cmd) { + const options = cmd; + const profile = await loadProfile(options.profile); + debug('%s.executeDescribeCampaign(%s)', profile.name, campaignName); + const cli = new ApiServerClient(profile.url); + try { + const response = await cli.getCampaign(options.project || profile.project, profile.token, campaignName); + getFilteredOutput(response, options); + } catch (err) { + printError(`Failed to describe campaign: ${err.status} ${err.message}`, options); + } + } +} +export class ExportCampaignCommand { + constructor(program) { + this.program = program; + } + + async execute(campaignName, cmd) { + const options = cmd; + const profile = await loadProfile(options.profile); + debug('%s.executeExportCampaignCommand(%s)', profile.name, campaignName); + const cli = new Catalog(profile.url); + const project = options.project || profile.project; + const path = `./${options.o || `${campaignName}.amp`}`; + try { + await cli.exportCampaign(project, profile.token, campaignName, options.deployable, path); + printSuccess(`Successfully exported Campaign ${campaignName} from project ${project} to file ${path}`); + } catch (e) { + printError(`Failed to export Campaign ${campaignName} from project ${project}. Error: ${e}`); + } + } +} +export class ImportCampaignCommand { + constructor(program) { + this.program = program; + } + + async execute(campaignFilepath, cmd) { + const options = cmd; + const profile = await loadProfile(options.profile); + debug('%s.executeImportCampaignCommand(%s)', profile.name, campaignFilepath); + const cli = new Catalog(profile.url); + try { + cli.importCampaign(options.project || profile.project, profile.token, campaignFilepath, options.deploy, options.overwrite); + } catch (err) { + printError(`Failed to import campaign: ${err.status} ${err.message}`, options); + } + } +} +export class DeployCampaignCommand { + constructor(program) { + this.program = program; + } + + async execute(campaignName, cmd) { + const options = cmd; + const profile = await loadProfile(options.profile); + debug('%s.executeDeployCampaignCommand(%s)', profile.name, campaignName); + const cli = new Catalog(profile.url); + try { + const response = await cli.deployCampaign(options.project || profile.project, profile.token, campaignName); + const output = response?.message ?? JSON.stringify(response, null, 2); + if (!response.warnings || response.warnings.length === 0) { + printSuccess(output, options); + } else { + printError('Campaign deployed with warnings', options, false); + printTable([{ column: 'Warnings', field: 'message' }], response.warnings.map((w) => ({ message: w }))); + } + } catch (err) { + handleError(err, options, 'Failed to deploy campaign'); + } + } +} + +export class UndeployCampaignCommand { + constructor(program) { + this.program = program; + } + + async execute(campaignName, cmd) { + const options = cmd; + const profile = await loadProfile(options.profile); + debug('%s.executeUndeployCampaignCommand(%s)', profile.name, campaignName); + const cli = new Catalog(profile.url); + try { + const response = await cli.undeployCampaign(options.project || profile.project, profile.token, campaignName); + const output = response?.message ?? JSON.stringify(response || response, null, 2); + if (!response.warnings || response.warnings.length === 0) { + printSuccess(output, options); + } else { + printError('Campaign undeployed with warnings', options, false); + printTable([{ column: 'Warnings', field: 'message' }], response.warnings.map((w) => ({ message: w }))); + } + } catch (err) { + handleError(err, options, 'Failed to undeploy campaign'); + } + } +} +export class UndeployMissionCommand { + constructor(program) { + this.program = program; + } + + async execute(campaignName, missionName, cmd) { + const options = cmd; + const profile = await loadProfile(options.profile); + debug('%s.executeUndeployMissionCommand(%s)', profile.name, missionName); + const cli = new Catalog(profile.url); + try { + const response = await cli.undeployMission(options.project || profile.project, profile.token, campaignName, missionName); + const output = response?.message ?? JSON.stringify(response, null, 2); + printSuccess(output, options); + } catch (err) { + handleError(err, options, 'Failed to undeploy mission'); + } + } +} +export class ListMissionsCommand { + constructor(program) { + this.program = program; + } + + // eslint-disable-next-line consistent-return + async execute(campaign, cmd) { + const options = cmd; + const profile = await loadProfile(options.profile); + debug('%s.executeListMissionsCommand(%s)', profile.name, campaign); + const cli = new Catalog(profile.url); + try { + const sortParam = (options.sort || '{}').replace(/"/g, '\''); + const filterParam = (options.filter || '{}').replace(/"/g, '\''); + const response = await cli.listMissions(options.project || profile.project, profile.token, campaign, filterParam, options.limit, options.skip, sortParam); + const data = Object.values(response); + // TODO remove --query on deprecation + if (options.json || options.query) { + getFilteredOutput(data, options); + } else { + printExtendedLogs(data, options); + const tableSpec = [ + { column: 'Name', field: 'name', width: 50 }, + { column: 'Title', field: 'title', width: 50 }, + { column: 'Description', field: 'description', width: 80 }, + { column: 'Status', field: 'lifecycleState', width: 30 }, + ]; + handleTable(tableSpec, data, null, 'No missions found'); + } + } catch (err) { + handleListFailure(_.get(err, 'response.errors[0]', err), options, 'Missions'); + } + } +} +export class DeployMissionCommand { + constructor(program) { + this.program = program; + } + + async execute(campaign, mission, cmd) { + const options = cmd; + const profile = await loadProfile(options.profile); + debug('%s.executeDeployMissionCommand(%s)', profile.name, campaign); + const cli = new Catalog(profile.url); + try { + const response = await cli.deployMission(options.project || profile.project, profile.token, campaign, mission); + const output = response?.message ?? JSON.stringify(response, null, 2); + printSuccess(output, options); + } catch (err) { + printError(`Failed to deploy mission ${mission} of campaign ${campaign}: ${err.status} ${err.message}`, options); + } + } +} +export class DescribeMissionCommand { + constructor(program) { + this.program = program; + } + + async execute(campaign, mission, cmd) { + const options = cmd; + const profile = await loadProfile(options.profile); + debug('%s.executeDescribeMissionCommand(%s)', profile.name, campaign); + const cli = new Catalog(profile.url); + try { + const response = await cli.getMission(options.project || profile.project, profile.token, campaign, mission); + printSuccess(JSON.stringify(response, null, 2), options); + } catch (err) { + printError(`Failed to describe mission ${mission} of campaign ${campaign}: ${err.status} ${err.message}`, options); + } + } +} diff --git a/src/commands/configure.js b/src/commands/configure.js index b6344d87..3a1f7d64 100644 --- a/src/commands/configure.js +++ b/src/commands/configure.js @@ -1,5 +1,15 @@ +import debugSetup from 'debug'; +import _ from 'lodash'; +import prompts from 'prompts'; +import chalk from 'chalk'; +import fs from 'node:fs'; +import { + readConfig, defaultConfig, generateJwt, loadProfile, durationRegex, +} from '../config.js'; +import { printSuccess, printError, useColor } from './utils.js'; +import { getTimeoutUnit, getGotEnvOverrides } from '../client/apiutils.js'; /* - * Copyright 2018 Cognitive Scale, Inc. All Rights Reserved. + * Copyright 2023 Cognitive Scale, Inc. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the “License”); * you may not use this file except in compliance with the License. @@ -13,140 +23,95 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - -const debug = require('debug')('cortex:cli'); -const co = require('co'); -const prompt = require('co-prompt'); -const chalk = require('chalk'); -const Auth = require('../client/auth'); -const { readConfig, writeConfig, defaultConfig } = require('../config'); -const { printSuccess, printError } = require('./utils'); - -const DEFAULT_CORTEX_URL = 'https://api.cortex.insights.ai'; - -module.exports.ConfigureCommand = class { - +const debug = debugSetup('cortex:cli'); +function _validatePatFile(patFile) { + if (!fs.existsSync(patFile)) { + printError(`Personal Access Token file does not exist at: ${patFile}`); + } + return JSON.parse(fs.readFileSync(patFile)); +} +export const ConfigureCommand = class { constructor(program) { this.program = program; } - execute(options) { + async execute(options) { + const { profile, file, project } = options; const config = readConfig(); - const profileName = options.profile || (config && config.currentProfile) || 'default'; - const profileUrl = this.program.url || (config && config.url) || undefined; - const profileAccount = this.program.account || (config && config.account) || undefined; - const profileUsername = this.program.username || (config && config.username) || undefined; - const profilePassword = this.program.password || undefined; - + const profileName = profile || _.get(config, 'currentProfile', 'default'); debug('configuring profile: %s', profileName); - - const profile = (config && config.getProfile(profileName)) || {}; + console.log(`Configuring profile ${chalk.green.bold(profileName)}:`); const cmd = this; - - co(function*(){ - const defaultCortexUrl = profile.url || DEFAULT_CORTEX_URL; - const defaultAccount = profile.account || ''; - const defaultUsername = profile.username || ''; - - console.log(`Configuring profile ${chalk.green.bold(profileName)}:`); - let cortexUrl = (profileUrl) ? profileUrl : yield prompt(`Cortex URL [${defaultCortexUrl}]: `); - let account = (profileAccount) ? profileAccount : yield prompt(`Account [${defaultAccount}]: `); - let username = (profileUsername) ? profileUsername : yield prompt(`Username [${defaultUsername}]: `); - const password = (profilePassword) ? profilePassword : yield prompt.password('Password: '); - - cortexUrl = cortexUrl || defaultCortexUrl; - account = account || defaultAccount; - username = username || defaultUsername; - - debug('cortexUrl: %s', cortexUrl); - debug('account: %s', account); - debug('username: %s', username); - - if (!cortexUrl) { - console.error(chalk.red('Cortex URL must be provided')); - return; - } - - if (!account) { - console.error(chalk.red('Cortex account name must be provided')); - return; - } - - if (!username) { - console.error(chalk.red('Cortex username must be provided')); - return; + try { + let patData = null; + if (file) { + patData = _validatePatFile(file); + } else { + const { patjson } = await prompts({ + type: 'text', + name: 'patjson', + message: 'Cortex Personal Access Config:', + }); + patData = JSON.parse(patjson); } - - const auth = new Auth(cortexUrl); - try { - const token = yield auth.login(account, username, password); - debug('token: %s', token); - cmd.saveConfig(config, profileName, cortexUrl, account, username, token); - console.log(`Configuration for profile ${chalk.green.bold(profileName)} saved.`); - } - catch (e) { - console.error(chalk.red(`LOGIN FAILED: ${e.message}`)); - } - }); + cmd.saveConfig(config, profileName, patData, project); + console.log(`Configuration for profile ${chalk.green.bold(profileName)} saved.`); + } catch (err) { + printError(err); + } } - saveConfig(config, profileName, url, account, username, token) { - if(!config) - config = defaultConfig(); - config.setProfile(profileName, {url, account, username, token}); + + saveConfig(config, profileName, cfg, project) { + if (!config) config = defaultConfig(); + config.setProfile(profileName, cfg, project); config.currentProfile = profileName; config.save(); } }; - -module.exports.SetProfileCommand = class { +export const SetProfileCommand = class { constructor(program) { this.program = program; } - execute(profileName, options) { + + async execute(profileName, options) { const config = readConfig(); - const profile = config.getProfile(profileName); + const profile = await config.getProfile(profileName); if (profile === undefined) { printError(`No profile named ${profileName}. Run cortex configure --profile ${profileName} to create it.`, options); return; } - config.currentProfile = profileName; config.save(); - console.log(`Current profile set to ${chalk.green.bold(profileName)}`); } }; - -module.exports.DescribeProfileCommand = class { - +export const DescribeProfileCommand = class { constructor(program) { this.program = program; } - execute(options) { + async execute(options) { const config = readConfig(); if (config === undefined) { - printError(`Configuration not found. Please run "cortex configure".`); + printError('Configuration not found. Please run "cortex configure".'); return; } - const profileName = options.profile || config.currentProfile; debug('describing profile: %s', profileName); - - const profile = config.getProfile(profileName); + const profile = await config.getProfile(profileName); if (profile === undefined) { printError(`No profile named ${profileName}. Run cortex configure --profile ${profileName} to create it.`, options); return; } - printSuccess(`Profile: ${profile.name}`, options); printSuccess(`Cortex URL: ${profile.url}`, options); - printSuccess(`Account: ${profile.account}`, options); printSuccess(`Username: ${profile.username}`, options); + printSuccess(`JWK: ${JSON.stringify(profile.jwk)}`, options); + printSuccess(`Project: ${profile.project || 'undefined'}`, options); + printSuccess(`Container Registry:\n\tName: ${Object.keys(profile.registries)[0]}\n\tURL: ${Object.values(profile.registries)[0]?.url || 'undefined'}`, options); } }; - -module.exports.ListProfilesCommand = class { +export const ListProfilesCommand = class { constructor(program) { this.program = program; } @@ -154,23 +119,118 @@ module.exports.ListProfilesCommand = class { execute(options) { const config = readConfig(); if (config === undefined) { - printError(`Configuration not found. Please run "cortex configure".`,options); + printError('Configuration not found. Please run "cortex configure".', options); return; } - const profiles = Object.keys(config.profiles); - for (let name of profiles) { + profiles.forEach((name) => { if (name === config.currentProfile) { - if (options.color === 'on') { + if (useColor(options)) { console.log(chalk.green.bold(name)); - } - else { + } else { console.log(`${name} [active]`); } - } - else { + } else { console.log(name); } + }); + } +}; +export const GetAccessToken = class { + constructor(program) { + this.program = program; + } + + async execute(options) { + const profile = await loadProfile(options.profile); + const ttl = options.ttl || '1d'; + if (!durationRegex.test(ttl)) { + printError(`Invalid --ttl "${ttl}" must be a number followed by ms,s,m,h,d,w,M,y`); + } + debug('%s.getAccesToken', profile.name); + const jwt = await generateJwt(profile, ttl); + return printSuccess(jwt, options); + } +}; +export const PrintEnvVars = class { + constructor(program) { + this.program = program; + } + + async execute(options) { + try { + // Token & URI are not picked from env variables, likely because + // this command is meant to help the user configure their env based + // on their profile. Picking up env variables would be inconsistent! + const vars = []; + const defaults = []; + const profile = await loadProfile(options.profile, false); + const ttl = options.ttl || '1d'; + if (!durationRegex.test(ttl)) { + printError(`Invalid --ttl "${ttl}" must be a number followed by ms,s,m,h,d,w,M,y`); + } + const jwt = await generateJwt(profile, ttl); + vars.push(`export CORTEX_TOKEN=${jwt}`); + vars.push(`export CORTEX_URI=${profile.url}`); + // not sure why we used URI previously ?? + vars.push(`export CORTEX_URL=${profile.url}`); + vars.push(`export CORTEX_PROJECT=${options.project || profile.project}`); + + // Include timeout options with units & default + const { timeout, retry } = getGotEnvOverrides(); + const unit = getTimeoutUnit(); + const len = 60; // fixed length to apply consistent spacing + timeout.forEach((t) => { + if (t.userDefined) { + // Print the exact value set by the user & units + const unitPart = `# (unit: ${unit})`; + const exportPart = `export ${t.envVar}=${t.envValue}`; + const spacing = ' '.repeat(len - exportPart.length); + vars.push(`${exportPart}${spacing}${unitPart}`); + } else { + // Print comment showing default & units + const defaultPart = `(default: ${t.defaultValue}, unit: ${unit})`; + const exportPart = `#export ${t.envVar}=`; + const spacing = ' '.repeat(len - exportPart.length); + defaults.push(`${exportPart}${spacing}${defaultPart}`); + } + }); + + // Include retry options with defaults + retry.forEach((v) => { + if (v.userDefined) { + // Print the exact value set by the user + const exportPart = `export ${v.envVar}=${v.envValue}`; + vars.push(`${exportPart}`); + } else { + // Print comment showing default + const defaultPart = `(default: ${v.defaultValue})`; + const exportPart = `#export ${v.envVar}=`; + const spacing = ' '.repeat(len - exportPart.length); + defaults.push(`${exportPart}${spacing}${defaultPart}`); + } + }); + + // Include reference text as bash comments (slightly verbose) + if (defaults.length > 0) { + vars.push('\n# The default value is used for the following environment variables.', + '# Non-negative integers values will be applied to timeouts, while all other', + '# values will disable the timeout. For additional information on the timeout', + '# meanings, refer to the corresponding timeout values at:', + '# https://github.com/sindresorhus/got/blob/main/documentation/6-timeout.md', + '#', + ...defaults); + } else { + vars.push('\n# Non-negative integer values will be applied to timeouts, while all other', + '# values will disable the timeout. For additional information on the timeout', + '# meanings, refer to the corresponding timeout values at:', + '# https://github.com/sindresorhus/got/blob/main/documentation/6-timeout.md', + '#', + ...defaults); + } + return printSuccess(vars.join('\n'), { color: 'off' }); + } catch (err) { + return printError(err.message, {}, true); } } }; diff --git a/src/commands/connections.js b/src/commands/connections.js index ca18212e..475392fe 100644 --- a/src/commands/connections.js +++ b/src/commands/connections.js @@ -1,304 +1,193 @@ -/* - * Copyright 2018 Cognitive Scale, Inc. All Rights Reserved. - * - * 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. - */ - -const fs = require('fs'); -const yeoman = require('yeoman-environment'); -const debug = require('debug')('cortex:cli'); -const { loadProfile } = require('../config'); -const Connections = require('../client/connections'); -const Content = require('../client/content'); -const { printSuccess, printError, filterObject, parseObject, printTable } = require('./utils'); - -module.exports.ListConnections = class ListConnections { - +import _ from 'lodash'; +import fs from 'node:fs'; +import debugSetup from 'debug'; +import dayjs from 'dayjs'; +import relativeTime from 'dayjs/plugin/relativeTime.js'; +import { loadProfile } from '../config.js'; +import Connections from '../client/connections.js'; +import Content from '../client/content.js'; +import { + printSuccess, + printError, + filterObject, + parseObject, + CONNECTIONTABLEFORMAT, + fileExists, + handleTable, + printExtendedLogs, + getFilteredOutput, + printErrorDetails, + handleError, +} from './utils.js'; + +const debug = debugSetup('cortex:cli'); +dayjs.extend(relativeTime); +export class ListConnections { constructor(program) { this.program = program; } - execute(options) { - const profile = loadProfile(options.profile); + // eslint-disable-next-line consistent-return + async execute(options) { + const profile = await loadProfile(options.profile); debug('%s.listConnections()', profile.name); - const conns = new Connections(profile.url); - conns.listConnections(profile.token).then((response) => { - if (response.success) { - let result = response.result.connections; - if (options.query) - result = filterObject(result, options); - - if (options.json) { - printSuccess(JSON.stringify(result, null, 2), options); - } - else { - let tableSpec = [ - { column: 'Name', field: 'name', width: 40 }, - { column: 'Title', field: 'title', width: 50 }, - { column: 'Description', field: 'description', width: 50 }, - { column: 'Connection Type', field: 'connectionType', width: 25 }, - { column: 'Writeable', field: 'allowWrite', width: 15 }, - { column: 'Created On', field: 'createdAt', width: 26 } - ]; - - printTable(tableSpec, result); - } + // eslint-disable-next-line consistent-return + try { + const response = await conns.listConnections(options.project || profile.project, profile.token, options.filter, options.limit, options.skip, options.sort); + const result = response.connections; + // TODO remove --query on deprecation + if (options.json || options.query) { + getFilteredOutput(result, options); + } else { + printExtendedLogs(result, options); + handleTable(CONNECTIONTABLEFORMAT, result, (o) => ({ ...o, createdAt: o.createdAt ? dayjs(o.createdAt).fromNow() : '-' }), 'No connections found'); } - else { - printError(`Failed to list connections: ${response.status} ${response.message}`, options); - } - }) - .catch((err) => { + } catch (err) { debug(err); - printError(`Failed to list connections: ${err.status} ${err.message}`, options); - }); + handleError(err, options, 'Failed to list connections'); + } } -}; - -module.exports.SaveConnectionCommand = class SaveConnectionCommand { - +} +export class SaveConnectionCommand { constructor(program) { this.program = program; } - getParamsValue(connectionDefinition, paramName) { - const results = connectionDefinition.params.filter(item => item.name === paramName); - if (results && results.length) { - return results[0]['value']; - - } else { - return undefined; - } - } - - stripJarPathFromParams(params) { - return params.filter(item => item.name !== 'jdbc_jar_file'); - } - - execute(connectionDefinition, options) { - const profile = loadProfile(options.profile); - debug('%s.executeSaveDefinition(%s)', profile.name, connectionDefinition); - - const connDefStr = fs.readFileSync(connectionDefinition); - const connObj = parseObject(connDefStr, options); - debug('%o', connObj); - - const jdbcJarFilePath = this.getParamsValue(connObj, 'jdbc_jar_file'); - const contentKey = this.getParamsValue(connObj, 'managed_content_key'); - - if (jdbcJarFilePath) { - const content = new Content(profile.url); - const connection = new Connections(profile.url); - - const payload = {'content': jdbcJarFilePath, 'key': contentKey}; - - content.uploadContent(profile.token, payload).then((response) => { - - let marshaledConnObj = connObj; - marshaledConnObj.params = this.stripJarPathFromParams(marshaledConnObj.params); - - connection.saveConnection(profile.token, marshaledConnObj).then((response) => { - if (response.success) { - printSuccess(`Connection saved`, options); - } - else { - printError(`Failed to save connection: ${response.status} ${response.message}`, options); - } - }) - .catch((err) => { - printError(`Failed to save connection: ${err.response.body.message}`, options); - }); - }) - .catch((err) => { - printError(`Failed to upload jdbc jar: ${err.status} ${err.message}`, options); - }); - } else { - - const connection = new Connections(profile.url); - connection.saveConnection(profile.token, connObj).then((response) => { - if (response.success) { - printSuccess(`Connection saved`, options); - } - else { - printError(`Failed to save connection: ${response.status} ${response.message}`, options); - } - }) - .catch((err) => { - printError(`Failed to save connection: ${err.response.body.message}`, options); - }); - } - } -}; + getParamsValue(connectionDefinition, paramName) { + const results = _.filter(_.get(connectionDefinition, 'params', []), (item) => item.name === paramName); + if (results && results.length) { + return results[0].value; + } + return undefined; + } -module.exports.DescribeConnectionCommand = class DescribeConnectionCommand { + stripJarPathFromParams(params) { + return params.filter((item) => item.name !== 'jdbc_jar_file'); + } + async execute(connectionDefinition, options) { + const profile = await loadProfile(options.profile); + debug('%s.executeSaveDefinition(%s)', profile.name, connectionDefinition); + if (!fileExists(connectionDefinition)) { + printError(`File does not exist at: ${connectionDefinition}`); + } + const connDefStr = fs.readFileSync(connectionDefinition); + const connObj = parseObject(connDefStr, options); + debug('%o', connObj); + const jdbcJarFilePath = this.getParamsValue(connObj, 'jdbc_jar_file'); + const contentKey = this.getParamsValue(connObj, 'managed_content_key') || this.getParamsValue(connObj, 'plugin_jar'); + if (jdbcJarFilePath && !jdbcJarFilePath.includes('--Insert jar file path--')) { + try { + const content = new Content(profile.url); + const connection = new Connections(profile.url); + await content.uploadContentStreaming(options.project || profile.project, profile.token, contentKey, jdbcJarFilePath); + const marshaledConnObj = connObj; + marshaledConnObj.params = this.stripJarPathFromParams(marshaledConnObj.params); + await connection.saveConnection(options.project || profile.project, profile.token, marshaledConnObj); + printSuccess('Connection saved', options); + } catch (err) { + handleError(err, options, 'Failed to upload jdbc jar'); + } + } else { + try { + const connection = new Connections(profile.url); + await connection.saveConnection(options.project || profile.project, profile.token, connObj); + printSuccess('Connection saved', options); + } catch (err) { + handleError(err, options, 'Failed to save connection'); + } + } + } +} +export class DescribeConnectionCommand { constructor(program) { this.program = program; } - execute(connectionName, options) { - const profile = loadProfile(options.profile); + async execute(connectionName, options) { + const profile = await loadProfile(options.profile); debug('%s.executeDescribeConnection(%s)', profile.name, connectionName); - const connection = new Connections(profile.url); - connection.describeConnection(profile.token, connectionName).then((response) => { - if (response.success) { - let result = filterObject(response.result, options); - printSuccess(JSON.stringify(result, null, 2), options); - } - else { - printError(`Failed to describe connection ${connectionName}: ${response.message}`, options); - } - }) - .catch((err) => { - printError(`Failed to describe connection ${connectionName}: ${err.status} ${err.message}`, options); - }); + try { + const response = await connection.describeConnection(options.project || profile.project, profile.token, connectionName); + getFilteredOutput(response, options); + } catch (err) { + handleError(err, options, `Failed to describe connection ${connectionName}`); + } } -}; - -module.exports.TestConnectionCommand = class TestConnectionCommand { - +} +export class DeleteConnectionCommand { constructor(program) { this.program = program; } - getParamsValue(connectionDefinition, paramName) { - const results = connectionDefinition.params.filter(item => item.name === paramName); - if (results && results.length) { - return results[0]['value']; - - } else { - return undefined; + async execute(connectionName, options) { + const profile = await loadProfile(options.profile); + debug('%s.executeDeleteConnection(%s)', profile.name, connectionName); + const connection = new Connections(profile.url); + try { + const response = await connection.deleteConnection(options.project || profile.project, profile.token, connectionName); + const result = filterObject(response.result, options); + return printSuccess(JSON.stringify(result, null, 2), options); + } catch (err) { + return handleError(err, { ...options, tableformat: 'DEPENDENCYTABLEFORMAT' }, `Failed to delete connection ${connectionName}`); } } +} - stripJarPathFromParams(params) { - return params.filter(item => item.name !== 'jdbc_jar_file'); - } - - execute(connectionDefinition, options) { - const profile = loadProfile(options.profile); - debug('%s.executeTestDefinition(%s)', profile.name, connectionDefinition); - - const connDefStr = fs.readFileSync(connectionDefinition); - const connObj = parseObject(connDefStr, options); - debug('%o', connObj); - - const jdbcJarFilePath = this.getParamsValue(connObj, 'jdbc_jar_file'); - const contentKey = this.getParamsValue(connObj, 'managed_content_key'); - - if (jdbcJarFilePath) { - const content = new Content(profile.url); - const connection = new Connections(profile.url); - - const payload = {'content': jdbcJarFilePath, 'key': contentKey}; - - content.uploadContent(profile.token, payload).then((response) => { - const connection = new Connections(profile.url); - - let marshaledConnObj = connObj; - marshaledConnObj.params = this.stripJarPathFromParams(marshaledConnObj.params); - - connection.testConnection(profile.token, marshaledConnObj).then((response) => { - if (response.success) { - printSuccess(`Connection successfully tested`, options); - } - else { - printError(`Failed while testing connection: ${response.message}`, options); - } - }) - .catch((err) => { - printError(`Failed while testing connection: ${err.response.body.message}`, options); - }); - }) - .catch((err) => { - printError(`Failed to upload jdbc jar: ${err.status} ${err.message}`, options); - }); - - } else { - const connection = new Connections(profile.url); - connection.testConnection(profile.token, connObj).then((response) => { - if (response.success) { - printSuccess(`Connection successfully tested`, options); - } - else { - printError(`Failed while testing connection: ${err.response.body.message}`, options); - } - }) - .catch((err) => { - printError(`Failed while testing connection: ${err.status} ${err.message}`, options); - }); - } - } -}; - -module.exports.ListConnectionsTypes = class ListConnectionsTypes { - +export class ListConnectionsTypes { constructor(program) { this.program = program; } - execute(options) { - const profile = loadProfile(options.profile); + // eslint-disable-next-line consistent-return + async execute(options) { + const profile = await loadProfile(options.profile); debug('%s.listConnectionsTypes()', profile.name); - const conns = new Connections(profile.url); - conns.listConnectionsTypes(profile.token).then((response) => { - if (response.success) { - let result = response.result.connectionTypes; - if (options.query) - result = filterObject(result, options); - - if (options.json) { - printSuccess(JSON.stringify(result, null, 2), options); - } - else { - let tableSpec = [ - { column: 'Name', field: 'name', width: 50 }, - { column: 'Title', field: 'title', width: 25 }, - { column: 'Description', field: 'description', width: 50 }, - { column: 'Created On', field: 'createdAt', width: 26 }, - { column: 'Updated On', field: 'updatedAt', width: 26 } - ]; - - printTable(tableSpec, result); + // eslint-disable-next-line consistent-return + try { + const response = await conns.listConnectionsTypes(profile.token, options.limit, options.skip, options.sort); + const result = response?.connectionTypes; + // TODO remove --query on deprecation + if (options.json || options.query) { + getFilteredOutput(result, options); + } else { + printExtendedLogs(result, options); + const tableSpec = [ + { + column: 'Name', + field: 'name', + width: 50, + }, + { + column: 'Title', + field: 'title', + width: 25, + }, + { + column: 'Description', + field: 'description', + width: 50, + }, + { + column: 'Created On', + field: 'createdAt', + width: 26, + }, + { + column: 'Updated On', + field: 'updatedAt', + width: 26, + }, + ]; + handleTable(tableSpec, result, null, 'No connection types found'); } - } - else { - printError(`Failed to list connection types: ${response.status} ${response.message}`, options); - } - }) - .catch((err) => { + } catch (err) { debug(err); - printError(`Failed to list connection types: ${err.status} ${err.message}`, options); - }); - } -}; - -module.exports.GenerateConnectionCommand = class GenerateConnectionCommand { - - constructor(program) { - this.program = program; - } - - execute(options) { - debug('%s.generateConnection()', options.profile); - const yenv = yeoman.createEnv(); - yenv.lookup(()=>{ - yenv.run('@c12e/cortex:connections', - { }, - (err) => { err ? printError(err) : printSuccess('Done.') }); - }); + printErrorDetails(err?.response, options, false); + handleError(err, options, 'Failed to list connection types'); + } } -}; +} diff --git a/src/commands/content.js b/src/commands/content.js index b550bde5..cc491688 100644 --- a/src/commands/content.js +++ b/src/commands/content.js @@ -1,176 +1,125 @@ -/* - * Copyright 2018 Cognitive Scale, Inc. All Rights Reserved. - * - * 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. - */ - -const fs = require('fs'); -const path = require('path'); -const yeoman = require('yeoman-environment'); -const debug = require('debug')('cortex:cli'); -const { loadProfile } = require('../config'); -const Content = require('../client/content'); -const { printSuccess, printError, filterObject, parseObject, printTable } = require('./utils'); - -module.exports.ListContent = class ListContent { +import _ from 'lodash'; +import process from 'node:process'; +import fs from 'node:fs'; +import { PromisePool } from '@supercharge/promise-pool'; +import debugSetup from 'debug'; +import { loadProfile } from '../config.js'; +import Content from '../client/content.js'; +import { + printSuccess, printError, getSourceFiles, humanReadableFileSize, handleTable, getFilteredOutput, checkProject, handleError, +} from './utils.js'; + +const debug = debugSetup('cortex:cli'); + +async function upload(contentClient, profile, options, contentKey, filePath) { + if (fs.lstatSync(filePath).isDirectory()) { + printError('Uploads of directories require the option --recursive | -r', options); + } + const showProgress = !!options.progress; + const contentType = _.get(options, 'contentType', 'application/octet-stream'); + try { + await contentClient.uploadContentStreaming(options.project || profile.project, profile.token, contentKey, filePath, showProgress, contentType); + printSuccess('Content successfully uploaded.', options); + } catch (err) { + handleError(err, options, `Failed to upload content ${filePath}`); + } +} +export class ListContent { constructor(program) { this.program = program; } - execute(options) { - const profile = loadProfile(options.profile); + async execute(options) { + const profile = await loadProfile(options.profile); debug('%s.listContent()', profile.name); - const content = new Content(profile.url); - content.listContent(profile.token).then((response) => { - if (response.success) { - if (options.query || options.json) { - let result = filterObject(response.message, options); - printSuccess(JSON.stringify(result, null, 2), options); - } - else { - let tableSpec = [ - { column: 'Key', field: 'Key', width: 70 }, - { column: 'Content Type', field: 'ContentType', width: 30 }, - { column: 'Last Modified', field: 'LastModified', width: 30 }, - { column: 'Size (bytes)', field: 'Size', width: 20 } - ]; - - printTable(tableSpec, response.message); - } - } - else { - printError(`Failed to list content: ${response.status} ${response.message}`, options); - } - }) - .catch((err) => { - debug(err); - printError(`Failed to list content: ${err.status} ${err.message}`, options); - }); + try { + const response = await content.listContent(options.project || profile.project, profile.token, options.prefix); + // TODO remove --query on deprecation + if (options.json || options.query) { + getFilteredOutput(response, options); + } else { + const tableSpec = [ + { column: 'Key', field: 'Key', width: 70 }, + { column: 'Content Type', field: 'ContentType', width: 30 }, + { column: 'Last Modified', field: 'LastModified', width: 30 }, + { column: 'Size (bytes)', field: 'Size', width: 20 }, + ]; + handleTable(tableSpec, response, null, 'No content found'); + } + } catch (err) { + handleError(err, options, 'Failed to list content'); + } } -}; - -module.exports.UploadContent = class UploadContent { - +} +export class UploadContent { constructor(program) { this.program = program; } - execute(contentKey, filePath, options) { - const profile = loadProfile(options.profile); - debug('%s.listContent()', profile.name); - const content = new Content(profile.url); - if (options.secure) { - const fileContent = fs.readFileSync(filePath); - const fileBaseName = path.basename(filePath); - - const secureContent = {}; - secureContent[fileBaseName] = new Buffer(fileContent).toString('base64'); - content.uploadSecureContent(profile.token, contentKey, secureContent).then((response) => { - if (response.success) { - printSuccess(`Secure content successfully uploaded.`, options); - } - else { - printError(`Failed to upload secure content: ${response.status} ${response.message}`, options); - } - }) - } - else { - const payload = {'content': filePath, 'key': contentKey}; - content.uploadContent(profile.token, payload).then((response) => { - if (response.success) { - printSuccess(`Content successfully uploaded.`, options); - } - else { - printError(`Failed to upload content: ${response.status} ${response.message}`, options); - } - }) - .catch((err) => { - debug(err); - printError(`Failed to upload content: ${err.status} ${err.message}`, options); - }); + async execute(contentKey, filePath, options) { + const profile = await loadProfile(options.profile); + const project = options.project || profile.project; + checkProject(project); + debug('%s.uploadContent()', profile.name); + const contentClient = new Content(profile.url); + const chunkSize = parseInt(options.chunkSize, 10); + if (options.recursive) { + const files = getSourceFiles(filePath); + const totalBytes = files.reduce((accum, current) => { + console.log(`${humanReadableFileSize(current.size)}\t${current.relative} -> ${contentKey}/${current.relative}`); + return accum + current.size; + }, 0); + console.log(`\nTotal:\n${humanReadableFileSize(totalBytes)}\t${filePath}`); + debug(`chunksize ${chunkSize}`); + if (!options.test) { + return PromisePool + .withConcurrency(chunkSize) + .for(files) + .handleError((err, item, pool) => { + pool.stop(); + process.exit(1); + }) + .process(async (item) => { + // TODO progress + await upload(contentClient, profile, options, `${contentKey}/${item.relative}`, item.canonical, false); + }); + } + return console.log('Test option set. Nothing uploaded.'); } + return upload(contentClient, profile, options, contentKey, filePath); } -}; - -module.exports.DeleteContent = class DeleteContent { - +} +export class DeleteContent { constructor(program) { this.program = program; } - execute(contentKey, options) { - const profile = loadProfile(options.profile); + async execute(contentKey, options) { + const profile = await loadProfile(options.profile); debug('%s.deleteContent()', profile.name); - const content = new Content(profile.url); - content.deleteContent(profile.token, contentKey).then((response) => { - if (response.success) { - printSuccess(`Content successfully deleted.`, options); - } - else { - printError(`Failed to delete content: ${response.status} ${response.message}`, options); - } - }) - .catch((err) => { - debug(err); - printError(`Failed to delete content: ${err.status} ${err.message}`, options); - }); + try { + await content.deleteContent(options.project || profile.project, profile.token, contentKey); + printSuccess('Content successfully deleted.', options); + } catch (err) { + handleError(err, options, 'Failed to delete content'); + } } -}; - -module.exports.DownloadContent = class DownloadContent { - +} +export class DownloadContent { constructor(program) { this.program = program; } - execute(contentKey, options) { - const profile = loadProfile(options.profile); + async execute(contentKey, options) { + const profile = await loadProfile(options.profile); debug('%s.DownloadContent()', profile.name); - const content = new Content(profile.url); - - // To download content from Secrets - if (options.secure) { - content.downloadSecureContent(profile.token, contentKey).then((response) => { - if (response.success) { - printSuccess(response.message, options); - } - else { - printError(`Failed to download secure content: ${response.status} ${response.message}`, options); - } - }) - .catch((err) => { - debug(err); - printError(`Failed to download secure content: ${err.status} ${err.message}`, options); - }); - } - else { - content.downloadContent(profile.token, contentKey).then((response) => { - if (response.success) { - printSuccess(response.message, options); - } - else { - printError(`Failed to download content: ${response.status} ${response.message}`, options); - } - }) - .catch((err) => { - debug(err); - printError(`Failed to download content: ${err.status} ${err.message}`, options); - }); - } - + const showProgress = !!options.progress; + content.downloadContent(options.project || profile.project, profile.token, contentKey, showProgress).catch((err) => { + handleError(err, options, 'Failed to download content'); + }); } -}; +} diff --git a/src/commands/datasets.js b/src/commands/datasets.js deleted file mode 100644 index 026db826..00000000 --- a/src/commands/datasets.js +++ /dev/null @@ -1,193 +0,0 @@ -/* - * Copyright 2018 Cognitive Scale, Inc. All Rights Reserved. - * - * 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. - */ - -const fs = require('fs'); -const debug = require('debug')('cortex:cli'); -const es = require('event-stream'); -const yeoman = require('yeoman-environment'); -const { loadProfile } = require('../config'); -const Datasets = require('../client/datasets'); -const { printSuccess, printError, filterObject, parseObject, printTable } = require('./utils'); - -module.exports.ListDatasets = class ListDatasets { - - constructor(program) { - this.program = program; - } - - execute(options) { - const profile = loadProfile(options.profile); - debug('%s.listDatasets()', profile.name); - - const datasets = new Datasets(profile.url); - datasets.listDatasets(profile.token).then((response) => { - if (response.success) { - let result = response.result; - if (options.query) { - result = filterObject(response.result, options); - } - if (options.json) { - printSuccess(JSON.stringify(result, null, 2), options); - } - else { - let tableSpec = [ - { column: 'Name', field: 'name', width: 40 }, - { column: 'Title', field: 'title', width: 50 }, - { column: 'Connection Name', field: 'connectionName', width: 25 }, - { column: 'Description', field: 'description', width: 50 }, - { column: 'Created On', field: 'createdAt', width: 26 }, - { column: 'Updated On', field: 'updatedAt', width: 26 }, - ]; - - printTable(tableSpec, result.datasets); - } - } - else { - printError(`Failed to list datasets: ${response.status} ${response.message}`, options); - } - }) - .catch((err) => { - debug(err); - printError(`Failed to list datasets: ${err.status} ${err.message}`, options); - }); - } -}; - -module.exports.SaveDatasetsCommand = class SaveDatasetsCommand { - - constructor(program) { - this.program = program; - } - - execute(datasetDefinition, options) { - const profile = loadProfile(options.profile); - debug('%s.executeSaveDatasetDefinition(%s)', profile.name, datasetDefinition); - - const connDefStr = fs.readFileSync(datasetDefinition); - const connObj = parseObject(connDefStr, options); - debug('%o', connObj); - - const datasets = new Datasets(profile.url); - datasets.saveDatasets(profile.token, connObj).then((response) => { - if (response.success) { - printSuccess(`Dataset saved`, options); - } - else { - printError(`Failed to save Dataset: ${response.status} ${response.message}`, options); - } - }) - .catch((err) => { // don't think we ever get here.. - printError(`Failed to save Datasets: ${err.status} ${err.message}`, options); - }); - } -}; - -module.exports.DescribeDatasetCommand = class DescribeDatasetCommand { - - constructor(program) { - this.program = program; - } - - execute(datasetName, options) { - const profile = loadProfile(options.profile); - debug('%s.executeDescribeDataset(%s)', profile.name, datasetName); - - const dataset = new Datasets(profile.url); - dataset.describeDataset(profile.token, datasetName).then((response) => { - if (response.success) { - let result = filterObject(response.result, options); - printSuccess(JSON.stringify(result, null, 2), options); - } - else { - printError(`Failed to describe dataset ${datasetName}: ${response.message}`, options); - } - }) - .catch((err) => { - printError(`Failed to describe dataset ${datasetName}: ${err.status} ${err.message}`, options); - }); - } -}; - -module.exports.GetDataframeCommand = class GetDataframeCommand { - - constructor(program) { - this.program = program; - } - - execute(datasetName, options) { - const profile = loadProfile(options.profile); - debug('%s.executeGetDataframe(%s)', profile.name, datasetName); - - const dataset = new Datasets(profile.url); - dataset.getDataframe(profile.token, datasetName).then((response) => { - if (response.success) { - printSuccess(JSON.stringify(response.result, null, 2), options); - } - else { - printError(`Failed to get dataframe ${datasetName}: ${response.message}`, options); - } - }) - .catch((err) => { - printError(`Failed to get dataframe ${datasetName}: ${err.status} ${err.message}`, options); - }); - } -}; - -module.exports.StreamDatasetCommand = class StreamDatasetCommand { - - constructor(program) { - this.program = program; - } - - execute(datasetName, options) { - const profile = loadProfile(options.profile); - debug('%s.executeStreamDataset(%s)', profile.name, datasetName); - - const dataset = new Datasets(profile.url); - dataset.streamDataset(profile.token, datasetName).then((response) => { - if (response.success) { - let stream = new es.Stream.PassThrough(); - stream.write(response.result.text); - stream.end(); - stream.pipe(process.stdout) - } - else { - printError(`Failed to stream dataset ${datasetName}: ${response.message}`, options); - } - }) - .catch((err) => { - printError(`Failed to stream dataset ${datasetName}: ${err.status} ${err.message}`, options); - }); - } -}; - -module.exports.GenerateDatasetCommand = class GenerateDatasetCommand { - - constructor(program) { - this.program = program; - } - - execute(options) { - debug('%s.generateDataset()', options.profile); - const yenv = yeoman.createEnv(); - const profile = options.profile; - yenv.lookup(()=>{ - yenv.run('@c12e/cortex:datasets', - {'cortexProfile': profile }, - (err) => { err ? printError(err) : printSuccess('Done.') }); - }); - } -}; diff --git a/src/commands/deploy.js b/src/commands/deploy.js new file mode 100644 index 00000000..a77d3f19 --- /dev/null +++ b/src/commands/deploy.js @@ -0,0 +1,345 @@ +/* + * Copyright 2023 Cognitive Scale, Inc. All Rights Reserved. + * + * 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 _ from 'lodash'; +import debugSetup from 'debug'; +import path from 'path'; +import fs from 'fs'; +// eslint-disable-next-line import/no-unresolved +import getStream from 'get-stream'; +import * as yauzl from 'yauzl'; +import yaml from 'js-yaml'; +import { loadProfile } from '../config.js'; +import Agents from '../client/agents.js'; +import Catalog from '../client/catalog.js'; +import Assessments from '../client/assessments.js'; +import Connections from '../client/connections.js'; +import Experiments from '../client/experiments.js'; +import Models from '../client/models.js'; +import Actions from '../client/actions.js'; +import Content from '../client/content.js'; +import { + printSuccess, printError, cleanInternalFields, jsonToYaml, writeToFile, fileExists, deleteFile, filterObject, handleError, +} from './utils.js'; + +const debug = debugSetup('cortex:cli'); + +const artifactsDir = '.fabric'; +const manifestFile = 'fabric.yaml'; +const manifestMeta = { + version: 1, + kind: 'deployment-manifest', + cortex: {}, +}; +function updateManifest(filepaths) { + const manifest = fileExists(manifestFile) ? yaml.load(fs.readFileSync(manifestFile).toString()) : manifestMeta; + Object.keys(filepaths).forEach((type) => { + const kind = type.toLowerCase(); + const entries = [..._.get(manifest, `cortex.${kind}`, []), ...filepaths[type]]; + _.set(manifest, `cortex.${kind}`, _.uniq(entries)); + }); + writeToFile(jsonToYaml(manifest), manifestFile); +} +async function addDependencies(url, token, project, resourceType, resourceName) { + const assessments = new Assessments(url); + const dependencies = await assessments.getDependenciesOfResource(token, project, resourceType, resourceName); + if (dependencies.data) { + const depsFilePath = path.join(artifactsDir, resourceName, '_dependencies.json'); + writeToFile(JSON.stringify(dependencies, null, 2), depsFilePath); + const depsManifest = {}; + depsManifest[`_dependencies.${resourceType}.${resourceName}`] = [depsFilePath]; + updateManifest(depsManifest); + } else { + printError(`Failed to get dependencies of ${resourceName} of type ${resourceType} in project ${project}: ${dependencies.status} ${dependencies.message}`); + } + return dependencies; +} +const DeployExperimentCommand = class { + constructor(program) { + this.program = program; + } + + async execute(experimentName, runId, options) { + const profile = await loadProfile(options.profile); + const project = options.project || profile.project; + debug('%s.exportDeployExperimentCommand%s)', profile.name, experimentName); + const manifest = {}; + const exports = {}; + const experiments = new Experiments(profile.url); + const model = new Models(profile.url); + const content = new Content(profile.url); + let modelName; + try { + let response = await experiments.describeExperiment(project, profile.token, experimentName); + let result = filterObject(response, options); + const expDesc = cleanInternalFields(result); + let filepath = path.join(artifactsDir, 'experiments', `${experimentName}.json`); + writeToFile(expDesc, filepath); + manifest.experiment = [filepath]; + // TODO ??? module.exports ??? experimentName; + modelName = result.modelId; + // export model if provided in experiment + if (modelName) { + response = await model.describeModel(project, profile.token, modelName, true); + result = filterObject(response, options); + const modelDesc = cleanInternalFields(result); + if (result.status && result.status === 'Published') { + filepath = path.join(artifactsDir, 'models', `${modelName}.json`); + writeToFile(modelDesc, filepath); + manifest.model = [filepath]; + // TODO ??? module.exports ??? modelName; + } else { + printError(`Only Published models can be exported. Model ${modelName} is in ${result.status}`); + } + } + let exportRun = runId; + if (!runId && options.latestRun) { + const { runs } = await experiments.listRuns(project, profile.token, experimentName, null, 1, JSON.stringify({ startTime: -1 })); + if (_.size(runs)) { + exportRun = runs[0].runId; + } + } + if (exportRun) { + response = await experiments.describeRun(project, profile.token, experimentName, exportRun); + result = filterObject(response, options); + const runDesc = cleanInternalFields(result); + filepath = path.join(artifactsDir, `experiments/${experimentName}/runs`, `${exportRun}.json`); + writeToFile(runDesc, filepath); + if (result.artifacts) { + await Promise.all(Object.values(result.artifacts) + .map((value) => content.downloadContent(project, profile.token, value, false, path.join(artifactsDir, value)))); + } + manifest.run = [filepath]; + // TODO ??? module.exports ??? exportRun; + } else { + printError('Provide runId or `--latestRun` option to export last run ', options); + } + updateManifest(manifest); + printSuccess(`Successfully exported ${JSON.stringify(exports)} in ${artifactsDir} and updated manifest file ${manifestFile}`); + } catch (err) { + handleError(err, options, 'Deploy experiment failed'); + } + } +}; + +const DeployConnectionCommand = class { + constructor(program) { + this.program = program; + } + + async execute(connectionName, options) { + const profile = await loadProfile(options.profile); + const project = options.project || profile.project; + debug('%s.exportDeploymentConnection%s)', profile.name, connectionName); + const connection = new Connections(profile.url); + try { + const response = await connection.describeConnection(project, profile.token, connectionName); + const result = filterObject(response.result, options); + const connectionDesc = cleanInternalFields(result); + const filepath = path.join(artifactsDir, 'connections', `${connectionName}.json`); + writeToFile(connectionDesc, filepath); + updateManifest({ connection: [filepath] }); + await addDependencies(profile.url, profile.token, project, 'Connection', connectionName); + printSuccess(`Successfully exported Connection ${connectionName} in ${artifactsDir} and updated manifest file ${manifestFile}`); + } catch (err) { + handleError(err, options, `Failed to export connection ${connectionName}`); + } + } +}; +export const DeploySnapshotCommand = class { + constructor(program) { + this.program = program; + } + + async execute(snapshotIds, options) { + const profile = await loadProfile(options.profile); + const project = options.project || profile.project; + debug('%s.exportDeploymentSnapshot(%s)', profile.name, snapshotIds); + const agents = new Agents(profile.url); + const promises = []; + snapshotIds.split(' ').forEach((snapshotId) => { + promises.push(agents.describeAgentSnapshot(project, profile.token, snapshotId).then(async (response) => { + let result = JSON.parse(response); + // export dependant MLOps artifacts + if (_.size(result.dependencies.connections)) { + await Promise.all(result.dependencies.connections.map(async (con) => { + if (_.size(con.connections)) { + await Promise.all(con.connections.map((c) => new DeployConnectionCommand(this.program).execute(c, options))); + } + })); + } + // export dependant MLOps artifacts + if (_.size(result.dependencies.mlOps)) { + await Promise.all(result.dependencies.mlOps.map(async (ml) => { + if (_.size(ml.experiments)) { + await new DeployExperimentCommand(this.program).execute(ml.experiments[0], _.size(ml.runs) ? ml.runs[0] : null, options); + } + })); + } + result = cleanInternalFields(result); + let filename = `${snapshotId}.json`; + if (options.yaml) { + result = jsonToYaml(result); + filename = `${snapshotId}.yaml`; + } + const filepath = path.join(artifactsDir, 'snapshots', filename); + writeToFile(result, filepath); + printSuccess(`Successfully exported agent snapshot ${filepath}`); + return filepath; + }).catch((err) => { + printError(`Failed to export agent snapshot ${snapshotId}: ${err.status} ${err.message}`, options); + })); + }); + Promise.all(promises).then((result) => { + updateManifest({ snapshots: result }); + printSuccess(`Successfully updated manifest file ${manifestFile}`); + }); + } +}; +export const DeployCampaignCommand = class { + constructor(program) { + this.program = program; + } + + processResource(project, campaign, stream, filename, entries) { + return new Promise((resolve, reject) => { + getStream.buffer(stream).then((content) => { + let type; + let filepath; + if (filename.endsWith('.yml') || filename.endsWith('.yaml')) { + const resource = yaml.load(content.toString()); + type = resource.kind; + filepath = path.join(artifactsDir, campaign, filename); + writeToFile(yaml.dump(resource), filepath); + } else { + type = path.extname(filename).replace(/\./g, ''); + filepath = path.join(artifactsDir, campaign, filename); + writeToFile(content, filepath); + } + const files = entries[type] || []; + files.push(filepath); + entries[type] = files; + resolve(filepath); + }).catch((e) => reject(e)); + }); + } + + async execute(campaignName, options) { + const profile = await loadProfile(options.profile); + const project = options.project || profile.project; + debug('%s.exportDeploymentCampaigns(%s)', profile.name, campaignName); + const catalog = new Catalog(profile.url); + await catalog.exportCampaign(project, profile.token, campaignName, options.deployable, `${campaignName}.zip`); + const filepaths = {}; + const promises = []; + yauzl.open(`${campaignName}.zip`, { lazyEntries: true }, (err, zipfile) => { + if (err) { + throw err; + } + zipfile.readEntry(); + zipfile.on('entry', (entry) => { + zipfile.openReadStream(entry, (e, readStream) => { + if (e) { + printError(e); + } + promises.push(this.processResource(project, campaignName, readStream, entry.fileName, filepaths)); + zipfile.readEntry(); + }); + }); + zipfile.on('error', (e) => printError(e)); + zipfile.once('end', async () => { + await addDependencies(profile.url, profile.token, project, 'Campaign', campaignName); + Promise.all(promises).then(() => { + updateManifest(filepaths); + printSuccess(`Successfully exported Campaign ${campaignName} in ${artifactsDir} and updated manifest file ${manifestFile}`); + }); + deleteFile(`${campaignName}.zip`); + }); + }); + } +}; +export const DeploySkillCommand = class { + constructor(program) { + this.program = program; + } + + async execute(skillName, options) { + const profile = await loadProfile(options.profile); + const project = options.project || profile.project; + debug('%s.exportDeploySkillCommand%s)', profile.name, skillName); + const manifest = {}; + const exports = {}; + const catalog = new Catalog(profile.url); + try { + let response = await catalog.describeSkill(project, profile.token, skillName, false); + // check if response is valid skill and not error. + const result = filterObject(response, options); + const skillDesc = cleanInternalFields(result); + const filepath = path.join(artifactsDir, 'skills', `${skillName}.json`); + writeToFile(skillDesc, filepath); + manifest.skill = [filepath]; + const dependencies = await addDependencies(profile.url, profile.token, project, 'Skill', skillName); + // export linked connections of skill + const connections = dependencies.data.filter((d) => d.type === 'Connection').map((d) => d.name); + if (connections) { + await Promise.all(connections.map((connection) => new DeployConnectionCommand(this.program).execute(connection, options))); + } + // export linked model/experiment of skill + const mlOps = Object.fromEntries(dependencies.data.filter((d) => ['Model', 'Experiment', 'ExperimentRun'].includes(d.type)).map((d) => [d.type, d.name])); + if (mlOps && mlOps.Experiment) { + await new DeployExperimentCommand(this.program).execute(mlOps.Experiment, (mlOps.ExperimentRun || '').split('-').pop(), options); + } + // export actions of the skill + const existingActions = result.actions.map((a) => a.name); + const actions = dependencies.data.filter((d) => d.type === 'Action' && !existingActions.includes(d.name)).map((d) => d.name); + if (actions.length > 0) { + const actionsClient = new Actions(profile.url); + await Promise.all(actions.map(async (a) => { + try { + response = await actionsClient.describeAction(project, profile.token, a); + const actionDesc = cleanInternalFields(filterObject(response.action, options)); + writeToFile(actionDesc, path.join(artifactsDir, 'actions', `${a}.json`)); + } catch (err) { + handleError(err, options, `Failed to export action ${a}`); + } + })); + manifest.action = actions.map((a) => path.join(artifactsDir, 'actions', `${a}.json`)); + } + // export types of skill + const types = dependencies.data.filter((d) => d.type === 'Type').map((d) => d.name); + if (types.length > 0) { + await Promise.all(types.map(async (type) => { + try { + response = await catalog.describeType(project, profile.token, type); + const typeDesc = cleanInternalFields(filterObject(response.type, options)); + writeToFile(typeDesc, path.join(artifactsDir, 'types', `${type}.json`)); + } catch (err) { + handleError(err, options, `Failed to export type ${type}`); + } + })); + manifest.type = types.map((a) => path.join(artifactsDir, 'types', `${a}.json`)); + } + updateManifest(manifest); + printSuccess(`Successfully exported ${JSON.stringify(Object.assign(exports, mlOps))} in ${artifactsDir} and updated manifest file ${manifestFile}`); + } catch (err) { + handleError(err, options, `Failed to export skill ${skillName}}`); + } + } +}; +export { DeployExperimentCommand as experiment }; +export { DeployExperimentCommand as model }; +export { DeployExperimentCommand as run }; +export { DeployExperimentCommand }; +export { DeployConnectionCommand }; diff --git a/src/commands/docker.js b/src/commands/docker.js new file mode 100644 index 00000000..6513e296 --- /dev/null +++ b/src/commands/docker.js @@ -0,0 +1,32 @@ +/* + * Copyright 2023 Cognitive Scale, Inc. All Rights Reserved. + * + * 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 { printError, printSuccess } from './utils.js'; +import { privateRegLogin } from '../client/dockerClient.js'; + +export default class DockerLoginCommand { + constructor(program) { + this.program = program; + } + + async execute(options) { + try { + await privateRegLogin(options); + printSuccess(JSON.stringify('Login Succeeded', null, 2), options); + } catch (err) { + printError(`Failed to docker login: ${err?.message ?? err}`, options); + } + } +} diff --git a/src/commands/environments.js b/src/commands/environments.js deleted file mode 100644 index 9324ba29..00000000 --- a/src/commands/environments.js +++ /dev/null @@ -1,156 +0,0 @@ -/* - * Copyright 2018 Cognitive Scale, Inc. All Rights Reserved. - * - * 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. - */ - -const fs = require('fs'); -const yeoman = require('yeoman-environment'); -const debug = require('debug')('cortex:cli'); -const { loadProfile } = require('../config'); -const Environments = require('../client/environments'); -const Content = require('../client/content'); -const { printSuccess, printError, filterObject, parseObject, printTable } = require('./utils'); -const _ = require('lodash/object'); -module.exports.ListEnvironments = class ListEnvironments { - - constructor(program) { - this.program = program; - } - - execute(options) { - const profile = loadProfile(options.profile); - debug('%s.listEnvironment()', profile.name); - - const conns = new Environments(profile.url); - conns.listEnvironments(profile.token).then((response) => { - if (response.success) { - let result = _.get(response,'result.environments',[]); - if (options.query) - result = filterObject(result, options); - - if (options.json) { - printSuccess(JSON.stringify(result, null, 2), options); - } - else { - let tableSpec = [ - { column: 'Name', field: 'name', width: 25 }, - { column: 'Label', field: 'label', width: 25 }, - { column: 'Type', field: 'type', width: 15 }, - { column: 'Console Url', field: 'consoleUrl', width: 50 }, - { column: 'White List', field: 'whiteList', width: 50 }, - { column: 'Black List', field: 'blackList', width: 50 }, - ]; - printTable(tableSpec, result); - } - } - else { - printError(`Failed to list environments: ${response.status} ${response.message}`, options); - } - }) - .catch((err) => { - debug(err); - printError(`Failed to list environments: ${err.status} ${err.message}`, options); - }); - } -}; - -module.exports.SaveEnvironmentCommand = class SaveEnvironmentCommand { - - constructor(program) { - this.program = program; - } - - execute(environmentDefinition, options) { - const profile = loadProfile(options.profile); - debug('%s.executeSaveDefinition(%s)', profile.name, environmentDefinition); - - const envDefStr = fs.readFileSync(environmentDefinition); - const envObj = parseObject(envDefStr, options); - debug('%o', envObj); - - const environment = new Environments(profile.url); - environment.saveEnvironment(profile.token, envObj) - .then((response) => { - if (response.success) { - printSuccess(`Environment \'${envObj.name}\' saved`, options); - } else { - printError(`Failed to save environment: ${response.status} ${response.message}`, options); - }}) - .catch((err) => { - printError(`Failed to save environment: ${err.response.body.message}`, options); - }); - } -}; - - -module.exports.DescribeEnvironmentCommand = class DescribeEnvironmentCommand { - - constructor(program) { - this.program = program; - } - - execute(environmentName, options) { - const profile = loadProfile(options.profile); - debug('%s.executeDescribeEnvironment(%s)', profile.name, environmentName); - - const environment = new Environments(profile.url); - environment.describeEnvironment(profile.token, environmentName).then((response) => { - if (response.success) { - let result = filterObject(response.result, options); - printSuccess(JSON.stringify(result, null, 2), options); - } - else { - printError(`Failed to describe environment ${environmentName}: ${response.message}`, options); - } - }) - .catch((err) => { - printError(`Failed to describe environment ${environmentName}: ${err.status} ${err.message}`, options); - }); - } -}; -module.exports.PromoteEnvironmentCommand = class PromoteEnvironmentCommand { - - constructor(program) { - this.program = program; - } - - execute(promoteDefinition, options) { - const profile = loadProfile(options.profile); - debug('%s.executePromoteDefinition(%s)', profile.name, promoteDefinition); - let promObj; - if ( promoteDefinition ) { - const promStr = fs.readFileSync(promoteDefinition); - promObj = parseObject(promStr, options); - } else if ( _.get(options,'snapshotId','').length > 0 && _.get(options,'environmentName','').length > 0) { - promObj = { snapshotId: options.snapshotId, environmentName: options.environmentName } - } else { - printError(`Either --snapshotId <..> and --environmentName <..> or a promotion definition file must be provided`, options); - return; - } - debug('%o', promObj); - - const environment = new Environments(profile.url); - environment.promoteEnvironment(profile.token, promObj) - .then((response) => { - if (response.success) { - printSuccess(`Snapshot: \'${promObj.snapshotId}\' successfully promoted to environment:\'${promObj.environmentName}\'`, options); - } else { - printError(`Failed to promote to environment: ${response.status} ${response.message}`, options); - }}) - .catch((err) => { - printError(`Failed to promote to environment: ${err.response.body.message}`, options); - }); - } -}; - diff --git a/src/commands/experiments.js b/src/commands/experiments.js new file mode 100644 index 00000000..689a03ec --- /dev/null +++ b/src/commands/experiments.js @@ -0,0 +1,253 @@ +import fs from 'node:fs'; +import _ from 'lodash'; +import debugSetup from 'debug'; +import dayjs from 'dayjs'; +import relativeTime from 'dayjs/plugin/relativeTime.js'; +import duration from 'dayjs/plugin/duration.js'; +import { loadProfile } from '../config.js'; +import Experiments from '../client/experiments.js'; +import { + printSuccess, + printError, + filterObject, + parseObject, + fileExists, + handleTable, + printExtendedLogs, + getFilteredOutput, + handleError, +} from './utils.js'; + +const debug = debugSetup('cortex:cli'); +dayjs.extend(duration); +dayjs.extend(relativeTime); +export class ListExperiments { + constructor(program) { + this.program = program; + } + + // eslint-disable-next-line consistent-return + async execute(options) { + const profile = await loadProfile(options.profile); + debug('%s.listExperiments()', profile.name); + const exp = new Experiments(profile.url); + try { + // eslint-disable-next-line consistent-return + const result = await exp.listExperiments(options.project || profile.project, options.model, profile.token, options.filter, options.limit, options.skip, options.sort); + // TODO remove --query on deprecation + if (options.json || options.query) { + getFilteredOutput(result, options); + } else { + printExtendedLogs(result.experiments, options); + const tableSpec = [ + { column: 'Name', field: 'name', width: 40 }, + { column: 'Title', field: 'title', width: 50 }, + { column: 'Version', field: '_version', width: 25 }, + { column: 'Description', field: 'description', width: 50 }, + ]; + handleTable(tableSpec, result.experiments, null, 'No experiments found'); + } + } catch (err) { + handleError(err, options, 'Failed to list experiments'); + } + } +} +export class DescribeExperimentCommand { + constructor(program) { + this.program = program; + } + + async execute(experimentName, options) { + const profile = await loadProfile(options.profile); + debug('%s.executeDescribeExperiment(%s)', profile.name, experimentName); + const exp = new Experiments(profile.url); + try { + const result = exp.describeExperiment(options.project || profile.project, profile.token, experimentName); + getFilteredOutput(result, options); + } catch (err) { + handleError(err, options, `Failed to describe experiment ${experimentName}`); + } + } +} +export class DeleteExperimentCommand { + constructor(program) { + this.program = program; + } + + async execute(experimentName, options) { + const profile = await loadProfile(options.profile); + debug('%s.executeDeleteExperiment(%s)', profile.name, experimentName); + const exp = new Experiments(profile.url); + try { + const response = await exp.deleteExperiment(options.project || profile.project, profile.token, experimentName); + const result = filterObject(response, options); + return printSuccess(JSON.stringify(result, null, 2), options); + } catch (err) { + return handleError(err, options, `Failed to delete experiment ${experimentName}`); + } + } +} +export class ListRuns { + constructor(program) { + this.program = program; + } + + // eslint-disable-next-line consistent-return + async execute(experimentName, options) { + const profile = await loadProfile(options.profile); + debug('%s.listRuns()', profile.name); + const exp = new Experiments(profile.url); + const sort = options.sort || JSON.stringify({ startTime: -1 }); + try { + // eslint-disable-next-line consistent-return + const result = await exp.listRuns(options.project || profile.project, profile.token, experimentName, options.filter, options.limit, sort, options.skip); + // TODO remove --query on deprecation + if (options.json || options.query) { + getFilteredOutput(result.runs, options); + } else { + printExtendedLogs(result.runs, options); + const tableSpec = [ + { column: 'Run ID', field: 'runId', width: 25 }, + { column: 'Start', field: 'startTime', width: 25 }, + { column: 'Took', field: 'took', width: 25 }, + { column: 'Params', field: 'params', width: 45 }, + { column: 'Metrics', field: 'metrics', width: 45 }, + { column: 'Artifacts', field: 'artifacts' }, + ]; + const trans = (o) => { + o.startTime = o.startTime ? dayjs.unix(o.startTime).format('YYYY-MM-DD HH:mm a') : ''; + o.took = o.took ? dayjs.duration(o.took, 'seconds').humanize() : ''; + o.params = _.map(o.params, (v, k) => `${k}: ${v}`).join('\n'); + o.metrics = _.map(o.metrics, (v, k) => `${k}: ${v}`).join('\n'); + o.artifacts = Object.keys(_.get(o, 'artifacts', {})).join(', '); + return o; + }; + handleTable(tableSpec, _.get(result, 'runs', []), trans, 'No runs found'); + } + } catch (err) { + handleError(err, options, 'Failed to list runs'); + } + } +} +export class DescribeRunCommand { + constructor(program) { + this.program = program; + } + + async execute(experimentName, runId, options) { + const profile = await loadProfile(options.profile); + debug('%s.executeDescribeRun(%s)', profile.name, runId); + const exp = new Experiments(profile.url); + try { + const response = await exp.describeRun(options.project || profile.project, profile.token, experimentName, runId); + getFilteredOutput(response.result, options); + } catch (err) { + handleError(err, options, `Failed to describe run ${experimentName}/${runId}`); + } + } +} + +export class DeleteRunCommand { + constructor(program) { + this.program = program; + } + + async execute(experimentName, runId, options) { + const profile = await loadProfile(options.profile); + debug('%s.executeDeleteRun(%s)', profile.name, runId); + const exp = new Experiments(profile.url); + try { + await exp.deleteRun(options.project || profile.project, profile.token, experimentName, runId); + return printSuccess(`Run ${runId} in experiment ${experimentName} deleted`, options); + } catch (err) { + return handleError(err, options, `Failed to delete run ${experimentName}/${runId}`); + } + } +} +export class DownloadArtifactCommand { + constructor(program) { + this.program = program; + } + + async execute(experimentName, runId, artifactName, options) { + const profile = await loadProfile(options.profile); + debug('%s.downloadArtifact(%s)', profile.name, artifactName); + const exp = new Experiments(profile.url); + const showProgress = !!options.progress; + // To download content from Secrets + exp.downloadArtifact(options.project || profile.project, profile.token, experimentName, runId, artifactName, showProgress).catch((err) => { + debug(err); + printError(`Failed to download artifact: ${err.status ? err.status : ''}- ${err.message ? err.message : ''}`, options); + }); + } +} +export class SaveExperimentCommand { + constructor(program) { + this.program = program; + } + + async execute(experimentDefinition, options) { + const profile = await loadProfile(options.profile); + debug('%s.executeSaveExperiment(%s)', profile.name, experimentDefinition); + if (!fileExists(experimentDefinition)) { + printError(`File does not exist at: ${experimentDefinition}`); + } + const experimentDefStr = fs.readFileSync(experimentDefinition); + const experiment = parseObject(experimentDefStr, options); + debug('%o', experiment); + const experiments = new Experiments(profile.url); + try { + await experiments.saveExperiment(options.project || profile.project, profile.token, experiment); + printSuccess('Experiment saved', options); + } catch (err) { + handleError(err, options, 'Failed to save experiment'); + } + } +} +export class CreateRunCommand { + constructor(program) { + this.program = program; + } + + async execute(runDefinition, options) { + const profile = await loadProfile(options.profile); + debug('%s.executeCreateRun(%s)', profile.name, runDefinition); + if (!fs.existsSync(runDefinition)) { + printError(`File does not exist at: ${runDefinition}`); + } + const runDefinitionStr = fs.readFileSync(runDefinition); + const run = parseObject(runDefinitionStr, options); + const experiments = new Experiments(profile.url); + try { + const response = await experiments.createRun(options.project || profile.project, profile.token, run); + printSuccess('Run created', options); + printSuccess(JSON.stringify(response, null, 2), options); + } catch (err) { + handleError(err, options, 'Failed to create run'); + } + } +} +export class UploadArtifactCommand { + constructor(program) { + this.program = program; + } + + async execute(experimentName, runId, filePath, artifact, options) { + const profile = await loadProfile(options.profile); + debug('%s.executeUploadArtifact()', profile.name); + const experiments = new Experiments(profile.url); + const upload = _.partial(UploadArtifactCommand.upload, experiments, profile, options); + upload(filePath, artifact, experimentName, runId); + } + + static async upload(experiments, profile, options, filePath, artifact, experimentName, runId) { + const contentType = _.get(options, 'contentType', 'application/octet-stream'); + try { + const response = await experiments.uploadArtifact(options.project || profile.project, profile.token, experimentName, runId, filePath, artifact, contentType); + printSuccess('Artifact successfully uploaded.', options); + printSuccess(JSON.stringify(response, null, 2), options); + } catch (err) { + handleError(err, options, 'Failed to upload Artifact'); + } + } +} diff --git a/src/commands/jobs.js b/src/commands/jobs.js deleted file mode 100644 index 31a20f11..00000000 --- a/src/commands/jobs.js +++ /dev/null @@ -1,168 +0,0 @@ -/* - * Copyright 2018 Cognitive Scale, Inc. All Rights Reserved. - * - * 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. - */ - -const fs = require('fs'); -const debug = require('debug')('cortex:cli'); -const { loadProfile } = require('../config'); -const Jobs = require('../client/jobs'); -const { printSuccess, printError, filterObject, parseObject, printTable } = require('./utils'); - -module.exports.ListJobs = class ListJobs { - - constructor(program) { - this.program = program; - } - - execute(options) { - const profile = loadProfile(options.profile); - debug('%s.listJobs()', profile.name); - - const jobs = new Jobs(profile.url); - jobs.listJobs(profile.token).then((response) => { - if (response.success) { - if (options.query || options.json) { - let result = filterObject(response.result.jobs, options); - printSuccess(JSON.stringify(result, null, 2), options); - } - else { - let tableSpec = [ - { column: 'Name', field: 'name', width: 40 }, - { column: 'Image', field: 'image', width: 90 }, - { column: 'Memory', field: 'memory', width: 10 }, - { column: 'Virtual CPUs', field: 'vcpus', width: 15 } - ]; - printTable(tableSpec, response.result.jobs); - } - } - else { - printError(`Failed to list jobs: ${response.status} ${response.message}`, options); - } - }) - .catch((err) => { - debug(err); - printError(`Failed to list jobs: ${err.status} ${err.message}`, options); - }); - } -}; - -module.exports.DescribeJob = class DescribeJob { - - constructor(program) { - this.program = program; - } - - execute(jobDefinition, options) { - const profile = loadProfile(options.profile); - debug('%s.describeJob()', profile.name); - - const jobs = new Jobs(profile.url); - jobs.describeJob(profile.token, jobDefinition).then((response) => { - if (response.success) { - if (options.json) { - let result = filterObject(response.result.job, options); - printSuccess(JSON.stringify(result, null, 2), options); - } - else { - let tableSpec = [ - { column: 'Name', field: 'name', width: 40 }, - { column: 'Image', field: 'image', width: 90 }, - { column: 'Memory', field: 'memory', width: 10 }, - { column: 'Virtual CPUs', field: 'vcpus', width: 15 } - ]; - printTable(tableSpec, [response.result.job]); - } - } - else { - printError(`Failed to describe job: ${response.status} ${response.message}`, options); - } - }) - .catch((err) => { - debug(err); - printError(`Failed to describe job: ${err.status} ${err.message}`, options); - }); - } -}; - -module.exports.JobStatus = class JobStatus { - - constructor(program) { - this.program = program; - } - - execute(jobDefinition, options) { - const profile = loadProfile(options.profile); - debug('%s.jobStatus()', profile.name); - - const jobs = new Jobs(profile.url); - jobs.jobStatus(profile.token, jobDefinition).then((response) => { - if (response.success) { - if (options.json) { - let result = response.result; - printSuccess(JSON.stringify(result, null, 2), options); - } - else { - let tableSpec = [ - { column: 'Submitted', field: 'SUBMITTED', width: 15 }, - { column: 'Pending', field: 'PENDING', width: 15 }, - { column: 'Runnable', field: 'RUNNABLE', width: 15 }, - { column: 'Starting', field: 'STARTING', width: 15 }, - { column: 'Running', field: 'RUNNING', width: 15 }, - { column: 'Succeeded', field: 'SUCCEEDED', width: 15 }, - { column: 'Failed', field: 'FAILED', width: 15 } - ]; - printTable(tableSpec, [response.result.stats]); - } - } - else { - printError(`Failed to describe job: ${response.status} ${response.message}`, options); - } - }) - .catch((err) => { - debug(err); - printError(`Failed to describe job: ${err.status} ${err.message}`, options); - }); - } -}; - -module.exports.SaveJob = class SaveJob { - - constructor(program) { - this.program = program; - } - - execute(jobDefinition, options) { - const profile = loadProfile(options.profile); - debug('%s.saveJob()', profile.name); - - const jobDefStr = fs.readFileSync(jobDefinition); - const job = parseObject(jobDefStr, options); - debug('%o', job); - - const jobs = new Jobs(profile.url); - jobs.saveJob(profile.token, job).then((response) => { - if (response.success) { - printSuccess('Job saved', options); - } - else { - printError(`Failed to save job: ${response.status} ${response.message}`, options); - } - }) - .catch((err) => { - debug(err); - printError(`Failed to save job: ${err.status} ${err.message}`, options); - }); - } -}; \ No newline at end of file diff --git a/src/commands/models.js b/src/commands/models.js new file mode 100644 index 00000000..617fd4bf --- /dev/null +++ b/src/commands/models.js @@ -0,0 +1,184 @@ +import fs from 'node:fs'; +import get from 'lodash/get.js'; +import pick from 'lodash/pick.js'; +import debugSetup from 'debug'; +import dayjs from 'dayjs'; +import relativeTime from 'dayjs/plugin/relativeTime.js'; +import { loadProfile } from '../config.js'; +import Models from '../client/models.js'; +import Experiments from '../client/experiments.js'; +import { + LISTTABLEFORMAT, + RUNTABLEFORMAT, + printExtendedLogs, + printSuccess, + printError, + filterObject, + parseObject, + fileExists, + handleTable, + getFilteredOutput, + handleError, +} from './utils.js'; + +const _ = { + get, + pick, +}; +const debug = debugSetup('cortex:cli'); +dayjs.extend(relativeTime); +export class SaveModelCommand { + constructor(program) { + this.program = program; + } + + async execute(modelDefinition, options) { + const profile = await loadProfile(options.profile); + debug('%s.executeSaveModel(%s)', profile.name, modelDefinition); + if (!fileExists(modelDefinition)) { + printError(`File does not exist at: ${modelDefinition}`); + } + const modelDefStr = fs.readFileSync(modelDefinition); + const model = parseObject(modelDefStr, options); + debug('%o', model); + const models = new Models(profile.url); + try { + const response = await models.saveModel(options.project || profile.project, profile.token, model); + printSuccess('Model saved', options); + printSuccess(JSON.stringify(_.pick(response, ['version', 'created', 'modelId']), null, 2)); + } catch (err) { + handleError(err, options, 'Failed to save model'); + } + } +} +export class ListModelsCommand { + constructor(program) { + this.program = program; + } + + // eslint-disable-next-line consistent-return + async execute(options) { + const profile = await loadProfile(options.profile); + debug('%s.executeListModels()', profile.name); + const models = new Models(profile.url); + // eslint-disable-next-line consistent-return + try { + const response = await models.listModels(options.project || profile.project, options.skip, options.limit, options.filter, options.sort, options.tags, profile.token); + const result = response.models; + // TODO remove --query on deprecation + if (options.json || options.query) { + getFilteredOutput(result, options); + } else { + printExtendedLogs(result, options); + handleTable(LISTTABLEFORMAT, result, (o) => ({ ...o, updatedAt: o.updatedAt ? dayjs(o.updatedAt).fromNow() : '-' }), 'No models found'); + } + } catch (err) { + handleError(err, options, 'Failed to list models'); + } + } +} +export class ListModelRunsCommand { + constructor(program) { + this.program = program; + } + + // eslint-disable-next-line consistent-return + async execute(modelName, options) { + const profile = await loadProfile(options.profile); + debug('%s.executeListModels()', profile.name); + const models = new Models(profile.url); + try { + // eslint-disable-next-line consistent-return + const response = await models.listModelRuns(options.project || profile.project, modelName, profile.token, options.filter, options.limit, options.skip, options.sort); + const result = response.runs; + // TODO remove --query on deprecation + if (options.json || options.query) { + getFilteredOutput(result, options); + } else { + printExtendedLogs(result, options); + handleTable(RUNTABLEFORMAT, result, (o) => ({ ...o, updatedAt: o.updatedAt ? dayjs(o.updatedAt).fromNow() : '-' }), 'No runs found'); + } + } catch (err) { + handleError(err, options, 'Failed to list model runs'); + } + } +} + +export class DescribeModelCommand { + constructor(program) { + this.program = program; + } + + async execute(modelName, options) { + const profile = await loadProfile(options.profile); + const models = new Models(profile.url); + debug('%s.executeDescribeModel(%s)', profile.name, modelName); + try { + const response = await models.describeModel(options.project || profile.project, profile.token, modelName, options.verbose); + getFilteredOutput(response.model, options); + } catch (err) { + handleError(err, options, `Failed to describe model ${modelName}`); + } + } +} +export const DeleteModelCommand = class { + constructor(program) { + this.program = program; + } + + async execute(modelName, options) { + const profile = await loadProfile(options.profile); + debug('%s.executeDeleteModel(%s)', profile.name, modelName); + const models = new Models(profile.url); + try { + const response = await models.deleteModel(options.project || profile.project, profile.token, modelName); + const result = filterObject(response, options); + return printSuccess(JSON.stringify(result, null, 2), options); + } catch (err) { + return handleError(err, { ...options, tableformat: 'DEPENDENCYTABLEFORMAT' }, `Failed to delete model "${modelName}"`); + } + } +}; +export class UpdateModelStatusCommand { + constructor(program) { + this.program = program; + } + + async execute(modelName, options, status) { + const profile = await loadProfile(options.profile); + const models = new Models(profile.url); + debug('%s.executeUpdateModelStatus(%s)', profile.name, modelName, status); + try { + await models.updateModelStatus(options.project || profile.project, profile.token, modelName, status); + printSuccess(`Model ${status}ed`, options); + } catch (err) { + printError(`Failed to publish model ${modelName}: ${err.status} ${err.message}`, options); + } + } +} +export class RegisterModelCommand { + constructor(program) { + this.program = program; + } + + async execute(modelDefinition, options) { + const profile = await loadProfile(options.profile); + const experiments = new Experiments(profile.url); + try { + debug('%s.executeRegisterModel(%s)', profile.name, modelDefinition); + const modelDefStr = fs.readFileSync(modelDefinition); + const model = parseObject(modelDefStr, options); + debug('%o', model); + const saveExperimentResponse = await experiments.saveExperiment(options.project || profile.project, profile.token, model); + const experimentName = _.get(saveExperimentResponse, 'result.name', ''); + const saveRunResponse = await experiments.createRun(options.project || profile.project, profile.token, experimentName); + const runId = _.get(saveRunResponse, 'result.runId', ''); + const contentType = _.get(options, 'contentType', 'application/octet-stream'); + await experiments.uploadArtifact(options.project || profile.project, profile.token, experimentName, runId, model.filePath, model.artifact, contentType); + printSuccess('Artifact successfully uploaded.', options); + printSuccess(JSON.stringify(saveRunResponse.result, null, 2), options); + } catch (err) { + handleError(err, options, 'Failed to upload model'); + } + } +} diff --git a/src/commands/pipelineRepositories.js b/src/commands/pipelineRepositories.js new file mode 100644 index 00000000..28fb1a8b --- /dev/null +++ b/src/commands/pipelineRepositories.js @@ -0,0 +1,193 @@ +import fs from 'node:fs'; +import debugSetup from 'debug'; +import dayjs from 'dayjs'; +import relativeTime from 'dayjs/plugin/relativeTime.js'; +import { loadProfile } from '../config.js'; +import Pipelines from '../client/pipelineRepositories.js'; +import { + fileExists, + printSuccess, + printError, + parseObject, + handleTable, + printExtendedLogs, + getFilteredOutput, + printCrossTable, + handleError, +} from './utils.js'; + +const debug = debugSetup('cortex:cli'); +dayjs.extend(relativeTime); + +export const SavePipelineRepoCommand = class { + constructor(program) { + this.program = program; + } + + async execute(pipelineRepoDefinition, options) { + const profile = await loadProfile(options.profile); + debug('%s.executeSavePipelineRepoDefinition(%s)', profile.name, pipelineRepoDefinition); + let repoObj = {}; + if (pipelineRepoDefinition && !fileExists(pipelineRepoDefinition)) { + printError(`File does not exist at: ${pipelineRepoDefinition}`); + } else if (pipelineRepoDefinition) { + const repoDefStr = fs.readFileSync(pipelineRepoDefinition); + repoObj = parseObject(repoDefStr, options); + } + debug('%s', repoObj); + + // Take options as overrides if both are provided.. + if (options?.name) { repoObj.name = options.name; } + if (options?.repo) { repoObj.repo = options.repo; } + if (options?.branch) { repoObj.branch = options.branch; } + + const repo = new Pipelines(profile.url); + try { + await repo.savePipelineRepo(options.project || profile.project, profile.token, repoObj); + return printSuccess('Pipeline Repository saved', options); + } catch (err) { + return handleError(err, options, 'Failed to save pipeline repository'); + } + } +}; + +export const ListPipelineRepoCommand = class { + constructor(program) { + this.program = program; + } + + async execute(options) { + const profile = await loadProfile(options.profile); + debug('%s.executeListPipelineRepos()', profile.name); + const repos = new Pipelines(profile.url); + try { + const response = await repos.listPipelineRepo(options.project || profile.project, profile.token, options.filter, options.limit, options.skip, options.sort); + const result = response.pipelineRepositories; + if (options.json) { + return getFilteredOutput(result, options); + } + printExtendedLogs(result, options); + const tableSpec = [ + { column: 'Name', field: 'name', width: 20 }, + { column: 'Repo', field: 'repo', width: 40 }, + { column: 'Branch', field: 'branch', width: 15 }, + { column: 'Modified', field: 'updatedAt', width: 22 }, + { column: 'Author', field: 'createdBy', width: 22 }, + ]; + return handleTable(tableSpec, result, (o) => ({ ...o, updatedAt: o.updatedAt ? dayjs(o.updatedAt).fromNow() : '-' }), 'No pipeline repositories found'); + } catch (err) { + return printError(`Failed to list pipeline repositories: ${err.status} ${err.message}`, options); + } + } +}; + +export const DescribePipelineRepoCommand = class { + constructor(program) { + this.program = program; + } + + async execute(pipelineRepoName, options) { + const profile = await loadProfile(options.profile); + debug('%s.executeDescribePipelineRepo(%s)', profile.name, pipelineRepoName); + const repos = new Pipelines(profile.url); + try { + const response = await repos.describePipelineRepo(options.project || profile.project, profile.token, pipelineRepoName); + return getFilteredOutput(response.pipelineRepository, options); + } catch (err) { + return handleError(err, options, 'Failed to describe pipeline repository'); + } + } +}; + +export const DeletePipelineRepoCommand = class { + constructor(program) { + this.program = program; + } + + async execute(pipelineRepoName, options) { + const profile = await loadProfile(options.profile); + debug('%s.executeDeleteProfileRepo(%s)', profile.name, pipelineRepoName); + const repos = new Pipelines(profile.url); + try { + const response = await repos.deletePipelineRepo(options.project || profile.project, profile.token, pipelineRepoName); + return printSuccess(response?.message ?? response, options); + } catch (err) { + return handleError(err, { ...options, tableformat: 'DEPENDENCYTABLEFORMAT' }, 'Failed to delete pipeline repository'); + } + } +}; + +export const UpdateRepoPipelinesCommand = class { + constructor(program) { + this.program = program; + } + + async execute(pipelineRepoName, options) { + const profile = await loadProfile(options.profile); + debug('%s.executeUpdateRepoPipelines(%s)', profile.name, pipelineRepoName); + const repos = new Pipelines(profile.url); + try { + const response = await repos.updateRepoPipelines(options.project || profile.project, profile.token, pipelineRepoName, options.skill, options.forceRecreate); + // Check for 'updateReport' property + if (response.updateReport) { + const { message, updateReport, success } = response; + if (options.json) { + return getFilteredOutput(updateReport, options); + } + return this.printAsTable(success, message, updateReport, options); + } + return printError(response.message, options); + } catch (err) { + return printError(`${err.status}: ${err.message}`, options); + } + } + + noChangesMade(updateReport) { + const noChangesMade = (updateReport?.added?.length === 0 + && updateReport?.deleted?.length === 0 + && updateReport?.updated?.length === 0 + && updateReport?.failed?.add?.length === 0 + && updateReport?.failed?.delete?.length === 0 + && updateReport?.failed?.update?.length === 0); + return noChangesMade; + } + + printAsTable(success, message, updateReport, options) { + if (success && this.noChangesMade(updateReport)) { + // no changes made -> no need to show update report + return printSuccess('Pipelines up to date! No changes made.', options); + } + // Top level message + if (success) { + printSuccess(`${message}\n`, options); + } else { + printError(`${message}\n`, options, false); // do not exit + } + // Constructs a crossed-table (i.e. nested table) with the left most column + // identifying the Type of Change (e.g. Added, Deleted) followed by columns + // 'Pipeline Name' & 'Details'. + const tableSpec = [ + { column: '', width: 20 }, // explicit width to keep table reasonable length + { column: 'Pipeline Name', width: 25, field: 'pipelineName' }, + { column: 'Details', width: 30, field: 'details' }, + ]; + const tableSections = { + Added: updateReport.added.map((pipelineName) => ({ pipelineName, details: '' })), + Updated: updateReport.updated.map((pipelineName) => ({ pipelineName, details: '' })), + Deleted: updateReport.deleted.map((pipelineName) => ({ pipelineName, details: '' })), + 'Failed to Add': updateReport.failed.add, + 'Failed to Update': updateReport.failed.update, + 'Failed to Delete': updateReport.failed.delete, + }; + debug('Constructed Table with values: %s, columns: %s', JSON.stringify(tableSections), JSON.stringify(tableSpec)); + // The Table must include the Pipeline name and error details for Pipelines + // that failed to be Added, Updated, Deleted. By default the Table only + // prints the # of characters allocated for each column. To ensure all + // details are presented set 'wordWrap' to True. + return printCrossTable(tableSpec, tableSections, { wordWrap: true }); + } + + printJson(updateReport, options) { + return getFilteredOutput(updateReport, options); + } +}; diff --git a/src/commands/pipelines.js b/src/commands/pipelines.js new file mode 100644 index 00000000..dbe39670 --- /dev/null +++ b/src/commands/pipelines.js @@ -0,0 +1,243 @@ +import debugSetup from 'debug'; +import fs from 'fs'; +import dayjs from 'dayjs'; +import _ from 'lodash'; +import relativeTime from 'dayjs/plugin/relativeTime.js'; +import { loadProfile } from '../config.js'; +import Pipelines from '../client/pipelines.js'; +import { + printError, + handleTable, + printExtendedLogs, + getFilteredOutput, + parseObject, + filterObject, + getQueryOptions, + printSuccess, + printTable, + printWarning, handleError, +} from './utils.js'; + +const debug = debugSetup('cortex:cli'); +dayjs.extend(relativeTime); + + + +export const ListPipelineCommand = class { + constructor(program) { + this.program = program; + } + + async execute(options) { + const profile = await loadProfile(options.profile); + debug('%s.executeListPipelines()', profile.name); + const pipelines = new Pipelines(profile.url); + try { + const filteringByRepo = options.repo && !options.filter; + const filter = filteringByRepo ? JSON.stringify({ gitRepoName: options.repo }) : options.filter; + if (options.repo && options.filter) { + printWarning('WARNING: --repo and --filter options are incompatible! The --filter option will be used', options); + } + const { pipelines: result } = await pipelines.listPipelines(options.project || profile.project, profile.token, filter, options.limit, options.skip, options.sort); + if (options.json) { + return getFilteredOutput(result, options); + } + printExtendedLogs(result, options); + const tableSpec = [ + { column: 'Name', field: 'name', width: 20 }, + { column: 'Git Repo Name', field: 'gitRepoName', width: 20 }, + { column: 'SHA', field: 'sha', width: 20 }, + { column: 'Modified', field: 'updatedAt', width: 22 }, + { column: 'Author', field: 'createdBy', width: 22 }, + ]; + return handleTable(tableSpec, result, (o) => ({ ...o, updatedAt: o.updatedAt ? dayjs(o.updatedAt).fromNow() : '-' }), 'No pipelines found'); + } catch (err) { + return handleError(err, options, 'Failed to list pipelines'); + } + } +}; + +export const DescribePipelineCommand = class { + constructor(program) { + this.program = program; + } + + async execute(pipelineName, gitRepoName, options) { + const profile = await loadProfile(options.profile); + debug('%s.executeDescribePipeline(%s, %s)', profile.name, pipelineName, gitRepoName); + const pipelines = new Pipelines(profile.url); + try { + const response = await pipelines.describePipeline(options.project || profile.project, profile.token, pipelineName, gitRepoName, options.sha); + return getFilteredOutput(response.pipeline, options); + } catch (err) { + return handleError(err, options, 'Failed to describe pipeline'); + } + } +}; + +export const RunPipelineCommand = class { + constructor(program) { + this.program = program; + } + + // eslint-disable-next-line consistent-return + async execute(pipelineName, gitRepoName, options) { + const profile = await loadProfile(options.profile); + debug('%s.executeRunPipeline(%s)', profile.name, pipelineName); + let params = {}; + if (options.params) { + try { + params = parseObject(options.params, options); + } catch (e) { + return printError(`Failed to parse params: ${options.params} Error: ${e}`, options); + } + } else if (options.paramsFile) { + if (!fs.existsSync(options.paramsFile)) { + return printError(`File does not exist at: ${options.paramsFile}`, options); + } + const paramsStr = fs.readFileSync(options.paramsFile); + params = parseObject(paramsStr, options); + } + const pipelines = new Pipelines(profile.url); + try { + const response = await pipelines.runPipeline(options.project || profile.project, profile.token, pipelineName, gitRepoName, params, options); + // Prefer 'runId' but support 'activationId' as a fallback + if (response?.activationId) { + response.runId = response?.runId || response?.activationId; + delete response.activationId; + } + + if (options.json) { + return getFilteredOutput(response, options); + } + if (response?.runId) { + return printSuccess(`Pipeline Run Submitted!\n\nUse "cortex pipelines describe-run ${response.runId}" to inspect the Pipeline Run`); + } + if (response?.message) { + // Print the message from server - expected when a Pipeline is scheduled + return printSuccess(response.message); + } + } catch (err) { + return handleError(err, options, 'Failed to run pipeline'); + } + } +}; + +export const DescribePipelineRunCommand = class { + constructor(program) { + this.program = program; + } + + getStateMatchingTransit(transit, states) { + const hasMatchingEndpoints = (s) => s?.from === transit?.from && s?.to === transit?.to; + return (states ?? []).find(hasMatchingEndpoints); + } + + extractBlocksFromRun(pipelineRun) { + // NOTE: 'plan.states' & 'transits' should be equivalent, so try + // extracting details of the Pipeline run from the corresponding + // 'state'. Only consider Blocks to avoid confusion from input/output. + // + // TODO:: states & transits aren't necessarily equivalent when running an + // single Block in a Pipeline. However, BUG https://cognitivescale.atlassian.net/browse/FAB-6294 + // makes it so the entire Pipeline is run. Once that is resolved, the CLI + // can be configured to accurately print the plan. + const blocks = pipelineRun?.transits?.map((t) => { + const state = this.getStateMatchingTransit(t, pipelineRun?.plan?.states); + // Skills are equivalent to Blocks, so it's clearer to rename the + // type. Plus extra Block metadata can be found in the Properties. + if (state?.type === 'skill') { + const props = state?.ref?.properties || []; + const blockProp = props.find((p) => p?.name === 'block'); + const typeProp = props.find((p) => p?.name === 'type'); + return { + type: typeProp?.value || '-', // i.e. block-type + start: t.start, + end: t.end, + status: t.status, + elapsed: t.end ? dayjs(t.end).diff(dayjs(t.start)) : 'N/A', + title: state?.ref?.title || t.title, + name: blockProp?.value || t.name, + }; + } + return undefined; + }).filter((x) => x); // filter undefined + return blocks; + } + + async execute(runId, options) { + const profile = await loadProfile(options.profile); + debug('%s.executeDescribePipelineRun(%s)', profile.name, runId); + const pipelines = new Pipelines(profile.url); + try { + const response = await pipelines.describePipelineRun(options.project || profile.project, profile.token, runId); + if (options.json) { + getFilteredOutput(response, options); + } else { + // Print table view of Pipeline Run + const result = filterObject(response, getQueryOptions(options)); + const tableSpec = [ + { column: 'Block Name', field: 'name', width: 40 }, + { column: 'Block Title', field: 'title', width: 40 }, + { column: 'Type', field: 'type', width: 20 }, + { column: 'Status', field: 'status', width: 20 }, + { column: 'Elapsed Time (ms)', field: 'elapsed', width: 30 }, + ]; + const blocks = this.extractBlocksFromRun(result); + const elapsed = result?.end ? dayjs(result.end).diff(dayjs(result.start)) : 'N/A'; + const logsAvailable = Object.prototype.hasOwnProperty.call(result, 'response') ? 'Yes' : 'No'; + printSuccess(`Status: ${result?.status}`); + printSuccess(`Elapsed Time (ms): ${elapsed}`); + printSuccess(`Logs Available: ${logsAvailable}`); + printSuccess('Details:'); + printTable(tableSpec, _.sortBy(blocks, ['start', 'end'])); + } + } catch (err) { + handleError(err, options, `Failed to describe pipeline run ${runId}`, options); + } + } +}; + +export const ListPipelineRunsCommand = class { + constructor(program) { + this.program = program; + } + + async execute(pipelineName, gitRepoName, options) { + const profile = await loadProfile(options.profile); + debug('%s.executeListPipelineRuns(%s, %s)', profile.name, pipelineName); + const pipelines = new Pipelines(profile.url); + try { + const response = await pipelines.listPipelineRuns(options.project || profile.project, + profile.token, pipelineName, gitRepoName, options.limit, options.skip, options.sort, options.filter); + const result = response.activations; + // TODO remove --query on deprecation + if (options.json || options.query) { + return getFilteredOutput(result, options); + } + printExtendedLogs(result, options); + const tableSpec = [ + { column: 'Name', field: 'name', width: 30 }, + { column: 'Run Id', field: 'activationId', width: 40 }, + { column: 'Status', field: 'status', width: 20 }, + { column: 'Started', field: 'start', width: 65 }, + ]; + const genName = (o) => { + if (o.agentName) { + return `${o.agentName} (Agent)`; + } + if (o.skillName) { + return `${o.skillName} (Skill)`; + } + return '-'; + }; + return handleTable(tableSpec, _.map(result, (o) => ({ + ...o, + name: genName(o), + start: o.start ? dayjs(o.start).fromNow() : '-', + })), null, 'No pipeline runs found'); + } catch (err) { + return handleError(err, options, 'Failed to list pipelne runs'); + } + } +}; diff --git a/src/commands/processors.js b/src/commands/processors.js deleted file mode 100644 index 91798108..00000000 --- a/src/commands/processors.js +++ /dev/null @@ -1,276 +0,0 @@ -/* - * Copyright 2018 Cognitive Scale, Inc. All Rights Reserved. - * - * 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. - */ - -const uuid = require('uuid'); -const fs = require('fs'); -const debug = require('debug')('cortex:cli'); -const { loadProfile } = require('../config'); -const Processors = require('../client/processors'); -const { printSuccess, printError, filterObject, parseObject, printTable } = require('./utils'); - -module.exports.ListRuntimesCommand = class ListRuntimesCommand { - - constructor(program) { - this.program = program; - } - - execute(options) { - const profile = loadProfile(options.profile); - debug('%s.executeListRuntimes()', profile.name); - - const processors = new Processors(profile.url); - processors.listRuntimes(profile.token).then((response) => { - if (response.success) { - let result = response.runtimes; - if (options.query) - result = filterObject(result, options); - - if(options.json) { - printSuccess(JSON.stringify(result, null, 2), options); - } - else { - const tableSpec = [ - { column: 'Title', field: 'title', width: 50 }, - { column: 'Name', field: 'name', width: 50 }, - { column: 'Type', field: 'runtimeType', width: 25 } - ]; - - printTable(tableSpec, result); - } - } - else { - printError(`Failed to list runtimes: ${response.status} ${response.message}`, options); - } - }) - .catch((err) => { - printError(`Failed to list runtimes: ${err.status} ${err.message}`, options); - }); - } -}; - -module.exports.ListRuntimeTypesCommand = class ListRuntimeTypesCommand { - - constructor(program) { - this.program = program; - } - - execute(options) { - const profile = loadProfile(options.profile); - debug('%s.executeListRuntimeTypes()', profile.name); - - const processors = new Processors(profile.url); - processors.listRuntimeTypes(profile.token).then((response) => { - if (response.success) { - let result = response.runtimeTypes; - if (options.query) - result = filterObject(result, options); - - if (options.json) { - printSuccess(JSON.stringify(result, null, 2), options); - } - else { - const tableSpec = [ - { column: 'Title', field: 'title', width: 50 }, - { column: 'Name', field: 'name', width: 25 }, - { column: 'Description', field: 'description', width: 75 } - ]; - - printTable(tableSpec, result); - } - } - else { - printError(`Failed to list runtime types: ${response.status} ${response.message}`, options); - } - }) - .catch((err) => { - printError(`Failed to list runtime types: ${err.status} ${err.message}`, options); - }); - } -}; - -module.exports.ListActionsCommand = class ListActionsCommand { - - constructor(program) { - this.program = program; - } - - execute(runtimeName, options) { - const profile = loadProfile(options.profile); - debug('%s.executeListRuntimeActionss()', profile.name); - - const processors = new Processors(profile.url); - processors.listRuntimeActions(profile.token, runtimeName).then((response) => { - if (response.success) { - let result = response.actions; - if (options.query) - result = filterObject(result, options); - - if (options.json) { - printSuccess(JSON.stringify(result, null, 2), options); - } - else { - const tableSpec = [ - { column: 'Name', field: 'name', width: 25 }, - { column: 'Version', field: 'version', width: 12 }, - { column: 'Kind', field: 'kind', width: 25 }, - { column: 'Memory', field: 'memory', width: 15 }, - { column: 'Timeout', field: 'timeout', width: 15 } - ]; - - const actions = result.map((action) => { - const a = {name: action.name, version: action.version, timeout: action.limits.timeout, memory: action.limits.memory, kind: 'blackbox'}; - - if (action.annotations && action.annotations.length > 0) { - a.kind = action.annotations[0].value; - } - - return a; - }); - - printTable(tableSpec, actions); - } - } - else { - printError(`Failed to list actions for runtime ${runtimeName}: ${response.status} ${response.message}`, options); - } - }) - .catch((err) => { - printError(`Failed to list actions for runtime ${runtimeName}: ${JSON.stringify(err.response.body)} `, options); - }); - } -}; - -module.exports.DescribeRuntimeCommand = class DescribeRuntimeCommand { - - constructor(program) { - this.program = program; - } - - execute(runtimeName, options) { - const profile = loadProfile(options.profile); - debug('%s.executeDescribeRuntime(%s)', profile.name, runtimeName); - - const processors = new Processors(profile.url); - processors.describeRuntime(profile.token, runtimeName).then((response) => { - if (response.success) { - let result = filterObject(response.runtime, options); - printSuccess(JSON.stringify(result, null, 2), options); - } - else { - printError(`Failed to describe processor runtime ${runtimeName}: ${response.message}`, options); - } - }) - .catch((err) => { - printError(`Failed to describe processor runtime ${runtimeName}: ${err.status} ${err.message}`, options); - }); - } -}; - -module.exports.DeleteRuntimeCommand = class DeleteRuntimeCommand { - - constructor(program) { - this.program = program; - } - - execute(runtimeName, options) { - const profile = loadProfile(options.profile); - debug('%s.executeDeleteRuntime(%s)', profile.name, runtimeName); - - const processors = new Processors(profile.url); - processors.deleteRuntime(profile.token, runtimeName).then((response) => { - if (response.success) { - printSuccess(`Processor runtime ${runtimeName} deleted`); - } - else { - printError(`Failed to delete processor runtime ${runtimeName}: ${response.message}`, options); - } - }) - .catch((err) => { - printError(`Failed to delete processor runtime ${runtimeName}: ${err.status} ${err.message}`, options); - }); - } -}; - -module.exports.SaveRuntimeCommand = class SaveRuntimeCommand { - - constructor(program) { - this.program = program; - } - - execute(runtimeDefinition, options) { - const profile = loadProfile(options.profile); - debug('%s.executeSaveRuntime(%s)', profile.name, runtimeDefinition); - - const runtimeStr = fs.readFileSync(runtimeDefinition); - const runtime = parseObject(runtimeStr, options); - - const processors = new Processors(profile.url); - processors.saveRuntime(profile.token, runtime).then((response) => { - if (response.success) { - printSuccess(`Processor runtime configuration saved at version ${response.version}`, options); - } - else { - printError(`Failed to save processor runtime: ${response.status} ${response.message}`, options); - } - }) - .catch((err) => { - printError(`Failed to save processor runtime: ${err.status} ${err.message}`, options); - }); - } -}; - -module.exports.InvokeActionCommand = class InvokeActionCommand { - - constructor(program) { - this.program = program; - } - - execute(runtimeName, actionId, options) { - const profile = loadProfile(options.profile); - debug('%s.executeInvokeAction(%s, %s)', profile.name, runtimeName, actionId); - - let params = {}; - if (options.params) { - params = parseObject(options.params, options); - } - else if (options.paramsFile) { - const paramsStr = fs.readFileSync(options.paramsFile); - params = parseObject(paramsStr, options); - } - - if (!params.token) params.token = profile.token; - if (!params.apiEndpoint) params.apiEndpoint = profile.url; - if (!params.instanceId) params.instanceId = uuid(); - if (!params.sessionId) params.sessionId = uuid(); - if (!params.channelId) params.channelId = uuid(); - - debug('params: %o', params); - - const processors = new Processors(profile.url); - processors.invokeRuntimeAction(profile.token, runtimeName, actionId, params).then((response) => { - if (response.success) { - let result = filterObject(response.result, options); - printSuccess(JSON.stringify(result, null, 2), options); - } - else { - printError(`Action invocation failed: ${response.status} ${response.message}`, options); - } - }) - .catch((err) => { - printError(`Failed to invoke action: ${err.response.body.message}`, options); - }); - } -}; \ No newline at end of file diff --git a/src/commands/project.js b/src/commands/project.js deleted file mode 100644 index f893d7b3..00000000 --- a/src/commands/project.js +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright 2018 Cognitive Scale, Inc. All Rights Reserved. - * - * 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. - */ - -const debug = require('debug')('cortex:cli'); -const yeoman = require('yeoman-environment'); -const { printSuccess, printError, filterObject, parseObject } = require('./utils'); - -module.exports.GenerateProjectCommand = class GenerateProjectCommand { - - constructor(program) { - this.program = program; - } - - execute(options) { - debug('generateProject()'); - const yenv = yeoman.createEnv(); - yenv.register(require.resolve('@c12e/generator-cortex'), 'cortex:app'); - // TODO: figure out which parameters to bubble up to the CLI, - // and plumb the values through to the yeoman generator. - yenv.run('cortex:app', { }); - } -}; diff --git a/src/commands/projects.js b/src/commands/projects.js new file mode 100644 index 00000000..697d1f78 --- /dev/null +++ b/src/commands/projects.js @@ -0,0 +1,112 @@ +import _ from 'lodash'; +import fs from 'node:fs'; +import debugSetup from 'debug'; +import { loadProfile } from '../config.js'; +import ApiServerClient from '../client/apiServerClient.js'; +import { + printSuccess, printError, parseObject, handleTable, printExtendedLogs, handleListFailure, getFilteredOutput, +} from './utils.js'; + +const debug = debugSetup('cortex:cli'); +export class CreateProjectCommand { + constructor(program) { + this.program = program; + } + + async execute(projectDefinition, command) { + const options = command; + const profile = await loadProfile(options.profile); + debug('%s.executeSaveProject(%s)', profile.name, projectDefinition); + let project = {}; + try { + if (projectDefinition) { + const projectDefStr = fs.readFileSync(projectDefinition); + project = parseObject(projectDefStr, options); + } + if (_.isEmpty(options.name || project.name)) { + printError('`name` must be provided', options); + } + const cli = new ApiServerClient(profile.url); + const response = await cli.createProject(profile.token, { + name: options.name || project.name, + title: options.title || project.title, + description: options.description || project.description, + }); + printSuccess(`Project ${response.name} saved`, options); + } catch (err) { + printError(`Failed to save project: ${err.message}`, options); + } + } +} +export class ListProjectsCommand { + constructor(program) { + this.program = program; + } + + // eslint-disable-next-line consistent-return + async execute(options) { + const profile = await loadProfile(options.profile); + debug('%s.executeListProjects()', profile.name); + const cli = new ApiServerClient(profile.url); + try { + const sortParam = (options.sort || '{}').replace(/"/g, '\''); + const filterParam = (options.filter || '{}').replace(/"/g, '\''); + const response = await cli.listProjects(profile.token, filterParam, options.limit, options.skip, sortParam); + const result = response; + // TODO remove --query on deprecation + if (options.json || options.query) { + getFilteredOutput(result, options); + } else { + printExtendedLogs(result, options); + const tableSpec = [ + { column: 'Name', field: 'name', width: 30 }, + { column: 'Title', field: 'title', width: 50 }, + { column: 'Description', field: 'description', width: 80 }, + ]; + handleTable(tableSpec, result, null, 'No projects found'); + } + } catch (err) { + handleListFailure(_.get(err, 'response.errors[0]', err), options, 'Projects'); + } + } +} +export class DescribeProjectCommand { + constructor(program) { + this.program = program; + } + + async execute(projectName, cmd) { + const options = cmd; + const profile = await loadProfile(options.profile); + debug('%s.executeDescribeProject(%s)', profile.name, projectName); + const cli = new ApiServerClient(profile.url); + try { + const response = await cli.getProject(profile.token, projectName); + getFilteredOutput(response, options); + } catch (err) { + printError(`Failed to describe project: ${err.status} ${err.message}`, options); + } + } +} +export class DeleteProjectCommand { + constructor(program) { + this.program = program; + } + + async execute(projectName, command) { + const options = command; + const profile = await loadProfile(options.profile); + debug('%s.executeDeleteProject(%s)', profile.name, projectName); + try { + const cli = new ApiServerClient(profile.url); + const status = await cli.deleteProject(profile.token, projectName); + if (status) { + printSuccess(`Project ${projectName} deleted`, options); + } else { + printError(`Project ${projectName} does not exist.`, options); + } + } catch (err) { + printError(`Failed to delete project: ${err.message}`, options); + } + } +} diff --git a/src/commands/roles.js b/src/commands/roles.js new file mode 100644 index 00000000..f9df63ac --- /dev/null +++ b/src/commands/roles.js @@ -0,0 +1,284 @@ +import debugSetup from 'debug'; +import { loadProfile } from '../config.js'; +import Roles from '../client/roles.js'; +import { + printSuccess, printError, filterObject, EXTERNALROLESFORMAT, handleTable, getFilteredOutput, handleError, +} from './utils.js'; +/* + * Copyright 2023 Cognitive Scale, Inc. All Rights Reserved. + * + * 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. + */ +const debug = debugSetup('cortex:cli'); +function createGrant(options) { + return { + project: options.project, + resource: options.resource, + actions: options.actions, + effect: options.deny ? 'deny' : 'allow', + }; +} +export const RoleDeleteCommand = class { + constructor(program) { + this.program = program; + } + + async execute(role, options) { + const profile = await loadProfile(options.profile); + debug('%s.deleteRole(%s)', profile.name, role); + const client = new Roles(profile.url, role); + try { + const response = await client.deleteRole(profile.token); + const result = filterObject(response.result, options); + printSuccess(JSON.stringify(result, null, 2), options); + } catch (err) { + handleError(err, options, 'Failed to delete role'); + } + } +}; +export const RoleCreateCommand = class { + constructor(program) { + this.program = program; + } + + async execute(role, options) { + const profile = await loadProfile(options.profile); + const form = createGrant(options); + form.role = role; + debug('%s.createRole(%s)', profile.name); + const client = new Roles(profile.url); + try { + const response = await client.createRole(profile.token, form); + const result = filterObject(response.result, options); + printSuccess(JSON.stringify(result, null, 2), options); + } catch (err) { + handleError(err, options, 'Failed to create role'); + } + } +}; +export const RoleAssignCommand = class { + constructor(program) { + this.program = program; + } + + async execute(role, options) { + const profile = await loadProfile(options.profile); + debug('%s.grantRole(%s)', profile.name, role); + const client = new Roles(profile.url, role); + const call = (deleteFlag, token, users) => { + if (deleteFlag) { + return client.removeUsersFromRole(token, users); + } + return client.addUsersToRole(token, users); + }; + try { + const response = await call(options.delete, profile.token, options.users); + const result = filterObject(response.result, options); + printSuccess(JSON.stringify(result, null, 2), options); + } catch (err) { + const func = (options.delete) ? 'unassign' : 'assign'; + printError(`Failed to ${func} user from role : ${err.status} ${err.message}`, options); + } + } +}; +export const RoleGrantCommand = class { + constructor(program) { + this.program = program; + } + + async execute(role, options) { + const profile = await loadProfile(options.profile); + const form = createGrant(options); + debug('%s.grantRole(%s)', profile.name, role); + const client = new Roles(profile.url, role); + const call = (deleteFlag, token, body) => { + if (deleteFlag) { + return client.removeGrantFromRole(token, body); + } + return client.createGrantForRole(token, body); + }; + try { + const response = await call(options.delete, profile.token, form); + const result = filterObject(response.result, options); + printSuccess(JSON.stringify(result, null, 2), options); + } catch (err) { + const func = (options.delete) ? 'delete' : 'create'; + handleError(err, options, `Failed to ${func} grant for role`); + } + } +}; +export const RoleDescribeCommand = class { + constructor(program) { + this.program = program; + } + + async execute(role, options) { + const profile = await loadProfile(options.profile); + debug('%s.describeForRole(%s)', profile.name, role); + const flags = []; + if (options.grants) { + flags.push('grants'); + } + if (options.users) { + flags.push('users'); + } + const client = new Roles(profile.url, role, flags); + try { + const response = await client.describeRole(profile.token); + const result = filterObject(response.result, options); + printSuccess(JSON.stringify(result, null, 2), options); + } catch (err) { + handleError(err, options, 'Failed to describe role'); + } + } +}; +export const RoleListCommand = class { + constructor(program) { + this.program = program; + } + + async execute(options) { + const profile = await loadProfile(options.profile); + debug('%s.listRole(%s)', profile.name); + const flags = []; + const client = new Roles(profile.url, flags); + try { + const response = await client.listRoles(profile.token, options.project); + const { result } = response; + // TODO remove --query on deprecation + if (options.json || options.query) { + getFilteredOutput(result, options); + } else { + handleTable([{ column: 'Role', field: 'role' }], result.roles.map((x) => ({ role: x })), null, 'No roles found'); + } + } catch (err) { + handleError(err, options, 'Failed to list roles'); + } + } +}; +export const RoleProjectAssignCommand = class { + constructor(program) { + this.program = program; + } + + async execute(project, options) { + const profile = await loadProfile(options.profile); + debug('%s.assignRoleProject(%s)', profile.name, project); + const client = new Roles(profile.url, null); + const call = (deleteFlag, token, assignProject, roles) => { + if (deleteFlag) { + return client.removeRolesFromProject(token, assignProject, roles); + } + return client.addRolesToProject(token, assignProject, roles); + }; + try { + const response = await call(options.delete, profile.token, project, options.roles); + const result = filterObject(response.result, options); + printSuccess(JSON.stringify(result, null, 2), options); + } catch (err) { + const func = (options.delete) ? 'unassign' : 'assign'; + handleError(err, options, `Failed to ${func} roles from project`); + } + } +}; +export const ExternalGroupListCommand = class { + constructor(program) { + this.program = program; + } + + async execute(options) { + const profile = await loadProfile(options.profile); + debug('%s.listExternalGroup(%s)', profile.name); + const client = new Roles(profile.url, null); + try { + const response = await client.listExternalGroups(profile.token); + const { result } = response; + // TODO remove --query on deprecation + if (options.json || options.query) { + getFilteredOutput(result, options); + } else { + handleTable(EXTERNALROLESFORMAT, result, (o) => ({ ...o, roles: o.roles.join(', ') }), null, 'No external roles found'); + } + } catch (err) { + handleError(err, options, 'Failed to list external groups'); + } + } +}; +export const ExternalGroupDescribeCommand = class { + constructor(program) { + this.program = program; + } + + async execute(externalGroup, options) { + const profile = await loadProfile(options.profile); + debug('%s.describeExternalGroup(%s)', profile.name, externalGroup); + const flags = []; + if (options.roles) { + flags.push('roles'); + } + if (options.users) { + flags.push('users'); + } + const client = new Roles(profile.url, null, flags); + try { + const response = await client.describeExternalGroup(profile.token, externalGroup); + const result = filterObject(response.result, options); + printSuccess(JSON.stringify(result, null, 2), options); + } catch (err) { + handleError(err, options, 'Failed to describe external group'); + } + } +}; +export const ExternalGroupDeleteCommand = class { + constructor(program) { + this.program = program; + } + + async execute(externalGroup, options) { + const profile = await loadProfile(options.profile); + debug('%s.deleteExternalGroup(%s)', profile.name, externalGroup); + const client = new Roles(profile.url, null); + try { + const response = await client.deleteExternalGroup(profile.token, externalGroup); + const result = filterObject(response.result, options); + printSuccess(JSON.stringify(result, null, 2), options); + } catch (err) { + handleError(err, options, 'Failed to delete external group'); + } + } +}; +export const ExternalGroupAssignCommand = class { + constructor(program) { + this.program = program; + } + + async execute(options) { + const profile = await loadProfile(options.profile); + debug('%s.externalGroupAssign(%s)', profile.name, options.role); + const client = new Roles(profile.url, options.role); + const call = (deleteFlag, token, externalGroup) => { + if (deleteFlag) { + return client.removeExternalGroupFromRole(token, externalGroup); + } + return client.addExternalGroupToRole(token, externalGroup); + }; + try { + const response = await call(options.delete, profile.token, options.externalGroup); + const result = filterObject(response.result, options); + printSuccess(JSON.stringify(result, null, 2), options); + } catch (err) { + const func = (options.delete) ? 'unassign' : 'assign'; + handleError(err, options, `Failed to ${func} external group from role`); + } + } +}; diff --git a/src/commands/secrets.js b/src/commands/secrets.js new file mode 100644 index 00000000..304c9c0e --- /dev/null +++ b/src/commands/secrets.js @@ -0,0 +1,119 @@ +import fs from 'node:fs'; +import debugSetup from 'debug'; +import getOr from 'lodash/fp/getOr.js'; +import isUndefined from 'lodash/fp/isUndefined.js'; +import map from 'lodash/fp/map.js'; +import { loadProfile } from '../config.js'; +import Secrets from '../client/secrets.js'; +import { + printSuccess, printError, filterObject, parseObject, printTable, handleTable, getFilteredOutput, handleError, +} from './utils.js'; + +const debug = debugSetup('cortex:cli'); +export const ListSecretsCommand = class { + constructor(program) { + this.program = program; + } + + async execute(options) { + const profile = await loadProfile(options.profile); + debug('%s.listsecrets(%s)', profile.name); + const secrets = new Secrets(profile.url); + try { + const result = await secrets.listSecrets(options.project || profile.project, profile.token); + // TODO remove --query on deprecation + if (options.json || options.query) { + getFilteredOutput(result, options); + } else { + const tableSpec = [ + { + column: 'Secret Key Name', + field: 'keyName', + width: 50, + }, + ]; + handleTable(tableSpec, map((x) => ({ keyName: x }), result), null, 'No secrets found'); + } + } catch (err) { + handleError(err, options, 'Failed to list secrets'); + } + } +}; +export const ReadSecretsCommand = class { + constructor(program) { + this.program = program; + } + + async execute(keyName, options) { + const profile = await loadProfile(options.profile); + debug('%s.readsecret(%s)', profile.name, keyName); + const secrets = new Secrets(profile.url); + try { + const response = await secrets.readSecret(options.project || profile.project, profile.token, keyName); + const result = filterObject(response, options); + if (options.json) printSuccess(JSON.stringify(result, null, 2), options); + else { + const tableSpec = [ + { column: 'Key Name', field: 'keyName', width: 50 }, + { column: 'Value (Not Shown)', field: 'value', width: 50 }, + ]; + printTable(tableSpec, [{ keyName, value: JSON.stringify(getOr(undefined, 'value', result), null, 2) }]); + } + } catch (err) { + handleError(err, options, 'Failed to read secure secret'); + } + } +}; +export const WriteSecretsCommand = class { + constructor(program) { + this.program = program; + } + + async execute(keyName, value, options) { + const profile = await loadProfile(options.profile); + debug('%s.writeSecret(%s)', profile.name, keyName); + let data = value; + if (options.data) { + try { + data = parseObject(options.data, options); + } catch (e) { + printError(`Failed to parse data: ${options.data} Error: ${e}`, options); + } + } else if (options.dataFile) { + const dataStr = fs.readFileSync(options.dataFile); + data = parseObject(dataStr, options); + } + if (isUndefined(data)) { + return printError('Failed to write secret : no value specified', options); + } + // FAB-1775: validate secret key name + const invalidCharsRegex = /^\w+([./-]?\w+)*$/; + if (!invalidCharsRegex.test(keyName)) { + return printError(`Failed to write secret : keyName did not conform to regex ${JSON.stringify(invalidCharsRegex)}`, options); + } + const secrets = new Secrets(profile.url); + try { + const response = secrets.writeSecret(options.project || profile.project, profile.token, keyName, data); + return printSuccess(response.message, options); + } catch (err) { + return handleError(err, options, 'Failed to write secret'); + } + } +}; +export const DeleteSecretCommand = class { + constructor(program) { + this.program = program; + } + + async execute(keyName, options) { + const profile = await loadProfile(options.profile); + debug('%s.deleteSecret(%s)', profile.name, keyName); + const secretsClient = new Secrets(profile.url); + try { + const response = await secretsClient.deleteSecret(options.project || profile.project, profile.token, keyName); + printSuccess(response?.message ?? response, options); + } catch (err) { + handleError(err, options, `Secret "${keyName}" deletion failed`); + } + } +}; diff --git a/src/commands/sessions.js b/src/commands/sessions.js new file mode 100644 index 00000000..990e7a1f --- /dev/null +++ b/src/commands/sessions.js @@ -0,0 +1,91 @@ +import fs from 'node:fs'; +import _ from 'lodash'; +import debugSetup from 'debug'; +import { loadProfile } from '../config.js'; +import Sessions from '../client/sessions.js'; +import { + SESSIONTABLEFORMAT, getFilteredOutput, + + printSuccess, printError, filterObject, parseObject, handleTable, handleError, +} from './utils.js'; + +const debug = debugSetup('cortex:cli'); +export class SaveSessionCommand { + constructor(program) { + this.program = program; + } + + async execute(sessionDefinition, options) { + const profile = await loadProfile(options.profile); + debug('%s.executeSaveSession(%s)', profile.name, sessionDefinition); + if (!fs.existsSync(sessionDefinition)) { + printError(`File does not exist at: ${sessionDefinition}`); + } + const sessionDefStr = fs.readFileSync(sessionDefinition); + const session = parseObject(sessionDefStr, options); + debug('%o', session); + try { + const sessions = new Sessions(profile.url); + const response = await sessions.saveSession(options.project || profile.project, profile.token, session); + printSuccess(_.get(response, 'message.message', 'Session saved'), options); + } catch (err) { + handleError(err, options, 'Failed to save session'); + } + } +} +export class ListSessionsCommand { + constructor(program) { + this.program = program; + } + + async execute(options) { + const profile = await loadProfile(options.profile); + debug('%s.executeListSessions()', profile.name); + const sessions = new Sessions(profile.url); + try { + const { sessions: result } = await sessions.listSessions(options.project || profile.project, profile.token, options.limit); + // TODO remove --query on deprecation + if (options.json || options.query) { + return getFilteredOutput(result, options); + } + return handleTable(SESSIONTABLEFORMAT, result, null, 'No sessions found'); + } catch (err) { + return handleError(err, options, 'Failed to list sessions'); + } + } +} +export class DescribeSessionCommand { + constructor(program) { + this.program = program; + } + + async execute(sessionName, options) { + const profile = await loadProfile(options.profile); + const sessions = new Sessions(profile.url); + debug('%s.executeDescribeSession(%s)', profile.name, sessionName); + try { + const response = await sessions.describeSession(options.project || profile.project, profile.token, sessionName, options.verbose); + getFilteredOutput(response.session, options); + } catch (err) { + handleError(err, options, `Failed to describe session ${sessionName}`); + } + } +} +export class DeleteSessionCommand { + constructor(program) { + this.program = program; + } + + async execute(sessionName, options) { + const profile = await loadProfile(options.profile); + debug('%s.executeDeleteSession(%s)', profile.name, sessionName); + const sessions = new Sessions(profile.url); + try { + const response = await sessions.deleteSession(options.project || profile.project, profile.token, sessionName); + const result = filterObject(response, options); + printSuccess(JSON.stringify(result, null, 2), options); + } catch (err) { + handleError(err, options, 'Failed to delete session'); + } + } +} diff --git a/src/commands/skills.js b/src/commands/skills.js index e032bba1..5af76a26 100644 --- a/src/commands/skills.js +++ b/src/commands/skills.js @@ -1,133 +1,259 @@ -/* - * Copyright 2018 Cognitive Scale, Inc. All Rights Reserved. - * - * 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. - */ - -const fs = require('fs'); -const yeoman = require('yeoman-environment'); -const debug = require('debug')('cortex:cli'); -const { loadProfile } = require('../config'); -const Catalog = require('../client/catalog'); -const { printSuccess, printError, filterObject, parseObject, printTable } = require('./utils'); - -module.exports.SaveSkillCommand = class SaveSkillCommand { +import _ from 'lodash'; +import fs from 'node:fs'; +import debugSetup from 'debug'; +import dayjs from 'dayjs'; +import relativeTime from 'dayjs/plugin/relativeTime.js'; +import { loadProfile } from '../config.js'; +import Catalog from '../client/catalog.js'; +import Agent from '../client/agents.js'; +import { + printSuccess, + printError, + printWarning, + filterObject, + parseObject, + LISTTABLEFORMAT, + isNumeric, + handleTable, + printExtendedLogs, + getFilteredOutput, + writeOutput, printErrorDetails, handleError, +} from './utils.js'; +const debug = debugSetup('cortex:cli'); +dayjs.extend(relativeTime); +export class SaveSkillCommand { constructor(program) { this.program = program; } - execute(skillDefinition, options) { - const profile = loadProfile(options.profile); - debug('%s.executeSaveSkill(%s)', profile.name, skillDefinition); - - const skillDefStr = fs.readFileSync(skillDefinition); - const skill = parseObject(skillDefStr, options); + async execute(skillDefinitions, options) { + try { + const profile = await loadProfile(options.profile); + await Promise.all(skillDefinitions.map(async (skillDefinition) => { + const skillDefStr = fs.readFileSync(skillDefinition); + const skill = parseObject(skillDefStr, options); + debug('%s.executeSaveSkill(%s)', profile.name, skillDefinition); + const catalog = new Catalog(profile.url); + if (!_.isEmpty(options.k8sResource)) { + const k8sResources = options.k8sResource.map((f) => JSON.stringify(parseObject(fs.readFileSync(f), options))); + if (_.isEmpty(_.get(skill, 'actions', []))) { + printError('Skill must contain an action to apply kubernetes resources', options, true); + } + if (skill.actions.length > 1) { + printWarning('Applying kubernetes resources to all actions'); + } + skill.actions.map((a) => a.k8sResources = k8sResources); + } + if (options.scaleCount) { + if (!isNumeric(options.scaleCount)) { + printError('--scaleCount must be a number', options); + } + if (skill.actions.length > 1) { + printWarning('Applying kubernetes resources to all actions'); + } + const scaleCount = parseInt(options.scaleCount, 10); + skill.actions.map((a) => a.scaleCount = scaleCount); + } + if (options.podspec) { + const paramsStr = fs.readFileSync(options.podspec); + const podSpec = parseObject(paramsStr, options); + if (_.isEmpty(_.get(skill, 'actions', []))) { + printError('Skill must contain an action', options, true); + } + skill.actions.map((a) => a.podSpec = podSpec); + } + const response = await catalog.saveSkill(options.project || profile.project, profile.token, skill); + printSuccess(`Skill saved: ${JSON.stringify(response.message)}`, options); + })); + } catch (err) { + handleError(err, options, 'Failed to save skill'); + } + } +} +export class ListSkillsCommand { + constructor(program) { + this.program = program; + } + async execute(options) { + const profile = await loadProfile(options.profile); + debug('%s.executeListSkills()', profile.name); const catalog = new Catalog(profile.url); - catalog.saveSkill(profile.token, skill).then((response) => { - if (response.success) { - printSuccess(`Skill saved`, options); + try { + const status = !_.get(options, 'nostatus', false); // default show status, if nostatus==true status == false + const shared = !_.get(options, 'noshared', false); + const { skills } = await catalog.listSkills(options.project || profile.project, profile.token, { status, shared }, options.filter, options.limit, options.skip, options.sort); + const tableFormat = LISTTABLEFORMAT; + let result = skills; + if (options.nostatus === undefined) { + result = skills.map((skillStat) => { + const statuses = _.isEmpty(skillStat.actionStatuses) ? skillStat.deployStatus : skillStat.actionStatuses.map((s) => `${s.name}: ${s.state}`).join(' '); + return { + ...skillStat, + status: statuses, + }; + }); + tableFormat.push({ column: 'Status', field: 'status', width: 30 }); } - else { - printError(`Failed to save skill: ${response.status} ${response.message}`, options); + // TODO remove --query on deprecation + if (options.json || options.query) { + return getFilteredOutput(result, options); } - }) - .catch((err) => { - printError(`Failed to save skill: ${err.status} ${err.response.body.error}`, options); - }); + printExtendedLogs(result, options); + return handleTable(tableFormat, _.sortBy(result, options.sort ? [] : ['name']), (o) => ({ ...o, updatedAt: o.updatedAt ? dayjs(o.updatedAt).fromNow() : '-' }), 'No skills found'); + } catch (err) { + printError(`Failed to list skills: (${err.message}): ${err.response?.body ?? ''}`, options, false); + return printErrorDetails(err.response, options, true); + } } -}; - -module.exports.ListSkillsCommand = class ListSkillsCommand { - +} +export class DescribeSkillCommand { constructor(program) { this.program = program; } - execute(options) { - const profile = loadProfile(options.profile); - debug('%s.executeListSkills()', profile.name); + async execute(skillName, options) { + const profile = await loadProfile(options.profile); + const { + verbose, output, project, + } = options; + debug('%s.executeDescribeSkill(%s)', profile.name, skillName); + const catalog = new Catalog(profile.url); + try { + const response = await catalog.describeSkill(project || profile.project, profile.token, skillName, verbose, output); + if ((output ?? 'json').toLowerCase() === 'json') return getFilteredOutput(response, options); + return writeOutput(response, options); + } catch (err) { + return printError(`Failed to describe skill ${skillName}: ${err.status} ${err.message}`, options); + } + } +} +export class UndeploySkillCommand { + constructor(program) { + this.program = program; + } + async execute(skillNames, options) { + const profile = await loadProfile(options.profile); const catalog = new Catalog(profile.url); - catalog.listSkills(profile.token).then((response) => { - if (response.success) { - let result = response.skills; - if (options.query) - result = filterObject(result, options); + await Promise.all(skillNames.map(async (skillName) => { + debug('%s.executeUndeploySkill(%s)', profile.name, skillName); + try { + const response = await catalog.unDeploySkill(options.project || profile.project, profile.token, skillName, options.verbose); + printSuccess(`Undeploy Skill ${skillName}: ${response.message}`, options); + } catch (err) { + handleError(err, options, `Failed to undeploy Skill ${skillName}`); + } + })); + } +} +export class SkillLogsCommand { + constructor(program) { + this.program = program; + } - if (options.json) { - printSuccess(JSON.stringify(result, null, 2), options); + async execute(skillName, actionName, options) { + try { + const profile = await loadProfile(options.profile); + debug('%s.executeSkillLogs(%s,%s)', profile.name, skillName, actionName); + + const catalog = new Catalog(profile.url); + const response = await catalog.skillLogs(options.project || profile.project, profile.token, skillName, actionName, options.raw); + if (options.raw) { + try { + const respJSON = JSON.parse(response); // happens with errors + if (respJSON?.body) { + printError(`Failed to fetch Skill/Action Logs ${skillName}/${actionName}: ${respJSON.body.message}`, options); + } + } catch (err) { + printSuccess(response, options); } - else { - const tableSpec = [ - { column: 'Title', field: 'title', width: 50 }, - { column: 'Name', field: 'name', width: 50 }, - { column: 'Version', field: '_version', width: 12 } - ]; + printSuccess(JSON.stringify(response.logs), options); + } + } catch (err) { + handleError(err, options, `Failed to fetch Skill/Action Logs ${skillName}/${actionName}`); + } + } +} +export class DeploySkillCommand { + constructor(program) { + this.program = program; + } - printTable(tableSpec, result); + async execute(skillNames, options) { + const profile = await loadProfile(options.profile); + const catalog = new Catalog(profile.url); + await Promise.all(skillNames.map(async (skillName) => { + debug('%s.executeDeploySkill(%s)', profile.name, skillName); + try { + const response = await catalog.deploySkill(options.project || profile.project, profile.token, skillName, options.stereotypes, options.dryrun, options.verbose); + if (options.dryrun) { + printSuccess(JSON.stringify(response, null, 2), options); + } else { + printSuccess(`Deployed Skill ${skillName}: ${response.message}`, options); } + } catch (err) { + handleError(err, options, `Failed to deploy Skill ${skillName}`); } - else { - printError(`Failed to list skills: ${response.status} ${response.message}`, options); - } - }) - .catch((err) => { - printError(`Failed to list skills: ${err.status} ${err.message}`, options); - }); + })); } -}; - -module.exports.DescribeSkillCommand = class DescribeSkillCommand { - +} +export class InvokeSkillCommand { constructor(program) { this.program = program; } - execute(skillName, options) { - const profile = loadProfile(options.profile); - debug('%s.executeDescribeSkill(%s)', profile.name, skillName); - - const catalog = new Catalog(profile.url); - catalog.describeSkill(profile.token, skillName).then((response) => { - if (response.success) { - let result = filterObject(response.skill, options); - printSuccess(JSON.stringify(result, null, 2), options); + async execute(skillName, inputName, options) { + const profile = await loadProfile(options.profile); + debug('%s.executeInvokeSkill(%s/%s)', profile.name, skillName, inputName); + let params = {}; + if (options.params) { + try { + params = parseObject(options.params, options); + } catch (e) { + printError(`Failed to parse params: ${options.params} Error: ${e}`, options); + } + } else if (options.paramsFile) { + if (!fs.existsSync(options.paramsFile)) { + printError(`File does not exist at: ${options.paramsFile}`); } - else { - printError(`Failed to describe skill ${skillName}: ${response.message}`, options); + const paramsStr = fs.readFileSync(options.paramsFile); + params = parseObject(paramsStr, options); + } + const agent = new Agent(profile.url); + try { + const response = await agent.invokeSkill(options.project || profile.project, profile.token, skillName, inputName, params, options.sync); + const result = filterObject(response, options); + printSuccess(JSON.stringify(result, null, 2), options); + } catch (err) { + let message = `Failed to invoke skill (${err.message})`; + if (err?.response?.body) { + message = `${message}: ${err.response.body}`; } - }) - .catch((err) => { - printError(`Failed to describe skill ${skillName}: ${err.status} ${err.message}`, options); - }); + printError(message, options, false); + printErrorDetails(err, options); + } } -}; - -module.exports.GenerateSkillCommand = class GenerateSkillCommand { - +} +export class DeleteSkillCommand { constructor(program) { this.program = program; } - execute(options) { - debug('%s.generateSkill()', options.profile); - const yenv = yeoman.createEnv(); - yenv.lookup(()=>{ - yenv.run('@c12e/cortex:skill', - { }, - (err) => { err ? printError(err) : printSuccess('Done.') }); - }); - } -}; + async execute(skillName, options) { + const profile = await loadProfile(options.profile); + debug('%s.executeDeleteSkill(%s)', profile.name, skillName); + const catalog = new Catalog(profile.url); + try { + const response = await catalog.deleteSkill(options.project || profile.project, profile.token, skillName); + const result = filterObject(response, options); + if (options.json) { + return printSuccess(JSON.stringify(result, null, 2), options); + } + return printSuccess(result.message); + } catch (err) { + return handleError(err, { ...options, tableformat: 'DEPENDENCYTABLEFORMAT' }, `Failed to delete skill ${skillName}`); + } + } +} diff --git a/src/commands/stereotype.js b/src/commands/stereotype.js new file mode 100644 index 00000000..c8cfbeb0 --- /dev/null +++ b/src/commands/stereotype.js @@ -0,0 +1,130 @@ +import _ from 'lodash'; +import fs from 'node:fs'; +import debugSetup from 'debug'; +import dayjs from 'dayjs'; +import relativeTime from 'dayjs/plugin/relativeTime.js'; +import { loadProfile } from '../config.js'; +import Catalog from '../client/catalog.js'; +import { + printSuccess, + printError, + printWarning, + filterObject, + parseObject, + LISTTABLEFORMAT, + handleTable, + printExtendedLogs, + getFilteredOutput, + writeOutput, + handleListFailure, + handleError, +} from './utils.js'; + +const debug = debugSetup('cortex:cli'); +dayjs.extend(relativeTime); +export class SaveStereotypeCommand { + constructor(program) { + this.program = program; + } + + async execute(documents, options) { + try { + const profile = await loadProfile(options.profile); + await Promise.all(documents.map(async (skillDefinition) => { + const skillDefStr = fs.readFileSync(skillDefinition); + const skill = parseObject(skillDefStr, options); + debug('%s.executeSaveStereotype(%s)', profile.name, skillDefinition); + const catalog = new Catalog(profile.url); + if (!_.isEmpty(options.k8sResource)) { + const k8sResources = options.k8sResource.map((f) => JSON.stringify(parseObject(fs.readFileSync(f), options))); + if (_.isEmpty(_.get(skill, 'action', []))) { + printError('Stereotype must contain an action to apply kubernetes resources', options, true); + } + if (skill.actions.length > 1) { + printWarning('Applying kubernetes resources to all actions'); + } + skill.actions.map((a) => a.k8sResources = k8sResources); + } + if (options.podspec) { + const paramsStr = fs.readFileSync(options.podspec); + const podSpec = parseObject(paramsStr, options); + if (_.isEmpty(_.get(skill, 'actions', []))) { + printError('Stereotype must contain an action', options, true); + } + skill.actions.map((a) => a.podSpec = podSpec); + } + const response = await catalog.saveStereotype(options.project || profile.project, profile.token, skill); + printSuccess(`Stereotype saved: ${JSON.stringify(response.message)}`, options); + })); + } catch (err) { + handleError(err, options, 'Failed to save skill stereotype'); + } + } +} +export class ListStereotypesCommand { + constructor(program) { + this.program = program; + } + + async execute(options) { + const profile = await loadProfile(options.profile); + debug('%s.executeListStereotypes()', profile.name); + const catalog = new Catalog(profile.url); + try { + const shared = !(options?.noshared ?? false); + const { stereotypes } = await catalog.listStereotypes(options.project || profile.project, profile.token, { shared }, options.filter, options.limit, options.skip, options.sort); + const tableFormat = LISTTABLEFORMAT; + // TODO remove --query on deprecation + if (options.json || options.query) { + return getFilteredOutput(stereotypes, options); + } + printExtendedLogs(stereotypes, options); + return handleTable(tableFormat, _.sortBy(stereotypes, options.sort ? [] : ['name']), (o) => ({ ...o, updatedAt: o.updatedAt ? dayjs(o.updatedAt).fromNow() : '-' }), 'No stereotypes found'); + } catch (err) { + return handleListFailure(err, {}, 'Skill stereotype'); + } + } +} +export class DescribeStereotypeCommand { + constructor(program) { + this.program = program; + } + + async execute(name, options) { + const profile = await loadProfile(options.profile); + const { + verbose, output, project, + } = options; + debug('%s.executeDescribeStereotype(%s)', profile.name, name); + const catalog = new Catalog(profile.url); + try { + const response = await catalog.describeStereotype(project || profile.project, profile.token, name, verbose, output); + if ((options?.output ?? 'json').toLowerCase() === 'json') return getFilteredOutput(response, options); + return writeOutput(response, options); + } catch (err) { + return handleError(err, options, `Failed to describe skill ${name}`); + } + } +} + +export class DeleteStereotypeCommand { + constructor(program) { + this.program = program; + } + + async execute(name, options) { + const profile = await loadProfile(options.profile); + debug('%s.executeDeleteStereotype(%s)', profile.name, name); + const catalog = new Catalog(profile.url); + try { + const response = await catalog.deleteStereotype(options.project || profile.project, profile.token, name); + const result = filterObject(response, options); + if (options.json) { + return printSuccess(JSON.stringify(result, null, 2), options); + } + return printSuccess(result.message); + } catch (err) { + return handleError(err, options, 'delete stereotype failed'); + } + } +} diff --git a/src/commands/tasks.js b/src/commands/tasks.js index 106c8c16..b1026455 100644 --- a/src/commands/tasks.js +++ b/src/commands/tasks.js @@ -1,5 +1,14 @@ +import debugSetup from 'debug'; +import _ from 'lodash'; +import dayjs from 'dayjs'; +import relativeTime from 'dayjs/plugin/relativeTime.js'; +import { loadProfile } from '../config.js'; +import Tasks from '../client/tasks.js'; +import { + printSuccess, printError, handleTable, printExtendedLogs, getFilteredOutput, handleListFailure, handleError, +} from './utils.js'; /* - * Copyright 2018 Cognitive Scale, Inc. All Rights Reserved. + * Copyright 2023 Cognitive Scale, Inc. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the “License”); * you may not use this file except in compliance with the License. @@ -13,161 +22,220 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - -const fs = require('fs'); -const debug = require('debug')('cortex:cli'); -const { loadProfile } = require('../config'); -const Tasks = require('../client/tasks'); -const { printSuccess, printError, filterObject, parseObject, printTable } = require('./utils'); - -module.exports.ListTasks = class ListTasks { - +const debug = debugSetup('cortex:cli'); +dayjs.extend(relativeTime); +const TASK_LIST_TABLE = [ + { column: 'Name', field: 'name', width: 60 }, + { column: 'Activation Id', field: 'activationId', width: 40 }, + { column: 'Skill Name', field: 'skillName', width: 40 }, + { column: 'Status', field: 'state', width: 20 }, + { column: 'Started', field: 'start', width: 25 }, + { column: 'Took', field: 'took', width: 25 }, +]; +const SCHED_LIST_TABLE = [ + { column: 'Name', field: 'name', width: 60 }, + { column: 'Agent Name', field: 'agentName', width: 40 }, + { column: 'Status', field: 'state', width: 20 }, + { column: 'Started', field: 'start', width: 25 }, + { column: 'Schedule', field: 'schedule', width: 12 }, + { column: 'Username', field: 'username', width: 40 }, +]; +export const ListTasksCommand = class { constructor(program) { this.program = program; } - execute(jobId, options) { - const profile = loadProfile(options.profile); - debug('%s.listTasks()', profile.name); - - const tasks = new Tasks(profile.url, jobId); - tasks.listTasks(profile.token).then((response) => { - if (response.success) { - if (options.query || options.json) { - let result = filterObject(response.result, options); - printSuccess(JSON.stringify(result, null, 2), options); + async execute(options) { + const profile = await loadProfile(options.profile); + const projectId = options.project || profile.project; + debug('%s.listTasks(%s)', profile.name); + const tasks = new Tasks(profile.url); + if (options.limit) { + const num = _.toNumber(options.limit); + if (Number.isNaN(num) || num < 0) { + printError('--limit <limit> must be a positive integer value'); + } + } + try { + const response = await tasks.listTasks(projectId, profile.token, options); + let format = 'k8sFormat'; // Assume old format + let taskList = _.get(response, 'tasks', []); + if (_.isEmpty(taskList)) { + return printSuccess('No tasks found'); } - else { - let tableSpec = [ - { column: 'Task ID', field: 'taskId', width: 40 }, - { column: 'Status', field: 'status', width: 40 }, - { column: 'Detail', field: 'statusReason', width: 40 }, - { column: 'Created', field: 'createdAt', width: 40 }, - { column: 'Started', field: 'startedAt', width: 40 }, - { column: 'Stopped', field: 'stoppedAt', width: 40 } - ]; - - printTable(tableSpec, response.result.tasks); + // Old response format ["taskName", "taskName",...] + if (_.isString(taskList[0])) { + taskList = _.map(taskList, (t) => ({ name: t })); + } else { + if (!_.has(taskList[0], 'status')) { // previous k8s format has [{ status},{},{}] + format = 'dbFormat'; + } + taskList = _.map(taskList, (t) => ({ + ...t, + state: t.status || t.state, + start: t.startTime ? dayjs(t.startTime).fromNow() : '-', + took: t.endTime ? dayjs(t.endTime).from(dayjs(t.startTime), true) : '-', + })); } - } - else { - printError(`Failed to list tasks: ${response.status} ${response.message}`, options); - } - }) - .catch((err) => { - debug(err); - printError(`Failed to list tasks: ${err.status} ${err.message}`, options); - }); + if (format === 'k8sFormat') { + switch (_.lowerCase(options.sort)) { + case 'asc': + taskList = _.sortBy(taskList, ['startTime']); + break; + case 'desc': + taskList = _.reverse(_.sortBy(taskList, ['startTime'])); + break; + default: + // return as-is order + break; + } + } + // TODO remove --query on deprecation + if (options.json || options.query) { + return getFilteredOutput(taskList, options); + } + printExtendedLogs(taskList, options); + const tableCols = options.scheduled ? SCHED_LIST_TABLE : TASK_LIST_TABLE; + return handleTable(tableCols, taskList); + } catch (err) { + return handleListFailure(err, {}, 'Tasks'); + } } }; - -module.exports.TaskLogs = class TaskLogs { - +export const DescribeTaskCommand = class { constructor(program) { this.program = program; } - execute(jobId, taskId, options) { - const profile = loadProfile(options.profile); - debug('%s.taskLogs()', profile.name); - - const tasks = new Tasks(profile.url, jobId); - tasks.taskLogs(profile.token, taskId).then((response) => { - if (response.success) { - if (options.query || options.json) { - let result = filterObject(response.result, options); - printSuccess(JSON.stringify(result, null, 2), options); - } - else { - let tableSpec = [ - { column: 'Status', field: 'status', width: 40 }, - { column: 'Message', field: 'message', width: 40 } - ]; - printTable(tableSpec, [response.result]); - } - } - else { - printError(`Failed to fetch logs: ${response.status} ${response.message}`, options); - } - }) - .catch((err) => { - debug(err); - printError(`Failed to fetch logs: ${err.status} ${err.message}`, options); - }); + async execute(taskName, options) { + const profile = await loadProfile(options.profile); + const queryParams = {}; + debug('%s.describeTask(%s)', profile.name, taskName); + if (options.k8s) queryParams.k8s = true; + const tasks = new Tasks(profile.url); + try { + const response = await tasks.getTask(options.project || profile.project, taskName, profile.token, queryParams); + return printSuccess(JSON.stringify(response, null, 2), options); + } catch (err) { + return handleError(err, options, `Failed to describe task ${taskName}`); + } } }; - -module.exports.CancelTask = class CancelTask { - +export class TaskLogsCommand { constructor(program) { this.program = program; } - execute(jobId, taskId, options) { - const profile = loadProfile(options.profile); - debug('%s.cancelTask()', profile.name); - - const message = options.message; - const tasks = new Tasks(profile.url, jobId); - tasks.cancelTask(profile.token, taskId, message).then((response) => { - if (response.success) { - if (options.query || options.json) { - let result = filterObject(response.result, options); - printSuccess(JSON.stringify(result, null, 2), options); - } - else { - let tableSpec = [ - { column: 'Status', field: 'status', width: 20 } - ]; - printTable(tableSpec, [response.result]); + async execute(taskName, options) { + const profile = await loadProfile(options.profile); + debug('%s.executeTaskLogs(%s)', profile.name, taskName); + const tasks = new Tasks(profile.url); + try { + const { task } = await tasks.getTask(options.project || profile.project, taskName, profile.token); + const isFollow = options.follow && (_.lowerCase(task?.state) === 'active' || _.lowerCase(task?.state) === 'queued'); + + const response = await tasks.taskLogs(options.project || profile.project, profile.token, taskName, isFollow, options.verbose); + if (isFollow) { + return new Promise((resolve, reject) => { + response.on('data', (chunk) => { + let data; + try { + data = chunk.toString(); + + // TODO: Improve following with proper standardized log response from pwgy & express-common + const eventDataLines = _.filter(data.split('\n'), (d) => !_.isEmpty(d)); + eventDataLines.forEach((edl) => { + let eventData; + if (edl.startsWith('data:')) { + // Extract the data part of SSE event + eventData = edl.substring(5).trim(); + } + + let parsedEventData = JSON.parse(eventData); + + if (typeof parsedEventData === 'string' && parsedEventData.startsWith('{')) { + parsedEventData = JSON.parse(parsedEventData); + } + // Split the data into individual lines + const lines = _.filter(parsedEventData?.log ? parsedEventData.log.split('\n') : parsedEventData.split('\n'), (d) => !_.isEmpty(d) && d !== '\n'); + + lines.forEach((line) => { + printSuccess(line); + }); + }); + } catch (e) { + printError(`${e.message}`, options); + } + }); + + response.on('error', (error) => reject(error)); + + response.on('end', () => resolve()); + }); + // eslint-disable-next-line no-else-return + } else { + if (response.success) { + return printSuccess(response.logs, options); } + return printError(`Failed to List Task Logs "${taskName}": ${response.message}`, options); } - else { - printError(`Failed to cancel task: ${response.status} ${response.message}`, options); - } - }) - .catch((err) => { - debug(err); - printError(`Failed to cancel task: ${err.status} ${err.message}`, options); - }); + } catch (err) { + return printError(err, options, `Failed to query Task Logs "${taskName}"`); + } + } +} +export class TaskDeleteCommand { + constructor(program) { + this.program = program; } -}; - -module.exports.DescribeTask = class DescribeTask { + async execute(taskName, options) { + const profile = await loadProfile(options.profile); + debug('%s.executeDeleteTask(%s)', profile.name, taskName); + const tasks = new Tasks(profile.url); + try { + const response = await tasks.deleteTask(options.project || profile.project, profile.token, taskName, options.verbose); + return printSuccess(JSON.stringify(response), options); + } catch (err) { + return handleError(err, options, `Error deleting ${taskName}`); + } + } +} +export class TaskPauseCommand { constructor(program) { this.program = program; } - execute(jobId, taskId, options) { - const profile = loadProfile(options.profile); - debug('%s.describeTask()', profile.name); + async execute(taskNames, options) { + const profile = await loadProfile(options.profile); + debug('%s.executePauseTask(%s)', profile.name, taskNames.join(',')); + const tasks = new Tasks(profile.url); + try { + return Promise.all(taskNames.map(async (taskName) => { + const response = await tasks.pauseTask(options.project || profile.project, profile.token, taskName, options.verbose); + return printSuccess(JSON.stringify(response), options); + })); + } catch (err) { + return handleError(err, options, `Error pausing "${taskNames.join(',')}"`); + } + } +} +export class TaskResumeCommand { + constructor(program) { + this.program = program; + } - const tasks = new Tasks(profile.url, jobId); - tasks.describeTask(profile.token, taskId).then((response) => { - if (response.success) { - if (options.query || options.json) { - let result = filterObject(response.result, options); - printSuccess(JSON.stringify(result, null, 2), options); - } - else { - let tableSpec = [ - { column: 'Id', field: 'id', width: 40 }, - { column: 'Status', field: 'status', width: 15 }, - { column: 'Created At', field: 'createdAt', width: 15 }, - { column: 'Started At', field: 'startedAt', width: 15 }, - { column: 'Stopped At', field: 'stoppedAt', width: 15 } - ]; - printTable(tableSpec, [response.result]); - } - } - else { - printError(`Failed to describe task: ${response.status} ${response.message}`, options); - } - }) - .catch((err) => { - debug(err); - printError(`Failed to describe task: ${err.status} ${err.message}`, options); - }); + async execute(taskNames, options) { + const profile = await loadProfile(options.profile); + debug('%s.executeResumeTask(%s)', profile.name, taskNames.join(',')); + const tasks = new Tasks(profile.url); + try { + return Promise.all(taskNames.map(async (taskName) => { + const response = await tasks.resumeTask(options.project || profile.project, profile.token, taskName, options.verbose); + return printSuccess(JSON.stringify(response), options); + })); + } catch (err) { + return handleError(err, options, `Error resume tasks "${taskNames.join(',')}`); + } } -}; \ No newline at end of file +} diff --git a/src/commands/types.js b/src/commands/types.js index 705641d4..28e1eb83 100644 --- a/src/commands/types.js +++ b/src/commands/types.js @@ -1,123 +1,96 @@ -/* - * Copyright 2018 Cognitive Scale, Inc. All Rights Reserved. - * - * 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. - */ - -const fs = require('fs'); -const debug = require('debug')('cortex:cli'); -const { loadProfile } = require('../config'); -const Catalog = require('../client/catalog'); -const { printSuccess, printError, filterObject, parseObject, printTable } = require('./utils'); - -module.exports.SaveTypeCommand = class SaveTypeCommand { - +import fs from 'node:fs'; +import debugSetup from 'debug'; +import dayjs from 'dayjs'; +import relativeTime from 'dayjs/plugin/relativeTime.js'; +import { loadProfile } from '../config.js'; +import Catalog from '../client/catalog.js'; +import { + LISTTABLEFORMAT, filterObject, printExtendedLogs, getFilteredOutput, + + printSuccess, parseObject, handleTable, handleError, +} from './utils.js'; + +const debug = debugSetup('cortex:cli'); +dayjs.extend(relativeTime); +export class SaveTypeCommand { constructor(program) { this.program = program; } - execute(typeDefinition, options) { - const profile = loadProfile(options.profile); + async execute(typeDefinition, options) { + const profile = await loadProfile(options.profile); debug('%s.executeSaveType(%s)', profile.name, typeDefinition); - - const typeDefStr = fs.readFileSync(typeDefinition); - const type = parseObject(typeDefStr, options); - debug('%o', type); - - let normalizedType = {}; - if ( !('types' in type) ) - normalizedType["types"] = [type]; - else - normalizedType = type; - - const catalog = new Catalog(profile.url); - catalog.saveType(profile.token, normalizedType).then((response) => { - if (response.success) { - printSuccess(`Type definition saved`, options); - } - else { - printError(`Failed to save type: ${response.status} ${response.message}`, options); - } - }) - .catch((err) => { - printError(`Failed to save type: ${err.status} ${err.response.body.error}`, options); - }); + try { + const typeDefStr = fs.readFileSync(typeDefinition); + const type = parseObject(typeDefStr, options); + debug('%o', type); + let normalizedType = {}; + if (!('types' in type)) normalizedType.types = [type]; + else normalizedType = type; + const catalog = new Catalog(profile.url); + await catalog.saveType(options.project || profile.project, profile.token, normalizedType); + return printSuccess('Type definition saved', options); + } catch (err) { + return handleError(err, options, 'Failed to save type'); + } } -}; - -module.exports.ListTypesCommand = class ListTypesCommand { - +} +export class ListTypesCommand { constructor(program) { this.program = program; } - execute(options) { - const profile = loadProfile(options.profile); + async execute(options) { + const profile = await loadProfile(options.profile); debug('%s.executeListTypes()', profile.name); - const catalog = new Catalog(profile.url); - catalog.listTypes(profile.token).then((response) => { - if (response.success) { - let result = response.types; - if (options.query) - result = filterObject(result, options); - - if (options.json) { - printSuccess(JSON.stringify(result, null, 2), options); + try { + const { types } = await catalog.listTypes(options.project || profile.project, profile.token, options.limit, options.skip, options.sort); + // TODO remove --query on deprecation + if (options.json || options.query) { + getFilteredOutput(types, options); + } else { + printExtendedLogs(types, options); + handleTable(LISTTABLEFORMAT, types, (o) => ({ ...o, updatedAt: o.updatedAt ? dayjs(o.updatedAt).fromNow() : '-' }), 'No types found'); } - else { - const tableSpec = [ - { column: 'Title', field: 'title', width: 50 }, - { column: 'Name', field: 'name', width: 50 }, - { column: 'Version', field: '_version', width: 12 } - ]; - - printTable(tableSpec, result); - } - } - else { - printError(`Failed to list types: ${response.status} ${response.message}`, options); - } - }) - .catch((err) => { - printError(`Failed to list types: ${err.status} ${err.message}`, options); - }); + } catch (err) { + handleError(err, options, 'Failed to list types'); + } } -}; - -module.exports.DescribeTypeCommand = class DescribeTypeCommand { - +} +export class DescribeTypeCommand { constructor(program) { this.program = program; } - execute(typeName, options) { - const profile = loadProfile(options.profile); + async execute(typeName, options) { + const profile = await loadProfile(options.profile); debug('%s.executeDescribeType(%s)', profile.name, typeName); - const catalog = new Catalog(profile.url); - catalog.describeType(profile.token, typeName).then((response) => { - if (response.success) { - let result = filterObject(response.type, options); - printSuccess(JSON.stringify(result, null, 2), options); - } - else { - printError(`Failed to describe type ${typeName}: ${response.message}`, options); - } - }) - .catch((err) => { - printError(`Failed to describe type ${typeName}: ${err.status} ${err.message}`, options); - }); + try { + const response = await catalog.describeType(options.project || profile.project, profile.token, typeName); + getFilteredOutput(response, options); + } catch (err) { + handleError(err, options, `Failed to describe type ${typeName}`); + } } -}; +} +export class DeleteTypeCommand { + constructor(program) { + this.program = program; + } + + async execute(typeName, options) { + const profile = await loadProfile(options.profile); + debug('%s.executeDeleteType(%s)', profile.name, typeName); + const catalog = new Catalog(profile.url); + try { + const response = await catalog.deleteType(options.project || profile.project, profile.token, typeName); + const result = filterObject(response, options); + return printSuccess(JSON.stringify(result, null, 2), options); + } catch (err) { + return handleError(err, options, `Failed to delete type ${typeName}`); + } + } +} diff --git a/src/commands/users.js b/src/commands/users.js new file mode 100644 index 00000000..ab2bb8e2 --- /dev/null +++ b/src/commands/users.js @@ -0,0 +1,211 @@ +import debugSetup from 'debug'; +import { loadProfile } from '../config.js'; +import Users from '../client/users.js'; +import { + printSuccess, printError, printWarning, filterObject, handleTable, getFilteredOutput, handleError, +} from './utils.js'; +/* + * Copyright 2023 Cognitive Scale, Inc. All Rights Reserved. + * + * 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. + */ +const debug = debugSetup('cortex:cli'); +function createGrant(options) { + return { + project: options.project, + resource: options.resource, + actions: options.actions, + effect: options.deny ? 'deny' : 'allow', + }; +} +export const UserGrantCommand = class { + constructor(program) { + this.program = program; + } + + async execute(user, options) { + const profile = await loadProfile(options.profile); + const form = createGrant(options); + debug('%s.grantUser(%s)', profile.name, user || 'self'); + const client = new Users(profile.url, user); + try { + const call = (deleteFlag, token, body) => { + if (deleteFlag) { + return client.removeGrantFromUser(token, body); + } + return client.createGrantForUser(token, body); + }; + const response = await call(options.delete, profile.token, form); + const result = filterObject(response.result, options); + printSuccess(JSON.stringify(result, null, 2), options); + } catch (err) { + const func = (options.delete) ? 'delete' : 'create'; + handleError(err, options, `Failed to ${func} grant for user`); + } + } +}; +export const UserDeleteCommand = class { + constructor(program) { + this.program = program; + } + + async execute(user, options) { + const profile = await loadProfile(options.profile); + debug('%s.deleteServiceUser(%s)', profile.name, user); + const client = new Users(profile.url, user); + try { + const response = await client.deleteServiceUser(profile.token, user); + const result = filterObject(response, options); + printSuccess(JSON.stringify(result, null, 2), options); + } catch (err) { + handleError(err, options, 'Failed to delete user'); + } + } +}; +export const UserCreateCommand = class { + constructor(program) { + this.program = program; + } + + async execute(user, options) { + const profile = await loadProfile(options.profile); + debug('%s.createServiceUser(%s)', profile.name, user); + const client = new Users(profile.url, user); + try { + const response = await client.createServiceUser(profile.token, user); + const result = filterObject(response, options); + printSuccess(JSON.stringify(result.config, null), options); + } catch (err) { + handleError(err, options, 'Failed to create user'); + } + } +}; +export const UserListCommand = class { + constructor(program) { + this.program = program; + } + + async execute(options) { + const profile = await loadProfile(options.profile); + debug('%s.listServiceUsers', profile.name); + const client = new Users(profile.url, 'self'); + try { + const response = await client.listServiceUsers(profile.token); + // TODO remove --query on deprecation + if (options.json || options.query) { + getFilteredOutput(response, options); + } else { + handleTable([{ + column: 'User', + field: 'user', + }], response.users.map((x) => ({ user: x })), null, 'No service users found'); + } + } catch (err) { + handleError(err, options, 'Failed to list service users'); + } + } +}; + +export const UserDescribeCommand = class { + constructor(program) { + this.program = program; + } + + async execute(cliUser, options) { + const profile = await loadProfile(options.profile); + debug('%s.describeForUser(%s)', profile.name, options.user || 'self'); + const flags = []; + if (options.grants) { + flags.push('grants'); + } + if (options.roles) { + flags.push('roles'); + } + if (options?.user !== undefined && cliUser !== undefined) { + printError('Bad options: --user <user> and <user> are mutually exclusive,', options); + } + const user = options?.user ?? cliUser; + if (user === undefined) { + printWarning('Defaulting to user from cortex profile', options); + } + const client = new Users(profile.url, user, flags); + try { + const response = await client.describeUser(profile.token); + const result = filterObject(response, options); + printSuccess(JSON.stringify(result, null, 2), options); + } catch (err) { + handleError(err, options, 'Failed to describe user'); + } + } +}; +export const UserProjectAssignCommand = class { + constructor(program) { + this.program = program; + } + + async execute(project, options) { + const profile = await loadProfile(options.profile); + debug('%s.assignUserProject(%s)', profile.name, project); + const client = new Users(profile.url, null); + const call = (deleteFlag, token, assignProject, users) => { + if (deleteFlag) { + return client.removeUsersFromProject(token, assignProject, users); + } + return client.addUsersToProject(token, assignProject, users); + }; + try { + const response = await call(options.delete, profile.token, project, options.users); + const result = filterObject(response.result, options); + printSuccess(JSON.stringify(result, null, 2), options); + } catch (err) { + const func = (options.delete) ? 'unassign' : 'assign'; + handleError(err, options, `Failed to ${func} users from project`); + } + } +}; +export const UserResetPATCommand = class { + constructor(program) { + this.program = program; + } + + async execute(options) { + const profile = await loadProfile(options.profile); + const { user } = options; + debug('%s.UserResetPATCommand(%s)', profile.name, user); + const client = new Users(profile.url, 'self'); + try { + const result = await client.resetUserPAT(profile.token, user); + printSuccess(JSON.stringify(result, null, 2), options); + } catch (err) { + handleError(err, options, 'Failed to invalidate user PAT'); + } + } +}; +export const UserGetPATCommand = class { + constructor(program) { + this.program = program; + } + + async execute(options) { + const profile = await loadProfile(options.profile); + const { user } = options; + debug('%s.UserGetPATCommand(%s)', profile.name); + const client = new Users(profile.url, 'self'); + try { + const result = await client.getUserPAT(profile.token, user); + printSuccess(JSON.stringify(result.config, null, 2), options); + } catch (err) { + handleError(err, options, 'Failed to fetch user PAT'); + } + } +}; diff --git a/src/commands/utils.js b/src/commands/utils.js index 42829b04..09bc0604 100644 --- a/src/commands/utils.js +++ b/src/commands/utils.js @@ -1,100 +1,649 @@ -/* - * Copyright 2018 Cognitive Scale, Inc. All Rights Reserved. - * - * 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 Table from 'cli-table3'; +import _ from 'lodash'; +// eslint-disable-next-line import/no-unresolved +import { HTTPError } from 'got'; +import { boolean } from 'boolean'; +import chalk from 'chalk'; +import debugSetup from 'debug'; +import fs from 'node:fs'; +import process from 'node:process'; +import { globSync } from 'glob'; +import { fileURLToPath } from 'node:url'; +import * as jmsepath from 'jmespath'; +import path from 'node:path'; +import yaml from 'js-yaml'; +import { exec } from 'node:child_process'; -const chalk = require('chalk'); -const jmsepath = require('jmespath'); -const yaml = require('js-yaml'); -const debug = require('debug')('cortex:cli'); -const Table = require('cli-table'); +const debug = debugSetup('cortex:cli'); +const MAX_NAME_LENGTH = 20; +const space = /\s+/; +const validNameRegex = /^[a-zA-Z][a-zA-Z0-9_-]*[a-zA-Z0-9]$/; +const specialCharsExceptHyphen = /[^A-Za-z0-9- ]/; +const beginAndEndWithHyphen = /^[-]+|[-]+$/; +const validationErrorMessage = 'Must be 20 characters or less, contain only alphanumeric characters, or -, and cannot begin with a number and cannot begin or end with -'; +const nameRequirementMessage = 'You must provide a name for the skill.'; -module.exports.constructError = function(error) { - const errorText = JSON.parse(error.response.text); - if (errorText.message) { - return {success: false, message: errorText.message, status: error.status}; - } else { - return {success: false, message: JSON.stringify(errorText), status: error.status}; + +export function useColor(options) { + return boolean(options?.color); +} + +export const printExtendedLogs = (data, options) => { + if (options.limit && Array.isArray(data) && data.length === Number(options.limit) && options.limit !== '0') { + // don't log if showing all the results + process.stderr.write(`Results limited to ${options.limit} rows\n`); } }; -module.exports.printSuccess = function(message, options) { - if (!options || options.color === 'on') { +export const printSuccess = (message, options) => { + if (useColor(options)) { console.log(chalk.green(message)); - } - else { + } else { console.log(message); } }; +export const printWarning = (message, options) => { + if (useColor(options)) { + console.warn(chalk.yellow(message)); + } else { + console.warn(message); + } +}; -module.exports.printError = function(message, options) { - if (!options || options.color === 'on') { +export function printError(message, options = { color: true }, exitProgram = true) { + if (useColor(options)) { console.error(chalk.red(message)); - } - else { + } else { console.error(message); } - process.exit(1) -}; - -module.exports.filterObject = function(obj, options) { - if (options.query) { - debug('filtering results with query: ' + options.query); - return jmsepath.search(obj, options.query); + if (exitProgram) { + process.exit(1); + } +} +/** + * Returns an array with string representations of the specified fields from the given object. + * + * @param {Array<string>} fields - fields to extract + * @param {Object<string, any>} obj - object to extract fields from + * @returns {Array<string>} array of strings + */ +function _extractValues(fields, obj) { + const rv = []; + fields.forEach((f) => rv.push((obj !== undefined && obj !== null && obj[f] !== undefined && obj[f] !== null) ? obj[f].toString() : '-')); + return rv; +} +/** + * Execute a sub command, return stdout on success, return stderr on failure + * @param commandStr + * @returns {Promise<*>} + */ +// eslint-disable-next-line require-await +export async function callMe(commandStr, stdoutHandler) { + return new Promise((resolve, reject) => { + const proc = exec(commandStr, (err, stdout) => { + if (err) { + reject(Error(err.message + stdout)); + } else { + resolve(stdout); + } + }); + if (stdoutHandler) { + proc.stdout.on('data', (data) => { + stdoutHandler(data); + }); + proc.stderr.on('data', (data) => { + stdoutHandler(data); + }); + } else { + // Pipe stdout to stderr in real time: + proc.stderr.pipe(process.stderr); + } + }); +} +function round(value, precision) { + const multiplier = 10 ** (precision || 0); + return Math.floor(value * multiplier) / multiplier; +} +function formatServiceInputParameter(inputParameter) { + if (inputParameter.type === 'array') { + return (`-Name: ${inputParameter.name}, Type: ${inputParameter.type}<${inputParameter.format}>`); + } + return (`-Name: ${inputParameter.name}, Type: ${inputParameter.type}`); +} +// connections get returns createdAt (not prefixed by _) field that cause saving exported connection to fail. Hence removing them manually +const systemFields = ['createdAt', 'updatedAt', 'createdBy', 'updatedBy']; +// Alternatively, we can use fs.rmdirSync(<path>, {recursive: true}), but that requires node v12+ +const deleteFolderRecursive = (filepath) => { + try { + if (fs.existsSync(filepath)) { + if (fs.lstatSync(filepath).isDirectory()) { + fs.readdirSync(filepath).forEach((file) => { + const curPath = path.join(filepath, file); + if (fs.lstatSync(curPath).isDirectory()) { // recurse + deleteFolderRecursive(curPath); + } else { // delete file + fs.unlinkSync(curPath); + } + }); + fs.rmdirSync(filepath); + } else { + fs.unlinkSync(filepath); + } + } + } catch (e) { + console.error(chalk.red(e.message)); } - return obj; }; -module.exports.parseObject = function(str, options) { - if (options.yaml) { - return yaml.safeLoad(str); +export function writeToFile(content, filepath, createDir = true) { + const dir = path.dirname(filepath); + if (createDir && !fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); } - - return JSON.parse(str); -}; + fs.writeFileSync(filepath, content); +} -function _extractValues(fields, obj) { - const rv = []; - fields.forEach((f) => rv.push((obj !== undefined && obj !== null && obj[f] !== undefined && obj[f] !== null)? obj[f].toString() : '-')); - return rv; +/** + * If provided write to file otherwise dump to stdout + * @param content + * @param opts - expects `outputFile` property or dumps to stdout + */ +export function writeOutput(content, opts) { + if (opts?.outputFile) { + return writeToFile(content, opts.outputFile); + } + return printSuccess(content, opts); } -module.exports.printTable = function(spec, objects, transform) { - transform = transform || function (obj) { return obj; }; +export function filterObject(obj, options) { + try { + if (options.query) { + debug(`filtering results with query: ${options.query}`); + return jmsepath.search(obj, options.query); + } + } catch (e) { + // TODO remove --query on deprecation + process.stderr.write(`error: invalid argument: ${options.query} \n`); + } + return obj; +} +export const getQueryOptions = (options) => { + // TODO remove --query on deprecation + if (options.query) process.stderr.write('[DEPRECATION WARNING] --query\n'); + if (options.query && options.json && typeof (options.json) === 'string') process.stderr.write('Warning! --query overrides --json args\n'); + // make --query override --json + const queryOptions = options.query || options.json; + // output JSON if JSMEpath isn't passed, default value is true + // set searchPath to null if there's no path + return { query: queryOptions === true ? null : queryOptions }; +}; +export const getFilteredOutput = (result, options) => { + const filteredResult = filterObject(result, getQueryOptions(options)); + if (filteredResult) { + // print extended logs iff --query arg is valid + printExtendedLogs(filteredResult, options); + writeOutput(JSON.stringify(filteredResult, null, 2), options); + } +}; +export const parseObject = (str) => yaml.load(str); +/** + * Prints a table based on the given data and table specification. + * + * @param {Array<Object>} spec - objects with 'column', 'width', and 'field' + * @param {Array} objects - data to reprsent in the table + * @param {CallableFunction<Object, Object> | undefined} transform + * @param {Object} tableOptions - object to provide to 'Table()' constructor + */ +export function printTable(spec, objects, transform, tableOptions = {}) { + const fn = transform ?? ((obj) => obj); const head = spec.map((s) => s.column); const colWidths = spec.map((s) => s.width); const fields = spec.map((s) => s.field); - const values = objects.map((obj) => _extractValues(fields, transform(obj))); + const values = objects.map((obj) => _extractValues(fields, fn(obj))); debug('printing fields: %o', fields); - - const table = new Table({head, colWidths, style: {head: ['cyan']}}); + const table = new Table({ + head, colWidths, style: { head: ['cyan'] }, ...tableOptions, + }); values.forEach((v) => table.push(v)); + console.log(table.toString()); +} +/** + * Prints a Cross-Table based on the given data & table spec. The spec defines + * the column structure and the values should be the Row objects to include in + * the table. Unlike 'printTable()', this function does NOT transform row + * values, as to allow for lower-level Table cell configuration. + * + * @param {Array<Object>} spec - objects with 'column', 'width', and 'fields' + * @param {Array<Object<string, Array<Object<string, string>>>>} sections - map from Row Title to a list of values in the column + * @param {Object<string, any>} tableOptions object to provide to 'Table()' constructor + */ +export function printCrossTable(spec, sections, tableOptions = {}) { + const head = spec.map((s) => s.column); + const colWidths = spec.map((s) => s.width); + const fields = spec.map((s) => s.field); + if (head[0] !== '') { + // Cross-tables should always include an empty string for the first + // header. Add it, if missing. + head.unshift(''); + colWidths.unshift(undefined); + } else { + // User provided first empty header, meaning that we should remove the + // non-applicable value (likely undefined) from fields. + fields.shift(); + } + debug('Printing Columns: %o, Rows: %o', head, Object.keys(sections)); + const table = new Table({ + head, colWidths, style: { head: ['cyan'] }, ...tableOptions, + }); + // Parse the sections to add rows to the table + Object.keys(sections).forEach((title) => { + // Add a cell with the specified Title that spans the total # of records + // in that section (minimum 1). + const records = sections[title]; + const titleRowSpan = (records?.length || 1); + const coloredTitle = chalk.cyan(title); + const values = [ + [ + { + rowSpan: titleRowSpan, content: coloredTitle, vAlign: 'center', hAlign: 'center', + }, + ], + ]; + if (records.length === 0) { + // No records to show for section, thus set default values in the + // cells next to the Title, so there is at least 1 row. + values[0].push('-', '-'); + } else { + // Populate the initial row lining up with the Title Cell, then add + // all other rows below that to fill out the section. + const rows = records.map((obj) => _extractValues(fields, obj)); + values[0].push(...rows.shift()); + values.push(...rows); + } + debug('Adding Table Section: %o', values); + table.push(...values); + }); console.log(table.toString()); +} + +export function getSourceFiles(source) { + const options = { silent: true }; + const normalizedSource = (source.endsWith('/')) ? source : `${source}/`; + const normalizedPath = path.posix.join(normalizedSource, '**', '*'); + const files = globSync(normalizedPath, options); + // files is an array of filenames. + return files.filter((fpath) => fs.lstatSync(fpath).isFile()).map((fpath) => ({ + canonical: fpath, + relative: path.relative(normalizedSource, fpath), + size: fs.lstatSync(fpath).size, + })); +} + +export const humanReadableFileSize = (sizeInBytes) => { + const ranges = { + K: 10 ** 3, + M: 10 ** 6, + G: 10 ** 9, + T: 10 ** 12, + }; + if (sizeInBytes < ranges.K) { + return `${sizeInBytes}B`; + } + if (sizeInBytes < ranges.M) { + return `${round(sizeInBytes / ranges.K, 1)}K`; + } + if (sizeInBytes < ranges.G) { + return `${round(sizeInBytes / ranges.M, 1)}M`; + } + if (sizeInBytes < ranges.T) { + return `${round(sizeInBytes / ranges.G, 1)}G`; + } + return sizeInBytes; // Umm fix logic +}; +export const formatAllServiceInputParameters = (allParameters) => { + if (allParameters.$ref != null) { + return `$ref:${allParameters.$ref}`; + } + return allParameters.map((inputParameters) => formatServiceInputParameter(inputParameters)).join('\n'); +}; + +export const formatValidationPath = (p) => { + let cnt = 0; + let res = ''; + const len = p.length; + p.forEach((s) => { + if (_.isNumber(s)) { + res += `[${s}]`; + } else if (cnt < len) res += s; + else res += s; + if (cnt < len - 1 && !_.isNumber(p[cnt + 1])) res += '.'; + cnt += 1; + }); + return res; +}; +export const cleanInternalFields = (jsobj) => JSON.stringify(jsobj, (a, obj) => { + if (a.startsWith('_') || systemFields.includes(a)) { + return undefined; + } + return obj; +}, 2); +export const jsonToYaml = (json) => { + let res = json; + if (typeof json === 'string') { + res = JSON.parse(json); + } + return yaml.dump(res); +}; +export const createFileStream = (filepath) => { + const dir = path.dirname(filepath); + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } + return fs.createWriteStream(filepath); +}; + +export const fileExists = (filepath) => fs.existsSync(filepath); +export const checkProject = (projectId) => { + if (_.isEmpty(projectId)) { + printError('\'project\' is required, please provide using \'cortex configure\' or --project'); + } +}; +export const LISTTABLEFORMAT = [ + { column: 'Name', field: 'name', width: 30 }, + { column: 'Title', field: 'title', width: 40 }, + { column: 'Description', field: 'description', width: 50 }, + { column: 'Modified', field: 'updatedAt', width: 26 }, + { column: 'Author', field: 'createdBy', width: 25 }, +]; +export const DEPENDENCYTABLEFORMAT = [ + { column: 'Dependency Name', field: 'name', width: 60 }, + { column: 'Dependency Type', field: 'type', width: 40 }, +]; +export const OPTIONSTABLEFORMAT = [ + { column: 'Option Type', field: 'type', width: 20 }, + { column: 'Message', field: 'message', width: 120 }, +]; +export const RUNTABLEFORMAT = [ + { column: 'Run ID', field: 'runId', width: 30 }, + { column: 'Experiment Name', field: 'experimentName', width: 40 }, + { column: 'Took', field: 'took', width: 50 }, + { column: 'Modified', field: '_updatedAt', width: 26 }, +]; +export const SESSIONTABLEFORMAT = [ + { column: 'Session ID', field: 'sessionId', width: 45 }, + { column: 'TTL', field: 'ttl', width: 15 }, + { column: 'Description', field: 'description', width: 70 }, +]; +export const isNumeric = (value) => /^-?\d+$/.test(value); +export const CONNECTIONTABLEFORMAT = [ + { column: 'Name', field: 'name', width: 40 }, + { column: 'Title', field: 'title', width: 50 }, + { column: 'Description', field: 'description', width: 50 }, + { column: 'Connection Type', field: 'connectionType', width: 25 }, + { column: 'Created On', field: 'createdAt', width: 26 }, +]; +export const EXTERNALROLESFORMAT = [ + { column: 'Group', field: 'group' }, + { column: 'Roles', field: 'roles' }, +]; + +const TABLEFORMATS = { + OPTIONSTABLEFORMAT, + LISTTABLEFORMAT, + CONNECTIONTABLEFORMAT, + SESSIONTABLEFORMAT, + RUNTABLEFORMAT, + DEPENDENCYTABLEFORMAT, + EXTERNALROLESFORMAT, +}; + +export const generateNameFromTitle = (title) => title.replace(specialCharsExceptHyphen, '') + .replace(space, '-') + .replace(beginAndEndWithHyphen, '') + .substr(0, MAX_NAME_LENGTH) + .replace(beginAndEndWithHyphen, '') + .toLowerCase(); +export const hasUppercase = (s) => /[A-Z]/.test(s); +export const validateName = (name) => (validNameRegex.test(name) && (name.length <= MAX_NAME_LENGTH) + ? { + status: true, + message: '', + } + : { + status: false, + message: name ? validationErrorMessage : nameRequirementMessage, + }); +export const handleTable = (spec, data, transformer, noDataMessage, tableOptions) => { + if (!data || data.length === 0) { + printSuccess(noDataMessage); + } else { + printTable(spec, data, transformer, tableOptions); + } +}; + +const transformTSOAValidation = (responseDetails) => { + if (responseDetails && typeof responseDetails === 'object') { + const keys = Object.keys(responseDetails); + if (keys.length > 0) { + const transformedResp = []; + for (let i = 0; i < keys.length; i += 1) { + const key = keys[i]; + const { message } = _.get(responseDetails, key, {}); + + // If the key includes a single dot (.) + if (key.includes('.') && key.split('.').length === 2) { + if (message) { + // Transform error object + transformedResp.push({ type: key.split('.')[1], message }); + } + // If key doesn't include a period dot (.) + } else if (!key.includes('.')) { + transformedResp.push({ type: key, message }); + } + } + if (transformedResp.length > 0) { + return transformedResp; + } + return null; + } + } + // Handle invalid or unexpected input + return null; +}; + +export const handleListFailure = (err, options, type) => { + let status; + let message; + let details; + // Newer code throws got HTTPError + if (err instanceof HTTPError) { + status = err.response.statusCode; + const jsonResp = JSON.parse(err.response.body ?? '{}'); + details = jsonResp.details; + message = jsonResp.message; + } else { + status = err.status ?? ''; + message = err.message; + details = err.details; + } + + if (status === 400) { + printError(`${type} list failed.`, options, false); + if (details !== undefined && details !== null) { + // TSOA API validation error case + if (!Array.isArray(details)) { + const transformedResponse = transformTSOAValidation(details); + if (transformedResponse !== null) { + details = transformedResponse; + } + } + printTable(OPTIONSTABLEFORMAT, details); + } else { + printError(message, options); + } + printError(''); // Just exit + } + return printError(`Failed to list ${type}: ${status} ${message}`, options); +}; + +export const handleDeleteFailure = (response, options, type) => { + if (response.status === 403) { // has dependencies + const tableFormat = DEPENDENCYTABLEFORMAT; + printError(`${type} deletion failed: ${response.message}.`, options, false); + printTable(tableFormat, response.details); + return printError(''); // Just exit + } + const defaultErrorMessage = `${type} deletion failed: ${response.status} ${response.message}.`; + return printError(defaultErrorMessage, options); }; -module.exports.exportDoc = function(program){ - console.log(JSON.stringify(program.commands.map((c)=>({ - name: c._name, - description: c._description, - usage: c.usage(), - options: c.options.map((o)=>({ - flags: o.flags, - defaultValue: o.defaultValue, - description: o.description - })) - })))); - process.exit(0); -}; \ No newline at end of file + +export function readPackageJSON(relPath) { + const absPath = fileURLToPath(new URL(relPath, import.meta.url)); + return JSON.parse(fs.readFileSync(absPath)); +} + +/** + * pretty print the 400 response from TSOA router + */ +export function printErrorDetails(response, options, exit = true) { + let details = response?.details; + if (!details && response?.body) { + details = parseObject(response?.body)?.details; + } + if (!details) return; // nothing to process... + printWarning('The following issues were found:', options); + const tableSpec = [ + { column: 'Path', field: 'path', width: 50 }, + { column: 'Message', field: 'message', width: 100 }, + ]; + if (_.isArray(details)) { + details.map((d) => d.path = formatValidationPath(d.path)); + } else { + details = Object.entries(details).map(([path2, body]) => ({ path: path2, message: body?.message })); + } + printTable(tableSpec, details); + if (exit) printError(''); // Just use this over exit() as tests already stub this call .. +} + +function checkForEmptyString(key, value) { + if (!value.trim()) { + printError(`error: <${key}> cannot be empty.`); + process.exit(1); // Exit with an error code + } +} + +export function checkForEmptyArgs(args) { + Object.keys(args).forEach((key) => { + if (typeof args[key] === 'object') { + args[key].forEach((val) => { + checkForEmptyString(key, val); + }); + } else { + checkForEmptyString(key, args[key]); + } + }); +} + +/** + * Returns an error object (with `success`, `message`, `details`, and `status` fields), from a given `Error`. + * + * @param {Error} error + * @return {object} An object describing the original error with additional details + */ +export const constructError = (error) => { + let details; + let respCode; + // fallback to text in message or standard error message + const errResp = error?.response; + let errorText = errResp?.body; + if (errorText?.trim().length === 0 || error.name === 'RequestError') { + errorText = error.message; + } else if (error.name === 'ParseError' || error.code === 'ERR_BODY_PARSE_FAILURE') { + errorText = 'Unable to parse response from server. Try running again with "--debug" for more details.'; + details = error.message; + } else if (error.name === 'TimeoutError' || error.code === 'ETIMEDOUT') { + errorText = error.message; + details = '\nTry running the command again with \'--no-timeout\' or increase\n' + + 'the network timeout values via environment variables. Run\n' + + '"cortex configure env" to view the environment variables\n' + + 'controlling the network timeout settings.'; + } + // if JSON was returned, look for either a message or error in it + try { + const resp = errResp ? JSON.parse(errorText) : {}; + respCode = resp.code; + if (resp?.message || resp?.error) errorText = resp?.message || resp?.error; + // eslint-disable-next-line prefer-destructuring + details = details || resp.details; + } catch (e) { + // Guess it wasn't JSON! + } + // todo make figuring out the status code more consistent? this might be a holdover from request vs got? + const status = errResp?.statusCode || respCode || error?.code || error?.status || ''; + return { + success: false, message: errorText, details, status, + }; +}; + +/** + * Prints a normalized version of any given Error and exits the program. + * + * @param {Error} error + * @param {object} options Options for printing the erorr (`tableFormat`) + * @param {string} prefix String to prefix the actual error message + */ +export function handleError(error, options, prefix = 'Error') { + let details; + let respCode; + // fallback to text in message or standard error message + const errResp = error?.response; + let errorText = errResp?.body; + if (errorText?.trim().length === 0 || error.name === 'RequestError') { + errorText = error.message; + } else if (error.name === 'ParseError' || error.code === 'ERR_BODY_PARSE_FAILURE') { + errorText = 'Unable to parse response from server. Try running again with "--debug" for more details.'; + details = error.message; + } else if (error.name === 'TimeoutError' || error.code === 'ETIMEDOUT') { + errorText = error.message; + details = '\nTry running the command again with \'--no-timeout\' or increase\n' + + 'the network timeout values via environment variables. Run\n' + + '"cortex configure env" to view the environment variables\n' + + 'controlling the network timeout settings.'; + } + // if JSON was returned, look for either a message or error in it + let responseDetails; + try { + const resp = errResp ? JSON.parse(errorText) : {}; + respCode = resp.code; + if (resp?.message || resp?.error) { + errorText = resp?.message || resp?.error; + } else { + errorText = error.message; // perhaps a vanilla Error + } + responseDetails = resp.details; + } catch (e) { + // Guess it wasn't JSON! + } + // TODO: make figuring out the status code more consistent? this might be a holdover from request vs got? + const status = errResp?.statusCode || respCode || error?.code || error?.status || ''; + printError(`${prefix}: ${status}, ${errorText}`, options, false); + if (responseDetails !== undefined && responseDetails !== null) { + const format = TABLEFORMATS[options.tableformat] ?? OPTIONSTABLEFORMAT; + // TSOA API validation error case + if (!Array.isArray(details)) { + const transformedResponse = transformTSOAValidation(details); + if (transformedResponse !== null) { + details = transformedResponse; + } + } + printTable(format, details); + } else if (details) { + printError(details, options, false); + } + printError(''); // Just exit +} + +export { deleteFolderRecursive as deleteFile }; diff --git a/src/commands/workspaces/build.js b/src/commands/workspaces/build.js new file mode 100644 index 00000000..e9f5b3cd --- /dev/null +++ b/src/commands/workspaces/build.js @@ -0,0 +1,72 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { Listr } from 'listr2'; +import { printError, printSuccess } from '../utils.js'; +import { getSkillInfo, buildImageTag } from './workspace-utils.js'; +import { loadProfile } from '../../config.js'; +import dockerCli from '../../client/dockerClient.js'; + +export default class WorkspaceBuildCommand { + constructor(program) { + this.program = program; + } + + async buildAction(target, action, stdoutHandler, options) { + const actionPath = path.join(target, 'actions', action.name); + const expectedDockerfile = path.join(actionPath, 'Dockerfile'); + try { + if (!fs.existsSync(expectedDockerfile)) { + throw Error(`Unable to build action '${action.name}': Missing Dockerfile '${expectedDockerfile}', \nCheck that the 'actions/<name>' folder and action's name match or add a 'Dockerfile' in the path provided`); + } + const imageTag = await buildImageTag(this.profile, action.image); + return dockerCli(options).build({ + imageTag, contextPath: actionPath, dockerFile: expectedDockerfile, stdoutHandler, + }); + } catch (err) { + printError(`${expectedDockerfile}:\n\t ${err.message}`, options); + return Promise.reject(err); + } + } + + async execute(folder, options) { + this.options = options; + let target = process.cwd(); + this.profile = await loadProfile(options.profile); + try { + if (folder) { + const fldr = folder.replace(/'|"/g, ''); + target = path.isAbsolute(fldr) ? folder : path.resolve(target, fldr); + } + if (options.skill) { + target = path.join(target, 'skills', options.skill, 'skill.yaml'); + if (!fs.existsSync(target)) { + throw new Error(`Skill ${options.skill} not found!`); + } + } + const skillInfo = await getSkillInfo(target); + if (skillInfo.length > 0) { + const tasks = new Listr([], { + }); + skillInfo.forEach((info) => { + const actions = info.skill.actions ? info.skill.actions : []; + actions.forEach((action) => { + tasks.add([ + { + title: `Building skill "${info.skill.name}" action "${action.name}"`, + task: async (ctx, task) => { + await this.buildAction(path.dirname(info.uri), action, (data) => task.output = data, options); + task.title = `Successfully built skill "${info.skill.name}" action "${action.name}"`; + }, + }, + ]); + }); + }); + await tasks.run(); + } else { + printSuccess('No skills found', options); + } + } catch (err) { + printError(`Build Failed: ${err.message}`, options); + } + } +} diff --git a/src/commands/workspaces/configure.js b/src/commands/workspaces/configure.js new file mode 100644 index 00000000..3ddb4b1f --- /dev/null +++ b/src/commands/workspaces/configure.js @@ -0,0 +1,135 @@ +import _ from 'lodash'; +import inquirer from 'inquirer'; +import chalk from 'chalk'; +// eslint-disable-next-line import/no-unresolved +import open from 'open'; +import dayjs from 'dayjs'; +import relativeTime from 'dayjs/plugin/relativeTime.js'; +import { got } from '../../client/apiutils.js'; // Use got with DEBUG and preet options +import { validateToken, persistToken } from './workspace-utils.js'; +import { readConfig } from '../../config.js'; +import { printSuccess, printError, useColor } from '../utils.js'; + +dayjs.extend(relativeTime); +const GITHUB_APP_CLIENTID = 'Iv1.e0e84c2a5fa7c935'; +const DEFAULT_TEMPLATE_REPO = 'CognitiveScale/cortex-code-templates'; +const DEFAULT_TEMPLATE_BRANCH = 'main'; +const GITHUB_DEVICECODE_REQUEST_URL = 'https://github.com/login/device/code'; +const GITHUB_DEVICECODE_RESPONSE_URL = 'https://github.com/login/oauth/access_token'; + +export default class WorkspaceConfigureCommand { + constructor(program) { + this.program = program; + } + + async execute(opts) { + this.options = opts; + try { + const config = readConfig(); + const currentProfile = config.profiles[config.currentProfile]; + console.log(`Configuring workspaces for profile ${chalk.green(config.currentProfile)}`); + const answers = await inquirer.prompt([ + { + type: 'input', + name: 'repo', + message: 'Template Repository URL: ', + default: _.get(currentProfile, 'templateConfig.repo', DEFAULT_TEMPLATE_REPO), + }, + { + type: 'input', + name: 'branch', + message: 'Template Repository Branch:', + default: _.get(currentProfile, 'templateConfig.branch', DEFAULT_TEMPLATE_BRANCH), + }, + ]); + const githubToken = await validateToken(); + if (!githubToken && !this.options.refresh) { + printError('Current Github credentials invalid. Reauthorization required.', this.options, false); + } + if (!githubToken || this.options.refresh) { + const { body: deviceCode } = await got.post(GITHUB_DEVICECODE_REQUEST_URL, { + json: { + client_id: GITHUB_APP_CLIENTID, + }, + responseType: 'json', + headers: { + Accept: 'application/json', + }, + }); + if (deviceCode) { + console.log(`Opening browser at ${deviceCode.verification_uri}`); + await open(deviceCode.verification_uri); + await new Promise((resolve, reject) => { + let expiry = deviceCode.expires_in; + let pollInterval = deviceCode.interval; + let accessToken; + const mom = dayjs().add(expiry, 'seconds'); + (function poller(options) { + process.stdout.write(`\x1b[0GPlease enter the following code to authorize the Cortex CLI: ${useColor(options) + ? chalk.bgBlackBright.whiteBright(`[ ${deviceCode.user_code} ]`) + : deviceCode.user_code} ${dayjs(mom.diff()).format(' [( Expires in] mm [minutes and] ss [seconds ) - CTRL-C to abort]')}`); + const pollTimer = setTimeout(async () => { + const { body } = await got.post(GITHUB_DEVICECODE_RESPONSE_URL, { + json: { + client_id: GITHUB_APP_CLIENTID, + device_code: deviceCode.device_code, + grant_type: 'urn:ietf:params:oauth:grant-type:device_code', + }, + responseType: 'json', + headers: { + Accept: 'application/json', + }, + }); + accessToken = body; + if (accessToken.access_token) { + clearTimeout(pollTimer); + persistToken(accessToken); + config.profiles[config.currentProfile].templateConfig = answers; + config.save(); + printSuccess('\x1b[0G\x1b[2KGithub token configuration successful.', options); + resolve(); + } else { + switch (accessToken.error) { + case 'authorization_pending': + expiry = Math.max(expiry - pollInterval, 0); + break; + case 'slow_down': + pollInterval += 5; + break; + case 'expired_token': + clearInterval(pollTimer); + printError('\x1b[0G\x1b[2KAccess code entry expired. Please re-run configure and try again.', options); + reject(); + return; + case 'incorrect_device_code': + printError('\x1b[0G\x1b[2KIncorrect Device Code entered.', options); + reject(); + return; + case 'access_denied': + clearInterval(pollTimer); + printError('\x1b[0G\x1b[2KAccess denied by user.', options); + reject(); + return; + default: + clearInterval(pollTimer); + printError(`\x1b[0G\x1b[2KAuthorization error: ${accessToken.error}. Please re-run configure and try again.`, options); + reject(); + return; + } + poller(options); + } + }, (pollInterval) * 1000); + }(this.options)); + }); + } else { + printError('\x1b[2KDevice Code request failed. Please try again.', this.options); + } + } else { + config.profiles[config.currentProfile].templateConfig = answers; + config.save(); + } + } catch (error) { + printError(error.message, this.options); + } + } +} diff --git a/src/commands/workspaces/generate.js b/src/commands/workspaces/generate.js new file mode 100644 index 00000000..79bc591a --- /dev/null +++ b/src/commands/workspaces/generate.js @@ -0,0 +1,185 @@ +import fs from 'node:fs'; +import path from 'path'; +import URL from 'url-parse'; +import { mkdir, writeFile } from 'fs/promises'; +import { minimatch } from 'minimatch'; +import inquirer from 'inquirer'; +import ghGot from 'gh-got'; +import { isText } from 'istextorbinary'; +import treeify from 'treeify'; +import { boolean } from 'boolean'; +import _ from 'lodash'; +import WorkspaceConfigureCommand from './configure.js'; +import { readConfig, loadProfile } from '../../config.js'; +import { printToTerminal, validateToken } from './workspace-utils.js'; +import { + printSuccess, printError, validateName, generateNameFromTitle, +} from '../utils.js'; + +const METADATA_FILENAME = 'metadata.json'; +export default class WorkspaceGenerateCommand { + constructor(program) { + this.program = program; + } + + async loadTemplateTree({ repo, branch }) { + printToTerminal('\x1b[0G\x1b[2KLoading templates...'); + this.gitRepo = repo; + this.branch = branch; + this.authorization = this.githubToken; + this.tree = await ghGot(`repos/${repo}/branches/${branch || 'main'}`, { + headers: { authorization: this.authorization }, + }) + .then((resp) => resp.body) + .then((resp) => ghGot(`repos/${repo}/git/trees/${resp.commit.sha}?recursive=true`, { + headers: { authorization: this.authorization }, + })) + .then((resp) => resp.body.tree) + .catch(() => []); + printToTerminal('\x1b[0G\x1b[2KTemplates loaded.\x1b[1E'); + } + + globTree(glob) { + const filterFunc = minimatch.filter(glob, { matchBase: true }); + return _.filter(this.tree, (f) => f.type === 'blob' && filterFunc(f.path)); + } + + async readFile(filePath) { + const response = await ghGot(`repos/${this.gitRepo}/contents/${filePath}?ref=${this.branch}`, { + headers: { authorization: this.authorization }, + }); + return Buffer.from(response.body.content, 'base64'); + } + + async getRegistry() { + const profile = await loadProfile(this.options.profile); + const registryUrl = _.get(this, 'options.registry') + || _.get(profile, `registries['${profile.currentRegistry}'].url`) + || (new URL(profile.url)).hostname.replace('api', 'private-registry'); + return registryUrl; + } + + async selectTemplate(templateName) { + const registryUrl = await this.getRegistry(); + const fileNames = this.globTree(METADATA_FILENAME); + const choices = await Promise.all(_.map(fileNames, async (value) => { + const data = JSON.parse((await this.readFile(value.path)).toString()); + return { + name: data.title, + value: { ...data, path: value.path }, + }; + })); + let template; + if (templateName) { + const templateChoice = _.find(choices, { name: templateName }); + if (templateChoice) { + template = templateChoice.value; + } else { + printError(`Template ${templateName} not found!`); + } + } + const answers = await inquirer.prompt([ + { + type: 'list', + name: 'template', + message: 'Select the template you want to use:', + choices, + }, + { + type: 'input', + name: 'name', + message: 'Enter a name for the skill:', + validate: (answer) => { + const validation = validateName(answer); + return validation.status || validation.message; + }, + }, + ], { + name: this.name, + registry: registryUrl, + template, + }).catch(() => { }); + return answers; + } + + async execute(name, destination, options) { + this.options = options; + this.name = name; + this.destination = destination; + this.config = readConfig(); + this.githubToken = await validateToken(); + const destinationPath = path.resolve(destination || process.cwd()); + if (!this.githubToken) { + printError(this.config.profiles[this.config.currentProfile].templateConfig + ? 'Github authorization is invalid. Running configuration now.\n' + : 'Workspace generator is not configured. Running configuration now.\n', this.options, false); + await (new WorkspaceConfigureCommand(this.program)).execute({ refresh: true }); + this.config = readConfig(); + } + if (name && fs.existsSync(path.join(destinationPath, 'skills', name))) { + printError(`Skill ${name} already exists!`, this.options); + return; + } + await this.loadTemplateTree(this.config.profiles[this.config.currentProfile].templateConfig); + if (this.tree.length) { + const template = await this.selectTemplate(options.template); + if (template) { + const templateFolder = path.posix.dirname(template.template.path); + const templateFiles = this.globTree(`${templateFolder}/**`); + const treeObj = {}; + if (fs.existsSync(path.join(destinationPath, 'skills', template.name))) { + printError(`Skill ${template.name} already exists!`, this.options); + return; + } + const generatedFiles = _.map(templateFiles, (f) => { + try { + const rootPathComponents = templateFolder.split('/'); + const destFile = _.drop(f.path.split('/'), rootPathComponents.length); + const targetPath = path.join(...destFile); + return _.template(targetPath, { interpolate: /__([\s\S]+?)__/g })({ + skillname: generateNameFromTitle(template.name), + }); + } catch (err) { + printError(err.message, this.options); + } + return undefined; + }).join('<br>'); + console.log(''); + await Promise.all(_.map(templateFiles, async (f) => { + try { + const fileName = path.posix.basename(f.path); + if (fileName !== METADATA_FILENAME) { + const templateVars = { + skillname: generateNameFromTitle(template.name), + generatedFiles, + template: template.template, + }; + let buf = await this.readFile(f.path); + /// Try not to template any non-text files. + if (isText(null, buf)) { + buf = Buffer.from(_.template(buf.toString(), { interpolate: /{{([\s\S]+?)}}/g })(templateVars)); + } + const relPath = f.path.slice(path.dirname(template.template.path).length); + const sourcePath = _.template(relPath, { interpolate: /__([\s\S]+?)__/g })(templateVars); + const targetPath = path.join(destinationPath, sourcePath); + await mkdir(path.dirname(targetPath), { recursive: true }); + await writeFile(targetPath, buf); + _.set(treeObj, sourcePath.split('/'), null); + } + } catch (err) { + printError(err.message, this.options); + } + })); + if (!options || !boolean(options.notree)) { + printSuccess('Generated the following files:'); + console.log(''); + console.log(destinationPath); + console.log(treeify.asTree(treeObj, true)); + } + printSuccess('Workspace generation complete.', this.options); + } + } else { + printError('Unable to retrieve templates', this.options); + } + } +} diff --git a/src/commands/workspaces/publish.js b/src/commands/workspaces/publish.js new file mode 100644 index 00000000..1e6dfabe --- /dev/null +++ b/src/commands/workspaces/publish.js @@ -0,0 +1,212 @@ +import { globSync } from 'glob'; +import fs from 'node:fs'; +import path from 'node:path'; +import { readFile } from 'node:fs/promises'; +import { v1 as uuid } from 'uuid'; +import { Listr } from 'listr2'; +import { parseObject, printSuccess, printError, handleError } from '../utils.js'; +import { getSkillInfo, buildImageTag } from './workspace-utils.js'; +import { loadProfile } from '../../config.js'; +import Catalog from '../../client/catalog.js'; +import Models from '../../client/models.js'; +import Content from '../../client/content.js'; +import Experiments from '../../client/experiments.js'; +import ApiServerClient from '../../client/apiServerClient.js'; +import getClient, { privateRegLogin } from '../../client/dockerClient.js'; + +export default class WorkspacePublishCommand { + constructor(program) { + this.program = program; + // this.docker = getClient(program.opts()); + } + + async pushAction(action, imageTag, options) { + const { skipPush, stdOutHandler } = options; + /// ------------------------------------- + /// RETAGGING IMAGE SO CORTEX PICKS IT UP + /// The K8s will not download new images unless the tag has changed ( unless ALWAYS PULL is specified) + /// TODO use git hash instead... + const buildId = uuid().substr(0, 6); + const newTag = `${imageTag}:${buildId}`; + const dockerCli = getClient(options); + await dockerCli.tag(imageTag, newTag); + /// ------------------------------------- + // Set image tag to new image tag for skill/action deployment later + // eslint-disable-next-line no-param-reassign + action.image = newTag; + if (skipPush) { + printSuccess(`Skipping docker push for: ${newTag}`); + return true; + } + return dockerCli.push(newTag, stdOutHandler); + } + + async execute(folder, options) { + this.options = options; + let target = process.cwd(); + const tasks = new Listr([], {}); + const globOpts = { + cwd: target, + absolute: true, + }; + if (folder) { + const fldr = folder.replace(/'|"/g, ''); + target = path.isAbsolute(fldr) ? folder : path.resolve(target, fldr); + } + if (options.skill) { + target = path.join(target, 'skills', options.skill, 'skill.yaml'); + if (!fs.existsSync(target)) { + throw new Error(`Skill ${options.skill} not found!`); + } + } + const skillInfo = await getSkillInfo(target); + if (skillInfo.length > 0) { + const profile = await loadProfile(options.profile); + /// If the user passed in a project name, validate it with the cluster + const project = options.project || profile.project; + if (project) { + const apiServer = new ApiServerClient(profile.url); + try { + await apiServer.getProject(profile.token, project); + } catch (err) { + handleError(err, options, `Project ${project} not found`); + } + } else { + throw new Error('Project must be specified'); + } + this.catalogClient = new Catalog(profile.url); + this.modelsClient = new Models(profile.url); + this.experimentClient = new Experiments(profile.url); + this.contentClient = new Content(profile.url); + try { + if (options?.skipPush !== true) await privateRegLogin(options); + } catch (err) { + printError(err.message, options); + } + tasks.add(skillInfo.map((info) => { + const skillName = info.skill.name; + return { + title: `Publishing skill ${skillName}`, + task: (sCtx, sTask) => { // Per skill + const subTasks = []; + const actions = info.skill.actions ? info.skill.actions : []; + subTasks.push(actions.map((action) => ({ + title: `Pushing docker images for ${action.name}`, + task: async (aCtx, aTask) => { + const tag = await buildImageTag(profile, action.image); + const imglist = await getClient(options).listImages(tag); + if (imglist.length !== 0) { + try { + await this.pushAction(action, tag, { + ...options, + stdOutHandler: (d) => aTask.output = d, + }); + aTask.title = `Pushed docker image ${action.name}`; + } catch (err) { + throw Error(`Unable to push docker image: ${err.message}`); + } + } else { + throw Error(`No image found for action ${action.name}. Has it been built?`); + } + }, + }))); + const typesFiles = globSync(`types/${skillName}/**/*.yaml`, globOpts); + subTasks.push(typesFiles.map((f) => ({ + title: `Publishing type ${path.basename(f)}`, + task: async (tCtx, tTask) => { + const typeData = await readFile(f); + if (typeData) { + const type = parseObject(typeData.toString()); + let normalizedType = {}; + if (!('types' in type)) { + normalizedType.types = [type]; + } else { + normalizedType = type; + } + await this.catalogClient.saveType(project, profile.token, normalizedType); + tTask.title = `Published type ${path.basename(f)}`; + } + }, + }))); + const modelsFiles = globSync(`models/${skillName}/**/*.yaml`, globOpts); + subTasks.push(modelsFiles.map((f) => ({ + title: `Publishing model ${path.basename(f)}`, + task: async (mCtx, mTask) => { + const modelData = await readFile(f); + if (modelData) { + const model = parseObject(modelData.toString()); + await this.modelsClient.saveModel(project, profile.token, model); + mTask.tile = `Published model ${model.name}`; + } + }, + }))); + const experimentsFiles = globSync(`experiments/${skillName}/*/*.yaml`, globOpts); + subTasks.push(experimentsFiles.map((expFile) => ({ + title: `Publishing experiment ${path.basename(expFile)}`, + task: async (eCtx, eTask) => { + const experimentData = await readFile(expFile); + if (experimentData) { + const experiment = parseObject(experimentData.toString()); + await this.experimentClient.saveExperiment(project, profile.token, experiment); + eTask.title = `Published experiment ${experiment.name}`; + const runsFiles = globSync(`experiments/${skillName}/${experiment.name}/runs/**/*.yaml`, globOpts); + await Promise.all(runsFiles.map(async (runFile) => { + const runData = await readFile(runFile); + if (runData) { + const run = parseObject(runData.toString()); + /// + /// Note: I would prefer to do an Update operation here instead, but the update endpoint + /// for experiment runs is currently broken. + /// + /// In the interim, I just delete the run (if it exists) and then re-add it. + /// + await this.experimentClient.deleteRun(project, profile.token, experiment.name, run.runId); + run.experimentName = experiment.name; + await this.experimentClient.createRun(project, profile.token, run); + eTask.title = `Published run ${run.runId}`; + const artifacts = globSync(`experiments/${skillName}/${experiment.name}/runs/${run.runId}/artifacts/*`, globOpts); + await Promise.all(artifacts.map(async (artifactUri) => { + const artifactName = path.basename(artifactUri) + .split('.')[0]; + await this.experimentClient.uploadArtifact(project, profile.token, experiment.name, run.runId, artifactUri, artifactName); + eTask.title = `Published artifact ${artifactName}`; + })); + } + })); + } else { + throw new Error('Failed to save experiment'); + } + }, + }))); + const contentFiles = globSync(`content/${skillName}/*`, globOpts); + subTasks.push(contentFiles.map((f) => ({ + title: `Uploading ${f} to managed content`, + task: async (mcCtx, mcTask) => { + const key = `content/${skillName}/${path.posix.basename(f, path.posix.extname(f))}`; + await this.contentClient.uploadContentStreaming(project, profile.token, key, f); + mcTask.tile = `Uploaded ${f}`; + }, + }))); + subTasks.push({ + title: `Saving skill ${skillName}`, + task: async (dCtx, dTask) => { + // pass options if we want to skip docker push for example + + await this.catalogClient.saveSkill(project, profile.token, info.skill); + dTask.title = `Skill ${info.skill.name} saved`; + }, + }); + return sTask.newListr(subTasks.flat(), { concurrent: false, rendererOptions: { collapseSubtasks: false } }); + }, + }; + })); + try { + await tasks.run(); + } catch (e) { + printError('Publication Failed', options, false); + } + } else { + printError('No skills found', options); + } + } +} diff --git a/src/commands/workspaces/registry.js b/src/commands/workspaces/registry.js new file mode 100644 index 00000000..df4be2bc --- /dev/null +++ b/src/commands/workspaces/registry.js @@ -0,0 +1,144 @@ +import path from 'path'; +import inquirer from 'inquirer'; +import _ from 'lodash'; +import { loadProfile, readConfig } from '../../config.js'; +import { printSuccess, printError } from '../utils.js'; + +export class WorkspaceAddRegistryCommand { + constructor(program) { + this.program = program; + } + + async execute(name, opts) { + await inquirer.prompt([ + { + type: 'input', + name: 'name', + message: 'Name for registry: ', + }, + { + type: 'input', + name: 'url', + message: 'Registry URL: ', + default: 'docker.io', + }, + { + type: 'input', + name: 'namespace', + message: 'Namespace: ', + }, + ], { + name, + url: opts.url, + namespace: opts.namespace, + }) + .then(async (answers) => { + const profile = await loadProfile(opts.profile); + profile.registries[answers.name] = { + name: answers.name, + url: answers.url, + namespace: answers.namespace, + }; + const cfg = readConfig(); + cfg.profiles[profile.name] = profile; + cfg.save(); + printSuccess(`Registry ${answers.name} added`, opts); + }) + .catch((error) => { + printError(error.message, opts); + }); + } +} +export class WorkspaceRemoveRegistryCommand { + constructor(program) { + this.program = program; + } + + async execute(name, options) { + const profile = await loadProfile(options.profile); + /// Use a function iteratee instead of the property test + const choices = _.map(_.filter(profile.registries, (r) => !r.isCortex && (!name || name === r.name)), (r) => ({ + name: r.name, + value: r.name, + })); + if (choices.length === 0) { + printError(name ? `Registry ${name} not found` : 'No registries found to remove', options); + return; + } + await inquirer.prompt([ + { + type: 'list', + name: 'name', + message: 'Select registry to remove: ', + choices, + }, + ], { + name, + }) + .then((answers) => { + delete profile.registries[answers.name]; + const cfg = readConfig(); + cfg.profiles[profile.name] = profile; + cfg.save(); + printSuccess(`Registry ${answers.name} removed`, options); + }) + .catch((error) => { + printError(error.message, options); + }); + } +} +export class WorkspaceActivateRegistryCommand { + constructor(program) { + this.program = program; + } + + async execute(regname, options) { + const name = regname || undefined; + const profile = await loadProfile(options.profile); + const choices = _.map(_.filter(profile.registries, (r) => !name || (r.name === name)), (r) => ({ + name: r.name, + value: r.name, + })); + if (choices.length === 0) { + printError(name ? `Registry ${name} not found` : 'No registries found to activate', options); + return; + } + await inquirer.prompt([ + { + type: 'list', + name: 'name', + message: 'Select registry to activate: ', + choices, + }, + ], { + name, + }) + .then((answers) => { + profile.currentRegistry = answers.name; + const cfg = readConfig(); + cfg.profiles[profile.name] = profile; + cfg.save(); + printSuccess(`Registry ${answers.name} activated`, options); + }) + .catch((error) => { + printError(error.message, options); + }); + } +} +export class WorkspaceListRegistryCommand { + constructor(program) { + this.program = program; + } + + async execute(options) { + const profile = await loadProfile(options.profile); + _.forEach(profile.registries, (r) => { + const logStr = `${r.name} - ${path.posix.join(r.url, r.namespace || '')}`; + if (r.name === profile.currentRegistry) { + printSuccess(logStr, options); + } else { + console.log(logStr); + } + }); + } +} diff --git a/src/commands/workspaces/workspace-utils.js b/src/commands/workspaces/workspace-utils.js new file mode 100644 index 00000000..1b14301f --- /dev/null +++ b/src/commands/workspaces/workspace-utils.js @@ -0,0 +1,121 @@ +import _ from 'lodash'; +import path from 'node:path'; +import { globSync } from 'glob'; +import yaml from 'js-yaml'; +import { + existsSync, readFileSync, writeFileSync, statSync, +} from 'node:fs'; +import urljoin from 'url-join'; +import { readFile } from 'node:fs/promises'; +import ghGot from 'gh-got'; +import { configDir, loadProfile } from '../../config.js'; +import { printWarning } from '../utils.js'; + +let keytar; +// Is loading keytar fails, use headless fallback for secrets.. +try { + // Don't load keytar for tests + if (process.env.NODE_ENV !== 'test') { + keytar = (await import('keytar'))?.default; + } +} catch (err) { + printWarning(`Unable to use keyring service, falling back to file-based keystore: ${err.message}`); +} +const CORTEX_CLI_AUTH_ID = 'cortex-cli:github'; +const CORTEX_CLI_AUTH_USER = 'cortex-cli'; + +// eslint-disable-next-line require-await +async function getPasswordHeaded() { + return keytar.getPassword(CORTEX_CLI_AUTH_ID, CORTEX_CLI_AUTH_USER); +} +function readLocalFile() { + const dataFile = path.join(configDir(), 'cache.dat'); + // nothing stored + if (!existsSync(dataFile)) return {}; + const rawData = readFileSync(dataFile); + const decoded = Buffer.from(rawData.toString(), 'base64').toString(); + return JSON.parse(decoded); +} +function getPasswordHeadless() { + return _.get(readLocalFile(), [CORTEX_CLI_AUTH_ID, CORTEX_CLI_AUTH_USER]); +} +async function storePasswordHeaded(val) { + await keytar.setPassword(CORTEX_CLI_AUTH_ID, CORTEX_CLI_AUTH_USER, val); +} +function storePasswordHeadless(val) { + const dataFile = path.join(configDir(), 'cache.dat'); + const originaldata = readLocalFile(); + const updated = _.set(originaldata, [CORTEX_CLI_AUTH_ID, CORTEX_CLI_AUTH_USER], val); + writeFileSync(dataFile, Buffer.from(JSON.stringify(updated)).toString('base64')); +} +export async function validateToken() { + try { + let authorization; + if (keytar !== undefined) { + authorization = await getPasswordHeaded(); + } else { + authorization = await getPasswordHeadless(); + } + if (authorization) { + const u = await ghGot('user', { headers: { authorization } }); + if (u?.statusCode === 200) return authorization; + } + } catch (err) { + console.error(err.message); + } + return undefined; +} +// eslint-disable-next-line require-await +export async function persistToken(token) { + const val = `${token.token_type} ${token.access_token}`; + if (keytar) { + return storePasswordHeaded(val); + } + return storePasswordHeadless(val); +} + +export function printToTerminal(txt) { + process.stdout.write(`${txt}\x1b[0K\x1b[1E`); +} + +export function saveTerminalPosition() { + process.stdout.write('\x1b7'); +} + +export function restoreTerminalPosition() { + process.stdout.write('\x1b8'); +} + +export function getSkillInfo(target) { + let skillFiles = []; + if (target.endsWith('skill.yaml')) { + skillFiles = statSync(path.resolve(target)) ? [target] : []; + } else { + skillFiles = globSync('./skills/**/skill.yaml', { + cwd: target, + absolute: true, + }); + } + return Promise.all(_.map(skillFiles, async (uri) => { + const skill = await readFile(uri).catch((err) => { console.error(`Unable to open skill: ${err.message}`); }); + return { + name: path.basename(uri), + uri, + skill: skill ? yaml.load(skill.toString()) : undefined, + }; + })); +} +export async function getCurrentRegistry(profile = undefined) { + const regProfile = profile || await loadProfile(); // old behavior was always getting "current" profile + // use internal name instead of key/label + return Object.values(regProfile.registries).find((r) => r.name = regProfile.currentRegistry); +} + +export async function buildImageTag(profile, actionName) { + if (actionName.includes('/')) { + return actionName; + } + const registry = await getCurrentRegistry(profile); + const namespace = registry.namespace || ''; + return urljoin(registry.url, namespace, actionName); +} diff --git a/src/compatibility.js b/src/compatibility.js new file mode 100644 index 00000000..cbd6a0a6 --- /dev/null +++ b/src/compatibility.js @@ -0,0 +1,144 @@ +import _ from 'lodash'; +import * as Table from 'cli-table3'; +import chalk from 'chalk'; +import concat from 'lodash/fp/concat.js'; +import debugSetup from 'debug'; +import filter from 'lodash/fp/filter.js'; +import getOr from 'lodash/fp/getOr.js'; +import keys from 'lodash/fp/keys.js'; +import last from 'lodash/fp/last.js'; +import isInstalledGlobally from 'is-installed-globally'; +import npmFetch from 'npm-registry-fetch'; +import Semver from 'semver'; +import { got } from './client/apiutils.js'; +import { loadProfile } from './config.js'; +import { handleError, printWarning, readPackageJSON } from './commands/utils.js'; + +const debug = debugSetup('cortex:cli'); + +const pkg = readPackageJSON('../../package.json'); + +function getAvailableVersions(name) { + debug('getAvailableVersions => %s', name); + return npmFetch + .json(pkg.name) + .then((manifest) => keys(getOr({}, 'versions', manifest))) + .then((versions) => _.uniq(concat(versions, pkg.version))) + .then((versions) => versions.sort(Semver.compare)) + .catch(() => { + throw new Error('Unable to determine CLI available versions'); + }); +} + +function getRequiredVersion(profile) { + const endpoint = `${profile.url}/fabric/v4/compatibility/applications/cortex-cli`; + debug('getRequiredVersion => %s', endpoint); + return got + .get(endpoint, { + headers: { + Authorization: `Bearer ${profile.token}`, + }, + }) + .json() + .then((res) => { + const { semver } = res; + return semver; + }).catch((err) => { + throw err; // let caller deal with it + }); +} + +export function notifyUpdate({ required = false, current, latest }) { + const message = `Update ${required ? chalk.bold('required') : 'available'} ${chalk.dim(current)}${chalk.reset(' → ')}${chalk.green(latest)}\nRun ${chalk.cyan(`npm i ${isInstalledGlobally ? '-g ' : ''}${pkg.name}@${latest}`)} to update`; + const table = new Table({ + chars: { + 'top-left': '╭', + 'top-right': '╮', + 'bottom-left': '╰', + 'bottom-right': '╯', + }, + style: { head: [], border: [] }, + }); + table.push([message]); + console.warn(table.toString()); +} + +function upgradeAvailable(args) { + process.on('exit', () => { + notifyUpdate(args); + }); +} + +function upgradeRequired(args) { + notifyUpdate({ required: true, ...args }); + process.exit(-1); +} + +// exported for testing purposes +export async function getCompatibility(profile) { + debug('getCompatibility => %s profile', profile.name); + // fail if unable to contact cortex service (allow caller to handle error) + const requirements = await getRequiredVersion(profile); + const satisfied = Semver.satisfies(pkg.version, requirements); + try { + // Warn user but don't fail if npm is unavailable + const versions = await getAvailableVersions(pkg.name); + debug('getCompatibility => versions: %s, requirements: %s', versions, requirements); + const compatibleVersions = filter((v) => Semver.satisfies(v, requirements), versions); + debug('getCompatibility => compatible versions: %s', compatibleVersions); + const { version: current } = pkg; + const latest = last(compatibleVersions); + debug('getCompatibility => satisfied: %s', satisfied); + return ({ current, latest, satisfied }); + } catch (e) { + printWarning(`Warning unable to check for cortex-cli update: ${e.message}`); + return ({ current: pkg.version, latest: pkg.version, satisfied }); + } +} + +function shouldDoCompatibilityCheck(doCheck) { + const skipCompatEnv = _.toLower(process.env.CORTEX_NO_COMPAT) !== 'true'; + return doCheck && skipCompatEnv; +} + +export async function doCompatibilityCheck(profile, doCheck = true) { + if (shouldDoCompatibilityCheck(doCheck)) { + try { + const { current, latest, satisfied } = await getCompatibility(profile); + if (!satisfied) { + upgradeRequired({ current, latest }); + } else if (Semver.gt(latest, current)) { + upgradeAvailable({ current, latest }); + } + } catch (error) { + handleError(error, undefined, 'Unable to contact cortex for compatibility check'); + } + } + return Promise.resolve(); +} + +/** + * Call the cli command + * 1) Check cli/server compability + * 2) Pass parsed args call command + * 3) handle exceptions + * @param fn + * @returns {(function(...[*]): Promise<*|undefined>)|*} + */ +export function callCommand(fn) { + return async (...args) => { + try { + const command = args.find((a) => a !== undefined && typeof a.opts === 'function'); + const options = command.opts(); + // Explicit duplicate compatibility check to avoid unnecessarily loading the Profile + if (shouldDoCompatibilityCheck(options.compat)) { + const { profile: profileName } = options; + const profile = await loadProfile(profileName); + await doCompatibilityCheck(profile); + } + return fn(...args); + } catch (err) { + handleError(err); + } + }; +} diff --git a/src/config.js b/src/config.js index 8a854eaa..208d7133 100644 --- a/src/config.js +++ b/src/config.js @@ -1,147 +1,545 @@ -/* - * Copyright 2018 Cognitive Scale, Inc. All Rights Reserved. - * - * 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. - */ - -const os = require('os'); -const fs = require('fs'); -const path = require('path'); -const Joi = require('joi'); -const debug = require('debug')('cortex:config'); - -module.exports.defaultConfig = defaultConfig = function(){ - return new Config({}); -}; +import _ from 'lodash'; +import os from 'os'; +import fs from 'node:fs'; +import path from 'path'; +import Joi from 'joi'; +import dayjs from 'dayjs'; +import debugSetup from 'debug'; +import * as jose from 'jose'; +import { printError } from './commands/utils.js'; +import Info from './client/info.js'; +import { getDefaultFeatures } from './features.js'; + +const debug = debugSetup('cortex:config'); -module.exports.readConfig = readConfig = function() { - const configDir = process.env.CORTEX_CONFIG_DIR || path.join(os.homedir(), '.cortex'); - if (!fs.existsSync(configDir)) { - fs.mkdirSync(configDir); +function configDir() { + return process.env.CORTEX_CONFIG_DIR || path.join(os.homedir(), '.cortex'); +} +// TODO deprecate CORTEX_URI +function getCortexUrlFromEnv() { + if (process.env.CORTEX_URI) { + return process.env.CORTEX_URI; } + return process.env.CORTEX_URL; +} - debug(`Reading config from ${configDir}`); +const durationRegex = /^([.\d]+)(ms|[smhdwMy])$/; - const configFile = path.join(configDir, 'config'); - if (fs.existsSync(configFile)) { - // deal with config versions - const configObj = JSON.parse(fs.readFileSync(configFile)); - if (configObj.version && configObj.version === '2') { - // version 2 - // debug('loaded v2 config: %o', configObj); - return new Config(configObj); - } +async function computeJwt(profile, serverTs, expiresIn = '2m') { + const { + username, issuer, audience, jwk, + } = profile; + const alg = jwk?.alg || 'EdDSA'; + const jwtSigner = await jose.importJWK(jwk, alg); + const [, amount, unit] = durationRegex.exec(expiresIn); + const expiry = dayjs(serverTs).add(_.toNumber(amount), unit).unix(); + return new jose.SignJWT({}) + .setProtectedHeader({ alg, kid: jwk.kid }) + .setSubject(username) + .setAudience(audience) + .setIssuer(issuer) + .setIssuedAt(Math.floor(serverTs / 1000)) // in seconds + .setExpirationTime(expiry) + .sign(jwtSigner, { kid: jwk.kid }); +} - // version 1 - // debug('loaded v1 config: %o', configObj); - return new Config({profiles: configObj}); - } +async function fetchInfoForProfile(profile, expiresIn = '2m') { + const infoClient = new Info(profile.url); + const infoResp = await infoClient.getInfo(); + const serverTs = infoResp?.serverTs ?? Date.now(); + const featureFlags = infoResp?.enabledFeatures ?? getDefaultFeatures(); + const jwt = await computeJwt(profile, serverTs, expiresIn); + return { jwt, featureFlags }; +} - return undefined; -}; +async function generateJwt(profile, expiresIn = '2m') { + const { jwt } = await fetchInfoForProfile(profile, expiresIn); + return jwt; +} -const ProfileSchema = Joi.object().keys({ +const ProfileSchema = Joi.object({ + name: Joi.string().optional(), + url: Joi.string().uri().required(), + username: Joi.string().required(), + // deprecated + account: Joi.string().optional(), + jwk: Joi.any().required(), + issuer: Joi.string().required(), + audience: Joi.string().required(), + token: Joi.string().optional(), + project: Joi.string().optional(), + featureFlags: Joi.object().optional(), +}); +const ProfileSchemaV4 = Joi.object().keys({ name: Joi.string().optional(), url: Joi.string().uri().required(), username: Joi.string().required(), - account: Joi.string().required(), - token: Joi.string().required() + // deprecated + account: Joi.string().optional(), + jwk: Joi.any().required(), + issuer: Joi.string().required(), + audience: Joi.string().required(), + token: Joi.string().optional(), + project: Joi.string().optional(), + registries: Joi.any().required(), + currentRegistry: Joi.string().required(), + featureFlags: Joi.object().optional(), +}); +const ProfileSchemaV5 = Joi.object().keys({ + name: Joi.string().optional(), + url: Joi.string().uri().required(), + username: Joi.string().required(), + // deprecated + account: Joi.string().optional(), + jwk: Joi.any().required(), + issuer: Joi.string().required(), + audience: Joi.string().required(), + token: Joi.string().optional(), + project: Joi.string().optional(), + registries: Joi.any().required(), + currentRegistry: Joi.string().required(), + templateConfig: Joi.any().required(), + featureFlags: Joi.object().optional(), }); -class Profile { +/* eslint-disable no-param-reassign */ +// profileType is a validated (Profile, ProfileV4, or ProfileV5) +async function loadDynamicProfileProps(profileType, useenv) { + // Side load JWT & Feature Flags - this is used to avoid redundant calls + // to the Info API (`fetchInfoForProfile()`). + if (process.env.CORTEX_TOKEN_SILENT) { + profileType.token = process.env.CORTEX_TOKEN_SILENT; + debug('Side loaded token from previous info request'); + } + if (process.env.CORTEX_FEATURE_FLAGS) { + try { + profileType.featureFlags = JSON.parse(process.env.CORTEX_FEATURE_FLAGS); + debug('Side loaded feature flags token from previous info request'); + } catch (err) { + // fail silently - let feature flags to be retrieved again + debug(`Failed to side loaded feature flags token from previous info request - ${err}`); + } + } + // Load user facing environment variables - these have higher priority than + // the above (silent) options. + if (useenv) { + let jwtFromEnv; + if (process.env.CORTEX_TOKEN) { + jwtFromEnv = process.env.CORTEX_TOKEN; + } + profileType.url = getCortexUrlFromEnv() || profileType.url; + profileType.token = jwtFromEnv || profileType.token; + profileType.project = process.env.CORTEX_PROJECT || profileType.project; + } + // In the case that either value is missing, call the Info API + if (!profileType.token || !profileType.featureFlags) { + debug(`JWT or Feature Flags is not defined - making info request - ${profileType.token}, ${profileType.featureFlags}`); + const { jwt, featureFlags } = await fetchInfoForProfile(profileType); + profileType.token = jwt; + profileType.featureFlags = featureFlags || getDefaultFeatures(); + } else { + debug('JWT & Feature Flags are defined - skipping info request'); + } + return profileType; +} +/* eslint-enable no-param-reassign */ - constructor(name, {url, username, tenantId, account, token}) { +class Profile { + constructor(name, { + url, username, issuer, audience, jwk, project, featureFlags, +}) { this.name = name; this.url = url; this.username = username; - this.account = tenantId || account; - this.token = token; + this.jwk = jwk; + this.issuer = issuer; + this.audience = audience; + this.project = project; + this.featureFlags = featureFlags; } validate() { - const {error, value} = Joi.validate(this, ProfileSchema); + const { error } = ProfileSchema.validate(this, { abortEarly: false }); if (error) { - throw new Error(`Invalid configuration profile <${this.name}>: ${error.details[0].message}. Please run "cortex configure".`); + throw new Error(`Invalid configuration profile <${this.name}>: ${error.details[0].message}. ` + + 'Please get your Personal Access Token from the Cortex Console and run "cortex configure".'); } return this; } toJSON() { - return {url: this.url, username: this.username, account: this.account, token: this.token}; + return { + url: this.url, + username: this.username, + issuer: this.issuer, + audience: this.audience, + jwk: this.jwk, + project: this.project, + featureFlags: this.featureFlags, + }; } } +class Config { + constructor({ version, profiles, currentProfile }) { + this.version = version || '3'; + this.profiles = profiles || {}; + this.currentProfile = currentProfile; + Object.keys(this.profiles).forEach((name) => { + this.profiles[name] = new Profile(name, this.profiles[name]); + }); + } -const ConfigSchema = Joi.object().keys({ - version: Joi.string().optional().default('1'), - profiles: Joi.object().optional(), - currentProfile: Joi.string().optional().default('default') -}); + async getProfile(name, useenv = true) { + const profile = this.profiles[name]; + if (!profile) { + debug('profile not found'); + return undefined; + } + const profileType = new Profile(name, profile).validate(); + await loadDynamicProfileProps(profileType, useenv); + return profileType; + } -class Config { + setProfile(name, { + url, username, issuer, audience, jwk, +}, project) { + const profile = new Profile(name, { + url, username, issuer, audience, jwk, project, + }); + profile.validate(); // do not set/save invalid profiles .. + this.profiles[name] = profile; + } + + toJSON() { + const profiles = {}; + Object.keys(this.profiles).forEach((name) => { + profiles[name] = this.profiles[name].toJSON(); + }); + return { + version: this.version, + profiles, + currentProfile: this.currentProfile, + }; + } - constructor({version, profiles, currentProfile}) { - this.version = version || '2'; + save() { + const dir = configDir(); + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir); + } + _.forEach(this.profiles, (profile) => { + delete profile.token; + }); + const configFile = path.join(dir, 'config'); + fs.writeFileSync(configFile, JSON.stringify(this.toJSON(), null, 2)); + } +} +class ProfileV4 { + constructor(name, { + url, username, issuer, audience, jwk, project, registries, currentRegistry, featureFlags, +}) { + this.name = name; + this.url = url; + this.username = username; + this.jwk = jwk; + this.issuer = issuer; + this.audience = audience; + this.project = project; + this.registries = registries || { + 'Cortex Private Registry': { + url: new URL(url).hostname.replace('api', 'private-registry'), + name: 'Cortex Private Registry', + isCortex: true, + }, + }; + this.currentRegistry = currentRegistry || 'Cortex Private Registry'; + this.featureFlags = featureFlags; + } + + validate() { + const { error } = ProfileSchemaV4.validate(this, { abortEarly: false }); + if (error) { + printError(`Invalid configuration profile <${this.name}>: ${error.details[0].message}. ` + + 'Please get your Personal Access Token from the Cortex Console and run "cortex configure".'); + } + return this; + } + + toJSON() { + return { + url: this.url, + username: this.username, + issuer: this.issuer, + audience: this.audience, + jwk: this.jwk, + project: this.project, + registries: this.registries, + currentRegistry: this.currentRegistry, + }; + } +} +class ConfigV4 { + constructor({ profiles, currentProfile, templateConfig }) { + this.version = '4'; this.profiles = profiles || {}; this.currentProfile = currentProfile; + Object.keys(this.profiles).forEach((name) => { + this.profiles[name] = new ProfileV4(name, this.profiles[name]); + }); + this.templateConfig = templateConfig || { + repo: 'CognitiveScale/cortex-code-templates', + branch: 'main', + }; + } - for (let name of Object.keys(this.profiles)) { - this.profiles[name] = new Profile(name, this.profiles[name]); - } + setProfile(name, { + url, username, issuer, audience, jwk, registries, currentRegistry, +}, project) { + const profile = new ProfileV4(name, { + url, username, issuer, audience, jwk, project, registries, currentRegistry, + }); + profile.validate(); // do not set/save invalid profiles .. + this.profiles[name] = profile; } - getProfile(name) { + toJSON() { + const profiles = {}; + Object.keys(this.profiles).forEach((name) => { + profiles[name] = this.profiles[name].toJSON(); + }); + return { + version: this.version, + profiles, + currentProfile: this.currentProfile, + templateConfig: this.templateConfig, + }; + } + + async getProfile(name, useenv = true) { const profile = this.profiles[name]; if (!profile) { - return null; + return undefined; } + const profileType = new ProfileV4(name, profile).validate(); + await loadDynamicProfileProps(profileType, useenv); + return profileType; + } + + save() { + const dir = configDir(); + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir); + } + _.forEach(this.profiles, (profile) => { + delete profile.token; + }); + const configFile = path.join(dir, 'config'); + fs.writeFileSync(configFile, JSON.stringify(this.toJSON(), null, 2)); + } +} +class ProfileV5 { + constructor(name, { + url, username, issuer, audience, jwk, project, registries, currentRegistry, templateConfig, featureFlags, +}, templateConfigV4) { + this.name = name; + this.url = url; + this.username = username; + this.jwk = jwk; + this.issuer = issuer; + this.audience = audience; + this.project = project; + this.registries = registries || { + 'Cortex Private Registry (generated from URL)': { + url: new URL(url).hostname.replace('api', 'private-registry'), + name: 'Cortex Private Registry', + isCortex: true, + }, + }; + this.currentRegistry = currentRegistry || 'Cortex Private Registry (generated from URL)'; + this.templateConfig = templateConfig || templateConfigV4 || { + repo: 'CognitiveScale/cortex-code-templates', + branch: 'main', + }; + this.featureFlags = featureFlags; + } + + validate() { + const { error } = ProfileSchemaV5.validate(this, { abortEarly: false }); + if (error) { + printError(`Invalid configuration profile <${this.name}>: ${error.details[0].message}. ` + + 'Please get your Personal Access Token from the Cortex Console and run "cortex configure".'); + } + return this; + } - return new Profile(name, profile).validate(); + toJSON() { + return { + url: this.url, + username: this.username, + issuer: this.issuer, + audience: this.audience, + jwk: this.jwk, + project: this.project, + registries: this.registries, + currentRegistry: this.currentRegistry, + templateConfig: this.templateConfig, + featureFlags: this.featureFlags, // TODO: is featureFlags needed as a field? + }; + } +} +class ConfigV5 { + constructor({ profiles, currentProfile, templateConfig }) { + this.version = '5'; + this.profiles = profiles || {}; + this.currentProfile = currentProfile; + Object.keys(this.profiles).forEach((name) => { + this.profiles[name] = new ProfileV5(name, this.profiles[name], templateConfig); + }); } - setProfile(name, {url, account, tenantId, username, token}) { - this.profiles[name] = new Profile(name, {url, username, account, tenantId, token}); + setProfile(name, { + url, username, issuer, audience, jwk, registries, currentRegistry, templateConfig, +}, project) { + const profile = new ProfileV5(name, { + url, username, issuer, audience, jwk, project, registries, currentRegistry, templateConfig, + }); + profile.validate(); // do not set/save invalid profiles .. + this.profiles[name] = profile; } toJSON() { const profiles = {}; - for (let name of Object.keys(this.profiles)) { + Object.keys(this.profiles).forEach((name) => { profiles[name] = this.profiles[name].toJSON(); - } + }); + return { + version: this.version, + profiles, + currentProfile: this.currentProfile, + }; + } - return {version: this.version, profiles: profiles, currentProfile: this.currentProfile}; + async getProfile(name, useenv = true) { + const profile = this.profiles[name]; + if (!profile) { + return undefined; + } + const profileType = new ProfileV5(name, profile).validate(); + await loadDynamicProfileProps(profileType, useenv); + return profileType; } save() { - const configDir = path.join(os.homedir(), '.cortex'); - if (!fs.existsSync(configDir)) { - fs.mkdirSync(configDir); + const dir = configDir(); + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir); } - - const configFile = path.join(configDir, 'config'); + _.forEach(this.profiles, (profile) => { + delete profile.token; + }); + const configFile = path.join(dir, 'config'); fs.writeFileSync(configFile, JSON.stringify(this.toJSON(), null, 2)); } } +function defaultConfig() { + return new ConfigV5({}); +} +function readConfig() { + try { + const dir = configDir(); + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir); + } + debug(`Reading config from ${dir}`); + const configFile = path.join(dir, 'config'); + if (fs.existsSync(configFile)) { + // deal with config versions + const configObj = JSON.parse(fs.readFileSync(configFile)); + if (configObj.version) { + switch (configObj.version) { + case '5': + return new ConfigV5(configObj); + case '4': + { + // version 5 (per-profile template support) + // Upgrade v4 to v5 + const cfg = new ConfigV5(configObj); + cfg.save(); + return cfg; + } + case '3': + { + // version 4 (template config support, with github token) + // Upgrade v3 to v4 + const cfg = new ConfigV4(configObj); + cfg.save(); + return cfg; + } + case '2': + fs.copyFileSync(configFile, path.join(dir, 'config_v2')); + defaultConfig().save(); + printError('Old profile found and moved to ~/.cortex/config_v2. Please run "cortex configure"'); + // version 2 + return new Config(configObj); + default: + // version 1 + return new Config({ profiles: configObj }); + } + } + } else { + debug('Configuration file does not exist'); + } + } catch (err) { + debug(`Failed to read config: ${err}`); + throw new Error(`Unable to load config: ${err.message}`); + } + return undefined; +} -module.exports.loadProfile = function(profileName) { +async function loadProfile(profileName, useenv = true) { + debug(`loadProfile() => ${profileName} (using env: ${useenv})`); const config = readConfig(); if (config === undefined) { - throw new Error(`Please configure the Cortex CLI by running "cortex configure"`); + printError('Please configure the Cortex CLI by running "cortex configure"'); } - const name = profileName || config.currentProfile || 'default'; - const profile = config.getProfile(name); + const profile = await config.getProfile(name, useenv); if (!profile) { - throw new Error(`Profile with name "${name}" could not be located in your configuration. Please run "cortex configure".`); + printError(`Profile with name "${name}" could not be located in your configuration. Please run "cortex configure".`); } return profile; +} + +async function loadProfileAllowingUnconfiguredCLI(profileName, useenv = true) { + debug('loadProfileAllowingUnconfiguredCLI() => %s (using env: %s)', profileName, useenv); + const config = readConfig(); + if (config === undefined) { + debug('Cortex CLI not configured'); + return undefined; // return even if config doesn't exist + } + const name = profileName || config.currentProfile || 'default'; + const profile = await config.getProfile(name, useenv); + debug('Loaded profile with name "%s"', name); + if (!profile) { + debug('Profile with name "%s" not found in configuration', name); + } + return profile; // return even if its undefined +} + +export { durationRegex }; +export { loadProfile }; +export { loadProfileAllowingUnconfiguredCLI }; +export { generateJwt }; +export { fetchInfoForProfile }; +export { defaultConfig }; +export { configDir }; +export { readConfig }; +export default { + durationRegex, + loadProfile, + loadProfileAllowingUnconfiguredCLI, + defaultConfig, + configDir, + readConfig, + fetchInfoForProfile, }; diff --git a/src/constants.js b/src/constants.js new file mode 100644 index 00000000..af8e161a --- /dev/null +++ b/src/constants.js @@ -0,0 +1,81 @@ +export const ALLOWED_QUERY_FIELDS = { + SKILL: { + filter: ['name', 'title', 'description', 'createdBy'], + sort: ['name', 'title', 'description', 'createdAt', 'createdBy', 'updatedAt'], + }, + AGENT: { + filter: ['name', 'title', 'description', 'createdBy'], + sort: ['name', 'title', 'description', 'createdAt', 'createdBy', 'updatedAt'], + }, + ACTION: { + filter: ['name', 'title', 'type', 'image', 'description', 'createdBy'], + sort: ['name', 'title', 'type', 'image', 'description', 'createdAt', 'createdBy', 'updatedAt'], + }, + SNAPSHOT: { + filter: ['snapshotId', 'title', '_isTip', '_createdBy'], + sort: ['snapshotId', 'title', '_isTip', '_createdAt', '_createdBy', '_updatedAt'], + }, + ASSESSMENT: { + filter: ['name', 'title', 'componentName', 'reportCount', '_createdBy'], + sort: ['name', 'title', 'componentName', 'reportCount', '_createdBy', '_updatedAt'], + }, + TYPE: { + filter: ['name', 'title', 'description', 'createdBy'], + sort: ['name', 'title', 'description', 'createdAt', 'createdBy', 'updatedAt'], + }, + CONNECTION: { + filter: ['name', 'title', 'description', 'createdBy', 'connectionType', 'allowWrite', 'contentType'], + sort: ['name', 'title', 'description', 'createdAt', 'connectionType', 'allowWrite', 'contentType', 'updatedAt'], + }, + CONNECTION_TYPE: { + filter: ['name', 'title', 'description', 'type', 'group'], + sort: ['name', 'title', 'description', 'type', 'group'], + }, + RESOURCE: { + filter: ['name', 'title', 'type', '_projectId'], + sort: ['resourceName', 'resourceTitle', 'resourceType', '_projectId'], + }, + EXPERIMENT: { + filter: ['name', 'title', 'description', 'modelId'], + sort: ['name', 'title', 'description', 'modelId', 'createdAt', 'updatedAt'], + }, + RUN: { + filter: ['runId', '_createdAt', 'startTime', 'endTime', 'took', 'experimentName'], + sort: ['runId', '_createdAt', 'startTime', 'endTime', 'took', 'experimentName', '_updatedAt'], + }, + MODEL: { + filter: ['name', 'title', 'description', 'createdBy', 'type', 'status'], + sort: ['name', 'title', 'description', 'createdAt', 'updatedAt', 'createdBy', 'type', 'status'], + }, + CAMPAIGN: { + filter: ['name', 'title', 'description', '_createdBy', 'lifecycleState'], + sort: ['name', 'title', 'description', '_createdAt', '_createdBy', 'lifecycleState', '_updatedAt'], + }, + MISSION: { + filter: ['name', 'title', 'description', '_createdBy', 'lifecycleState'], + sort: ['name', 'title', 'description', '_createdAt', '_createdBy', 'lifecycleState', '_updatedAt'], + }, + PROJECT: { + filter: ['name', 'title', 'description'], + sort: ['name', 'title', 'description', '_createdAt', '_updatedAt'], + }, + ACTIVATION: { + filter: ['status', 'start', 'end', 'skillName', 'agentName'], + sort: ['status', 'start', 'end', 'skillName', 'agentName'], + }, + TASK: { + filter: ['name', 'state', 'startTime', 'endTime', 'skillName', 'actionName', 'jobType', 'scheduled'], + sort: ['name', 'state', 'startTime', 'endTime', 'skillName', 'actionName', 'jobType'], + }, +}; +export const DEFAULT_LIST_LIMIT_COUNT = '20'; +export const DEFAULT_LIST_SKIP_COUNT = '0'; +export const DEFAULT_LIST_SORT_PARAMS = { + updatedAt: 'updatedAt', + _updatedAt: '_updatedAt', + start: 'start', + startTime: 'startTime', // tasks +}; +export const LIST_JSON_HELP_TEXT = 'Output results in JSON, supports JMESPath query to filter the response data'; +export const QUERY_JSON_HELP_TEXT = 'A JMESPath query to use in filtering the response data'; +export const GET_DEFAULT_SORT_CLI_OPTION = (param) => JSON.stringify({ [param]: -1 }); diff --git a/src/features.js b/src/features.js new file mode 100644 index 00000000..79a094ed --- /dev/null +++ b/src/features.js @@ -0,0 +1,109 @@ +import debugSetup from 'debug'; + +const debug = debugSetup('cortex:config'); + +export function allGAFeaturesEnabled(featureFlags) { + return (featureFlags?.ga?.enabled ?? false) || process.env.ALL_GA_FEATURES_ENABLED; +} + +export function previewFeaturesEnabled(featureFlags) { + return (featureFlags?.preview?.enabled ?? false) || process.env.PREVIEW_FEATURES_ENABLED; +} + +export class FeatureFlag { + /** + * Creates a Feature Flag. + * + * @param {string} flagName + * @param {string} status GA | Preview + * @param {string[]} subcommands CLI subcommands corresponding to the Feature Flag + */ + constructor(flagName, status, subcommands) { + this.name = flagName; + this.status = status; + this.subcommands = subcommands; + } +} + +// exported for testing purposes +// Group -> Feature Flag -> CLI Subcommands +export const FLAG_SUBCOMMANDS = { + ga: { }, // TODO: to be finalized + preview: { + 'data-fabric-pipelines': ['pipelines'], + }, + default: { + // default commands - always enabled. These should be grouped into 'ga' + // features, but those are TBD, so existing commands count as default. + all: [ + 'actions', 'agents', 'applications', 'assessments', 'campaigns', 'configure', 'connections', 'content', 'deploy', + 'docker', 'experiments', 'missions', 'models', 'projects', 'roles', 'secrets', 'sessions', + 'skills', 'stereotypes', 'tasks', 'types', 'users', 'workspaces', + ], + }, +}; + +export function getDefaultFeatures() { + return { + ga: {}, + preview: { + enabled: false, + features: { + 'data-fabric-pipelines': { + enabled: false, + }, + }, + }, + }; +} + +export function getAllSubcommands() { + return [...FLAG_SUBCOMMANDS.default.all, ...FLAG_SUBCOMMANDS.preview['data-fabric-pipelines']]; +} + +export class FeatureController { + constructor(profile) { + this.profile = profile; + } + + /** + * Computes the set of subcommands that are supported. + * + * @param {object} featureFlagSet feature flag group (e.g. ga or preview) + * @param {Object.<string,string[]>} flagSubcommands map of feature flags to corresponding subcommands + * @param {boolean} allEnabled whether all feature flags in the group should be treated as enabled + * @return {string[]} list of strings + */ + collectSubcommands(featureFlagSet, flagSubcommands, allEnabled = false) { + if (allEnabled) { + // collect all subcommands + return Object.keys(flagSubcommands) + .map((k) => flagSubcommands[k]) + .flat(); + } + // only include subcommands for features that individuall enabled + return Object.keys(flagSubcommands) + .filter((k) => featureFlagSet?.features?.[k]?.enabled ?? false) + .map((k) => flagSubcommands[k]) + .flat(); + } + + /** + * Return the list of supported subcommands for the Profile. + * @returns {string[]} + */ + getSupportedSubCommands() { + // Collect GA Features & Preview Features, but always include defaults + const previewSubcommands = this.collectSubcommands(this.profile.featureFlags?.preview ?? {}, FLAG_SUBCOMMANDS.preview, previewFeaturesEnabled(this.profile.featureFlags)); + debug('Collected Preview subcommands: %s', JSON.stringify(previewSubcommands)); + const gaSubcommands = this.collectSubcommands(this.profile.featureFlags?.ga ?? {}, FLAG_SUBCOMMANDS.ga, allGAFeaturesEnabled(this.profile.featureFlags)); + debug('Collected GA subcommands: %s', JSON.stringify(gaSubcommands)); + debug('Collected default commands: %s', JSON.stringify(FLAG_SUBCOMMANDS.default.all)); + const supportedCommands = [ + ...FLAG_SUBCOMMANDS.default.all, + ...gaSubcommands, + ...previewSubcommands, + ]; + return supportedCommands; + } +} diff --git a/src/parsers.js b/src/parsers.js new file mode 100644 index 00000000..599cd9dd --- /dev/null +++ b/src/parsers.js @@ -0,0 +1,38 @@ +import _ from 'lodash'; +import { program } from 'commander'; + +function nonEmptyStringParser(opts) { + const { message = 'empty string argument', variadic = false } = opts; + return function parseNonEmptyString(value, oldValue) { + if (_.isEmpty(value)) { + const error = `error: ${message}`; + console.error(error); + program._exit(1, 'cortex.invalidStringArgument', error); + } + if (variadic) { + if (!Array.isArray(oldValue)) { + return [value]; + } + return oldValue.concat(value); + } + return value; + }; +} +// append relevant prefix to keys +function transformDynamicParams(params, prefix) { + const jsonParams = JSON.parse(params); + Object.keys(jsonParams) + .forEach((key) => { + if (!_.startsWith(prefix, key)) { + jsonParams[`${prefix}${key}`] = jsonParams[key]; + delete jsonParams[key]; + } + }); + return JSON.stringify(jsonParams); +} +export { nonEmptyStringParser }; +export { transformDynamicParams }; +export default { + nonEmptyStringParser, + transformDynamicParams, +}; diff --git a/test/compatibility.js b/test/compatibility.js new file mode 100644 index 00000000..d0e705c0 --- /dev/null +++ b/test/compatibility.js @@ -0,0 +1,142 @@ +import chai from 'chai'; +import chaiAsPromised from 'chai-as-promised'; +import semver from 'semver'; +import nock from 'nock'; +import npmFetch from 'npm-registry-fetch'; +import sinon from 'sinon'; +import * as compatibility from '../src/compatibility.js'; +import { readPackageJSON } from '../src/commands/utils.js'; + +chai.use(chaiAsPromised); +const { expect } = chai; + +const pkg = readPackageJSON('../../package.json'); +let sandbox; + +describe('compatibility checks', () => { + const profile = { + url: 'http://example.com', + account: 'testAccount', + projectId: 'testTenant', + username: 'testUser', + token: 'testToken', + }; + describe('with a compatible application version', () => { + before(() => { + sandbox = sinon.createSandbox(); + sandbox.stub(npmFetch, 'json') + .returns(Promise.resolve({ versions: { [pkg.version]: {} } })); + nock(profile.url) + .get('/fabric/v4/compatibility/applications/cortex-cli') + .reply(200, () => ({ semver: pkg.version })); + }); + after(() => { + sandbox.restore(); + nock.cleanAll(); + nock.enableNetConnect(); + }); + it('should resolve as satisfied', () => { + const expected = { current: pkg.version, latest: pkg.version, satisfied: true }; + return expect(compatibility.getCompatibility(profile)).to.become(expected); + }); + }); + describe('with an incompatible application version', () => { + const nextMajorVersion = semver.inc(pkg.version, 'major'); + before(() => { + nock(profile.url) + .get('/fabric/v4/compatibility/applications/cortex-cli') + .reply(200, () => ({ semver: nextMajorVersion })); + sandbox = sinon.createSandbox(); + sandbox.stub(npmFetch, 'json') + .returns(Promise.resolve({ + versions: { + [pkg.version]: {}, + [nextMajorVersion]: {}, + }, + })); + }); + after(() => { + sandbox.restore(); + nock.cleanAll(); + nock.enableNetConnect(); + }); + it('should resolve as unsatisfied', () => { + const expected = { current: pkg.version, latest: nextMajorVersion, satisfied: false }; + return expect(compatibility.getCompatibility(profile)).to.become(expected); + }); + // it('should resolve as box', () => { + // // Just added a dumb test to test output.. + // compatibility.notifyUpdate({ required: false, current: '1.0.0', latest: '2.0.0' }); + // }); + }); + describe('when the compatibility service cannot be reached', () => { + before(() => { + nock(profile.url) + .get('/fabric/v4/compatibility/applications/cortex-cli') + .reply(500, () => ('BOOM')); + sandbox = sinon.createSandbox(); + sandbox.stub(npmFetch, 'json') + .returns(Promise.resolve({ versions: { [pkg.version]: {} } })); + }); + after(() => { + sandbox.restore(); + nock.cleanAll(); + nock.enableNetConnect(); + }); + it('should reject', () => expect(compatibility.getCompatibility(profile)).to.be.rejected); + }); + describe('when the npm cannot be reached', () => { + before(() => { + nock(profile.url) + .get('/fabric/v4/compatibility/applications/cortex-cli') + .reply(200, () => ({ semver: pkg.version })); + sandbox = sinon.createSandbox(); + sandbox.stub(npmFetch, 'json') + .throws(new Error('BOOM!')); + }); + after(() => { + sandbox.restore(); + nock.cleanAll(); + nock.enableNetConnect(); + }); + it('should not reject', () => { + // should default to current package version... + const expected = { current: pkg.version, latest: pkg.version, satisfied: true }; + return expect(compatibility.getCompatibility(profile)).to.become(expected); + }); + }); + describe('when compatibility check should be skipped', () => { + beforeEach(() => { + sandbox = sinon.createSandbox(); + sandbox.stub(npmFetch, 'json') + .returns(Promise.resolve({ versions: { [pkg.version]: {} } })); + nock(profile.url) + .get('/fabric/v4/compatibility/applications/cortex-cli') + .reply(200, () => ({ semver: pkg.version })); + delete process.env.CORTEX_NO_COMPAT; + }); + afterEach(() => { + sandbox.restore(); + nock.cleanAll(); + nock.enableNetConnect(); + delete process.env.CORTEX_NO_COMPAT; + }); + it('runs compatiblity check by default', async () => { + await compatibility.doCompatibilityCheck(profile); + sandbox.assert.calledOnce(npmFetch.json); + }); + + it('should skip the compatibility check - env', async () => { + process.env.CORTEX_NO_COMPAT = 'true'; + await compatibility.doCompatibilityCheck(profile); + // verify that check skipped by checking npmFetch wasn't called + sandbox.assert.notCalled(npmFetch.json); + }); + + it('should skip the compatibility check - args', async () => { + await compatibility.doCompatibilityCheck(profile, false); + // verify that check skipped by checking npmFetch wasn't called + sandbox.assert.notCalled(npmFetch.json); + }); + }); +}); diff --git a/test/configure.js b/test/configure.js new file mode 100644 index 00000000..65177f62 --- /dev/null +++ b/test/configure.js @@ -0,0 +1,240 @@ +import mockedEnv from 'mocked-env'; +import _ from 'lodash'; +import assert from 'assert'; +import os from 'os'; +import path from 'path'; +import fs from 'node:fs'; +import sinon from 'sinon'; +import { expect } from 'chai'; +import yaml from 'js-yaml'; +import { stripAnsi } from './utils.js'; +import Info from '../src/client/info.js'; +import { create } from '../bin/cortex-configure.js'; + +const iatDate = new Date('January 11, 2011 11:11:11').getTime(); +function parseJwt(jwt) { + const parts = jwt.split('.'); + const header = JSON.parse(Buffer.from(parts[0], 'base64') + .toString('ascii')); + const payload = JSON.parse(Buffer.from(parts[1], 'base64') + .toString('ascii')); + return { + header, + payload, + }; +} +describe('configure', () => { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cortex-cli')); + let restore; + let sandbox; + let printSpy; + let errorSpy; + before(() => { + restore = mockedEnv({ + CORTEX_CONFIG_DIR: tmpDir, + }); + }); + beforeEach(() => { + // delete require.cache[require.resolve('commander')]; + // delete require.cache[require.resolve('../bin/cortex-configure')]; + sandbox = sinon.createSandbox(); + sandbox.stub(process, 'exit').throws(Error('EXIT')); + sandbox.stub(Info.prototype, 'getInfo').callsFake(() => ({ + serverTs: iatDate, + })); + printSpy = sandbox.spy(console, 'log'); + errorSpy = sandbox.spy(console, 'error'); + fs.copyFileSync('./test/cortex/config', path.join(tmpDir, 'config')); + // reset env variables + delete process.env.CORTEX_TOKEN; + delete process.env.CORTEX_URI; + delete process.env.CORTEX_TIMEOUT_LOOKUP; + delete process.env.CORTEX_TIMEOUT_CONNECT; + delete process.env.CORTEX_TIMEOUT_SECURE_CONNECT; + delete process.env.CORTEX_TIMEOUT_SOCKET; + delete process.env.CORTEX_TIMEOUT_RESPONSE; + delete process.env.CORTEX_API_RETRY; + }); + afterEach(() => { + sandbox.restore(); + }); + after(() => { + restore(); + }); + function getPrintedLines() { + return _.flatten(printSpy.args).map((s) => stripAnsi(s)); + } + function getErrorLines() { + return _.flatten(errorSpy.args).map((s) => stripAnsi(s)); + } + it('lists profiles', async () => { + await create().parseAsync(['node', 'configure', 'list']); + sandbox.assert.calledTwice(printSpy); + // default is set at default... + expect(getPrintedLines()).to.eql(['default', 'other']); + }); + it('get profile other', async () => { + await create().parseAsync(['node', 'configure', 'describe', 'other']); + const output = getPrintedLines(); + expect(output).to.length(6); + expect(output[4]).to.eql('Project: otherProj'); + }); + it('ttl token default ttl', async () => { + await create().parseAsync(['node', 'configure', 'token']); + const output = getPrintedLines(); + const { payload } = parseJwt(output[0]); + expect(payload.sub).to.equal('test_user'); + expect(payload.aud).to.equal('cortex'); + expect(payload.iat).to.equal(iatDate / 1000); + expect(payload.exp).to.equal((iatDate / 1000) + (24 * 3600)); // Expect 1 day default + expect(output).to.length(1); + }); + it('ttl token default with other profile', async () => { + await create().parseAsync(['node', 'configure', 'token', '--profile', 'other']); + const output = getPrintedLines(); + const { payload } = parseJwt(output[0]); + expect(payload.sub).to.equal('other_user'); + expect(payload.aud).to.equal('cortex'); + expect(payload.iat).to.equal(iatDate / 1000); + expect(payload.exp).to.equal((iatDate / 1000) + (24 * 3600)); // Expect 1 day default + expect(output).to.length(1); + }); + it('ttl token with ttl', async () => { + await create().parseAsync(['node', 'configure', 'token', '--ttl', '5m', '--profile', 'other']); + const output = getPrintedLines(); + const { payload } = parseJwt(output[0]); + expect(payload.sub).to.equal('other_user'); + expect(payload.aud).to.equal('cortex'); + expect(payload.iat).to.equal(iatDate / 1000); + expect(payload.exp).to.equal((iatDate / 1000) + (5 * 60)); + }); + it('ttl token with bad ttl', async () => { + try { + await create() + .parseAsync(['node', 'configure', 'token', '--ttl', '99mx']); + assert.fail('Should have thrown'); + } catch (err) { + // NOP + } + const output = getErrorLines(); + expect(output[0]).to.contain('Invalid --ttl'); + }); + it('ttl token default with bad profile', async () => { + try { + await create().parseAsync(['node', 'configure', 'token', '--profile', 'nothere']); + assert.fail('Should have thrown'); + } catch (err) { + // NOP + } + const output = getErrorLines(); + expect(output[0]).to.contain('Profile with name "nothere" could not be located in your configuration.'); + assert(process.exit.calledWith(1)); + }); + it('create profile pat file', async () => { + await create().parseAsync(['node', 'configure', '--file', './test/cortex/pat-file.json', '--project', 'test', '--profile', 'conftest']); + const output = getPrintedLines(); + expect(output).to.length(2); + expect(output[1]).to.eql('Configuration for profile conftest saved.'); + const conf = yaml.load(fs.readFileSync(path.join(tmpDir, 'config'))); + expect(conf.profiles.conftest).to.haveOwnProperty('project', 'test'); + printSpy.resetHistory(); + await create().parseAsync(['node', 'configure', 'list']); + expect(getPrintedLines()).to.eql(['default', 'other', 'conftest']); + }); + + describe('Configure Env', () => { + const project = 'testproject'; + const totalVars = 4 + 5 + 1; // # default vars + # timeouts vars + # retries var + it('prints cortex env variables', async () => { + await create().parseAsync(['node', 'configure', 'env', '--project', project]); + const output = getPrintedLines()[0].split('\n'); + expect(output?.length).to.be.above(totalVars); // above because of additional text + // token is dynamic (not checking exact value) + // eslint-disable-next-line no-unused-expressions + expect(output.some((v) => v.includes('export CORTEX_TOKEN='))).to.be.true; + expect(output).to.include('export CORTEX_URI=http://localhost:8000'); + expect(output).to.include('export CORTEX_URL=http://localhost:8000'); + expect(output).to.include(`export CORTEX_PROJECT=${project}`); + // default timeouts + expect(output).to.include('#export CORTEX_TIMEOUT_LOOKUP= (default: 75, unit: ms)'); + expect(output).to.include('#export CORTEX_TIMEOUT_CONNECT= (default: 400, unit: ms)'); + expect(output).to.include('#export CORTEX_TIMEOUT_SECURE_CONNECT= (default: 400, unit: ms)'); + expect(output).to.include('#export CORTEX_TIMEOUT_SOCKET= (default: 1000, unit: ms)'); + expect(output).to.include('#export CORTEX_TIMEOUT_RESPONSE= (default: 2000, unit: ms)'); + expect(output).to.include('#export CORTEX_API_RETRY= (default: 3)'); + }); + + it('configure env does NOT pick up CORTEX_TOKEN & CORTEX_URI environment variables', async () => { + // Token & URI are not picked from env variables, likely because + // this command is meant to help the user configure their env based + // on their profile. Picking up env variables would be + // inconsistent. + process.env.CORTEX_TOKEN = 'asdf'; + process.env.CORTEX_URI = 'http://localhost:5000'; + await create().parseAsync(['node', 'configure', 'env', '--project', project]); + const output = getPrintedLines()[0].split('\n'); + expect(output?.length).to.be.above(totalVars); // above because of additional text + // token is dynamic (not checking exact value) + // eslint-disable-next-line no-unused-expressions + expect(output.some((v) => v.includes('export CORTEX_TOKEN='))).to.be.true; + expect(output).to.include('export CORTEX_URI=http://localhost:8000'); + expect(output).to.include('export CORTEX_URL=http://localhost:8000'); + expect(output).to.include(`export CORTEX_PROJECT=${project}`); + // default timeouts + expect(output).to.include('#export CORTEX_TIMEOUT_LOOKUP= (default: 75, unit: ms)'); + expect(output).to.include('#export CORTEX_TIMEOUT_CONNECT= (default: 400, unit: ms)'); + expect(output).to.include('#export CORTEX_TIMEOUT_SECURE_CONNECT= (default: 400, unit: ms)'); + expect(output).to.include('#export CORTEX_TIMEOUT_SOCKET= (default: 1000, unit: ms)'); + expect(output).to.include('#export CORTEX_TIMEOUT_RESPONSE= (default: 2000, unit: ms)'); + expect(output).to.include('#export CORTEX_API_RETRY= (default: 3)'); + }); + + it('prints user defined timeout environment variables', async () => { + process.env.CORTEX_API_RETRY = 0; + process.env.CORTEX_TIMEOUT_LOOKUP = 100; + process.env.CORTEX_TIMEOUT_CONNECT = 200; + process.env.CORTEX_TIMEOUT_SECURE_CONNECT = 'false'; + process.env.CORTEX_TIMEOUT_SOCKET = 1000; + process.env.CORTEX_TIMEOUT_RESPONSE = 2000; + await create().parseAsync(['node', 'configure', 'env', '--project', project]); + const output = getPrintedLines()[0].split('\n'); + expect(output?.length).to.be.above(totalVars); // above because of additional text + // token is dynamic (not checking exact value) + // eslint-disable-next-line no-unused-expressions + expect(output.some((v) => v.includes('export CORTEX_TOKEN='))).to.be.true; + expect(output).to.include('export CORTEX_URI=http://localhost:8000'); + expect(output).to.include('export CORTEX_URL=http://localhost:8000'); + expect(output).to.include(`export CORTEX_PROJECT=${project}`); + // prints user defined variables + expect(output).to.include('export CORTEX_TIMEOUT_LOOKUP=100 # (unit: ms)'); + expect(output).to.include('export CORTEX_TIMEOUT_CONNECT=200 # (unit: ms)'); + expect(output).to.include('export CORTEX_TIMEOUT_SECURE_CONNECT=false # (unit: ms)'); + expect(output).to.include('export CORTEX_TIMEOUT_SOCKET=1000 # (unit: ms)'); + expect(output).to.include('export CORTEX_TIMEOUT_RESPONSE=2000 # (unit: ms)'); + expect(output).to.include('export CORTEX_API_RETRY=0'); + }); + + it('prints user defined timeout environment variables along with defaults', async () => { + process.env.CORTEX_API_RETRY = 0; + process.env.CORTEX_TIMEOUT_CONNECT = 200; + process.env.CORTEX_TIMEOUT_SECURE_CONNECT = 'false'; + await create().parseAsync(['node', 'configure', 'env', '--project', project]); + const output = getPrintedLines()[0].split('\n'); + expect(output?.length).to.be.above(totalVars); // above because of additional text + // token is dynamic (not checking exact value) + // eslint-disable-next-line no-unused-expressions + expect(output.some((v) => v.includes('export CORTEX_TOKEN='))).to.be.true; + expect(output).to.include('export CORTEX_URI=http://localhost:8000'); + expect(output).to.include('export CORTEX_URL=http://localhost:8000'); + expect(output).to.include(`export CORTEX_PROJECT=${project}`); + // default timeouts + expect(output).to.include('#export CORTEX_TIMEOUT_LOOKUP= (default: 75, unit: ms)'); + expect(output).to.include('#export CORTEX_TIMEOUT_SOCKET= (default: 1000, unit: ms)'); + expect(output).to.include('#export CORTEX_TIMEOUT_RESPONSE= (default: 2000, unit: ms)'); + // prints user defined variables + expect(output).to.include('export CORTEX_TIMEOUT_CONNECT=200 # (unit: ms)'); + expect(output).to.include('export CORTEX_TIMEOUT_SECURE_CONNECT=false # (unit: ms)'); + expect(output).to.include('export CORTEX_API_RETRY=0'); + }); + }); +}); diff --git a/test/connections.js b/test/connections.js new file mode 100644 index 00000000..8b135736 --- /dev/null +++ b/test/connections.js @@ -0,0 +1,199 @@ +import chai from 'chai'; +import mockedEnv from 'mocked-env'; +import nock from 'nock'; +import _ from 'lodash'; +import sinon from 'sinon'; +import { create } from '../bin/cortex-connections.js'; +import { + compatibilityApi, compatiblityResponse, infoApi, infoResponse, stripAnsi, +} from './utils.js'; + +const PROJECT = 'project'; + +describe('Connections', () => { + let restoreEnv; + let sandbox; + let printSpy; + let errorSpy; + let warnSpy; + const serverUrl = 'http://localhost:8000'; + beforeEach(() => { + if (!nock.isActive()) { + nock.activate(); + } + restoreEnv = mockedEnv({ + CORTEX_CONFIG_DIR: './test/cortex', + }); + sandbox = sinon.createSandbox(); + sandbox.stub(process, 'exit').throws(Error('EXIT')); + printSpy = sandbox.spy(console, 'log'); + errorSpy = sandbox.spy(console, 'error'); + warnSpy = sandbox.spy(console, 'warn'); + nock(serverUrl).get(compatibilityApi()).reply(200, compatiblityResponse()); + nock(serverUrl).persist().get(infoApi()).reply(200, infoResponse()); + }); + + afterEach(() => { + // clean up mocks that may not have been called + restoreEnv(); + nock.cleanAll(); + sandbox.restore(); + }); + + function getPrintedLines() { + return _.flatten(printSpy.args).map((s) => stripAnsi(s)); + } + + function getErrorLines() { + return _.flatten(errorSpy.args).map((s) => stripAnsi(s)); + } + + // eslint-disable-next-line no-unused-vars + function getWarningLines() { + return _.flatten(warnSpy.args).map((s) => stripAnsi(s)); + } + + describe('List Connections', () => { + const response = { + success: true, + connections: [ + { + name: 'content', + title: 'Cortex Managed Content', + connectionType: 'managedContent', + description: 'Built in storage for files managed by the platform.connectionType: managedContent', + allowWrite: true, + }, + { + _id: '661111111187c46c78a93a95', + name: 'json-f2251', + title: 'json', + connectionType: 's3', + allowWrite: false, + contentType: 'json', + params: [ + { name: 'json/multiline', value: 'true', _id: '661021b7c087c46c78a93a96' }, { name: 'secretKey', value: '#SECURE.super-secret', _id: '661021b7c087c46c78a93a97' }, + { name: 'json/style', value: 'lines', _id: '661021b7c087c46c78a93a98' }, + { name: 'uri', value: 's3a://dev-bucket/datasets/file.json', _id: '661021b7c087c46c78a93a99' }, + { name: 'publicKey', value: '00000000000000000000', _id: '661021b7c087c46c78a93a9a' }, + ], + description: 'asdf', + tags: [], + createdAt: '2024-04-05T16:07:19.160Z', + updatedAt: '2024-04-05T16:07:19.160Z', + _version: 1, + }, + ], + }; + + it('lists connections - empty', async () => { + const empty = { success: true, connections: [] }; + nock(serverUrl).get(/\/fabric\/v4\/projects\/.*\/connections.*/).reply(200, empty); + await create().parseAsync(['node', 'connections', 'list', '--project', PROJECT]); + const output = getPrintedLines(); + const errs = getErrorLines(); + // eslint-disable-next-line no-unused-expressions + chai.expect(errs.join('')).to.be.empty; + chai.expect(output.join('')).to.contain('No connections found'); + nock.isDone(); + }); + + it('lists connections', async () => { + nock(serverUrl) + .get(/\/fabric\/v4\/projects\/.*\/connections/) + .query({ + limit: 20, + skip: 0, + sort: JSON.stringify({ updatedAt: -1 }), + }) + .reply(200, response); + await create().parseAsync(['node', 'connections', 'list', '--project', PROJECT]); + const output = getPrintedLines().join(''); + const errs = getErrorLines().join(''); + // Only check for expected strings - would be better to compare the table length + const expectedKeys = [ + 'Name', 'Title', 'Description', 'Connection Type', 'Created On', // Table fields + 'content', 'Cortex Managed Content', 'Built in storage for files managed by the', 'managedContent', '-', // MC connection + 'json-f2251', 'json', 'asdf', 's3', // JSON connection - minus created on + ]; + expectedKeys.forEach((k) => { + chai.expect(output).to.contain(k, `Expected string ${k} not found!`); + }); + // eslint-disable-next-line no-unused-expressions + chai.expect(errs).to.be.empty; + nock.isDone(); + }); + + it('lists connections in JSON', async () => { + nock(serverUrl) + .get(/\/fabric\/v4\/projects\/.*\/connections/) + .query({ + limit: 20, + skip: 0, + sort: JSON.stringify({ updatedAt: -1 }), + }) + .reply(200, response); + await create().parseAsync(['node', 'connections', 'list', '--project', PROJECT, '--json']); + const output = getPrintedLines().join(''); + const errs = getErrorLines().join(''); + const jsonOutput = JSON.parse(output); + chai.expect(jsonOutput).to.deep.equal(response.connections); + // eslint-disable-next-line no-unused-expressions + chai.expect(errs).to.be.empty; + nock.isDone(); + }); + }); + + describe('Describe Connection', () => { + const connName = 'json-f2251'; + const response = { + success: true, + name: connName, + title: 'json', + description: 'asdf', + tags: [], + connectionType: 's3', + contentType: 'json', + allowRead: true, + allowWrite: false, + params: [ + { + name: 'json/multiline', + value: 'true', + }, + { + name: 'secretKey', + value: '#SECURE.super-secret', + }, + { + name: 'json/style', + value: 'lines', + }, + { + name: 'uri', + value: 's3a://dev-bucket/datasets/file.json', + }, + { + name: 'publicKey', + value: '00000000000000000000', + }, + ], + createdAt: '2024-04-05T16:07:19.160Z', + _version: 1, + }; + + it('should describe a Connection', async () => { + nock(serverUrl) + .get(/\/fabric\/v4\/projects\/.*\/connections\/json-f2251/) + .reply(200, response); + await create().parseAsync(['node', 'connections', 'describe', connName, '--project', PROJECT]); + const output = getPrintedLines().join(''); + const errs = getErrorLines().join(''); + const jsonOutput = JSON.parse(output); + chai.expect(jsonOutput).to.deep.equal(response); + // eslint-disable-next-line no-unused-expressions + chai.expect(errs).to.be.empty; + nock.isDone(); + }); + }); +}); diff --git a/test/content.js b/test/content.js new file mode 100644 index 00000000..4f09cfc2 --- /dev/null +++ b/test/content.js @@ -0,0 +1,232 @@ +import path from 'node:path'; +import url from 'node:url'; +import chai from 'chai'; +import mockedEnv from 'mocked-env'; +import sinon from 'sinon'; +import nock from 'nock'; +import _ from 'lodash'; +import { create } from '../bin/cortex-content.js'; +import { humanReadableFileSize } from '../src/commands/utils.js'; +import { + compatibilityApi, compatiblityResponse, infoApi, infoResponse, stripAnsi, +} from './utils.js'; +import Info from '../src/client/info.js'; +import Content from '../src/client/content.js'; + +const PROJECT = 'project'; +const { expect } = chai; +describe('Upload Directory', () => { + let restoreEnv; + let sandbox; + let printSpy; + let errorSpy; + const serverUrl = 'http://localhost:8000'; + function getPrintedLines() { + return _.flatten(printSpy.args).map((s) => stripAnsi(s)); + } + + function getErrorLines() { + return _.flatten(errorSpy.args).map((s) => stripAnsi(s)); + } + + before(() => { + restoreEnv = mockedEnv({ + CORTEX_CONFIG_DIR: './test/cortex', + }); + }); + + beforeEach(() => { + if (!nock.isActive()) { + nock.activate(); + } + sandbox = sinon.createSandbox(); + sandbox.stub(process, 'exit'); + sandbox.stub(Info.prototype, 'getInfo').callsFake(() => ({ + serverTs: Date.now(), + })); + printSpy = sandbox.spy(console, 'log'); + errorSpy = sandbox.spy(console, 'error'); + nock(serverUrl).get(compatibilityApi()).reply(200, compatiblityResponse()); + }); + + after(() => restoreEnv()); + afterEach(() => { + sandbox.restore(); + nock.cleanAll(); + }); + + it('test upload successfull', async () => { + const program = create(); + const contentKey = '.shared'; + sandbox.stub(Content.prototype, 'uploadContentStreaming').returns({ success: true }); + const dirname = url.fileURLToPath(new URL('.', import.meta.url)).split(path.sep).join(path.posix.sep); + const filePath = path.posix.join(dirname, 'cortex'); + await program.parseAsync(['node', 'content', 'upload', '--project', 'FOO', '--recursive', contentKey, filePath]); + const lines = getPrintedLines(); + expect(lines).to.include('326B\tpat-file.json -> .shared/pat-file.json'); + expect(lines).to.include('1.6K\tconfig -> .shared/config'); + expect(lines).to.include('483B\tsample/upload/config -> .shared/sample/upload/config'); + // eslint-disable-next-line no-unused-expressions + expect(getErrorLines()).is.empty; // Assume no errors + }); + it('test upload chunkSize 1', async () => { + const program = create(); + const contentKey = '.shared'; + // can't contact cluster.. + // sandbox.stub(Content.prototype, 'uploadContentStreaming').returns({ success: true }); + const dirname = url.fileURLToPath(new URL('.', import.meta.url)).split(path.sep).join(path.posix.sep); + const filePath = path.posix.join(dirname, 'cortex'); + await program.parseAsync(['node', 'content', 'upload', '--project', 'FOO', '--chunkSize', 1, '--recursive', contentKey, filePath]); + const lines = getPrintedLines(); + const errLines = getErrorLines(); + expect(lines).to.include('326B\tpat-file.json -> .shared/pat-file.json'); + expect(lines).to.include('1.6K\tconfig -> .shared/config'); + expect(lines).to.include('483B\tsample/upload/config -> .shared/sample/upload/config'); + // eslint-disable-next-line no-unused-expressions + expect(errLines).to.have.length(0); // Don't understand why this would error ? + }); +}); + +describe('List Content', () => { + let restoreEnv; + let sandbox; + let printSpy; + let errorSpy; + let warnSpy; + const serverUrl = 'http://localhost:8000'; + beforeEach(() => { + if (!nock.isActive()) { + nock.activate(); + } + restoreEnv = mockedEnv({ + CORTEX_CONFIG_DIR: './test/cortex', + }); + sandbox = sinon.createSandbox(); + sandbox.stub(process, 'exit').throws(Error('EXIT')); + printSpy = sandbox.spy(console, 'log'); + errorSpy = sandbox.spy(console, 'error'); + warnSpy = sandbox.spy(console, 'warn'); + nock(serverUrl).get(compatibilityApi()).reply(200, compatiblityResponse()); + nock(serverUrl).persist().get(infoApi()).reply(200, infoResponse()); + }); + + afterEach(() => { + // clean up mocks that may not have been called + restoreEnv(); + nock.cleanAll(); + sandbox.restore(); + }); + + function getPrintedLines() { + return _.flatten(printSpy.args).map((s) => stripAnsi(s)); + } + + function getErrorLines() { + return _.flatten(errorSpy.args).map((s) => stripAnsi(s)); + } + + // eslint-disable-next-line no-unused-vars + function getWarningLines() { + return _.flatten(warnSpy.args).map((s) => stripAnsi(s)); + } + + const response = [ + { + Key: 'hello_world.bundle', + Size: 4176, + ContentType: 'application/octet-stream', + LastModified: '2024-04-04T21:22:31.000Z', + ETag: '0x8D1111111111110', + ContentLength: 4176, + }, + { + Key: 'someKey', + Size: 4057177, + ContentType: 'application/octet-stream', + LastModified: '2024-04-02T19:01:38.000Z', + ETag: '0x8DC111111111111', + ContentLength: 4057177, + }, + ]; + + it('should list content - EMPTY', async () => { + nock(serverUrl) + .get(/\/fabric\/v4\/projects\/.*\/content/) + .reply(200, []); + await create().parseAsync(['node', 'content', 'list', '--project', PROJECT]); + const output = getPrintedLines().join(''); + const errs = getErrorLines().join(''); + // Only check for expected strings - would be better to compare the table length + chai.expect(output).to.contain('No content found'); + // eslint-disable-next-line no-unused-expressions + chai.expect(errs).to.be.empty; + nock.isDone(); + }); + + it('should list content', async () => { + nock(serverUrl) + .get(/\/fabric\/v4\/projects\/.*\/content/) + .reply(200, response); + await create().parseAsync(['node', 'content', 'list', '--project', PROJECT]); + const output = getPrintedLines().join(''); + const errs = getErrorLines().join(''); + // Only check for expected strings - would be better to compare the table length + const extractKeys = ['Key', 'Size', 'ContentType', 'LastModified']; + const expectedKeys = [ + 'Key', 'Content Type', 'Last Modified', 'Size (bytes)', + ...Object.values(_.pick(response[0], extractKeys)), + ...Object.values(_.pick(response[1], extractKeys)), + ]; + expectedKeys.forEach((k) => { + chai.expect(output).to.contain(k, `Expected string ${k} not found!`); + }); + // eslint-disable-next-line no-unused-expressions + chai.expect(errs).to.be.empty; + nock.isDone(); + }); +}); + +describe('Human Readable File Size', () => { + it('bytes', (done) => { + const sizeInBytes = 260; + const formattedFileSize = humanReadableFileSize(sizeInBytes); + expect(formattedFileSize).to.have.string('260B'); + done(); + }); + it('KB', (done) => { + const sizeInBytes = 1200; + const formattedFileSize = humanReadableFileSize(sizeInBytes); + expect(formattedFileSize).to.have.string('1.2K'); + done(); + }); + it('MB', (done) => { + const sizeInBytes = 1000000; + const formattedFileSize = humanReadableFileSize(sizeInBytes); + expect(formattedFileSize).to.have.string('1M'); + done(); + }); + it('1MB less some', (done) => { + const sizeInBytes = (10 ** 3) - 1; + const formattedFileSize = humanReadableFileSize(sizeInBytes); + expect(formattedFileSize).to.have.string('999B'); + done(); + }); + it('1k 200 bytes less', (done) => { + const sizeInBytes = 1000000 - 200; + const formattedFileSize = humanReadableFileSize(sizeInBytes); + expect(formattedFileSize).to.have.string('999.8K'); + done(); + }); + it('1k less', (done) => { + const sizeInBytes = (10 ** 6) - 1; + const formattedFileSize = humanReadableFileSize(sizeInBytes); + expect(formattedFileSize).to.have.string('999.9K'); + done(); + }); + it('several GB', (done) => { + const sizeInBytes = 32 * (10 ** 9) + 345 * (10 ** 6); + const formattedFileSize = humanReadableFileSize(sizeInBytes); + expect(formattedFileSize).to.have.string('32.3G'); + done(); + }); +}); diff --git a/test/cortex/config b/test/cortex/config index 0d688a03..0806ff6a 100644 --- a/test/cortex/config +++ b/test/cortex/config @@ -1,12 +1,57 @@ { - "version": "2", - "profiles": { - "test": { - "url": "http://localhost:8000", - "username": "test_user", - "account": "test_account", - "token": "DUMMY_TOKEN" + "version": "5", + "profiles": { + "default": { + "url": "http://localhost:8000", + "username": "test_user", + "issuer": "cognitivescale.com", + "audience": "cortex", + "jwk": { + "crv": "Ed25519", + "x": "yA1XUDSEoKWa28bCMFljlXwlEFbSVmQQ7ftIMNtB69U", + "d": "bRujm6B2EFaPTp56cU3z3K1w_5oFleLeL3hsLgqCVLc", + "kty": "OKP", + "kid": "X4u2Hv4VxL77bE8N9dT4ltVBoJsSLTH4bY1a5WL6wNY" + }, + "registries": { + "Cortex Private Registry": { + "url": "localhost", + "name": "Cortex Private Registry", + "isCortex": true } + }, + "currentRegistry": "Cortex Private Registry", + "templateConfig": { + "repo": "CognitiveScale/cortex-code-templates", + "branch": "main" + } }, - "currentProfile": "test" -} \ No newline at end of file + "other": { + "url": "http://other:8000", + "username": "other_user", + "issuer": "cognitivescale.com", + "audience": "cortex", + "jwk": { + "crv": "Ed25519", + "x": "yA1XUDSEoKWa28bCMFljlXwlEFbSVmQQ7ftIMNtB69U", + "d": "bRujm6B2EFaPTp56cU3z3K1w_5oFleLeL3hsLgqCVLc", + "kty": "OKP", + "kid": "X4u2Hv4VxL77bE8N9dT4ltVBoJsSLTH4bY1a5WL6wNY" + }, + "project": "otherProj", + "registries": { + "Cortex Private Registry": { + "url": "other", + "name": "Cortex Private Registry", + "isCortex": true + } + }, + "currentRegistry": "Cortex Private Registry", + "templateConfig": { + "repo": "CognitiveScale/cortex-code-templates", + "branch": "main" + } + } + }, + "currentProfile": "default" +} diff --git a/test/cortex/pat-file.json b/test/cortex/pat-file.json new file mode 100644 index 00000000..5f7c07be --- /dev/null +++ b/test/cortex/pat-file.json @@ -0,0 +1 @@ +{"jwk":{"crv":"Ed25519","x":"aTytuuFfEP0QJROVh0Z2WhfkHzzoway1rBkcvsdujAs","d":"JyTqz_bmfU1Ebd2DNlKxBV8p0VrXK75vhu8BNSUZXzI","kty":"OKP","kid":"kxlCpYJAKipLWu0piXvLhed5ZBiStv-_xFn2H0Kregs"},"issuer":"cognitivescale.com","audience":"cortex","username":"3bacf7bb-7732-4210-910c-07829e2330fa","url":"https://192.168.39.129:30581"} \ No newline at end of file diff --git a/test/cortex/sample/upload/config b/test/cortex/sample/upload/config new file mode 100644 index 00000000..5f629a3b --- /dev/null +++ b/test/cortex/sample/upload/config @@ -0,0 +1,19 @@ +{ + "version": "3", + "profiles": { + "default": { + "url": "http://localhost:8000", + "username": "test_user", + "issuer": "cognitivescale.com", + "audience": "cortex", + "jwk": { + "crv": "Ed25519", + "x": "yA1XUDSEoKWa28bCMFljlXwlEFbSVmQQ7ftIMNtB69U", + "d": "bRujm6B2EFaPTp56cU3z3K1w_5oFleLeL3hsLgqCVLc", + "kty": "OKP", + "kid": "X4u2Hv4VxL77bE8N9dT4ltVBoJsSLTH4bY1a5WL6wNY" + } + } + }, + "currentProfile": "default" +} diff --git a/test/docgen.js b/test/docgen.js new file mode 100644 index 00000000..915d126b --- /dev/null +++ b/test/docgen.js @@ -0,0 +1,25 @@ +import chai from 'chai'; +import chaiAsPromised from 'chai-as-promised'; +import { globSync } from 'glob'; +import docGen from '../generate_docs.js'; + +chai.use(chaiAsPromised); +const { expect } = chai; + +describe('Test docgen', () => { + beforeEach(() => { + process.env.CORTEX_SKIP_INIT_PROFILE = '1'; + }); + afterEach(() => { + delete process.env.CORTEX_SKIP_INIT_PROFILE; + }); + const cmdJses = globSync('./bin/*.js', { absolute: true }); + cmdJses.forEach((commandJS) => { + it(`Generate docs for ${commandJS}`, async () => { + const { default: program } = await import(commandJS); + const docStr = docGen(program); + // eslint-disable-next-line no-unused-expressions + expect(docStr).not.be.empty; + }); + }); +}); diff --git a/test/docker-registry.js b/test/docker-registry.js new file mode 100644 index 00000000..488dd183 --- /dev/null +++ b/test/docker-registry.js @@ -0,0 +1,22 @@ +import { assert } from 'chai'; +import Actions from '../src/client/actions.js'; + +const action = new Actions('https://api.cortex-dev.insights.ai'); +describe('_cortexRegistryImagePath()', () => { + it('returns the full URL of the image in the private registry', () => { + const registryUrl = 'private-registry.cortex.insights.ai'; + const tenant = 'tenant'; + const i1 = 'image'; + const i2 = 'c12e/image'; + const i3 = 'docker.io/library/image'; + const t1 = 'image:1234'; + const t2 = 'c12e/image:1234'; + const t3 = 'docker.io/library/image:1234'; + [i1, i2, i3].forEach((i) => { + assert.equal(action._cortexRegistryImagePath(registryUrl, i, tenant), 'private-registry.cortex.insights.ai/tenant/image'); + }); + [t1, t2, t3].forEach((i) => { + assert.equal(action._cortexRegistryImagePath(registryUrl, i, tenant), 'private-registry.cortex.insights.ai/tenant/image:1234'); + }); + }); +}); diff --git a/test/features.js b/test/features.js new file mode 100644 index 00000000..bcd6f642 --- /dev/null +++ b/test/features.js @@ -0,0 +1,123 @@ +import chai from 'chai'; +import chaiAsPromised from 'chai-as-promised'; +import nock from 'nock'; +import mockedEnv from 'mocked-env'; +import npmFetch from 'npm-registry-fetch'; +import sinon from 'sinon'; +import { FLAG_SUBCOMMANDS, FeatureController } from '../src/features.js'; +import { readPackageJSON } from '../src/commands/utils.js'; + +chai.use(chaiAsPromised); + +const pkg = readPackageJSON('../../package.json'); +let sandbox; + +let restoreEnv; +describe('Feature Flags', () => { + const profile = { + url: 'http://example.com', + account: 'testAccount', + projectId: 'testTenant', + username: 'testUser', + token: 'testToken', + }; + + before(() => { + sandbox = sinon.createSandbox(); + sandbox.stub(npmFetch, 'json') + .returns(Promise.resolve({ versions: { [pkg.version]: {} } })); + nock(profile.url) + .get('/fabric/v4/compatibility/applications/cortex-cli') + .reply(200, () => ({ semver: pkg.version })); + restoreEnv = mockedEnv({}); + }); + + after(() => { + sandbox.restore(); + nock.cleanAll(); + nock.enableNetConnect(); + restoreEnv(); + }); + + const previewDisabledFlags = { + ga: { enabled: true, features: {} }, + preview: { + enabled: false, + features: { + 'data-fabric-pipelines': { + enabled: false, + }, + }, + }, + }; + // TODO: Use structuredClone() instead of JSON stringify & parse, once node 18 is minimum + const allPreviewEnabledFlags = JSON.parse(JSON.stringify(previewDisabledFlags)); + allPreviewEnabledFlags.preview.enabled = true; + + // TODO: Use structuredClone() instead of JSON stringify & parse, once node 18 is minimum + const individualPreviewEnabledFlags = JSON.parse(JSON.stringify(previewDisabledFlags)); + individualPreviewEnabledFlags.preview.features['data-fabric-pipelines'].enabled = true; + + const defaultSubcommands = FLAG_SUBCOMMANDS.default.all; + const defaultAndPreviewSubcommands = [...FLAG_SUBCOMMANDS.default.all, ...FLAG_SUBCOMMANDS.preview['data-fabric-pipelines']]; + chai.expect(defaultSubcommands.length).to.not.equal(defaultAndPreviewSubcommands.length); + + describe('getSupportedSubcommands', () => { + it('displays default subcommands if the server does not respond with feature flags', () => { + delete profile.featureFlags; // explicitly remove feature flags for testing purposes + const controller = new FeatureController(profile); + const subcommands = controller.getSupportedSubCommands(); + chai.expect(subcommands).to.eql(defaultSubcommands); + }); + + it('displays default subcommands when preview features are disabled by server', () => { + delete process.env.PREVIEW_FEATURES_ENABLED; + profile.featureFlags = previewDisabledFlags; // assign feature flags for testing purposes + const controller = new FeatureController(profile); + const subcommands = controller.getSupportedSubCommands(); + chai.expect(subcommands).to.eql(defaultSubcommands); + }); + + it('displays preview subcommands if PREVIEW_FEATURES_ENABLED=1 and Preview Flags are DISABLED by server', () => { + process.env.PREVIEW_FEATURES_ENABLED = '1'; + profile.featureFlags = previewDisabledFlags; // assign feature flags for testing purposes + const controller = new FeatureController(profile); + const subcommands = controller.getSupportedSubCommands(); + chai.expect(subcommands).to.eql(defaultAndPreviewSubcommands); + }); + + it('displays default and preview subcommands when PREVIEW_FEATURES_ENABLED=1 and all preview flags are enabled', () => { + process.env.PREVIEW_FEATURES_ENABLED = '1'; + profile.featureFlags = allPreviewEnabledFlags; // assign feature flags for testing purposes + const controller = new FeatureController(profile); + const subcommands = controller.getSupportedSubCommands(); + chai.expect(subcommands).to.eql(defaultAndPreviewSubcommands); + }); + + it('displays default and preview subcommands when PREVIEW_FEATURES_ENABLED=1 and individual preview flags are enabled', () => { + process.env.PREVIEW_FEATURES_ENABLED = '1'; + profile.featureFlags = individualPreviewEnabledFlags; // assign feature flags for testing purposes + console.log(JSON.stringify(profile.featureFlags)); + const controller = new FeatureController(profile); + const subcommands = controller.getSupportedSubCommands(); + chai.expect(subcommands).to.eql(defaultAndPreviewSubcommands); + }); + + it('displays default and preview subcommands when PREVIEW_FEATURES_ENABLED is not set and and all preview flags are enabled', () => { + delete process.env.PREVIEW_FEATURES_ENABLED; + profile.featureFlags = allPreviewEnabledFlags; // assign feature flags for testing purposes + const controller = new FeatureController(profile); + const subcommands = controller.getSupportedSubCommands(); + chai.expect(subcommands).to.eql(defaultAndPreviewSubcommands); + }); + + it('displays default and preview subcommands when PREVIEW_FEATURES_ENABLED is not set and individual preview flags are enabled', () => { + delete process.env.PREVIEW_FEATURES_ENABLED; + profile.featureFlags = individualPreviewEnabledFlags; // assign feature flags for testing purposes + console.log(JSON.stringify(profile.featureFlags)); + const controller = new FeatureController(profile); + const subcommands = controller.getSupportedSubCommands(); + chai.expect(subcommands).to.eql(defaultAndPreviewSubcommands); + }); + }); +}); diff --git a/test/files/pat.json b/test/files/pat.json new file mode 100644 index 00000000..2393b8a4 --- /dev/null +++ b/test/files/pat.json @@ -0,0 +1,13 @@ +{ + "url": "http://localhost:8000", + "username": "test_user", + "issuer": "cognitivescale.com", + "audience": "cortex", + "jwk": { + "crv": "Ed25519", + "x": "yA1XUDSEoKWa28bCMFljlXwlEFbSVmQQ7ftIMNtB69U", + "d": "bRujm6B2EFaPTp56cU3z3K1w_5oFleLeL3hsLgqCVLc", + "kty": "OKP", + "kid": "X4u2Hv4VxL77bE8N9dT4ltVBoJsSLTH4bY1a5WL6wNY" + } +} diff --git a/test/pipelineRepositories.js b/test/pipelineRepositories.js new file mode 100644 index 00000000..0712e7ae --- /dev/null +++ b/test/pipelineRepositories.js @@ -0,0 +1,484 @@ +import chai from 'chai'; +import mockedEnv from 'mocked-env'; +import nock from 'nock'; +import _ from 'lodash'; +import sinon from 'sinon'; +import yaml from 'js-yaml'; +import { mkdtemp, writeFile } from 'node:fs/promises'; +import { join } from 'node:path'; +import { tmpdir } from 'node:os'; +import { create } from '../bin/cortex-pipelines-repos.js'; +import { + infoApi, infoResponse, compatibilityApi, compatiblityResponse, stripAnsi, +} from './utils.js'; + +const PROJECT = 'project'; + +describe('Pipelines', () => { + let restoreEnv; + let sandbox; + let printSpy; + let errorSpy; + const serverUrl = 'http://localhost:8000'; + const exampleRepo = 'git@github.com:organization/repository-name.git'; + beforeEach(() => { + if (!nock.isActive()) { + nock.activate(); + } + restoreEnv = mockedEnv({ + CORTEX_CONFIG_DIR: './test/cortex', + }); + sandbox = sinon.createSandbox(); + sandbox.stub(process, 'exit').throws(Error('EXIT')); + printSpy = sandbox.spy(console, 'log'); + errorSpy = sandbox.spy(console, 'error'); + nock(serverUrl).get(compatibilityApi()).reply(200, compatiblityResponse()); + nock(serverUrl).persist().get(infoApi()).reply(200, infoResponse()); + process.env.PREVIEW_FEATURES_ENABLED = '1'; + }); + + afterEach(() => { + // clean up mocks that may not have been called + restoreEnv(); + nock.cleanAll(); + sandbox.restore(); + delete process.env.PREVIEW_FEATURES_ENABLED; + }); + + function getPrintedLines() { + return _.flatten(printSpy.args).map((s) => stripAnsi(s)); + } + function getErrorLines() { + return _.flatten(errorSpy.args).map((s) => stripAnsi(s)); + } + + describe('Pipeline Repos', () => { + describe('#List Repos', () => { + it('lists pipeline repositories - empty', async () => { + const response = { success: true, pipelineRepositories: [] }; + nock(serverUrl).get(/\/fabric\/v4\/projects\/.*\/pipeline-repositories/).reply(200, response); + await create().parseAsync(['node', 'repos', 'list', '--project', PROJECT]); + const output = getPrintedLines(); + const errs = getErrorLines(); + // eslint-disable-next-line no-unused-expressions + chai.expect(errs.join('')).to.be.empty; + chai.expect(output.join('')).to.contain('No pipeline repositories found'); + nock.isDone(); + }); + + it('lists pipeline repositories as JSON - empty', async () => { + const response = { success: true, pipelineRepositories: [] }; + nock(serverUrl).get(/\/fabric\/v4\/projects\/.*\/pipeline-repositories/).reply(200, response); + await create().parseAsync(['node', 'repos', 'list', '--project', PROJECT, '--json']); + const output = getPrintedLines(); + const errs = getErrorLines(); + const roundTripped = JSON.parse(output.join('')); + // eslint-disable-next-line no-unused-expressions + chai.expect(roundTripped).to.be.an('array').that.is.empty; + // eslint-disable-next-line no-unused-expressions + chai.expect(errs.join('')).to.be.empty; + nock.isDone(); + }); + + it('lists pipeline repositories', async () => { + const response = { + success: true, + pipelineRepositories: [ + { name: 'repo1', repo: exampleRepo, branch: 'main' }, + { name: 'repo2', repo: exampleRepo, branch: 'develop' }, + ], + }; + nock(serverUrl).get(/\/fabric\/v4\/projects\/.*\/pipeline-repositories.*/).reply(200, response); + await create().parseAsync(['node', 'repos', 'list', '--project', PROJECT]); + const output = getPrintedLines().join(''); + const errs = getErrorLines().join(''); + chai.expect(output).to.contain('repo1'); + chai.expect(output).to.contain('main'); + chai.expect(output).to.contain('repo2'); + chai.expect(output).to.contain('develop'); + // eslint-disable-next-line no-unused-expressions + chai.expect(errs).to.be.empty; + nock.isDone(); + }); + }); + + it('describes a pipeline repository ', async () => { + const response = { + success: true, + pipelineRepository: { + name: 'repo1', + repo: exampleRepo, + branch: 'develop', + _projectId: PROJECT, + deleted: false, + version: 1, + }, + }; + nock(serverUrl).get(/\/fabric\/v4\/projects\/.*\/pipeline-repositories\/.*/).reply(200, response); + await create().parseAsync(['node', 'repos', 'describe', 'repo1', '--project', PROJECT]); + const output = getPrintedLines().join(''); + const errs = getErrorLines().join(''); + const description = JSON.parse(output); + chai.expect(description).to.deep.equal(response.pipelineRepository); + // eslint-disable-next-line no-unused-expressions + chai.expect(errs).to.be.empty; + nock.isDone(); + }); + + describe('#Save Pipeline Repo', () => { + it('saves a pipeline repository (json)', async () => { + const repo = { + name: 'repo1', + repo: exampleRepo, + branch: 'develop', + }; + const response = { + success: true, + version: 1, + message: 'pipelineRepository repo1 saved.', + }; + // save definition of 'repo' to temporary file + const dir = await mkdtemp(join(tmpdir(), 'tmp-')); + const filename = join(dir, 'repo1.json'); + await writeFile(filename, JSON.stringify(repo)); + // invoke the CLI + nock(serverUrl).post(/\/fabric\/v4\/projects\/.*\/pipeline-repositories/).reply(200, response); + await create().parseAsync(['node', 'repos', 'save', filename, '--project', PROJECT]); + const output = getPrintedLines().join(''); + const errs = getErrorLines().join(''); + // verify response + chai.expect(output).to.contain('Pipeline Repository saved'); + // eslint-disable-next-line no-unused-expressions + chai.expect(errs).to.be.empty; + nock.isDone(); + }); + + it('saves a pipeline repository (yaml)', async () => { + const repo = { + name: 'repo1', + repo: exampleRepo, + branch: 'develop', + }; + const response = { + success: true, + version: 1, + message: 'pipelineRepository repo1 saved.', + }; + // save definition of 'repo' to temporary file + const dir = await mkdtemp(join(tmpdir(), 'tmp-')); + const filename = join(dir, 'repo1.yaml'); + await writeFile(filename, yaml.dump(repo)); + // invoke the CLI + nock(serverUrl).post(/\/fabric\/v4\/projects\/.*\/pipeline-repositories/).reply(200, response); + await create().parseAsync(['node', 'repos', 'save', '-y', filename, '--project', PROJECT]); + const output = getPrintedLines().join(''); + const errs = getErrorLines().join(''); + // verify response + chai.expect(output).to.contain('Pipeline Repository saved'); + // eslint-disable-next-line no-unused-expressions + chai.expect(errs).to.be.empty; + nock.isDone(); + }); + + it('saves a pipeline repository using CLI flags', async () => { + const response = { + success: true, + version: 1, + message: 'pipelineRepository testpipeline saved.', + }; + // invoke the CLI + nock(serverUrl).post(/\/fabric\/v4\/projects\/.*\/pipeline-repositories/).reply(200, response); + await create().parseAsync(['node', 'repos', 'save', '--name', 'testpipeline', '--branch', 'develop', '--repo', 'mc://test.key', '--project', PROJECT]); + const output = getPrintedLines().join(''); + const errs = getErrorLines().join(''); + // verify response + chai.expect(output).to.contain('Pipeline Repository saved'); + // eslint-disable-next-line no-unused-expressions + chai.expect(errs).to.be.empty; + nock.isDone(); + }); + }); + + it('deletes a pipeline repository', async () => { + const response = { + success: true, + message: 'Successfully deleted pipelineRepository repo1', + }; + nock(serverUrl).delete(/\/fabric\/v4\/projects\/.*\/pipeline-repositories\/.*/).reply(200, response); + await create().parseAsync(['node', 'repos', 'delete', 'repo1', '--project', PROJECT]); + const output = getPrintedLines().join(''); + const errs = getErrorLines().join(''); + chai.expect(output).to.contain(response.message); + // eslint-disable-next-line no-unused-expressions + chai.expect(errs).to.be.empty; + nock.isDone(); + }); + + it('fails to delete a pipeline repository ', async () => { + const response = { + success: false, + message: 'Failed to delete pipelineRepository repo1', + status: 503, + }; + nock(serverUrl).delete(/\/fabric\/v4\/projects\/.*\/pipeline-repositories\/.*/).reply(200, response); + try { + await create().parseAsync(['node', 'repos', 'delete', 'repo1', '--project', PROJECT]); + } catch (err) { + const output = getPrintedLines().join(''); + const errs = getErrorLines().join(''); + // eslint-disable-next-line no-unused-expressions + chai.expect(output).to.be.empty; + chai.expect(errs).to.contain('Pipeline Repository deletion failed: 503 Failed to delete pipelineRepository repo1.'); + nock.isDone(); + } + }); + + describe('#Update Pipelines', () => { + it('should update-pipelines - all success', async () => { + const response = { + message: 'All Pipelines updated successfully!', + code: 200, + success: true, + updateReport: { + added: ['pipeline1', 'pipeline4'], + updated: ['pipeline2'], + deleted: ['pipeline3'], + failed: { + add: [], + delete: [], + update: [], + }, + }, + }; + nock(serverUrl).post(/\/fabric\/v4\/projects\/.*\/pipeline-repositories\/.*\/update/).reply(200, response); + await create().parseAsync(['node', 'repos', 'update-pipelines', 'repo1', '--project', PROJECT]); + const output = getPrintedLines().join(''); + const errs = getErrorLines().join(''); + // eslint-disable-next-line no-unused-expressions + chai.expect(output).to.equal( + `${response.message}\n` + + '┌────────────────────┬─────────────────────────┬──────────────────────────────┐\n' + + '│ │ Pipeline Name │ Details │\n' + + '├────────────────────┼─────────────────────────┼──────────────────────────────┤\n' + + '│ │ pipeline1 │ │\n' + + '│ Added ├─────────────────────────┼──────────────────────────────┤\n' + + '│ │ pipeline4 │ │\n' + + '├────────────────────┼─────────────────────────┼──────────────────────────────┤\n' + + '│ Updated │ pipeline2 │ │\n' + + '├────────────────────┼─────────────────────────┼──────────────────────────────┤\n' + + '│ Deleted │ pipeline3 │ │\n' + + '├────────────────────┼─────────────────────────┼──────────────────────────────┤\n' + + '│ Failed to Add │ - │ - │\n' + + '├────────────────────┼─────────────────────────┼──────────────────────────────┤\n' + + '│ Failed to Update │ - │ - │\n' + + '├────────────────────┼─────────────────────────┼──────────────────────────────┤\n' + + '│ Failed to Delete │ - │ - │\n' + + '└────────────────────┴─────────────────────────┴──────────────────────────────┘', + ); + // eslint-disable-next-line no-unused-expressions + chai.expect(errs).to.be.empty; + nock.isDone(); + }); + + it('should update-pipelines with a custom skill and forceRecreate query params', async () => { + const response = { + success: true, + code: 200, + message: 'All Pipelines updated successfully!', + updateReport: { + added: ['pipeline1'], + updated: ['pipeline2'], + deleted: ['pipeline3'], + failed: { + add: [], + delete: [], + update: [], + }, + }, + }; + + nock(serverUrl) + .post(/\/fabric\/v4\/projects\/.*\/pipeline-repositories\/.*\/update/) + .query({ skillName: 'my-skill', forceRecreate: true }) + .reply(200, response); + await create().parseAsync(['node', 'repos', 'update-pipelines', 'repo1', '--project', PROJECT, '--skill', 'my-skill', '--force-recreate']); + const output = getPrintedLines().join(''); + const errs = getErrorLines().join(''); + chai.expect(output).to.equal( + `${response.message}\n` + + '┌────────────────────┬─────────────────────────┬──────────────────────────────┐\n' + + '│ │ Pipeline Name │ Details │\n' + + '├────────────────────┼─────────────────────────┼──────────────────────────────┤\n' + + '│ Added │ pipeline1 │ │\n' + + '├────────────────────┼─────────────────────────┼──────────────────────────────┤\n' + + '│ Updated │ pipeline2 │ │\n' + + '├────────────────────┼─────────────────────────┼──────────────────────────────┤\n' + + '│ Deleted │ pipeline3 │ │\n' + + '├────────────────────┼─────────────────────────┼──────────────────────────────┤\n' + + '│ Failed to Add │ - │ - │\n' + + '├────────────────────┼─────────────────────────┼──────────────────────────────┤\n' + + '│ Failed to Update │ - │ - │\n' + + '├────────────────────┼─────────────────────────┼──────────────────────────────┤\n' + + '│ Failed to Delete │ - │ - │\n' + + '└────────────────────┴─────────────────────────┴──────────────────────────────┘', + ); + // eslint-disable-next-line no-unused-expressions + chai.expect(errs).to.be.empty; + nock.isDone(); + }); + + it('should report no changes when updating-pipelines with an empty report', async () => { + // set up mock - should report no changes, but it was successful + const response = { + success: true, + updateReport: { + added: [], + updated: [], + deleted: [], + failed: { + add: [], + delete: [], + update: [], + }, + }, + }; + nock(serverUrl) + .post(/\/fabric\/v4\/projects\/.*\/pipeline-repositories\/.*\/update/) + .reply(200, response); + + // Execute update + await create().parseAsync(['node', 'repos', 'update-pipelines', 'repo1', '--project', PROJECT]); + const output = getPrintedLines().join(''); + const errs = getErrorLines().join(''); + chai.expect(output).to.equal('Pipelines up to date! No changes made.'); + // eslint-disable-next-line no-unused-expressions + chai.expect(errs).to.be.empty; + nock.isDone(); + }); + + it('should report pipeline failures and error details in the error message', async () => { + // set up mock - should report no changes, but it was successful + const response = { + success: false, + code: 200, + message: 'Individual Pipeline(s) failed to be updated! Check the Pipeline Update Report.', + updateReport: { + added: [ + 'hello_world', + 'ingest_lookup', + 'medallion-s3', + 'simple', + 'skill_test', + ], + updated: [], + deleted: [], + failed: { + add: [ + { + pipelineName: 'error-invalid-config1', + details: 'Failed to parse Pipeline Configuration. Error: Invalid value for spec.blocks. At least 1 block is required', + }, + { + pipelineName: 'error-invalid-config2', + details: 'Failed to parse Pipeline Configuration. Error: Invalid value for key "type" on BlockConfig. Expected one of [dbt, streaming] but got "invalid-custom"', + }, + { + pipelineName: 'error-invalid-config3', + details: 'Failed to parse Pipeline Configuration. Error: Invalid value for key "apiVersion" on PipelineConfig. Expected string but got undefined', + }, + { + pipelineName: 'error-invalid-config4', + details: 'Failed to parse Pipeline Configuration. Error: Invalid value for key "executor" on BlockConfig. Expected one of [pyspark] but got undefined', + }, + { + pipelineName: 'error-invalid-config5', + details: 'Failed to parse Pipeline Configuration. SyntaxError: Unexpected token H in JSON at position 0', + }, + { + pipelineName: 'error-multiple-config', + details: 'Multiple config files found', + }, + { + pipelineName: 'error-no-config', + details: 'Config file not found', + }, + ], + update: [], + delete: [], + }, + }, + }; + nock(serverUrl) + .post(/\/fabric\/v4\/projects\/.*\/pipeline-repositories\/.*\/update/) + .reply(200, response); + + // Execute update + await create().parseAsync(['node', 'repos', 'update-pipelines', 'repo1', '--project', PROJECT]); + const output = getPrintedLines().join(''); + const errs = getErrorLines().join(''); + chai.expect(output).to.equal( + '┌────────────────────┬─────────────────────────┬──────────────────────────────┐\n' + + '│ │ Pipeline Name │ Details │\n' + + '├────────────────────┼─────────────────────────┼──────────────────────────────┤\n' + + '│ │ hello_world │ │\n' + + '│ ├─────────────────────────┼──────────────────────────────┤\n' + + '│ │ ingest_lookup │ │\n' + + '│ ├─────────────────────────┼──────────────────────────────┤\n' + + '│ Added │ medallion-s3 │ │\n' + + '│ ├─────────────────────────┼──────────────────────────────┤\n' + + '│ │ simple │ │\n' + + '│ ├─────────────────────────┼──────────────────────────────┤\n' + + '│ │ skill_test │ │\n' + + '├────────────────────┼─────────────────────────┼──────────────────────────────┤\n' + + '│ Updated │ - │ - │\n' + + '├────────────────────┼─────────────────────────┼──────────────────────────────┤\n' + + '│ Deleted │ - │ - │\n' + + '├────────────────────┼─────────────────────────┼──────────────────────────────┤\n' + + '│ │ error-invalid-config1 │ Failed to parse Pipeline │\n' + + '│ │ │ Configuration. Error: │\n' + + '│ │ │ Invalid value for │\n' + + '│ │ │ spec.blocks. At least 1 │\n' + + '│ │ │ block is required │\n' + + '│ ├─────────────────────────┼──────────────────────────────┤\n' + + '│ │ error-invalid-config2 │ Failed to parse Pipeline │\n' + + '│ │ │ Configuration. Error: │\n' + + '│ │ │ Invalid value for key "type" │\n' + + '│ │ │ on BlockConfig. Expected one │\n' + + '│ │ │ of [dbt, streaming] but got │\n' + + '│ │ │ "invalid-custom" │\n' + + '│ ├─────────────────────────┼──────────────────────────────┤\n' + + '│ │ error-invalid-config3 │ Failed to parse Pipeline │\n' + + '│ │ │ Configuration. Error: │\n' + + '│ │ │ Invalid value for key │\n' + + '│ │ │ "apiVersion" on │\n' + + '│ Failed to Add │ │ PipelineConfig. Expected │\n' + + '│ │ │ string but got undefined │\n' + + '│ ├─────────────────────────┼──────────────────────────────┤\n' + + '│ │ error-invalid-config4 │ Failed to parse Pipeline │\n' + + '│ │ │ Configuration. Error: │\n' + + '│ │ │ Invalid value for key │\n' + + '│ │ │ "executor" on BlockConfig. │\n' + + '│ │ │ Expected one of [pyspark] │\n' + + '│ │ │ but got undefined │\n' + + '│ ├─────────────────────────┼──────────────────────────────┤\n' + + '│ │ error-invalid-config5 │ Failed to parse Pipeline │\n' + + '│ │ │ Configuration. SyntaxError: │\n' + + '│ │ │ Unexpected token H in JSON │\n' + + '│ │ │ at position 0 │\n' + + '│ ├─────────────────────────┼──────────────────────────────┤\n' + + '│ │ error-multiple-config │ Multiple config files found │\n' + + '│ ├─────────────────────────┼──────────────────────────────┤\n' + + '│ │ error-no-config │ Config file not found │\n' + + '├────────────────────┼─────────────────────────┼──────────────────────────────┤\n' + + '│ Failed to Update │ - │ - │\n' + + '├────────────────────┼─────────────────────────┼──────────────────────────────┤\n' + + '│ Failed to Delete │ - │ - │\n' + + '└────────────────────┴─────────────────────────┴──────────────────────────────┘', + ); + // eslint-disable-next-line no-unused-expressions + chai.expect(errs).to.equal(`${response.message}\n`); + nock.isDone(); + }); + }); + }); +}); diff --git a/test/pipelines.js b/test/pipelines.js new file mode 100644 index 00000000..b4b5e31c --- /dev/null +++ b/test/pipelines.js @@ -0,0 +1,385 @@ +import chai from 'chai'; +import mockedEnv from 'mocked-env'; +import nock from 'nock'; +import _ from 'lodash'; +import sinon from 'sinon'; +import { create } from '../bin/cortex-pipelines.js'; +import { + compatibilityApi, compatiblityResponse, infoApi, infoResponse, stripAnsi, +} from './utils.js'; + +const PROJECT = 'project'; + +describe('Pipelines', () => { + let restoreEnv; + let sandbox; + let printSpy; + let errorSpy; + let warnSpy; + const serverUrl = 'http://localhost:8000'; + beforeEach(() => { + if (!nock.isActive()) { + nock.activate(); + } + restoreEnv = mockedEnv({ + CORTEX_CONFIG_DIR: './test/cortex', + }); + sandbox = sinon.createSandbox(); + sandbox.stub(process, 'exit').throws(Error('EXIT')); + printSpy = sandbox.spy(console, 'log'); + errorSpy = sandbox.spy(console, 'error'); + warnSpy = sandbox.spy(console, 'warn'); + nock(serverUrl).get(compatibilityApi()).reply(200, compatiblityResponse()); + nock(serverUrl).persist().get(infoApi()).reply(200, infoResponse()); + process.env.PREVIEW_FEATURES_ENABLED = '1'; + }); + + afterEach(() => { + // clean up mocks that may not have been called + restoreEnv(); + nock.cleanAll(); + sandbox.restore(); + delete process.env.PREVIEW_FEATURES_ENABLED; + }); + + function getPrintedLines() { + return _.flatten(printSpy.args).map((s) => stripAnsi(s)); + } + + function getErrorLines() { + return _.flatten(errorSpy.args).map((s) => stripAnsi(s)); + } + + function getWarningLines() { + return _.flatten(warnSpy.args).map((s) => stripAnsi(s)); + } + + describe('List Pipelines as an unsupported preview feature', () => { + it('should print that the response was unintelligable', async () => { + const response = '<html></html>'; + nock(serverUrl).get(/\/fabric\/v4\/projects\/.*\/pipelines/).reply(200, response, { 'content-type': 'text/html' }); + try { + await create().parseAsync(['node', 'pipelines', 'list', '--project', PROJECT]); + chai.expect(0).to.equal(1, 'Failure expected!'); // because 'EXIT' signal is mocked with Error + } catch { + const output = getPrintedLines(); + const errs = getErrorLines(); + // eslint-disable-next-line no-unused-expressions + chai.expect(output.join('')).to.be.empty; + chai.expect(errs.join('')).to.contain('Unable to parse response from server. Try running again with "--debug" for more details.'); + nock.isDone(); + } + }); + }); + + describe('Pipeline', () => { + describe('#ListPipelines', () => { + it('lists pipelines - empty', async () => { + const response = { success: true, pipelines: [] }; + nock(serverUrl).get(/\/fabric\/v4\/projects\/.*\/pipelines.*/).reply(200, response); + await create().parseAsync(['node', 'pipelines', 'list', '--project', PROJECT]); + + const output = getPrintedLines(); + const errs = getErrorLines(); + // eslint-disable-next-line no-unused-expressions + chai.expect(errs.join('')).to.be.empty; + chai.expect(output.join('')).to.contain('No pipelines found'); + nock.isDone(); + }); + + it('lists pipelines', async () => { + const response = { + success: true, + pipelines: [ + { name: 'pipeline1', gitRepoName: 'repo1', sha: 'qwerty' }, + { name: 'pipeline2', gitRepoName: 'repo2', sha: 'abcd' }, + ], + }; + nock(serverUrl).get(/\/fabric\/v4\/projects\/.*\/pipelines.*/).reply(200, response); + await create().parseAsync(['node', 'pipelines', 'list', '--project', PROJECT]); + const output = getPrintedLines().join(''); + const errs = getErrorLines().join(''); + chai.expect(output).to.contain('pipeline1'); + chai.expect(output).to.contain('qwerty'); + chai.expect(output).to.contain('pipeline2'); + chai.expect(output).to.contain('qwerty'); + // eslint-disable-next-line no-unused-expressions + chai.expect(errs).to.be.empty; + nock.isDone(); + }); + + it('lists pipelines in a repo', async () => { + const response = { + success: true, + pipelines: [ + { name: 'pipeline1', gitRepoName: 'repo1', sha: 'qwerty' }, + { name: 'pipeline2', gitRepoName: 'repo1', sha: 'abcd' }, + ], + }; + // NOTE: '--repo' is a shortcut for '--filter', and all query params are + // required by nock for the match to be succesful + nock(serverUrl) + .get(/\/fabric\/v4\/projects\/.*\/pipelines.*/) + .query({ + filter: JSON.stringify({ gitRepoName: 'repo1' }), + limit: 20, + sort: JSON.stringify({ updatedAt: -1 }), + skip: 0, + }).reply(200, response); + await create().parseAsync(['node', 'pipelines', 'list', '--project', PROJECT, '--repo', 'repo1']); + const output = getPrintedLines().join(''); + const errs = getErrorLines().join(''); + chai.expect(output).to.contain('pipeline1'); + chai.expect(output).to.contain('qwerty'); + chai.expect(output).to.contain('pipeline2'); + chai.expect(output).to.contain('qwerty'); + // eslint-disable-next-line no-unused-expressions + chai.expect(errs).to.be.empty; + nock.isDone(); + }); + + it('warns that --filter has a higher priority that --repo when listing pipelines', async () => { + const response = { + success: true, + pipelines: [ + { name: 'pipeline1', gitRepoName: 'repo1', sha: 'qwerty' }, + { name: 'pipeline2', gitRepoName: 'repo1', sha: 'abcd' }, + ], + }; + nock(serverUrl) + .get(/\/fabric\/v4\/projects\/.*\/pipelines/) + .query({ + filter: JSON.stringify({ name: 'pipeline' }), // matches --filter + limit: 20, + sort: JSON.stringify({ updatedAt: -1 }), + skip: 0, + }) + .reply(200, response); + await create().parseAsync(['node', 'pipelines', 'list', '--project', PROJECT, '--repo', 'repo1', '--filter', '{"name":"pipeline"}']); + const output = getPrintedLines().join(''); + const warn = getWarningLines().join(''); + const errs = getErrorLines().join(''); + chai.expect(output).to.contain('pipeline1'); + chai.expect(output).to.contain('qwerty'); + chai.expect(output).to.contain('pipeline2'); + chai.expect(output).to.contain('qwerty'); + // eslint-disable-next-line no-unused-expressions + chai.expect(errs).to.be.empty; + chai.expect(warn).to.contain('WARNING: --repo and --filter options are incompatible! The --filter option will be used'); + nock.isDone(); + }); + }); + + it('describes a pipeline', async () => { + const response = { + success: true, + pipeline: { + name: 'pipeline1', + sha: 'qwerty', + gitRepoName: 'repo1', + _projectId: PROJECT, + deleted: false, + version: 1, + }, + }; + nock(serverUrl).get(/\/fabric\/v4\/projects\/.*\/pipelines\/.*/).reply(200, response); + await create().parseAsync(['node', 'pipelines', 'describe', 'pipeline1', 'repo1', '--project', PROJECT]); + const output = getPrintedLines().join(''); + const errs = getErrorLines().join(''); + const description = JSON.parse(output); + chai.expect(description).to.deep.equal(response.pipeline); + // eslint-disable-next-line no-unused-expressions + chai.expect(errs).to.be.empty; + nock.isDone(); + }); + + it('describes a non-existent pipeline', async () => { + const response = { + success: false, + message: 'pipeline with name nonExistentPipeline not found', + status: 404, + }; + nock(serverUrl).get(/\/fabric\/v4\/projects\/.*\/pipelines\/.*/).reply(404, response); + try { + await create().parseAsync(['node', 'pipelines', 'describe', 'nonExistentPipeline', 'repo1', '--project', PROJECT]); + } catch (err) { + const output = getPrintedLines().join(''); + const errs = getErrorLines().join(''); + // eslint-disable-next-line no-unused-expressions + chai.expect(output).to.be.empty; + chai.expect(errs).to.contain('Failed to describe pipeline: 404, pipeline with name nonExistentPipeline not found'); + nock.isDone(); + } + }); + + describe('#runPipeline()', () => { + it('runs a Pipeline', async () => { + const response = { + success: true, + activationId: '3d77f2d3-90ef-4061-a17d-fa2aecb3c6a4', + }; + nock(serverUrl).post(/\/fabric\/v4\/projects\/.*\/pipelines\/.*\/run/).reply(200, response); + await create().parseAsync(['node', 'pipelines', 'run', 'pipeline1', 'repo1', '--project', PROJECT]); + const output = getPrintedLines().join(''); + const errs = getErrorLines().join(''); + chai.expect(output).to.equal(`Pipeline Run Submitted!\n\nUse "cortex pipelines describe-run ${response.activationId}" to inspect the Pipeline Run`); + // eslint-disable-next-line no-unused-expressions + chai.expect(errs).to.be.empty; + nock.isDone(); + }); + + it('runs a Pipeline and prints the output as JSON', async () => { + const response = { + success: true, + activationId: '3d77f2d3-90ef-4061-a17d-fa2aecb3c6a4', + }; + nock(serverUrl).post(/\/fabric\/v4\/projects\/.*\/pipelines\/.*\/run/).reply(200, response); + await create().parseAsync(['node', 'pipelines', 'run', 'pipeline1', 'repo1', '--project', PROJECT, '--json']); + const output = getPrintedLines().join(''); + const errs = getErrorLines().join(''); + + const expected = { ...response }; + expected.runId = expected.activationId; // output includes runId NOT activationId + delete expected.activationId; + + const actual = JSON.parse(output); + chai.expect(actual).to.deep.equal(expected); + // eslint-disable-next-line no-unused-expressions + chai.expect(errs).to.be.empty; + nock.isDone(); + }); + + it('runs a Pipeline on a set schedule with a default schedule name', async () => { + const pipeline = 'pipeline-name'; + const cron = '@hourly'; + const response = { + success: true, + message: `Scheduled Pipeline "pipeline1" with task "project-pipeline1" using cron "${cron}"`, + }; + nock(serverUrl) + .post(`/fabric/v4/projects/${PROJECT}/pipelines/${pipeline}/run`) + .query({ scheduleCron: cron }) + .reply(200, response); + await create().parseAsync(['node', 'pipelines', 'run', pipeline, 'repo1', '--project', PROJECT, '--scheduleCron', cron]); + const output = getPrintedLines().join(''); + const errs = getErrorLines().join(''); + chai.expect(output).to.equal(response.message); + // eslint-disable-next-line no-unused-expressions + chai.expect(errs).to.be.empty; + nock.isDone(); + }); + + it('runs a Pipeline on a set schedule with a user specified schedule name', async () => { + const pipeline = 'pipeline-name'; + const name = 'my-custom-schedule'; + const cron = '@hourly'; + const response = { + success: true, + message: `Scheduled Pipeline "pipeline1" with task "${name}" using cron "${cron}"`, + }; + nock(serverUrl) + .post(`/fabric/v4/projects/${PROJECT}/pipelines/${pipeline}/run`) + .query({ scheduleCron: cron, scheduleName: name }) + .reply(200, response); + await create().parseAsync(['node', 'pipelines', 'run', pipeline, 'repo1', '--project', PROJECT, '--scheduleCron', cron, '--scheduleName', name]); + const output = getPrintedLines().join(''); + const errs = getErrorLines().join(''); + chai.expect(output).to.equal(response.message); + // eslint-disable-next-line no-unused-expressions + chai.expect(errs).to.be.empty; + nock.isDone(); + }); + + it('fails to run a Pipeline due to params-file not found', async () => { + try { + await create().parseAsync(['node', 'pipelines', 'run', 'pipeline1', 'repo1', '--project', PROJECT, '--params-file', '/tmp/not-found.json']); + } catch { + const output = getPrintedLines().join(''); + const errs = getErrorLines().join(''); + chai.expect(errs).to.include('File does not exist at: /tmp/not-found.json'); + // eslint-disable-next-line no-unused-expressions + chai.expect(output).to.be.empty; + nock.isDone(); + } + }); + + it('fails to run a Pipeline due to invalid params', async () => { + try { + await create().parseAsync(['node', 'pipelines', 'run', 'pipeline1', 'repo1', '--project', PROJECT, '--params', '{']); + } catch { + const output = getPrintedLines().join(''); + const errs = getErrorLines().join(''); + chai.expect(errs).to.include('Failed to parse params: { Error: YAMLException: unexpected end of the stream within a flow collection (2:1)'); + // eslint-disable-next-line no-unused-expressions + chai.expect(output).to.be.empty; + nock.isDone(); + } + }); + }); + + xit('list pipeline runs', async () => { + const response = { + success: true, + activations: [ + { + activationId: '4ea4199-1590-46ca-b49d-2f2574ef5873', + status: 'COMPLETE', + start: 1696875227868, + end: 1696875239664, + agentName: 'toy-pipeline', + }, + ], + }; + nock(serverUrl).get(/\/fabric\/v4\/projects\/.*\/pipelines\/.*\/run/).query({ gitRepoName: 'repo1' }).reply(200, response); + await create().parseAsync(['node', 'pipelines', 'list-runs', 'pipeline1', 'repo1', '--project', PROJECT]); + const errs = getErrorLines().join(''); + // eslint-disable-next-line no-unused-expressions + chai.expect(errs).to.be.empty; + nock.isDone(); + }); + + describe('#describePipelineRun()', () => { + xit('describe pipeline run', async () => { + const response = { + success: true, + agentName: 'toy-pipeline', + channelId: 'd9e97efc-c663-43e7-a435-2b85d6977d1c', + payload: { + config: '/app/conf/spark-conf-sensa-data-pipeline.json', + app_command: [ + '--var', + 'key=value', + '--pipeline', + 'claro_pipeline', + '--config', + 'config_catalog.yml', + '.', + '--repo', + 'git@github.com:CognitiveScale/sensa-data-pipelines.git', + '--block', + 'someblock', + ], + }, + projectId: 'mc-test', + requestId: '54ea4199-1590-46ca-b49d-2f2574ef5873', + serviceName: 'input', + sessionId: '54ea4199-1590-46ca-b49d-2f2574ef5873', + sync: false, + timestamp: 1696875227736, + token: 'eyJhbGciOiJxm_K3fAA', + username: 'cortex@example.com', + start: 1696875227868, + status: 'COMPLETE', + end: 1696875239664, + response: {}, + }; + nock(serverUrl).get(/\/fabric\/v4\/projects\/.*\/pipelines\/.*\/run\/.*/).query({ gitRepoName: 'repo1' }).reply(200, response); + await create().parseAsync(['node', 'pipelines', 'describe-run', 'pipeline1', 'repo1', 'd9e97efc-c663-43e7-a435-2b85d6977d1c', '--project', PROJECT]); + const errs = getErrorLines().join(''); + // eslint-disable-next-line no-unused-expressions + chai.expect(errs).to.be.empty; + nock.isDone(); + }); + }); + }); +}); diff --git a/test/sanity.js b/test/sanity.js deleted file mode 100644 index cb979248..00000000 --- a/test/sanity.js +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright 2018 Cognitive Scale, Inc. All Rights Reserved. - * - * 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. - */ - -const commander = require('commander'); -const { ListProfilesCommand } = require('../src/commands/configure'); - -describe('sanity check', function () { - it('lists profiles', function sanity(done) { - const program = new commander.Command(); - const command = new ListProfilesCommand(program); - command.execute({color: 'on'}); - done(); - }); -}); \ No newline at end of file diff --git a/test/tasks.js b/test/tasks.js new file mode 100644 index 00000000..41ccd62a --- /dev/null +++ b/test/tasks.js @@ -0,0 +1,176 @@ +import chai from 'chai'; +import mockedEnv from 'mocked-env'; +import nock from 'nock'; +import _ from 'lodash'; +import sinon from 'sinon'; +import Tasks from '../src/client/tasks.js'; +import { + compatibilityApi, compatiblityResponse, infoApi, infoResponse, stripAnsi, +} from './utils.js'; +import { create } from '../bin/cortex-tasks.js'; + +const { expect } = chai; +const PROJECT = 'project'; +describe('Tasks', () => { + let restoreEnv; + let sandbox; + let printSpy; + let errorSpy; + const serverUrl = 'http://localhost:8000'; + before(() => { + if (!nock.isActive()) { + nock.activate(); + } + restoreEnv = mockedEnv({ + CORTEX_CONFIG_DIR: './test/cortex', + }); + sandbox = sinon.createSandbox(); + sandbox.stub(process, 'exit').throws(Error('EXIT')); + printSpy = sandbox.spy(console, 'log'); + errorSpy = sandbox.spy(console, 'error'); + }); + after(() => { + restoreEnv(); + sandbox.restore(); + nock.restore(); + }); + beforeEach(() => { + nock(serverUrl).get(compatibilityApi()).reply(200, compatiblityResponse()); + nock(serverUrl).persist().get(infoApi()).reply(200, infoResponse()); + }); + afterEach(() => { + // clean up mocks that may not have been called + nock.cleanAll(); + }); + function getPrintedLines() { + return _.flatten(printSpy.args).map((s) => stripAnsi(s)); + } + function getErrorLines() { + return _.flatten(errorSpy.args).map((s) => stripAnsi(s)); + } + it('list tasks command (old response)', async () => { + const response = { success: true, tasks: ['task0', 'task1'] }; + nock(serverUrl).get(/\/fabric\/v4\/projects\/.*\/tasks.*/).reply(200, response); + await create().parseAsync(['node', 'tasks', 'list', '--project', PROJECT]); + const output = getPrintedLines(); + const errs = getErrorLines(); + chai.expect(output.join('')).to.contain('task0'); + chai.expect(output.join('')).to.contain('task1'); + // eslint-disable-next-line no-unused-expressions + chai.expect(errs).to.be.empty; + nock.isDone(); + }); + it('list tasks command JSON no tasks', async () => { + const response = { success: true, tasks: [] }; + nock(serverUrl).get(/\/fabric\/v4\/projects\/.*\/tasks.*/).reply(200, response); + await create().parseAsync(['node', 'tasks', 'list', '--project', PROJECT, '--json']); + const output = getPrintedLines(); + const errs = getErrorLines(); + chai.expect(output.join('')).to.contain('No tasks found'); + // eslint-disable-next-line no-unused-expressions + chai.expect(errs).to.be.empty; + nock.isDone(); + }); + it('list tasks command JSON without JSMEsearch', async () => { + const response = { success: true, tasks: ['task0', 'task1'] }; + nock(serverUrl).get(/\/fabric\/v4\/projects\/.*\/tasks.*/).reply(200, response); + await create().parseAsync(['node', 'tasks', 'list', '--project', PROJECT, '--json']); + const output = getPrintedLines(); + const errs = getErrorLines(); + chai.expect(output.join('')).to.contain('name'); + chai.expect(output.join('')).to.contain('task0'); + chai.expect(output.join('')).to.contain('task1'); + // eslint-disable-next-line no-unused-expressions + chai.expect(errs).to.be.empty; + nock.isDone(); + }); + it('list tasks command JSON with JSMEsearch', async () => { + const now = Date.now(); + const response = { + success: true, + tasks: [ + { + name: 'task0', startTime: now - (30 * 60000), endTime: now - (10 * 60000), activationId: 'xxxxxx1', skillName: 'skill1', actionName: 'action1', status: 'COMPLETE', + }, + { + name: 'task1', startTime: now - (40 * 60000), endTime: now - (10 * 60000), activationId: 'xxxxxx2', skillName: 'skill2', actionName: 'action1', status: 'FAIL', + }, + ], + }; + nock(serverUrl).get(/\/fabric\/v4\/projects\/.*\/tasks.*/).reply(200, response); + await create().parseAsync(['node', 'tasks', 'list', '--project', PROJECT, '--json', '[].{skillName: skillName, name: name}']); + const output = getPrintedLines(); + const errs = getErrorLines(); + chai.expect(output.join('')).to.not.contain('activationId'); + chai.expect(output.join('')).to.contain('skillName'); + chai.expect(output.join('')).to.contain('task0'); + chai.expect(output.join('')).to.contain('task1'); + // eslint-disable-next-line no-unused-expressions + chai.expect(errs).to.be.empty; + nock.isDone(); + }); + it('list tasks command QUERY should work as JSON alias with JSMEsearch', async () => { + const now = Date.now(); + const response = { + success: true, + tasks: [ + { + name: 'task0', startTime: now - (30 * 60000), endTime: now - (10 * 60000), activationId: 'xxxxxx1', skillName: 'skill1', actionName: 'action1', status: 'COMPLETE', + }, + { + name: 'task1', startTime: now - (40 * 60000), endTime: now - (10 * 60000), activationId: 'xxxxxx2', skillName: 'skill2', actionName: 'action1', status: 'FAIL', + }, + ], + }; + nock(serverUrl).get(/\/fabric\/v4\/projects\/.*\/tasks.*/).reply(200, response); + await create().parseAsync(['node', 'tasks', 'list', '--project', PROJECT, '--query', '[].{skillName: skillName, name: name}']); + const output = getPrintedLines(); + const errs = getErrorLines(); + chai.expect(output.join('')).to.not.contain('activationId'); + chai.expect(output.join('')).to.contain('skillName'); + chai.expect(output.join('')).to.contain('task0'); + chai.expect(output.join('')).to.contain('task1'); + // eslint-disable-next-line no-unused-expressions + chai.expect(errs).to.be.empty; + nock.isDone(); + }); + it('list tasks command', async () => { + const now = Date.now(); + const response = { + success: true, + tasks: [ + { + name: 'task0', startTime: now - (30 * 60000), endTime: now - (10 * 60000), activationId: 'xxxxxx1', skillName: 'skill1', actionName: 'action1', status: 'COMPLETE', + }, + { + name: 'task1', startTime: now - (40 * 60000), endTime: now - (10 * 60000), activationId: 'xxxxxx2', skillName: 'skill2', actionName: 'action1', status: 'FAIL', + }, + ], + }; + nock(serverUrl).get(/\/fabric\/v4\/projects\/.*\/tasks.*/).reply(200, response); + await create().parseAsync(['node', 'tasks', 'list', '--project', PROJECT]); + const output = getPrintedLines().join(''); + const errs = getErrorLines(); + chai.expect(output).to.contain('task0', output); + chai.expect(output).to.contain('20 minutes', output); + chai.expect(output).to.contain('30 minutes', output); + chai.expect(output).to.contain('30 minutes ago', output); + chai.expect(output).to.contain('40 minutes ago', output); + chai.expect(output).to.contain('task0', output); + chai.expect(output).to.contain('task1', output); + // eslint-disable-next-line no-unused-expressions + chai.expect(errs).to.be.empty; + nock.isDone(); + }); + it('list tasks client JSON', async () => { + const taskCtl = new Tasks(serverUrl); + const options = { + json: true, + }; + const response = { success: true, tasks: ['task0', 'task1'] }; + nock(serverUrl).get(/\/fabric\/v4\/projects\/.*\/tasks.*/).reply(200, response); + const taskList = await taskCtl.listTasks(PROJECT, '', options); + console.log(`taskList: ${JSON.stringify(taskList)}`); + expect(taskList).to.deep.equal(response); + }); +}); diff --git a/test/testUtilFunctions.js b/test/testUtilFunctions.js new file mode 100644 index 00000000..33104f75 --- /dev/null +++ b/test/testUtilFunctions.js @@ -0,0 +1,124 @@ +import { expect } from 'chai'; +import _ from 'lodash'; +import mockedEnv from 'mocked-env'; +import sinon from 'sinon'; +import { getQueryOptions, getFilteredOutput } from '../src/commands/utils.js'; +import { stripAnsi } from './utils.js'; + +describe('Test getQueryOptions function', () => { + it('should have empty query string if options.json is true', () => { + const options = { + json: true, + }; + const queryOptions = getQueryOptions(options); + expect(queryOptions.query).to.eql(null); + }); + it('should retain the query string if options.json is not true', () => { + const queryPath = '[].{name: name, title: title}'; + const options = { + json: queryPath, + }; + const queryOptions = getQueryOptions(options); + expect(queryOptions.query).to.eql(queryPath); + }); + // TODO remove --query on deprecation + // handle cases 'cortex entity command --query <path> --json' + it('--query [arg] should override --json [arg] the if both are passed', () => { + const queryPath = '[].{name: name, title: title}'; + const jsonPath = '[].{name: name}'; + const options = { + json: jsonPath, + query: queryPath, + }; + const queryOptions = getQueryOptions(options); + expect(queryOptions.query).to.eql(queryPath); + }); +}); +describe('Test getFilteredOutput function', () => { + let restore; + let printSpy; + let printSpyConsole; + before(() => { + restore = mockedEnv({}); + }); + beforeEach(() => { + printSpy = sinon.spy(process.stderr, 'write'); + printSpyConsole = sinon.spy(console, 'log'); + }); + afterEach(() => { + printSpy.restore(); + printSpyConsole.restore(); + }); + after(() => { + restore(); + }); + function getPrintedLines() { + return _.flatten(printSpy.args).map((s) => stripAnsi(s)); + } + function getPrintedConsoleLines() { + return _.flatten(printSpyConsole.args).map((s) => stripAnsi(s)); + } + it('should print Deprecation warning when query option is present', () => { + const queryPath = '[].{name: name, title: title}'; + const options = { + query: queryPath, + }; + getFilteredOutput([], options); + expect(getPrintedLines()).to.eql(['[DEPRECATION WARNING] --query\n']); + }); + it('should print error message when query has invalid input', () => { + // handle cases 'cortex entity command --query <path> --json' + const queryPath = '--json'; + const options = { + query: queryPath, + }; + getFilteredOutput([], options); + expect(getPrintedLines()[1]).to.eql(`error: invalid argument: ${queryPath} \n`); + }); + it('should print json output with correct params', () => { + // handle cases 'cortex entity command --query <path> --json' + const options = { + json: true, + }; + const data = { name: 'test-name', title: 'test-title' }; + getFilteredOutput(data, options); + expect(getPrintedConsoleLines()[0]).to.contain('name'); + expect(getPrintedConsoleLines()[0]).to.contain('test-name'); + expect(getPrintedConsoleLines()[0]).to.contain('title'); + expect(getPrintedConsoleLines()[0]).to.contain('test-title'); + }); + it('should print json output with correct query args', () => { + const options = { + query: 'title', + }; + const data = { name: 'test-name', title: 'test-title' }; + getFilteredOutput(data, options); + expect(getPrintedLines()[0]).to.contain(['[DEPRECATION WARNING] --query']); + expect(getPrintedConsoleLines()[0]).to.not.contain('name'); + expect(getPrintedConsoleLines()[0]).to.not.contain('test-name'); + expect(getPrintedConsoleLines()[0]).to.contain('test-title'); + }); + it('should print json output with correct json args without Deprecation warning', () => { + const options = { + json: 'title', + }; + const data = { name: 'test-name', title: 'test-title' }; + getFilteredOutput(data, options); + expect(getPrintedLines()).to.not.contain(['[DEPRECATION WARNING] --query']); + expect(getPrintedConsoleLines()[0]).to.not.contain('name'); + expect(getPrintedConsoleLines()[0]).to.not.contain('test-name'); + expect(getPrintedConsoleLines()[0]).to.contain('test-title'); + }); + it('should override json path when both query and json path are passed', () => { + const options = { + json: 'title', + query: 'name', + }; + const data = { name: 'test-name', title: 'test-title' }; + getFilteredOutput(data, options); + expect(getPrintedLines()[0]).to.contain(['[DEPRECATION WARNING] --query']); + expect(getPrintedConsoleLines()[0]).to.not.contain('title'); + expect(getPrintedConsoleLines()[0]).to.not.contain('test-title'); + expect(getPrintedConsoleLines()[0]).to.contain('test-name'); + }); +}); diff --git a/test/testUtils.js b/test/testUtils.js new file mode 100644 index 00000000..59254a40 --- /dev/null +++ b/test/testUtils.js @@ -0,0 +1,413 @@ +import _ from 'lodash'; +import { expect } from 'chai'; +import mockedEnv from 'mocked-env'; +import sinon from 'sinon'; +import { + handleTable, + printExtendedLogs, + handleListFailure, + handleDeleteFailure, + checkForEmptyArgs, + printCrossTable, +} from '../src/commands/utils.js'; +import { stripAnsi } from './utils.js'; + +const sampleData = [ + { user: 'user-1', name: 'user-name-1' }, + { user: 'user-2', name: 'user-name-2' }, + { user: 'user-3', name: 'user-name-3' }, +]; +const tableSpec = [ + { column: 'User', field: 'user' }, + { column: 'Name', field: 'name' }, +]; +let restore; +describe('Handle table function test', () => { + let printSpy; + before(() => { + restore = mockedEnv({}); + }); + beforeEach(() => { + printSpy = sinon.spy(console, 'log'); + }); + afterEach(() => { + printSpy.restore(); + }); + after(() => { + restore(); + }); + function getPrintedLines() { + return _.flatten(printSpy.args).map((s) => stripAnsi(s)); + } + it('should print supplied error message when data is empty', () => { + handleTable(tableSpec, [], null, 'No users found'); + expect(getPrintedLines()).to.eql(['No users found']); + }); + it('should print a table when data is present', () => { + handleTable(tableSpec, sampleData, null, 'No users found'); + expect(getPrintedLines()).to.eql([ + '┌────────┬─────────────┐\n' + + '│ User │ Name │\n' + + '├────────┼─────────────┤\n' + + '│ user-1 │ user-name-1 │\n' + + '├────────┼─────────────┤\n' + + '│ user-2 │ user-name-2 │\n' + + '├────────┼─────────────┤\n' + + '│ user-3 │ user-name-3 │\n' + + '└────────┴─────────────┘', + ]); + }); +}); +describe('Printing Cross-Tables', () => { + let printSpy; + before(() => { + restore = mockedEnv({}); + }); + beforeEach(() => { + printSpy = sinon.spy(console, 'log'); + }); + afterEach(() => { + printSpy.restore(); + }); + after(() => { + restore(); + }); + function getPrintedLines() { + return _.flatten(printSpy.args).map((s) => stripAnsi(s)); + } + + it('should print a cross-table when data is present', () => { + const tableSections = { + first: [sampleData[0]], + second: [], + third: [sampleData[1], sampleData[2]], + }; + printCrossTable(tableSpec, tableSections); + expect(getPrintedLines()).to.eql([ + '┌────────┬────────┬─────────────┐\n' + + '│ │ User │ Name │\n' + + '├────────┼────────┼─────────────┤\n' + + '│ first │ user-1 │ user-name-1 │\n' + + '├────────┼────────┼─────────────┤\n' + + '│ second │ - │ - │\n' + + '├────────┼────────┼─────────────┤\n' + + '│ │ user-2 │ user-name-2 │\n' + + '│ third ├────────┼─────────────┤\n' + + '│ │ user-3 │ user-name-3 │\n' + + '└────────┴────────┴─────────────┘', + ]); + }); + + it('should print a cross-table with an explicit column widths (abbreviates fields)', () => { + const tableSpec2 = JSON.parse(JSON.stringify(tableSpec)); // TODO: structuredClone() + tableSpec2.unshift({ column: '', width: 5 }); + tableSpec2[1].width = 10; + tableSpec2[2].width = 10; + const tableSections = { + first: [], + second: [sampleData[0]], + reallyReallyReallyLongName: [sampleData[1], sampleData[2]], + }; + printCrossTable(tableSpec2, tableSections); + expect(getPrintedLines()).to.eql([ + '┌─────┬──────────┬──────────┐\n' + + '│ │ User │ Name │\n' + + '├─────┼──────────┼──────────┤\n' + + '│ fi… │ - │ - │\n' + + '├─────┼──────────┼──────────┤\n' + + '│ se… │ user-1 │ user-na… │\n' + + '├─────┼──────────┼──────────┤\n' + + '│ │ user-2 │ user-na… │\n' + + '│ re… ├──────────┼──────────┤\n' + + '│ │ user-3 │ user-na… │\n' + + '└─────┴──────────┴──────────┘', + ]); + }); + + // Skipped because there are different ways to wrap the 'reallyReallyReallyLongName' if you ignore word boundaries + // and consider varying whitespace in horizontal centering. Noticeable difference in results between Pipeline & + // local run of tests. + xit('should print a cross-table with an explicit column widths and table options', () => { + const tableSpec2 = JSON.parse(JSON.stringify(tableSpec)); // TODO: structuredClone() + tableSpec2.unshift({ column: '', width: 25 }); + tableSpec2[1].width = 5; + tableSpec2[2].width = 5; + const tableSections = { + first: [], + second: [sampleData[0]], + reallyReallyReallyLongName: [sampleData[1], sampleData[2]], + }; + printCrossTable(tableSpec2, tableSections, { + // Verify that tableOptions are honored - expect columns 2 & 3 to wrap words + wordWrap: true, + wrapOnWordBoundary: false, + }); + expect(getPrintedLines()).to.be.oneOf([ + '┌─────────────────────────┬─────┬─────┐\n' + + '│ │ Use │ Nam │\n' + + '│ │ r │ e │\n' + + '├─────────────────────────┼─────┼─────┤\n' + + '│ first │ - │ - │\n' + + '├─────────────────────────┼─────┼─────┤\n' + + '│ │ use │ use │\n' + + '│ │ r-1 │ r-n │\n' + + '│ second │ │ ame │\n' + + '│ │ │ -1 │\n' + + '├─────────────────────────┼─────┼─────┤\n' + + '│ │ use │ use │\n' + + '│ │ r-2 │ r-n │\n' + + '│ │ │ ame │\n' + + '│ │ │ -2 │\n' + + '│ reallyReallyReally ├─────┼─────┤\n' + + '│ LongName │ use │ use │\n' + + '│ │ r-3 │ r-n │\n' + + '│ │ │ ame │\n' + + '│ │ │ -3 │\n' + + '└─────────────────────────┴─────┴─────┘', + ]); + }); + + it('should print a cross-table when no data is present', () => { + const tableSections = { + first: [], + second: [], + third: [], + fourth: [], + }; + printCrossTable(tableSpec, tableSections); + expect(getPrintedLines()).to.eql([ + '┌────────┬──────┬──────┐\n' + + '│ │ User │ Name │\n' + + '├────────┼──────┼──────┤\n' + + '│ first │ - │ - │\n' + + '├────────┼──────┼──────┤\n' + + '│ second │ - │ - │\n' + + '├────────┼──────┼──────┤\n' + + '│ third │ - │ - │\n' + + '├────────┼──────┼──────┤\n' + + '│ fourth │ - │ - │\n' + + '└────────┴──────┴──────┘', + ]); + }); +}); +describe('Test printExtendedLogs function', () => { + let printSpy; + const data = [ + { key1: 'val1' }, + { key2: 'val2' }, + { key3: 'val3' }, + { key4: 'val4' }, + ]; + const limit = 5; + before(() => { + restore = mockedEnv({}); + }); + beforeEach(() => { + printSpy = sinon.spy(process.stderr, 'write'); + }); + afterEach(() => { + printSpy.restore(); + }); + after(() => { + restore(); + }); + function getPrintedLines() { + return _.flatten(printSpy.args).map((s) => stripAnsi(s)); + } + it('should print the result limited message if data length is equal to limit value', () => { + printExtendedLogs([...data, { key5: 'val5' }], { limit }); + expect(getPrintedLines()).to.eql([`Results limited to ${limit} rows\n`]); + }); + it('should not print the result limited message if data length is less than limit value', () => { + printExtendedLogs(data, { limit }); + expect(getPrintedLines()).to.eql([]); + }); +}); +describe('Test handleListFailure function', () => { + let printSpy; + before(() => { + restore = mockedEnv({}); + }); + beforeEach(() => { + printSpy = sinon.spy(console, 'log'); + sinon.stub(process, 'exit'); + }); + afterEach(() => { + printSpy.restore(); + process.exit.restore(); + }); + after(() => { + restore(); + }); + function getPrintedLines() { + return _.flatten(printSpy.args).map((s) => stripAnsi(s)); + } + const errResponse = { + status: 400, + details: [ + { type: 'type1', message: 'message1' }, + { type: 'type2', message: 'message2' }, + { type: 'type3', message: 'message3' }, + ], + }; + it('should print tabular output if status is 400', () => { + handleListFailure(errResponse, null, 'test-resource'); + expect((getPrintedLines()[0])).to.equal('┌────────────────────┬────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐\n' + + '│ Option Type │ Message │\n' + + '├────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤\n' + + '│ type1 │ message1 │\n' + + '├────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤\n' + + '│ type2 │ message2 │\n' + + '├────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤\n' + + '│ type3 │ message3 │\n' + + '└────────────────────┴────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘'); + // eslint-disable-next-line no-unused-expressions + expect(process.exit.calledWith(1)).to.be.true; + }); + const errResponseTsoa = { + status: 400, + details: { + 'query.type1': { + message: 'message1', + value: '-1', + }, + }, + }; + it('should print tabular output if status is 400 with TSOA response', () => { + handleListFailure(errResponseTsoa, null, 'test-resource'); + expect((getPrintedLines()[0])).to.equal('┌────────────────────┬────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐\n' + + '│ Option Type │ Message │\n' + + '├────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤\n' + + '│ type1 │ message1 │\n' + + '└────────────────────┴────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘'); + // eslint-disable-next-line no-unused-expressions + expect(process.exit.calledWith(1)).to.be.true; + }); + const errResponseTsoaWithoutKey = { + status: 400, + details: { + type1: { + message: 'message1', + value: '-1', + }, + }, + }; + it('should print tabular output if status is 400 with TSOA response without dot in the key', () => { + handleListFailure(errResponseTsoaWithoutKey, null, 'test-resource'); + expect((getPrintedLines()[0])).to.equal('┌────────────────────┬────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐\n' + + '│ Option Type │ Message │\n' + + '├────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤\n' + + '│ type1 │ message1 │\n' + + '└────────────────────┴────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘'); + // eslint-disable-next-line no-unused-expressions + expect(process.exit.calledWith(1)).to.be.true; + }); +}); +describe('Test handleDeleteFailure', () => { + let printSpy; + let errorSpy; + before(() => { + restore = mockedEnv({}); + }); + beforeEach(() => { + printSpy = sinon.spy(console, 'log'); + errorSpy = sinon.spy(console, 'error'); + sinon.stub(process, 'exit'); + }); + afterEach(() => { + printSpy.restore(); + errorSpy.restore(); + process.exit.restore(); + }); + after(() => { + restore(); + }); + function getPrintedLines() { + return _.flatten(printSpy.args).map((s) => stripAnsi(s)); + } + function getErrorLines() { + return _.flatten(errorSpy.args).map((s) => stripAnsi(s)); + } + const depErrorResponse = { + status: 403, + message: 'message', + details: [ + { type: 'type1', name: 'name1' }, + { type: 'type2', name: 'name2' }, + { type: 'type3', name: 'name3' }, + ], + }; + const serverErrorResponse = { + status: 500, + message: 'Server Unavailable', + details: [], + }; + it('should print tabular output if status is 403', () => { + handleDeleteFailure(depErrorResponse, null, 'test-resource'); + expect(getErrorLines()[0]).to.equal('test-resource deletion failed: message.'); + expect((getPrintedLines()[0])).to.equal('┌────────────────────────────────────────────────────────────┬────────────────────────────────────────┐\n' + + '│ Dependency Name │ Dependency Type │\n' + + '├────────────────────────────────────────────────────────────┼────────────────────────────────────────┤\n' + + '│ name1 │ type1 │\n' + + '├────────────────────────────────────────────────────────────┼────────────────────────────────────────┤\n' + + '│ name2 │ type2 │\n' + + '├────────────────────────────────────────────────────────────┼────────────────────────────────────────┤\n' + + '│ name3 │ type3 │\n' + + '└────────────────────────────────────────────────────────────┴────────────────────────────────────────┘'); + // eslint-disable-next-line no-unused-expressions + expect(process.exit.calledWith(1)).to.be.true; + }); + it('should print the default error message is not 403 (500)', () => { + handleDeleteFailure(serverErrorResponse, null, 'test-resource'); + expect(getErrorLines()[0]).to.equal('test-resource deletion failed: 500 Server Unavailable.'); + // eslint-disable-next-line no-unused-expressions + expect(process.exit.calledWith(1)).to.be.true; + }); +}); + +describe('checkForEmptyArgs', () => { + let printSpy; + let errorSpy; + before(() => { + restore = mockedEnv({}); + }); + beforeEach(() => { + printSpy = sinon.spy(console, 'log'); + errorSpy = sinon.spy(console, 'error'); + sinon.stub(process, 'exit'); + }); + afterEach(() => { + printSpy.restore(); + errorSpy.restore(); + process.exit.restore(); + }); + after(() => { + restore(); + }); + it('should throw an error for empty argument', () => { + const args = { + arg1: 'value1', + arg2: '', + arg3: 'value3', + }; + + const expectedErrorMessage = 'error: <arg2> cannot be empty.'; + + try { + checkForEmptyArgs(args); + } catch (error) { + // Check if the error message matches the expected error message + expect(error.message).to.equal(expectedErrorMessage); + } + }); + + it('should not throw an error when arguments are not empty', () => { + const args = { + arg1: 'value1', + arg2: 'value2', + arg3: 'value3', + }; + + expect(() => checkForEmptyArgs(args)).to.not.throw(); + }); + }); diff --git a/test/useragent.js b/test/useragent.js new file mode 100644 index 00000000..005f1eba --- /dev/null +++ b/test/useragent.js @@ -0,0 +1,46 @@ +import chai from 'chai'; +import chaiAsPromised from 'chai-as-promised'; +import nock from 'nock'; +import sinon from 'sinon'; +import npmFetch from 'npm-registry-fetch'; +import * as compatibility from '../src/compatibility.js'; +import { readPackageJSON } from '../src/commands/utils.js'; + +const pkg = readPackageJSON('../../package.json'); + +chai.use(chaiAsPromised); + +let sandbox; +describe('useragent', () => { + const profile = { + url: 'http://example.com', + account: 'testAccount', + projectId: 'testTenant', + username: 'testUser', + token: 'testToken', + }; + before(() => { + if (!nock.isActive()) { + nock.activate(); + } + // Compatibility API call is as good as any to verify the request has user-agent, + // so mock it the same way we do for that test, adding capture of headers. + sandbox = sinon.createSandbox(); + sandbox.stub(npmFetch, 'json') + .returns(Promise.resolve({ versions: { [pkg.version]: {} } })); + }); + after(() => { + sandbox.restore(); + nock.cleanAll(); + }); + it('should exist in request headers', async () => { + nock(profile.url) + .get('/fabric/v4/compatibility/applications/cortex-cli') + .reply(200, () => ({ semver: pkg.version })) + .matchHeader('user-agent', (val) => { + console.log(val); + return val.includes(`${pkg.name}/${pkg.version}`); + }); + await compatibility.getCompatibility(profile); + }); +}); diff --git a/test/users.js b/test/users.js new file mode 100644 index 00000000..8ef99eaa --- /dev/null +++ b/test/users.js @@ -0,0 +1,103 @@ +import chai from 'chai'; +import mockedEnv from 'mocked-env'; +import nock from 'nock'; +import _ from 'lodash'; +import sinon from 'sinon'; +import { create } from '../bin/cortex-users.js'; +import { + compatibilityApi, compatiblityResponse, infoApi, infoResponse, stripAnsi, +} from './utils.js'; + +describe('Users', () => { + let restoreEnv; + let sandbox; + let printSpy; + let errorSpy; + let warnSpy; + const serverUrl = 'http://localhost:8000'; + beforeEach(() => { + if (!nock.isActive()) { + nock.activate(); + } + restoreEnv = mockedEnv({ + CORTEX_CONFIG_DIR: './test/cortex', + }); + sandbox = sinon.createSandbox(); + sandbox.stub(process, 'exit').throws(Error('EXIT')); + printSpy = sandbox.spy(console, 'log'); + errorSpy = sandbox.spy(console, 'error'); + warnSpy = sandbox.spy(console, 'warn'); + nock(serverUrl).get(compatibilityApi()).reply(200, compatiblityResponse()); + nock(serverUrl).persist().get(infoApi()).reply(200, infoResponse()); + }); + + afterEach(() => { + // clean up mocks that may not have been called + restoreEnv(); + nock.cleanAll(); + sandbox.restore(); + }); + + function getPrintedLines() { + return _.flatten(printSpy.args).map((s) => stripAnsi(s)); + } + + function getErrorLines() { + return _.flatten(errorSpy.args).map((s) => stripAnsi(s)); + } + + // eslint-disable-next-line no-unused-vars + function getWarningLines() { + return _.flatten(warnSpy.args).map((s) => stripAnsi(s)); + } + + describe('List Users', () => { + const response = { + success: true, + users: ['test-service', 'abc1', 'asdf', 'abc3'], + }; + + it('lists users - empty', async () => { + const empty = { success: true, users: [] }; + nock(serverUrl).get(/\/fabric\/v4\/accounts\/service/).reply(200, empty); + await create().parseAsync(['node', 'users', 'list']); + const output = getPrintedLines(); + const errs = getErrorLines(); + // eslint-disable-next-line no-unused-expressions + chai.expect(errs.join('')).to.be.empty; + chai.expect(output.join('')).to.contain('No service users found'); + nock.isDone(); + }); + + it('lists users', async () => { + nock(serverUrl) + .get(/\/fabric\/v4\/accounts\/service/) + .reply(200, response); + await create().parseAsync(['node', 'users', 'list']); + const output = getPrintedLines().join(''); + const errs = getErrorLines().join(''); + // Only check for expected strings - would be better to compare the table length + const expectedKeys = ['User', ...response.users]; + expectedKeys.forEach((k) => { + chai.expect(output).to.contain(k, `Expected string ${k} not found!`); + }); + // eslint-disable-next-line no-unused-expressions + chai.expect(errs).to.be.empty; + nock.isDone(); + }); + + it('lists users in JSON', async () => { + nock(serverUrl) + .get(/\/fabric\/v4\/accounts\/service/) + .reply(200, response); + await create().parseAsync(['node', 'users', 'list', '--json']); + const output = getPrintedLines().join(''); + const errs = getErrorLines().join(''); + const jsonOutput = JSON.parse(output); + chai.expect(jsonOutput).to.deep.equal(response); + // eslint-disable-next-line no-unused-expressions + chai.expect(errs).to.be.empty; + nock.isDone(); + }); + }); +}); diff --git a/test/utils.js b/test/utils.js new file mode 100644 index 00000000..12a21e20 --- /dev/null +++ b/test/utils.js @@ -0,0 +1,40 @@ +import { getDefaultFeatures } from '../src/features.js'; +// COPIED from https://github.com/chalk/ansi-regex ( esm not cjs :( ) +function ansiRegex({ onlyFirst = false } = {}) { + const pattern = [ + '[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]+)*|[a-zA-Z\\d]+(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)', + '(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~]))', + ].join('|'); + return new RegExp(pattern, onlyFirst ? undefined : 'g'); +} +function stripAnsi(s) { + return s.replace(ansiRegex(), ''); +} +function compatiblityResponse() { + return { semver: '>=0.12.11' }; // arbitrary +} +function compatibilityApi() { + return '/fabric/v4/compatibility/applications/cortex-cli'; +} +function infoApi() { + return '/fabric/v4/info'; +} +function infoResponse() { + const flags = getDefaultFeatures(); + flags.preview.enabled = true; + return flags; +} +export { ansiRegex }; +export { stripAnsi }; +export { compatibilityApi }; +export { compatiblityResponse }; +export { infoApi }; +export { infoResponse }; +export default { + ansiRegex, + stripAnsi, + compatibilityApi, + compatiblityResponse, + infoApi, + infoResponse, +}; diff --git a/test/workspaces.js b/test/workspaces.js new file mode 100644 index 00000000..f788c61b --- /dev/null +++ b/test/workspaces.js @@ -0,0 +1,95 @@ +import chai from 'chai'; +import mockedEnv from 'mocked-env'; +import _ from 'lodash'; +import sinon from 'sinon'; +import fs from 'fs'; +import nock from 'nock'; +import isCI from 'is-ci'; +import { stripAnsi } from './utils.js'; +import { create } from '../bin/cortex-workspaces.js'; +import * as wsutils from '../src/commands/workspaces/workspace-utils.js'; + +describe('Workspaces', () => { + let restoreEnv; + let sandbox; + let printSpy; + let errorSpy; + before(() => { + if (!nock.isActive()) { + nock.activate(); + } + restoreEnv = mockedEnv({ + CORTEX_CONFIG_DIR: '../cortex', + }); + sandbox = sinon.createSandbox(); + printSpy = sandbox.spy(console, 'log'); + errorSpy = sandbox.spy(console, 'error'); + fs.mkdirSync('test/workspaces', { recursive: true }); + process.chdir('test/workspaces'); + }); + // beforeEach(() => { + // delete require.cache[require.resolve('commander')]; + // delete require.cache[require.resolve('../bin/cortex-workspaces')]; + // }); + after(() => { + restoreEnv(); + sandbox.restore(); + process.chdir('..'); + nock.cleanAll(); + fs.rmSync('cortex/cache.dat'); + fs.rmSync('workspaces', { recursive: true }); + }); + function getPrintedLines() { + return _.flatten(printSpy.args).map((s) => stripAnsi(s)); + } + function getErrorLines() { + return _.flatten(errorSpy.args).map((s) => stripAnsi(s)); + } + it('headless workspace persist token', async () => { + nock('https://api.github.com') + .get('/user').reply(200, { statusCode: 200 }); + // test that headless token storage works ... + const tokenToStore = { token_type: 'token_type', access_token: 'access_token' }; + await wsutils.persistToken(tokenToStore); + const token = await wsutils.validateToken(); + chai.expect(token).equal(`${tokenToStore.token_type} ${tokenToStore.access_token}`); + }); + // eslint-disable-next-line func-names + xit('should generate a job skill', async () => { + if (!isCI) { + await create().parseAsync(['node', 'workspaces', 'generate', 'job1', '--notree', '--template', 'Custom Job Skill']); + const output = getPrintedLines(); + const errs = getErrorLines(); + chai.expect(fs.existsSync('docs/job1/README.md')); + chai.expect(fs.existsSync('skills/job1/skill.yaml')); + chai.expect(fs.existsSync('skills/job1/actions/job1/main.py')); + chai.expect(fs.existsSync('skills/job1/actions/job1/requirements.txt')); + chai.expect(fs.existsSync('skills/job1/actions/job1/Dockerfile')); + chai.expect(fs.existsSync('skills/job1/invoke/request/message.json')); + chai.expect(output.join('')).to.contain('Workspace generation complete.'); + // eslint-disable-next-line no-unused-expressions + chai.expect(errs).to.be.empty; + } else { + this.skip(); + } + }); + // eslint-disable-next-line func-names + xit('should generate a daemon skill', async function () { + if (!isCI) { + await create().parseAsync(['node', 'workspaces', 'generate', 'dmn1', '--notree', '--template', 'Custom Daemon Skill']); + const output = getPrintedLines(); + const errs = getErrorLines(); + chai.expect(fs.existsSync('docs/dmn1/README.md')); + chai.expect(fs.existsSync('skills/dmn1/skill.yaml')); + chai.expect(fs.existsSync('skills/dmn1/actions/dmn1/main.py')); + chai.expect(fs.existsSync('skills/dmn1/actions/dmn1/requirements.txt')); + chai.expect(fs.existsSync('skills/dmn1/actions/dmn1/Dockerfile')); + chai.expect(fs.existsSync('skills/dmn1/invoke/request/message.json')); + chai.expect(output.join('')).to.contain('Workspace generation complete.'); + // eslint-disable-next-line no-unused-expressions + chai.expect(errs).to.be.empty; + } else { + this.skip(); + } + }); +});