diff --git a/README-template.md b/README-template.md index a48e7ac..97378be 100644 --- a/README-template.md +++ b/README-template.md @@ -24,6 +24,20 @@ This is necessary for the cli to work and avoid dependency conflicts, when insta 2. Use the cli through npx e.g. `npx @octomind/octomind -h` +## Autocompletion + +On macOS and linux you can enable autocompletion by running the following command (works for bash, zsh & fish): +```bash +npx @octomind/octomind install-completion +``` +You can disable autocompletion by running the following command: +```bash +npx @octomind/octomind uninstall-completion +``` + +Autocompletion will only work if you have installed the package globally and created a config file with `npx @octomind/octomind init`. +This way even entityIds like environmentIds or testCaseIds will be autocompleted. + ${commands} ## Output Formats diff --git a/README.md b/README.md index c5b9849..4441198 100644 --- a/README.md +++ b/README.md @@ -24,9 +24,23 @@ This is necessary for the cli to work and avoid dependency conflicts, when insta 2. Use the cli through npx e.g. `npx @octomind/octomind -h` +## Autocompletion + +On macOS and linux you can enable autocompletion by running the following command (works for bash, zsh & fish): +```bash +npx @octomind/octomind install-completion +``` +You can disable autocompletion by running the following command: +```bash +npx @octomind/octomind uninstall-completion +``` + +Autocompletion will only work if you have installed the package globally and created a config file with `npx @octomind/octomind init`. +This way even entityIds like environmentIds or testCaseIds will be autocompleted. + # octomind -Octomind cli tool. Version: 1.1.3. Additional documentation see https://octomind.dev/docs/api-reference/ +Octomind cli tool. Version: 1.3.0. Additional documentation see https://octomind.dev/docs/api-reference/ **Usage:** `octomind [options] [command]` @@ -38,13 +52,13 @@ Octomind cli tool. Version: 1.1.3. Additional documentation see https://octomind # octomind CLI Documentation -Octomind cli tool. Version: 1.1.3. Additional documentation see https://octomind.dev/docs/api-reference/ +Octomind cli tool. Version: 1.3.0. Additional documentation see https://octomind.dev/docs/api-reference/ -## Commands +## Setup ## init -Initialize configuration by setting up API key +Initialize configuration by setting up API key. This will create a config file in ~/.config/octomind.json **Usage:** `init [options]` @@ -58,218 +72,255 @@ Initialize configuration by setting up API key ## switch-test-target -Switch to a different test target +Switch to a different test target. This will list all available test targets and update the config file in ~/.config/octomind.json **Usage:** `switch-test-target [options]` -## debug +## Completion -run test cases against local build +## install-completion -**Usage:** `debug [options]` +Install tab completion + +**Usage:** `install-completion [options]` + +## uninstall-completion + +Uninstall tab completion + +**Usage:** `uninstall-completion [options]` + +## completion + +Tab completion + +**Usage:** `completion [options]` + +## Environments + +## list-environments + +List all environments + +**Usage:** `list-environments [options]` ### Options | Option | Description | Required | Default | |--------|-------------|----------|--------| | `-j, --json` | Output raw JSON response | No | | -| `-u, --url ` | url the tests should run against | Yes | | -| `-i, --id [uuid]` | id of the test case you want to run, if not provided will run all test cases in the test target | No | | -| `-e, --environment-id [uuid]` | id of the environment you want to run against, if not provided will run all test cases against the default environment | No | | -| `-a, --test-target-id [uuid]` | id of the test target of the test case, if not provided will use the test target id from the config | No | | -| `--headless` | if we should run headless without the UI of playwright and the browser | No | | -| `--persist` | if we should write playwright config and files to current directory, you can then run 'npx playwright test' to run them again | No | | -| `--grep [substring]` | filter test cases by substring | No | | +| `-t, --test-target-id [id]` | Test target ID, if not provided will use the test target id from the config | No | | -## execute +## create-environment -Execute test cases +Create a new environment -**Usage:** `execute [options]` +**Usage:** `create-environment [options]` ### Options | Option | Description | Required | Default | |--------|-------------|----------|--------| | `-j, --json` | Output raw JSON response | No | | -| `-u, --url ` | URL to test | Yes | | +| `-n, --name ` | Environment name | Yes | | +| `-d, --discovery-url ` | Discovery URL | Yes | | | `-t, --test-target-id [id]` | Test target ID, if not provided will use the test target id from the config | No | | -| `-e, --environment-name [name]` | Environment name | No | default | -| `-d, --description [text]` | Test description | No | | -| `-g, --tags [tags]` | comma separated list of tags | No | | -| `-v, --variables-to-overwrite [variables]` | JSON object of variables to overwrite | No | | +| `--test-account-username [username]` | Test account username | No | | +| `--test-account-password [password]` | Test account password | No | | +| `--test-account-otp-initializer-key [key]` | Test account OTP initializer key | No | | +| `--basic-auth-username [username]` | Basic auth username | No | | +| `--basic-auth-password [password]` | Basic auth password | No | | +| `--private-location-name [name]` | Private location name | No | | -## test-report +## environment -Get test report details +Get an environment -**Usage:** `test-report [options]` +**Usage:** `environment [options]` ### Options | Option | Description | Required | Default | |--------|-------------|----------|--------| | `-j, --json` | Output raw JSON response | No | | -| `-r, --test-report-id ` | Test report ID | Yes | | +| `-e, --environment-id ` | Environment ID | Yes | | | `-t, --test-target-id [id]` | Test target ID, if not provided will use the test target id from the config | No | | -## register-location +## update-environment -Register a private location +Update an existing environment -**Usage:** `register-location [options]` +**Usage:** `update-environment [options]` ### Options | Option | Description | Required | Default | |--------|-------------|----------|--------| | `-j, --json` | Output raw JSON response | No | | -| `-n, --name ` | Location name | Yes | | -| `-p, --password ` | Proxy password | Yes | | -| `-u, --username ` | Proxy user | Yes | | -| `-a, --address
` | Location address | Yes | | +| `-e, --environment-id ` | Environment ID | Yes | | +| `-t, --test-target-id [id]` | Test target ID, if not provided will use the test target id from the config | No | | +| `-n, --name [name]` | Environment name | No | | +| `-d, --discovery-url [url]` | Discovery URL | No | | +| `--test-account-username [username]` | Test account username | No | | +| `--test-account-password [password]` | Test account password | No | | +| `--test-account-otp-initializer-key [key]` | Test account OTP initializer key | No | | +| `--basic-auth-username [username]` | Basic auth username | No | | +| `--basic-auth-password [password]` | Basic auth password | No | | +| `--private-location-name [name]` | Private location name | No | | -## unregister-location +## delete-environment -Unregister a private location +Delete an environment -**Usage:** `unregister-location [options]` +**Usage:** `delete-environment [options]` ### Options | Option | Description | Required | Default | |--------|-------------|----------|--------| | `-j, --json` | Output raw JSON response | No | | -| `-n, --name ` | Location name | Yes | | +| `-e, --environment-id ` | Environment ID | Yes | | +| `-t, --test-target-id [id]` | Test target ID, if not provided will use the test target id from the config | No | | -## list-private-locations +## Execute -List all private locations +## debug -**Usage:** `list-private-locations [options]` +run test cases against local build + +**Usage:** `debug [options]` ### Options | Option | Description | Required | Default | |--------|-------------|----------|--------| | `-j, --json` | Output raw JSON response | No | | +| `-u, --url ` | url the tests should run against | Yes | | +| `-c, --test-case-id [uuid]` | id of the test case you want to run, if not provided will run all test cases in the test target | No | | +| `-e, --environment-id [uuid]` | id of the environment you want to run against, if not provided will run all test cases against the default environment | No | | +| `-t, --test-target-id [uuid]` | id of the test target of the test case, if not provided will use the test target id from the config | No | | +| `--headless` | if we should run headless without the UI of playwright and the browser | No | | +| `--persist` | if we should write playwright config and files to current directory, you can then run 'npx playwright test' to run them again | No | | +| `--grep [substring]` | filter test cases by substring | No | | -## list-environments +## execute -List all environments +Execute test cases to create a test report -**Usage:** `list-environments [options]` +**Usage:** `execute [options]` ### Options | Option | Description | Required | Default | |--------|-------------|----------|--------| | `-j, --json` | Output raw JSON response | No | | +| `-u, --url ` | URL to test | Yes | | | `-t, --test-target-id [id]` | Test target ID, if not provided will use the test target id from the config | No | | +| `-e, --environment-name [name]` | Environment name | No | default | +| `-d, --description [text]` | Test description | No | | +| `-g, --tags [tags]` | comma separated list of tags | No | | +| `-v, --variables-to-overwrite [variables]` | JSON object of variables to overwrite | No | | -## create-environment +## create-discovery -Create a new environment +Create a new test case discovery -**Usage:** `create-environment [options]` +**Usage:** `create-discovery [options]` ### Options | Option | Description | Required | Default | |--------|-------------|----------|--------| | `-j, --json` | Output raw JSON response | No | | -| `-n, --name ` | Environment name | Yes | | -| `-d, --discovery-url ` | Discovery URL | Yes | | +| `-n, --name ` | Discovery name | Yes | | +| `-p, --prompt ` | Discovery prompt | Yes | | | `-t, --test-target-id [id]` | Test target ID, if not provided will use the test target id from the config | No | | -| `--test-account-username [username]` | Test account username | No | | -| `--test-account-password [password]` | Test account password | No | | -| `--test-account-otp-initializer-key [key]` | Test account OTP initializer key | No | | -| `--basic-auth-username [username]` | Basic auth username | No | | -| `--basic-auth-password [password]` | Basic auth password | No | | -| `--private-location-name [name]` | Private location name | No | | +| `-e, --entry-point-url-path [path]` | Entry point URL path | No | | +| `--prerequisite-id [id]` | Prerequisite test case ID | No | | +| `--external-id [id]` | External identifier | No | | +| `--assigned-tag-ids [ids]` | Comma-separated list of tag IDs | No | | +| `--folder-id [id]` | Folder ID | No | | -## update-environment +## Notifications -Update an existing environment +## notifications -**Usage:** `update-environment [options]` +Get notifications for a test target + +**Usage:** `notifications [options]` ### Options | Option | Description | Required | Default | |--------|-------------|----------|--------| | `-j, --json` | Output raw JSON response | No | | -| `-e, --environment-id ` | Environment ID | Yes | | | `-t, --test-target-id [id]` | Test target ID, if not provided will use the test target id from the config | No | | -| `-n, --name [name]` | Environment name | No | | -| `-d, --discovery-url [url]` | Discovery URL | No | | -| `--test-account-username [username]` | Test account username | No | | -| `--test-account-password [password]` | Test account password | No | | -| `--test-account-otp-initializer-key [key]` | Test account OTP initializer key | No | | -| `--basic-auth-username [username]` | Basic auth username | No | | -| `--basic-auth-password [password]` | Basic auth password | No | | -| `--private-location-name [name]` | Private location name | No | | -## delete-environment +## Private Locations -Delete an environment +## register-location -**Usage:** `delete-environment [options]` +Register a private location + +**Usage:** `register-location [options]` ### Options | Option | Description | Required | Default | |--------|-------------|----------|--------| | `-j, --json` | Output raw JSON response | No | | -| `-e, --environment-id ` | Environment ID | Yes | | -| `-t, --test-target-id [id]` | Test target ID, if not provided will use the test target id from the config | No | | +| `-n, --name ` | Location name | Yes | | +| `-p, --password ` | Proxy password | Yes | | +| `-u, --username ` | Proxy user | Yes | | +| `-a, --address
` | Location address | Yes | | -## start-private-location +## unregister-location -Start a private location worker, see https://octomind.dev/docs/proxy/private-location +Unregister a private location -**Usage:** `start-private-location [options]` +**Usage:** `unregister-location [options]` ### Options | Option | Description | Required | Default | |--------|-------------|----------|--------| -| `-n, --name [name]` | Location name | No | | -| `-u, --username [username]` | Proxy user | No | | -| `-p, --password [password]` | Proxy password | No | | -| `-l, --host-network` | Use host network (default: false). If set you can use localhost directly | No | false | - -## stop-private-location - -Stop a private location worker, see https://octomind.dev/docs/proxy/private-location - -**Usage:** `stop-private-location [options]` +| `-j, --json` | Output raw JSON response | No | | +| `-n, --name ` | Location name | Yes | | -## notifications +## list-private-locations -Get notifications for a test target +List all private locations -**Usage:** `notifications [options]` +**Usage:** `list-private-locations [options]` ### Options | Option | Description | Required | Default | |--------|-------------|----------|--------| | `-j, --json` | Output raw JSON response | No | | -| `-t, --test-target-id [id]` | Test target ID, if not provided will use the test target id from the config | No | | -## list-test-cases +## start-private-location -List all test cases +Start a private location worker, see https://octomind.dev/docs/proxy/private-location -**Usage:** `list-test-cases [options]` +**Usage:** `start-private-location [options]` ### Options | Option | Description | Required | Default | |--------|-------------|----------|--------| -| `-j, --json` | Output raw JSON response | No | | -| `-t, --test-target-id [id]` | Test target ID, if not provided will use the test target id from the config | No | | +| `-n, --name [name]` | Location name | No | | +| `-u, --username [username]` | Proxy user | No | | +| `-p, --password [password]` | Proxy password | No | | +| `-l, --host-network` | Use host network (default: false). If set you can use localhost directly | No | false | + +## stop-private-location + +Stop a private location worker, see https://octomind.dev/docs/proxy/private-location + +**Usage:** `stop-private-location [options]` + +## Test Cases ## delete-test-case @@ -299,25 +350,36 @@ Get details of a specific test case | `-c, --test-case-id ` | Test case ID | Yes | | | `-t, --test-target-id [id]` | Test target ID, if not provided will use the test target id from the config | No | | -## create-discovery +## list-test-cases -Create a new test case discovery +List all test cases -**Usage:** `create-discovery [options]` +**Usage:** `list-test-cases [options]` ### Options | Option | Description | Required | Default | |--------|-------------|----------|--------| | `-j, --json` | Output raw JSON response | No | | -| `-n, --name ` | Discovery name | Yes | | -| `-p, --prompt ` | Discovery prompt | Yes | | | `-t, --test-target-id [id]` | Test target ID, if not provided will use the test target id from the config | No | | -| `-e, --entry-point-url-path [path]` | Entry point URL path | No | | -| `--prerequisite-id [id]` | Prerequisite test case ID | No | | -| `--external-id [id]` | External identifier | No | | -| `--assigned-tag-ids [ids]` | Comma-separated list of tag IDs | No | | -| `--folder-id [id]` | Folder ID | No | | + +## Test Reports + +## test-report + +Get test report details + +**Usage:** `test-report [options]` + +### Options + +| Option | Description | Required | Default | +|--------|-------------|----------|--------| +| `-j, --json` | Output raw JSON response | No | | +| `-r, --test-report-id ` | Test report ID | Yes | | +| `-t, --test-target-id [id]` | Test target ID, if not provided will use the test target id from the config | No | | + +## Test Targets ## list-test-targets diff --git a/package.json b/package.json index c465c0d..efd4df1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@octomind/octomind", - "version": "1.2.0", + "version": "1.3.0", "description": "a command line client for octomind apis", "main": "./dist/index.js", "packageManager": "pnpm@10.13.1", @@ -13,7 +13,8 @@ }, "scripts": { "gendoc": "tsx scripts/generate-docs.ts > README.md", - "lint": "pnpm apigen &&npx genversion -des src/version.ts && biome check", + "lint": "pnpm apigen && npx genversion -des src/version.ts && biome check", + "tsc": "tsc --project tsconfig.build.json", "apigen": "openapi-typescript https://app.octomind.dev/openapi.yaml --output src/api.ts", "build": "pnpm apigen && npx genversion -des src/version.ts && pnpm gendoc && tsc --project tsconfig.build.json", "octomind": "pnpm apigen && tsx src/index.ts", @@ -21,18 +22,23 @@ "test:watch": "pnpm apigen && npx genversion -e src/version.ts && jest --watch" }, "keywords": [], - "author": "", + "author": "Stefan Rinke", "license": "MIT", "dependencies": { "@playwright/test": "1.54.1", + "@types/shell-quote": "^1.7.5", "commander": "14.0.0", "openapi-fetch": "0.14.0", - "otplib": "12.0.1" + "otplib": "12.0.1", + "shell-quote": "^1.8.3", + "tabtab": "^3.0.2" }, "devDependencies": { "@biomejs/biome": "2.1.3", "@types/jest": "30.0.0", "@types/node": "24.1.0", + "@types/tabtab": "3.0.4", + "genversion": "3.2.0", "jest": "30.0.5", "openapi-typescript": "7.8.0", "ts-jest": "29.4.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f0e94e7..e7bbad3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: '@playwright/test': specifier: 1.54.1 version: 1.54.1 + '@types/shell-quote': + specifier: ^1.7.5 + version: 1.7.5 commander: specifier: 14.0.0 version: 14.0.0 @@ -20,6 +23,12 @@ importers: otplib: specifier: 12.0.1 version: 12.0.1 + shell-quote: + specifier: ^1.8.3 + version: 1.8.3 + tabtab: + specifier: ^3.0.2 + version: 3.0.2 devDependencies: '@biomejs/biome': specifier: 2.1.3 @@ -30,6 +39,12 @@ importers: '@types/node': specifier: 24.1.0 version: 24.1.0 + '@types/tabtab': + specifier: 3.0.4 + version: 3.0.4 + genversion: + specifier: 3.2.0 + version: 3.2.0 jest: specifier: 30.0.5 version: 30.0.5(@types/node@24.1.0)(ts-node@10.9.2(@types/node@24.1.0)(typescript@5.7.3)) @@ -638,9 +653,15 @@ packages: '@types/node@24.1.0': resolution: {integrity: sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w==} + '@types/shell-quote@1.7.5': + resolution: {integrity: sha512-+UE8GAGRPbJVQDdxi16dgadcBfQ+KG2vgZhV1+3A1XmHbmwcdwhCUwIdy+d3pAGrbvgRoVSjeI9vOWyq376Yzw==} + '@types/stack-utils@2.0.3': resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} + '@types/tabtab@3.0.4': + resolution: {integrity: sha512-gmh8JsmIYPGRqk8Xb4dmulV37TpLwg0Quo3GJ0LgEcl4v0O92F14PGebBd7LHv9GBEw2KbmBSrvU0/NzIy5AoA==} + '@types/yargs-parser@21.0.3': resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} @@ -762,10 +783,22 @@ packages: resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} engines: {node: '>=6'} + ansi-escapes@3.2.0: + resolution: {integrity: sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==} + engines: {node: '>=4'} + ansi-escapes@4.3.2: resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} engines: {node: '>=8'} + ansi-regex@3.0.1: + resolution: {integrity: sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==} + engines: {node: '>=4'} + + ansi-regex@4.1.1: + resolution: {integrity: sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==} + engines: {node: '>=6'} + ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} @@ -774,6 +807,10 @@ packages: resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==} engines: {node: '>=12'} + ansi-styles@3.2.1: + resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} + engines: {node: '>=4'} + ansi-styles@4.3.0: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} @@ -870,6 +907,10 @@ packages: caniuse-lite@1.0.30001727: resolution: {integrity: sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==} + chalk@2.4.2: + resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} + engines: {node: '>=4'} + chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} @@ -881,6 +922,9 @@ packages: resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} engines: {node: '>=10'} + chardet@0.7.0: + resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} + ci-info@4.3.0: resolution: {integrity: sha512-l+2bNRMiQgcfILUi33labAZYIWlH1kWDp+ecNo5iisRKrbm0xcRyCww71/YU0Fkw0mAFpz9bJayXPjey6vkmaQ==} engines: {node: '>=8'} @@ -888,6 +932,13 @@ packages: cjs-module-lexer@2.1.0: resolution: {integrity: sha512-UX0OwmYRYQQetfrLEZeewIFFI+wSTofC+pMBLNuH3RUuu/xzG1oz84UCEDOSoQlN3fZ4+AzmV50ZYvGqkMh9yA==} + cli-cursor@2.1.0: + resolution: {integrity: sha512-8lgKz8LmCRYZZQDpRyT2m5rKJ08TnU4tR9FFFW2rxpxR1FzWi4PQ/NfyODchAatHaUgnSPVcx/R5w6NuTBzFiw==} + engines: {node: '>=4'} + + cli-width@2.2.1: + resolution: {integrity: sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw==} + cliui@8.0.1: resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} engines: {node: '>=12'} @@ -899,10 +950,16 @@ packages: collect-v8-coverage@1.0.2: resolution: {integrity: sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==} + color-convert@1.9.3: + resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} + color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} + color-name@1.1.3: + resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} + color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} @@ -913,6 +970,10 @@ packages: resolution: {integrity: sha512-2uM9rYjPvyq39NwLRqaiLtWHyDC1FvryJDa2ATTVims5YAS4PupsEQsDvP14FqhFr0P49CYDugi59xaxJlTXRA==} engines: {node: '>=20'} + commander@7.2.0: + resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} + engines: {node: '>= 10'} + concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} @@ -979,6 +1040,9 @@ packages: error-ex@1.3.2: resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} + es6-promisify@6.1.1: + resolution: {integrity: sha512-HBL8I3mIki5C1Cc9QjKUenHtnG0A5/xA8Q/AllRcfiwl2CZFXGK7ddBiCoRwAix4i2KxcQfjtIVcrVbB3vbmwg==} + esbuild@0.25.5: resolution: {integrity: sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==} engines: {node: '>=18'} @@ -988,6 +1052,10 @@ packages: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} + escape-string-regexp@1.0.5: + resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} + engines: {node: '>=0.8.0'} + escape-string-regexp@2.0.0: resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} engines: {node: '>=8'} @@ -1009,6 +1077,10 @@ packages: resolution: {integrity: sha512-P0te2pt+hHI5qLJkIR+iMvS+lYUZml8rKKsohVHAGY+uClp9XVbdyYNJOIjSRpHVp8s8YqxJCiHUkSYZGr8rtQ==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + external-editor@3.1.0: + resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==} + engines: {node: '>=4'} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -1018,6 +1090,10 @@ packages: fb-watchman@2.0.2: resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} + figures@2.0.0: + resolution: {integrity: sha512-Oa2M9atig69ZkfwiApY8F2Yy+tzMbazyvqv21R0NsSC8floSOC09BbT1ITWAdoMGQvJ/aZnR1KMwdx9tvHnTNA==} + engines: {node: '>=4'} + filelist@1.0.4: resolution: {integrity: sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==} @@ -1025,6 +1101,9 @@ packages: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} + find-package@1.0.0: + resolution: {integrity: sha512-yVn71XCCaNgxz58ERTl8nA/8YYtIQDY9mHSrgFBfiFtdNNfY0h183Vh8BRkKxD8x9TUw3ec290uJKhDVxqGZBw==} + find-up@4.1.0: resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} engines: {node: '>=8'} @@ -1050,6 +1129,11 @@ packages: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} + genversion@3.2.0: + resolution: {integrity: sha512-OIYSX6XYA8PHecLDCTri30hadSZfAjZ8Iq1+BBDXqLWP4dRLuJNLoNjsSWtTpw97IccK2LDWzkEstxAB8GdN7g==} + engines: {node: '>=10.0.0'} + hasBin: true + get-caller-file@2.0.5: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} @@ -1076,6 +1160,10 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + has-flag@3.0.0: + resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} + engines: {node: '>=4'} + has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} @@ -1091,6 +1179,10 @@ packages: resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} engines: {node: '>=10.17.0'} + iconv-lite@0.4.24: + resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} + engines: {node: '>=0.10.0'} + import-local@3.2.0: resolution: {integrity: sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==} engines: {node: '>=8'} @@ -1111,9 +1203,17 @@ packages: inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + inquirer@6.5.2: + resolution: {integrity: sha512-cntlB5ghuB0iuO65Ovoi8ogLHiWGs/5yNrtUcKjFhSSiVeAIVpD7koaSU9RM8mpXw5YDi9RdYXGQMaOURB7ycQ==} + engines: {node: '>=6.0.0'} + is-arrayish@0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + is-fullwidth-code-point@2.0.0: + resolution: {integrity: sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==} + engines: {node: '>=4'} + is-fullwidth-code-point@3.0.0: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} @@ -1334,6 +1434,9 @@ packages: lodash.memoize@4.1.2: resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} + lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} @@ -1357,6 +1460,10 @@ packages: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} + mimic-fn@1.2.0: + resolution: {integrity: sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==} + engines: {node: '>=4'} + mimic-fn@2.1.0: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} engines: {node: '>=6'} @@ -1372,13 +1479,23 @@ packages: resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} engines: {node: '>=16 || 14 >=14.17'} + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + minipass@7.1.2: resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} engines: {node: '>=16 || 14 >=14.17'} + mkdirp@0.5.6: + resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} + hasBin: true + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + mute-stream@0.0.7: + resolution: {integrity: sha512-r65nCZhrbXXb6dXOACihYApHw2Q6pV0M3V0PSxd74N0+D8nzAdEAITq2oAjA1jVnKI+tGvEBUpqiMh0+rW6zDQ==} + napi-postinstall@0.3.2: resolution: {integrity: sha512-tWVJxJHmBWLy69PvO96TZMZDrzmw5KeiZBz3RHmiM2XZ9grBJ2WgMAFVVg25nqp3ZjTFUs2Ftw1JhscL3Teliw==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} @@ -1404,6 +1521,10 @@ packages: once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + onetime@2.0.1: + resolution: {integrity: sha512-oyyPpiMaKARvvcgip+JV+7zci5L8D1W9RZIz2l1o08AM3pfspitVWnPt3mzHcBPp12oYMTy0pqrFs/C+m3EwsQ==} + engines: {node: '>=4'} + onetime@5.1.2: resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} engines: {node: '>=6'} @@ -1420,6 +1541,10 @@ packages: peerDependencies: typescript: ^5.x + os-tmpdir@1.0.2: + resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==} + engines: {node: '>=0.10.0'} + otplib@12.0.1: resolution: {integrity: sha512-xDGvUOQjop7RDgxTQ+o4pOol0/3xSZzawTiPKRrHnQWAy0WjhNs/5HdIDJCrqC4MBynmjXgULc6YfioaxZeFgg==} @@ -1442,6 +1567,9 @@ packages: package-json-from-dist@1.0.1: resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + parents@1.0.1: + resolution: {integrity: sha512-mXKF3xkoUt5td2DoxpLmtOmZvko9VfFpwRwkKDHSNvgmpLAeBo18YDhcPbBzJq+QLCHMbGOfzia2cX4U+0v9Mg==} + parse-json@5.2.0: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} engines: {node: '>=8'} @@ -1462,6 +1590,10 @@ packages: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} + path-platform@0.11.15: + resolution: {integrity: sha512-Y30dB6rab1A/nfEKsZxmr01nUotHX0c/ZiIAsCTatEe1CmS5Pm5He7fZ195bPT7RdquoaL8lLxFCMQi/bS7IJg==} + engines: {node: '>= 0.8.0'} + path-scurry@1.11.1: resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} engines: {node: '>=16 || 14 >=14.18'} @@ -1528,6 +1660,21 @@ packages: resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + restore-cursor@2.0.0: + resolution: {integrity: sha512-6IzJLuGi4+R14vwagDHX+JrXmPVtPpn4mffDJ1UdR7/Edm87fl6yi8mMBIVvFtJaNTUvjughmW4hwLhRG7gC1Q==} + engines: {node: '>=4'} + + run-async@2.4.1: + resolution: {integrity: sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==} + engines: {node: '>=0.12.0'} + + rxjs@6.6.7: + resolution: {integrity: sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==} + engines: {npm: '>=2.0.0'} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + semver@6.3.1: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true @@ -1545,6 +1692,10 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} + shell-quote@1.8.3: + resolution: {integrity: sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==} + engines: {node: '>= 0.4'} + signal-exit@3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} @@ -1574,6 +1725,10 @@ packages: resolution: {integrity: sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==} engines: {node: '>=10'} + string-width@2.1.1: + resolution: {integrity: sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==} + engines: {node: '>=4'} + string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -1582,6 +1737,14 @@ packages: resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} engines: {node: '>=12'} + strip-ansi@4.0.0: + resolution: {integrity: sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow==} + engines: {node: '>=4'} + + strip-ansi@5.2.0: + resolution: {integrity: sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==} + engines: {node: '>=6'} + strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} @@ -1606,6 +1769,10 @@ packages: resolution: {integrity: sha512-HRVVSbCCMbj7/kdWF9Q+bbckjBHLtHMEoJWlkmYzzdwhYMkjkOwubLM6t7NbWKjgKamGDrWL1++KrjUO1t9oAQ==} engines: {node: '>=18'} + supports-color@5.5.0: + resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} + engines: {node: '>=4'} + supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} @@ -1618,6 +1785,9 @@ packages: resolution: {integrity: sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==} engines: {node: ^14.18.0 || >=16.0.0} + tabtab@3.0.2: + resolution: {integrity: sha512-jANKmUe0sIQc/zTALTBy186PoM/k6aPrh3A7p6AaAfF6WPSbTx1JYeGIGH162btpH+mmVEXln+UxwViZHO2Jhg==} + test-exclude@6.0.0: resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} engines: {node: '>=8'} @@ -1626,6 +1796,13 @@ packages: resolution: {integrity: sha512-OEI0IWCe+Dw46019YLl6V10Us5bi574EvlJEOcAkB29IzQ/mYD1A6RyNHLjZPiHCmuodxvgF6U+vZO1L15lxVA==} engines: {node: '>=0.2.6'} + through@2.3.8: + resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} + + tmp@0.0.33: + resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} + engines: {node: '>=0.6.0'} + tmpl@1.0.5: resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} @@ -1674,6 +1851,9 @@ packages: '@swc/wasm': optional: true + tslib@1.14.1: + resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} + tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} @@ -1705,6 +1885,10 @@ packages: unrs-resolver@1.11.1: resolution: {integrity: sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==} + untildify@3.0.3: + resolution: {integrity: sha512-iSk/J8efr8uPT/Z4eSUywnqyrQU7DSdMfdqK4iWEaUVVmcP5JcnpRqmVMwcwcnmI1ATFNgC5V90u09tBynNFKA==} + engines: {node: '>=4'} + update-browserslist-db@1.1.3: resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==} hasBin: true @@ -2438,8 +2622,14 @@ snapshots: dependencies: undici-types: 7.8.0 + '@types/shell-quote@1.7.5': {} + '@types/stack-utils@2.0.3': {} + '@types/tabtab@3.0.4': + dependencies: + '@types/node': 24.1.0 + '@types/yargs-parser@21.0.3': {} '@types/yargs@17.0.33': @@ -2517,14 +2707,24 @@ snapshots: ansi-colors@4.1.3: {} + ansi-escapes@3.2.0: {} + ansi-escapes@4.3.2: dependencies: type-fest: 0.21.3 + ansi-regex@3.0.1: {} + + ansi-regex@4.1.1: {} + ansi-regex@5.0.1: {} ansi-regex@6.1.0: {} + ansi-styles@3.2.1: + dependencies: + color-convert: 1.9.3 + ansi-styles@4.3.0: dependencies: color-convert: 2.0.1 @@ -2642,6 +2842,12 @@ snapshots: caniuse-lite@1.0.30001727: {} + chalk@2.4.2: + dependencies: + ansi-styles: 3.2.1 + escape-string-regexp: 1.0.5 + supports-color: 5.5.0 + chalk@4.1.2: dependencies: ansi-styles: 4.3.0 @@ -2651,10 +2857,18 @@ snapshots: char-regex@1.0.2: {} + chardet@0.7.0: {} + ci-info@4.3.0: {} cjs-module-lexer@2.1.0: {} + cli-cursor@2.1.0: + dependencies: + restore-cursor: 2.0.0 + + cli-width@2.2.1: {} + cliui@8.0.1: dependencies: string-width: 4.2.3 @@ -2665,16 +2879,24 @@ snapshots: collect-v8-coverage@1.0.2: {} + color-convert@1.9.3: + dependencies: + color-name: 1.1.3 + color-convert@2.0.1: dependencies: color-name: 1.1.4 + color-name@1.1.3: {} + color-name@1.1.4: {} colorette@1.4.0: {} commander@14.0.0: {} + commander@7.2.0: {} + concat-map@0.0.1: {} convert-source-map@2.0.0: {} @@ -2719,6 +2941,8 @@ snapshots: dependencies: is-arrayish: 0.2.1 + es6-promisify@6.1.1: {} + esbuild@0.25.5: optionalDependencies: '@esbuild/aix-ppc64': 0.25.5 @@ -2749,6 +2973,8 @@ snapshots: escalade@3.2.0: {} + escape-string-regexp@1.0.5: {} + escape-string-regexp@2.0.0: {} esprima@4.0.1: {} @@ -2776,6 +3002,12 @@ snapshots: jest-mock: 30.0.5 jest-util: 30.0.5 + external-editor@3.1.0: + dependencies: + chardet: 0.7.0 + iconv-lite: 0.4.24 + tmp: 0.0.33 + fast-deep-equal@3.1.3: {} fast-json-stable-stringify@2.1.0: {} @@ -2784,6 +3016,10 @@ snapshots: dependencies: bser: 2.1.1 + figures@2.0.0: + dependencies: + escape-string-regexp: 1.0.5 + filelist@1.0.4: dependencies: minimatch: 5.1.6 @@ -2792,6 +3028,10 @@ snapshots: dependencies: to-regex-range: 5.0.1 + find-package@1.0.0: + dependencies: + parents: 1.0.1 + find-up@4.1.0: dependencies: locate-path: 5.0.0 @@ -2812,6 +3052,12 @@ snapshots: gensync@1.0.0-beta.2: {} + genversion@3.2.0: + dependencies: + commander: 7.2.0 + ejs: 3.1.10 + find-package: 1.0.0 + get-caller-file@2.0.5: {} get-package-type@0.1.0: {} @@ -2842,6 +3088,8 @@ snapshots: graceful-fs@4.2.11: {} + has-flag@3.0.0: {} + has-flag@4.0.0: {} html-escaper@2.0.2: {} @@ -2855,6 +3103,10 @@ snapshots: human-signals@2.1.0: {} + iconv-lite@0.4.24: + dependencies: + safer-buffer: 2.1.2 + import-local@3.2.0: dependencies: pkg-dir: 4.2.0 @@ -2871,8 +3123,26 @@ snapshots: inherits@2.0.4: {} + inquirer@6.5.2: + dependencies: + ansi-escapes: 3.2.0 + chalk: 2.4.2 + cli-cursor: 2.1.0 + cli-width: 2.2.1 + external-editor: 3.1.0 + figures: 2.0.0 + lodash: 4.17.21 + mute-stream: 0.0.7 + run-async: 2.4.1 + rxjs: 6.6.7 + string-width: 2.1.1 + strip-ansi: 5.2.0 + through: 2.3.8 + is-arrayish@0.2.1: {} + is-fullwidth-code-point@2.0.0: {} + is-fullwidth-code-point@3.0.0: {} is-generator-fn@2.1.0: {} @@ -3269,6 +3539,8 @@ snapshots: lodash.memoize@4.1.2: {} + lodash@4.17.21: {} + lru-cache@10.4.3: {} lru-cache@5.1.1: @@ -3292,6 +3564,8 @@ snapshots: braces: 3.0.3 picomatch: 2.3.1 + mimic-fn@1.2.0: {} + mimic-fn@2.1.0: {} minimatch@3.1.2: @@ -3306,10 +3580,18 @@ snapshots: dependencies: brace-expansion: 2.0.2 + minimist@1.2.8: {} + minipass@7.1.2: {} + mkdirp@0.5.6: + dependencies: + minimist: 1.2.8 + ms@2.1.3: {} + mute-stream@0.0.7: {} + napi-postinstall@0.3.2: {} natural-compare@1.4.0: {} @@ -3328,6 +3610,10 @@ snapshots: dependencies: wrappy: 1.0.2 + onetime@2.0.1: + dependencies: + mimic-fn: 1.2.0 + onetime@5.1.2: dependencies: mimic-fn: 2.1.0 @@ -3348,6 +3634,8 @@ snapshots: typescript: 5.7.3 yargs-parser: 21.1.1 + os-tmpdir@1.0.2: {} + otplib@12.0.1: dependencies: '@otplib/core': 12.0.1 @@ -3370,6 +3658,10 @@ snapshots: package-json-from-dist@1.0.1: {} + parents@1.0.1: + dependencies: + path-platform: 0.11.15 + parse-json@5.2.0: dependencies: '@babel/code-frame': 7.27.1 @@ -3389,6 +3681,8 @@ snapshots: path-key@3.1.1: {} + path-platform@0.11.15: {} + path-scurry@1.11.1: dependencies: lru-cache: 10.4.3 @@ -3438,6 +3732,19 @@ snapshots: resolve-pkg-maps@1.0.0: {} + restore-cursor@2.0.0: + dependencies: + onetime: 2.0.1 + signal-exit: 3.0.7 + + run-async@2.4.1: {} + + rxjs@6.6.7: + dependencies: + tslib: 1.14.1 + + safer-buffer@2.1.2: {} + semver@6.3.1: {} semver@7.7.2: {} @@ -3448,6 +3755,8 @@ snapshots: shebang-regex@3.0.0: {} + shell-quote@1.8.3: {} + signal-exit@3.0.7: {} signal-exit@4.1.0: {} @@ -3472,6 +3781,11 @@ snapshots: char-regex: 1.0.2 strip-ansi: 6.0.1 + string-width@2.1.1: + dependencies: + is-fullwidth-code-point: 2.0.0 + strip-ansi: 4.0.0 + string-width@4.2.3: dependencies: emoji-regex: 8.0.0 @@ -3484,6 +3798,14 @@ snapshots: emoji-regex: 9.2.2 strip-ansi: 7.1.0 + strip-ansi@4.0.0: + dependencies: + ansi-regex: 3.0.1 + + strip-ansi@5.2.0: + dependencies: + ansi-regex: 4.1.1 + strip-ansi@6.0.1: dependencies: ansi-regex: 5.0.1 @@ -3500,6 +3822,10 @@ snapshots: supports-color@10.0.0: {} + supports-color@5.5.0: + dependencies: + has-flag: 3.0.0 + supports-color@7.2.0: dependencies: has-flag: 4.0.0 @@ -3512,6 +3838,17 @@ snapshots: dependencies: '@pkgr/core': 0.2.9 + tabtab@3.0.2: + dependencies: + debug: 4.4.1(supports-color@10.0.0) + es6-promisify: 6.1.1 + inquirer: 6.5.2 + minimist: 1.2.8 + mkdirp: 0.5.6 + untildify: 3.0.3 + transitivePeerDependencies: + - supports-color + test-exclude@6.0.0: dependencies: '@istanbuljs/schema': 0.1.3 @@ -3520,6 +3857,12 @@ snapshots: thirty-two@1.0.2: {} + through@2.3.8: {} + + tmp@0.0.33: + dependencies: + os-tmpdir: 1.0.2 + tmpl@1.0.5: {} to-regex-range@5.0.1: @@ -3564,6 +3907,8 @@ snapshots: v8-compile-cache-lib: 3.0.1 yn: 3.1.1 + tslib@1.14.1: {} + tslib@2.8.1: optional: true @@ -3608,6 +3953,8 @@ snapshots: '@unrs/resolver-binding-win32-ia32-msvc': 1.11.1 '@unrs/resolver-binding-win32-x64-msvc': 1.11.1 + untildify@3.0.3: {} + update-browserslist-db@1.1.3(browserslist@4.25.1): dependencies: browserslist: 4.25.1 diff --git a/scripts/generate-docs.ts b/scripts/generate-docs.ts index 1591554..ff66fb6 100755 --- a/scripts/generate-docs.ts +++ b/scripts/generate-docs.ts @@ -7,7 +7,7 @@ import * as path from 'path'; /** * Generate markdown documentation for all commands in the CLI */ -function generateCommandDocs(command: Command, level = 1): string { +export function generateCommandDocs(command: Command, level = 1): string { let docs = ''; // Skip the root command if it's the first level @@ -19,13 +19,11 @@ function generateCommandDocs(command: Command, level = 1): string { docs += `${command.description()}\n\n`; } - // Add usage information const usage = command.usage(); if (usage) { docs += `**Usage:** \`${command.name()} ${usage}\`\n\n`; } - // Add options const options = command.options; if (options && options.length > 0) { docs += '### Options\n\n'; @@ -45,19 +43,61 @@ function generateCommandDocs(command: Command, level = 1): string { } } - // Process subcommands const subcommands = command.commands; if (subcommands && subcommands.length > 0) { - // If this is the root command, add a title if (level === 1) { docs += `# ${command.name()} CLI Documentation\n\n`; docs += `${command.description()}\n\n`; - docs += '## Commands\n\n'; + + const commandsByGroup = new Map(); + const ungroupedCommands: Command[] = []; + + subcommands.forEach(subcommand => { + const helpGroup = subcommand.helpGroup(); + if (helpGroup) { + if (!commandsByGroup.has(helpGroup)) { + commandsByGroup.set(helpGroup, []); + } + const groupCommands = commandsByGroup.get(helpGroup); + if (groupCommands) { + groupCommands.push(subcommand); + } + } else { + ungroupedCommands.push(subcommand); + } + }); + + const allGroups = Array.from(commandsByGroup.keys()); + const setupGroup = allGroups.find(group => group === 'setup'); + const otherGroups = allGroups.filter(group => group !== 'setup').sort(); + const sortedGroups = setupGroup ? [setupGroup, ...otherGroups] : otherGroups; + + sortedGroups.forEach(groupName => { + const groupCommands = commandsByGroup.get(groupName); + if (groupCommands) { + const capitalizedGroupName = groupName.split('-').map(word => + word.charAt(0).toUpperCase() + word.slice(1) + ).join(' '); + + docs += `## ${capitalizedGroupName}\n\n`; + + groupCommands.forEach(subcommand => { + docs += generateCommandDocs(subcommand, level + 1); + }); + } + }); + + if (ungroupedCommands.length > 0) { + docs += '## Other Commands\n\n'; + ungroupedCommands.forEach(subcommand => { + docs += generateCommandDocs(subcommand, level + 1); + }); + } + } else { + subcommands.forEach(subcommand => { + docs += generateCommandDocs(subcommand, level + 1); + }); } - - subcommands.forEach(subcommand => { - docs += generateCommandDocs(subcommand, level + 1); - }); } return docs; @@ -68,23 +108,17 @@ function generateCommandDocs(command: Command, level = 1): string { */ async function main() { try { - // Get the program object from buildCmd const program = buildCmd(); - // Generate markdown documentation const markdown = generateCommandDocs(program); - // Check if README template exists const templatePath = path.join(__dirname, '..', 'README-template.md'); if (fs.existsSync(templatePath)) { - // Load template and replace ${commands} with generated docs const template = fs.readFileSync(templatePath, 'utf8'); const result = template.replace('${commands}', markdown); - // Output the result to console (can be redirected to a file) console.log(result); } else { - // If template doesn't exist, just output the docs console.log(markdown); } } catch (error) { @@ -93,5 +127,6 @@ async function main() { } } -// Run the main function -main().catch(console.error); +if (require.main === module) { + main().catch(console.error); +} diff --git a/src/cli.ts b/src/cli.ts index d5084cd..b08e8c4 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,15 +1,32 @@ -import { Command, Option, program } from "commander"; +import { Option } from "commander"; +import { + CompletableCommand, + environmentIdCompleter, + installCompletion, + optionsCompleter, + tabCompletion, + testCaseIdCompleter, + testReportIdCompleter, + testTargetIdCompleter, + uninstallCompletion, +} from "./completion"; import { Config, loadConfig, saveConfig } from "./config"; import { runDebugtopus } from "./debugtopus"; import { promptUser, resolveTestTargetId } from "./helpers"; import { startPrivateLocationWorker, stopPLW } from "./plw"; import { + CreateDiscoveryBody, createDiscovery, createEnvironment, deleteEnvironment, deleteTestCase, + ExecuteTestsBody, executeTests, + GetEnvironmentOptions, + GetTestCaseParams, + GetTestReportParams, + getEnvironment, listEnvironments, listNotifications, listPrivateLocations, @@ -23,15 +40,26 @@ import { import { getTestTargets, listTestTargets } from "./tools/test-targets"; import { version } from "./version"; -const createCommandWithCommonOptions = (command: string): Command => { - return program - .command(command) - .option("-j, --json", "Output raw JSON response"); -}; +export const BINARY_NAME = "octomind"; const splitter = (value: string): string[] => value.split(/,| |\|/); const toJSON = (value: string): object => JSON.parse(value); +type TestTargetWrapperOptions = GetEnvironmentOptions & + GetTestCaseParams & + GetTestReportParams & + CreateDiscoveryBody & + ExecuteTestsBody; + +const addTestTargetWrapper = + (fn: (options: TestTargetWrapperOptions) => Promise) => + async (options: TestTargetWrapperOptions) => { + const resolvedTestTargetId = await resolveTestTargetId( + options.testTargetId, + ); + void fn({ ...options, testTargetId: resolvedTestTargetId }); + }; + const selectTestTarget = async (): Promise => { const testTargets = await getTestTargets(); await listTestTargets({}); @@ -46,7 +74,17 @@ const selectTestTarget = async (): Promise => { const testTargetIndex = await promptUser( "Enter number of the test target you want to use (optional, press Enter to skip): ", ); - const testTargetId = testTargets[Number.parseInt(testTargetIndex) - 1].id; + const testTargetIndexAsInt = Number.parseInt(testTargetIndex); + + if ( + Number.isNaN(testTargetIndexAsInt) || + testTargetIndexAsInt < 1 || + testTargetIndexAsInt > testTargets.length + ) { + console.log("❌ could not find a test target with the index you provided"); + process.exit(1); + } + const testTargetId = testTargets[testTargetIndexAsInt - 1].id; if (!testTargetId) { console.log("❌ could not find a test target with the index you provided"); process.exit(1); @@ -59,17 +97,33 @@ const testTargetIdOption = new Option( "Test target ID, if not provided will use the test target id from the config", ); -export const buildCmd = (): Command => { +const createCommandWithCommonOptions = ( + program: CompletableCommand, + command: string, +): CompletableCommand => { + return program + .completableCommand(command) + .option("-j, --json", "Output raw JSON response") as CompletableCommand; +}; + +export const buildCmd = (): CompletableCommand => { + const program = new CompletableCommand(); + program - .name("octomind") + .name(BINARY_NAME) .description( `Octomind cli tool. Version: ${version}. Additional documentation see https://octomind.dev/docs/api-reference/`, ) .version(version); program - .command("init") - .description("Initialize configuration by setting up API key") + .completer(testTargetIdCompleter) + .completer(optionsCompleter) + .completableCommand("init") + .description( + "Initialize configuration by setting up API key. This will create a config file in ~/.config/octomind.json", + ) + .helpGroup("setup") .option("-t, --test-target-id ", "Test target ID") .option("-k, --api-key ", "the api key for authentication") .option("-f, --force", "Force overwrite existing configuration") @@ -99,7 +153,7 @@ export const buildCmd = (): Command => { } } - let apiKey; + let apiKey: string = ""; if (!options.apiKey) { apiKey = await promptUser( "Enter your API key. Go to https://octomind.dev/docs/run-tests/execution-curl#create-an-api-key to learn how to generate one: ", @@ -112,15 +166,17 @@ export const buildCmd = (): Command => { // saving here to be able to use the api key for the test targets const newApiKeyConfig = { ...existingConfig, - apiKey: options.apiKey ?? apiKey, + + apiKey: options.apiKey, }; + await saveConfig(newApiKeyConfig); const testTargetId = await selectTestTarget(); const newConfig: Config = { ...existingConfig, - apiKey: options.apiKey ?? apiKey, + apiKey: options.apiKey, testTargetId: options.testTargetId ?? testTargetId, }; @@ -138,8 +194,11 @@ export const buildCmd = (): Command => { ); program - .command("switch-test-target") - .description("Switch to a different test target") + .completableCommand("switch-test-target") + .description( + "Switch to a different test target. This will list all available test targets and update the config file in ~/.config/octomind.json", + ) + .helpGroup("setup") .action(async () => { const testTargetId = await selectTestTarget(); const existingConfig = await loadConfig(); @@ -151,11 +210,16 @@ export const buildCmd = (): Command => { console.log(`✨ Switched to test target: ${testTargetId}`); }); - createCommandWithCommonOptions("debug") + createCommandWithCommonOptions(program, "debug") + .completer(environmentIdCompleter) + .completer(testTargetIdCompleter) + .completer(testCaseIdCompleter) + .completer(optionsCompleter) .description("run test cases against local build") + .helpGroup("execute") .requiredOption("-u, --url ", "url the tests should run against") .option( - "-i, --id [uuid]", + "-c, --test-case-id [uuid]", "id of the test case you want to run, if not provided will run all test cases in the test target", ) .option( @@ -163,7 +227,7 @@ export const buildCmd = (): Command => { "id of the environment you want to run against, if not provided will run all test cases against the default environment", ) .option( - "-a, --test-target-id [uuid]", + "-t, --test-target-id [uuid]", "id of the test target of the test case, if not provided will use the test target id from the config", ) .option( @@ -175,16 +239,13 @@ export const buildCmd = (): Command => { "if we should write playwright config and files to current directory, you can then run 'npx playwright test' to run them again", ) .option("--grep [substring]", "filter test cases by substring") - .action(async (options, command) => { - const resolvedTestTargetId = await resolveTestTargetId( - options.testTargetId, - ); - command.setOptionValue("testTargetId", resolvedTestTargetId); - void runDebugtopus(options); - }); + .action(addTestTargetWrapper(runDebugtopus)); - createCommandWithCommonOptions("execute") - .description("Execute test cases") + createCommandWithCommonOptions(program, "execute") + .completer(testTargetIdCompleter) + .completer(optionsCompleter) + .description("Execute test cases to create a test report") + .helpGroup("execute") .requiredOption("-u, --url ", "URL to test") .addOption(testTargetIdOption) .option("-e, --environment-name [name]", "Environment name", "default") @@ -195,56 +256,54 @@ export const buildCmd = (): Command => { "JSON object of variables to overwrite", toJSON, ) - .action(async (options) => { - const testTargetId = await resolveTestTargetId(options.testTargetId); - await executeTests({ - ...options, - testTargetId, - }); - }); + .action(addTestTargetWrapper(executeTests)); - createCommandWithCommonOptions("test-report") + createCommandWithCommonOptions(program, "test-report") + .completer(testReportIdCompleter) + .completer(testTargetIdCompleter) + .completer(optionsCompleter) .description("Get test report details") + .helpGroup("test-reports") .requiredOption("-r, --test-report-id ", "Test report ID") .addOption(testTargetIdOption) - .action(async (options) => { - const testTargetId = await resolveTestTargetId(options.testTargetId); - await listTestReport({ - ...options, - testTargetId, - }); - }); + .action(addTestTargetWrapper(listTestReport)); - createCommandWithCommonOptions("register-location") + createCommandWithCommonOptions(program, "register-location") + .completer(optionsCompleter) .description("Register a private location") + .helpGroup("private-locations") .requiredOption("-n, --name ", "Location name") .requiredOption("-p, --password ", "Proxy password") .requiredOption("-u, --username ", "Proxy user") .requiredOption("-a, --address
", "Location address") .action(registerLocation); - createCommandWithCommonOptions("unregister-location") + createCommandWithCommonOptions(program, "unregister-location") + .completer(optionsCompleter) .description("Unregister a private location") + .helpGroup("private-locations") .requiredOption("-n, --name ", "Location name") .action(unregisterLocation); - createCommandWithCommonOptions("list-private-locations") + createCommandWithCommonOptions(program, "list-private-locations") + .completer(optionsCompleter) .description("List all private locations") + .helpGroup("private-locations") .action(listPrivateLocations); - createCommandWithCommonOptions("list-environments") + createCommandWithCommonOptions(program, "list-environments") + .completer(testTargetIdCompleter) .description("List all environments") + .helpGroup("environments") .addOption(testTargetIdOption) - .action(async (options, command) => { - const resolvedTestTargetId = await resolveTestTargetId( - options.testTargetId, - ); - command.setOptionValue("testTargetId", resolvedTestTargetId); - void listEnvironments(options); - }); - createCommandWithCommonOptions("create-environment") + .action(addTestTargetWrapper(listEnvironments)); + + createCommandWithCommonOptions(program, "create-environment") + .completer(testTargetIdCompleter) + .completer(optionsCompleter) .description("Create a new environment") + .helpGroup("environments") .requiredOption("-n, --name ", "Environment name") .requiredOption("-d, --discovery-url ", "Discovery URL") .addOption(testTargetIdOption) @@ -257,15 +316,25 @@ export const buildCmd = (): Command => { .option("--basic-auth-username [username]", "Basic auth username") .option("--basic-auth-password [password]", "Basic auth password") .option("--private-location-name [name]", "Private location name") - .action(async (_, options) => - createEnvironment({ - ...options, - testTargetId: await resolveTestTargetId(options.testTargetId), - }), - ); - createCommandWithCommonOptions("update-environment") + .action(addTestTargetWrapper(createEnvironment)); + + createCommandWithCommonOptions(program, "environment") + .completer(environmentIdCompleter) + .completer(testTargetIdCompleter) + .completer(optionsCompleter) + .description("Get an environment") + .helpGroup("environments") + .requiredOption("-e, --environment-id ", "Environment ID") + .addOption(testTargetIdOption) + .action(addTestTargetWrapper(getEnvironment)); + + createCommandWithCommonOptions(program, "update-environment") + .completer(environmentIdCompleter) + .completer(testTargetIdCompleter) + .completer(optionsCompleter) .description("Update an existing environment") + .helpGroup("environments") .requiredOption("-e, --environment-id ", "Environment ID") .addOption(testTargetIdOption) .option("-n, --name [name]", "Environment name") @@ -279,24 +348,27 @@ export const buildCmd = (): Command => { .option("--basic-auth-username [username]", "Basic auth username") .option("--basic-auth-password [password]", "Basic auth password") .option("--private-location-name [name]", "Private location name") - .action(async (_, options) => - updateEnvironment({ - ...options, - testTargetId: await resolveTestTargetId(options.testTargetId), - }), - ); - createCommandWithCommonOptions("delete-environment") + .action(addTestTargetWrapper(updateEnvironment)); + + createCommandWithCommonOptions(program, "delete-environment") + .completer(environmentIdCompleter) + .completer(testTargetIdCompleter) .description("Delete an environment") + .helpGroup("environments") .requiredOption("-e, --environment-id ", "Environment ID") .addOption(testTargetIdOption) - .action(deleteEnvironment); + .action(addTestTargetWrapper(deleteEnvironment)); program - .command("start-private-location") + + .completer(optionsCompleter) + .completableCommand("start-private-location") .description( "Start a private location worker, see https://octomind.dev/docs/proxy/private-location", ) + .helpGroup("private-locations") + .option("-n, --name [name]", "Location name") .option("-u, --username [username]", "Proxy user") .option("-p, --password [password]", "Proxy password") @@ -308,60 +380,46 @@ export const buildCmd = (): Command => { .action(startPrivateLocationWorker); program - .command("stop-private-location") + + .completableCommand("stop-private-location") + .completer(optionsCompleter) .description( "Stop a private location worker, see https://octomind.dev/docs/proxy/private-location", ) + .helpGroup("private-locations") + .action(stopPLW); - createCommandWithCommonOptions("notifications") + createCommandWithCommonOptions(program, "notifications") + .completer(testTargetIdCompleter) .description("Get notifications for a test target") + .helpGroup("notifications") .addOption(testTargetIdOption) - .action(async (options, command) => { - const resolvedTestTargetId = await resolveTestTargetId( - options.testTargetId, - ); - command.setOptionValue("testTargetId", resolvedTestTargetId); - void listNotifications(options); - }); - createCommandWithCommonOptions("list-test-cases") - .description("List all test cases") - .addOption(testTargetIdOption) - .action(async (options, command) => { - const resolvedTestTargetId = await resolveTestTargetId( - options.testTargetId, - ); - command.setOptionValue("testTargetId", resolvedTestTargetId); - void listTestCases({ ...options, status: "ENABLED" }); - }); + .action(addTestTargetWrapper(listNotifications)); - createCommandWithCommonOptions("delete-test-case") + createCommandWithCommonOptions(program, "delete-test-case") + .completer(testCaseIdCompleter) .description("Delete a test case") .requiredOption("-c, --test-case-id ", "Test case ID") .addOption(testTargetIdOption) - .action(async (options, command) => { - const resolvedTestTargetId = await resolveTestTargetId( - options.testTargetId, - ); - command.setOptionValue("testTargetId", resolvedTestTargetId); - void deleteTestCase({ ...options, testTargetId: resolvedTestTargetId }); - }); + .helpGroup("test-cases") + .action(addTestTargetWrapper(deleteTestCase)); - createCommandWithCommonOptions("test-case") + createCommandWithCommonOptions(program, "test-case") + .completer(testCaseIdCompleter) + .completer(testTargetIdCompleter) .description("Get details of a specific test case") + .helpGroup("test-cases") .requiredOption("-c, --test-case-id ", "Test case ID") .addOption(testTargetIdOption) - .action(async (options, command) => { - const resolvedTestTargetId = await resolveTestTargetId( - options.testTargetId, - ); - command.setOptionValue("testTargetId", resolvedTestTargetId); - void listTestCase(options); - }); + .action(addTestTargetWrapper(listTestCase)); - createCommandWithCommonOptions("create-discovery") + createCommandWithCommonOptions(program, "create-discovery") + .completer(testTargetIdCompleter) + .completer(optionsCompleter) .description("Create a new test case discovery") + .helpGroup("execute") .requiredOption("-n, --name ", "Discovery name") .requiredOption("-p, --prompt ", "Discovery prompt") .addOption(testTargetIdOption) @@ -374,17 +432,38 @@ export const buildCmd = (): Command => { splitter, ) .option("--folder-id [id]", "Folder ID") - .action(async (options, command) => { - const resolvedTestTargetId = await resolveTestTargetId( - options.testTargetId, - ); - command.setOptionValue("testTargetId", resolvedTestTargetId); - void createDiscovery(options); - }); - createCommandWithCommonOptions("list-test-targets") + .action(addTestTargetWrapper(createDiscovery)); + + createCommandWithCommonOptions(program, "list-test-cases") + .completer(testTargetIdCompleter) + .description("List all test cases") + .helpGroup("test-cases") + .addOption(testTargetIdOption) + .action(addTestTargetWrapper(listTestCases)); + + createCommandWithCommonOptions(program, "list-test-targets") .description("List all test targets") + .helpGroup("test-targets") .action(listTestTargets); + program + .completableCommand("install-completion") + .description("Install tab completion") + .helpGroup("completion") + .action(installCompletion); + program + .completableCommand("uninstall-completion") + .description("Uninstall tab completion") + .helpGroup("completion") + .action(uninstallCompletion); + + program + .completableCommand("completion") + .description("Tab completion") + .helpGroup("completion") + .allowExcessArguments(true) + .action(() => tabCompletion(program)); + return program; }; diff --git a/src/completion.ts b/src/completion.ts new file mode 100644 index 0000000..f5a0c77 --- /dev/null +++ b/src/completion.ts @@ -0,0 +1,203 @@ +import { Command } from "commander"; +import { parse } from "shell-quote"; +import { install, log, parseEnv, TabtabEnv, uninstall } from "tabtab"; + +import { BINARY_NAME } from "./cli"; +import { loadConfig } from "./config"; +import { getTestReports } from "./tools"; +import { getEnvironments } from "./tools/environments"; +import { getTestCases } from "./tools/test-cases"; +import { getTestTargets } from "./tools/test-targets"; + +// returns whether the completer has handled the completion +type CompleterFn = ( + command: CompletableCommand, + env: TabtabEnv, +) => Promise; + +export class CompletableCommand extends Command { + private _completerFn: CompleterFn[] = []; + + completer(completerFn?: CompleterFn): CompletableCommand { + if (completerFn) { + this._completerFn.push(completerFn); + } + return this; + } + + getCompleter(): CompleterFn[] { + return this._completerFn; + } + + override createCommand(name?: string): CompletableCommand { + return new CompletableCommand(name); + } + + // Helper method to create completable commands + completableCommand( + nameAndArgs: string, + description?: string, + ): CompletableCommand { + const cmd = description + ? this.command(nameAndArgs, description) + : this.command(nameAndArgs); + // Ensure it has the right prototype + Object.setPrototypeOf(cmd, CompletableCommand.prototype); + return cmd as CompletableCommand; + } +} + +const includesOrUndefined = ( + array: (T | undefined)[], + value: T | undefined, +): boolean => { + return ( + array.includes(value) || (value === undefined && array.includes(undefined)) + ); +}; + +const logOptions = async (command: CompletableCommand, line: string) => { + const argv = parse(line).map((arg) => arg.toString()); + const usedOptions = command.options + .filter( + (option) => + includesOrUndefined(argv, option.long) || + includesOrUndefined(argv, option.short), + ) + .map((option) => option.flags); + log( + command.options + .filter((option) => option.long) + .filter((option) => !usedOptions.includes(option.flags)) + .map((option) => option.long ?? ""), + ); + log( + command.options + .filter((option) => option.short) + .filter((option) => !usedOptions.includes(option.flags)) + .map((option) => option.short ?? ""), + ); +}; + +export const optionsCompleter = async ( + command: CompletableCommand, + env: TabtabEnv, +): Promise => { + await logOptions(command, env.line); + return true; +}; + +export const testTargetIdCompleter = async ( + _command: CompletableCommand, + env: TabtabEnv, +): Promise => { + if (env.prev === "-t" || env.prev === "--test-target-id") { + const testTargets = await getTestTargets(); + log(testTargets.map((testTarget) => testTarget.id)); + return true; + } + return false; +}; + +export const environmentIdCompleter = async ( + _command: CompletableCommand, + env: TabtabEnv, +): Promise => { + if (env.prev === "-e" || env.prev === "--environment-id") { + const config = await loadConfig(); + if (config.testTargetId) { + const environments = await getEnvironments({ + testTargetId: config.testTargetId, + }); + log(environments.map((environment) => environment.id)); + return true; + } + } + return false; +}; + +export const testCaseIdCompleter = async ( + _command: CompletableCommand, + env: TabtabEnv, +): Promise => { + if (env.prev === "-c" || env.prev === "--test-case-id") { + const config = await loadConfig(); + if (config.testTargetId) { + const testCases = await getTestCases({ + testTargetId: config.testTargetId, + status: "ENABLED", + }); + log(testCases.map((testCase) => testCase.id)); + return true; + } + } + return false; +}; + +export const testReportIdCompleter = async ( + _command: CompletableCommand, + env: TabtabEnv, +): Promise => { + if (env.prev === "-r" || env.prev === "--test-report-id") { + const config = await loadConfig(); + if (config.testTargetId) { + const testReports = await getTestReports({ + testTargetId: config.testTargetId, + }); + if (testReports) { + log(testReports.map((testReport) => testReport?.id ?? "")); + } + } + return true; + } + return false; +}; + +export const tabCompletion = async (program: CompletableCommand) => { + const env = parseEnv(process.env); + if (!env.complete) return; + + const argv = parse(env.line).map((arg) => arg.toString()); + + const command = program.commands.find( + (c) => c.name() === argv[1], + ) as CompletableCommand; + if (command) { + const completers = command.getCompleter(); + if (completers.length > 0) { + for (const completer of completers) { + const handled = await completer(command, env); + if (handled) { + return; + } + } + } + return; + } + + log(["--help"]); + log( + program.options + .filter((option) => option.long) + .map((option) => option.long ?? ""), + ); + log( + program.options + .filter((option) => option.short) + .map((option) => option.short ?? ""), + ); + log(program.commands.map((c) => c.name())); +}; + +export const installCompletion = async () => { + await install({ + name: BINARY_NAME, + completer: BINARY_NAME, + }).catch((err) => console.error("INSTALL ERROR", err)); +}; + +export const uninstallCompletion = async () => { + await uninstall({ + name: BINARY_NAME, + }).catch((err) => console.error("UNINSTALL ERROR", err)); +}; diff --git a/src/index.ts b/src/index.ts index ee05ac7..05df6f4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,5 +2,4 @@ import { buildCmd } from "./cli"; -const cmd = buildCmd(); -cmd.parse(); +buildCmd().parse(); diff --git a/src/tools/environments.ts b/src/tools/environments.ts index f0ef0de..78d3555 100644 --- a/src/tools/environments.ts +++ b/src/tools/environments.ts @@ -11,6 +11,11 @@ export type UpdateEnvironmentOptions = paths["/apiKey/v2/test-targets/{testTargetId}/environments/{environmentId}"]["patch"]["requestBody"]["content"]["application/json"]; export type EnvironmentResponse = components["schemas"]["EnvironmentResponse"]; +export type GetEnvironmentOptions = { + testTargetId: string; + environmentId: string; +}; + export const listEnvironments = async ( options: GetEnvironmentsOptions & ListOptions, ): Promise => { @@ -102,16 +107,15 @@ export const createEnvironment = async ( }; export const updateEnvironment = async ( - options: UpdateEnvironmentOptions & { - testTargetId: string; - environmentId: string; - testAccountUsername?: string; - testAccountPassword?: string; - basicAuthUsername?: string; - basicAuthPassword?: string; - privateLocationName?: string; - testAccountOtpInitializerKey?: string; - } & ListOptions, + options: UpdateEnvironmentOptions & + GetEnvironmentOptions & { + testAccountUsername?: string; + testAccountPassword?: string; + basicAuthUsername?: string; + basicAuthPassword?: string; + privateLocationName?: string; + testAccountOtpInitializerKey?: string; + } & ListOptions, ): Promise => { const { data, error } = await client.PATCH( "/apiKey/v2/test-targets/{testTargetId}/environments/{environmentId}", @@ -155,11 +159,32 @@ export const updateEnvironment = async ( console.log(` Updated At: ${response.updatedAt}`); }; +export const getEnvironment = async ( + options: GetEnvironmentOptions & ListOptions, +): Promise => { + const environments = await getEnvironments({ + testTargetId: options.testTargetId, + }); + + const environment = environments.find((e) => e.id === options.environmentId); + if (!environment) { + throw new Error("environment not found"); + } + + if (options.json) { + logJson({ environment }); + return; + } + + console.log("Environment:"); + console.log(`- Name: ${environment.name}`); + console.log(` ID: ${environment.id}`); + console.log(` Discovery URL: ${environment.discoveryUrl}`); + console.log(` Updated At: ${environment.updatedAt}`); +}; + export const deleteEnvironment = async ( - options: { - testTargetId: string; - environmentId: string; - } & ListOptions, + options: GetEnvironmentOptions & ListOptions, ): Promise => { const { error } = await client.DELETE( "/apiKey/v2/test-targets/{testTargetId}/environments/{environmentId}", diff --git a/src/tools/index.ts b/src/tools/index.ts index ee0d767..e6da8ad 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -5,3 +5,4 @@ export * from "./playwright"; export * from "./private-locations"; export * from "./test-cases"; export * from "./test-reports"; +export * from "./test-targets"; diff --git a/src/tools/test-reports.ts b/src/tools/test-reports.ts index e53548e..d84ebbe 100644 --- a/src/tools/test-reports.ts +++ b/src/tools/test-reports.ts @@ -40,8 +40,8 @@ export const executeTests = async ( console.log("Test execution started successfully!"); console.log("Test Report URL:", data.testReportUrl); console.log("Report Status:", data.testReport?.status); - - if (data.testReport?.testResults?.length ?? 0 > 0) { + const numberOfTestResults = data.testReport?.testResults?.length ?? 0; + if (numberOfTestResults > 0) { console.log("\nTest Results:"); data.testReport?.testResults?.forEach((result) => { console.log(`- Test ${result.testCaseId}: ${result.status}`); @@ -56,6 +56,25 @@ export const executeTests = async ( } }; +export const getTestReports = async (options: { + testTargetId: string; +}): Promise => { + const { data, error } = await client.GET( + "/apiKey/v2/test-targets/{testTargetId}/test-reports", + { + params: { + path: { + testTargetId: options.testTargetId, + }, + }, + }, + ); + + handleError(error); + + return data?.data; +}; + export const listTestReport = async ( options: GetTestReportParams & ListOptions, ): Promise => { @@ -82,8 +101,8 @@ export const listTestReport = async ( console.log("Test Report Details:"); console.log("Status:", response.status); console.log("Execution URL:", response.executionUrl); - - if ((response.testResults ?? []).length > 0) { + const numberOfTestResults = response.testResults?.length ?? 0; + if (numberOfTestResults > 0) { console.log("\nTest Results:"); for (const result of response.testResults ?? []) { console.log(`- Test ${result.testCaseId}: ${result.status}`); diff --git a/tests/cli.spec.ts b/tests/cli.spec.ts index 9d7ba32..b15075a 100644 --- a/tests/cli.spec.ts +++ b/tests/cli.spec.ts @@ -1,4 +1,4 @@ -import { program } from "commander"; +import { Command } from "commander"; import { buildCmd } from "../src/cli"; import { executeTests } from "../src/tools"; import { runDebugtopus } from "../src/debugtopus"; @@ -13,9 +13,10 @@ jest.mock("../src/config", () => ({ const originalConsoleLog = console.log; const originalConsoleError = console.error; +let program: Command; beforeAll(() => { - buildCmd(); + program = buildCmd(); program.exitOverride((err) => { throw err; }); diff --git a/tests/completion.spec.ts b/tests/completion.spec.ts new file mode 100644 index 0000000..8dd2081 --- /dev/null +++ b/tests/completion.spec.ts @@ -0,0 +1,189 @@ +import { + environmentIdCompleter, + optionsCompleter, + testCaseIdCompleter, + testReportIdCompleter, + testTargetIdCompleter, + CompletableCommand, +} from "../src/completion"; + +import { TabtabEnv, log } from "tabtab"; +import { getTestTargets } from "../src/tools/test-targets"; +import { getEnvironments } from "../src/tools/environments"; +import { loadConfig } from "../src/config"; +import { getTestCases } from "../src/tools/test-cases"; +import { getTestReports } from "../src/tools/test-reports"; + +jest.mock("../src/tools/test-targets"); +jest.mock("tabtab"); +jest.mock("../src/config"); +jest.mock("../src/tools/environments"); +jest.mock("../src/tools/test-cases"); +jest.mock("../src/tools/test-reports"); + +describe("completion", () => { + const env: TabtabEnv = { + line: "debug --test-target-id", + prev: "--test-target-id", + complete: false, + words: 3, + point: 0, + partial: "", + last: "", + lastPartial: "", + }; + + const mockCommand: CompletableCommand = { + name: jest.fn(), + getCompleter: jest.fn(), + options: [ + { + flags: "--test-target-id, -t", + name: "test-target-id", + type: "string", + short: "-t", + long: "--test-target-id", + }, + { + flags: "--environment-id, -e", + name: "environment-id", + type: "string", + short: "-e", + long: "--environment-id", + }, + { + flags: "--test-case-id, -c", + name: "test-case-id", + type: "string", + short: "-c", + long: "--test-case-id", + }, + { + flags: "--test-report-id, -r", + name: "test-report-id", + type: "string", + short: "-r", + long: "--test-report-id", + }, + ], + } as unknown as CompletableCommand; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it("should complete test target id", async () => { + (getTestTargets as jest.Mock).mockResolvedValue([ + { + id: "test-target-1", + app: "test-app-1", + }, + { + id: "test-target-2", + app: "test-app-2", + }, + ]); + const result = await testTargetIdCompleter(mockCommand, env); + expect(result).toEqual(true); + expect(log).toHaveBeenCalledWith(["test-target-1", "test-target-2"]); + }); + + it("should not complete test target id when prev is not --test-target-id", async () => { + env.prev = "-c"; + const result = await testTargetIdCompleter(mockCommand, env); + expect(result).toEqual(false); + expect(log).not.toHaveBeenCalled(); + }); + + it("should complete environment id", async () => { + (loadConfig as jest.Mock).mockResolvedValue({ + testTargetId: "test-target-1", + }); + (getEnvironments as jest.Mock).mockResolvedValue([ + { + id: "environment-1", + name: "environment-1", + }, + { + id: "environment-2", + name: "environment-2", + }, + ]); + env.prev = "-e"; + const result = await environmentIdCompleter(mockCommand, env); + expect(result).toEqual(true); + expect(log).toHaveBeenCalledWith(["environment-1", "environment-2"]); + }); + + it("should not complete environment id when prev is not --environment-id", async () => { + env.prev = "-c"; + const result = await environmentIdCompleter(mockCommand, env); + expect(result).toEqual(false); + expect(log).not.toHaveBeenCalled(); + }); + + it("should complete test case id", async () => { + (loadConfig as jest.Mock).mockResolvedValue({ + testTargetId: "test-target-1", + }); + (getTestCases as jest.Mock).mockResolvedValue([ + { + id: "test-case-1", + name: "test-case-1", + }, + { + id: "test-case-2", + name: "test-case-2", + }, + ]); + env.prev = "-c"; + const result = await testCaseIdCompleter(mockCommand, env); + expect(result).toEqual(true); + expect(log).toHaveBeenCalledWith(["test-case-1", "test-case-2"]); + }); + + it("should not complete test case id when prev is not --test-case-id", async () => { + env.prev = "-r"; + const result = await testCaseIdCompleter(mockCommand, env); + expect(result).toEqual(false); + expect(log).not.toHaveBeenCalled(); + }); + + it("should complete test report id", async () => { + (loadConfig as jest.Mock).mockResolvedValue({ + testTargetId: "test-target-1", + }); + (getTestReports as jest.Mock).mockResolvedValue([ + { + id: "test-report-1", + name: "test-report-1", + }, + { + id: "test-report-2", + name: "test-report-2", + }, + ]); + env.prev = "-r"; + const result = await testReportIdCompleter(mockCommand, env); + expect(result).toEqual(true); + expect(log).toHaveBeenCalledWith(["test-report-1", "test-report-2"]); + }); + + it("should not complete test report id when prev is not --test-report-id", async () => { + env.prev = "-c"; + const result = await testReportIdCompleter(mockCommand, env); + expect(result).toEqual(false); + expect(log).not.toHaveBeenCalled(); + }); + + it("should complete options of command", async () => { + const result = await optionsCompleter(mockCommand, env); + expect(result).toEqual(true); + expect(log).toHaveBeenNthCalledWith(1, [ + "--environment-id", + "--test-case-id", + "--test-report-id", + ]); + expect(log).toHaveBeenNthCalledWith(2, ["-e", "-c", "-r"]); + }); +}); diff --git a/tests/scripts/generate-docs.spec.ts b/tests/scripts/generate-docs.spec.ts new file mode 100644 index 0000000..7025302 --- /dev/null +++ b/tests/scripts/generate-docs.spec.ts @@ -0,0 +1,53 @@ +import { generateCommandDocs } from "../../scripts/generate-docs"; +import { Command } from "commander"; + +describe("generateCommandDocs", () => { + const mockCommand = new Command(); + mockCommand.command("test").description("test").option("-t, --test", "test"); + mockCommand.command("test2").description("test2").helpGroup("test"); + mockCommand.command("test3").description("test3").helpGroup("setup"); + it("should generate documentation", () => { + const docs = generateCommandDocs(mockCommand); + expect(docs).toMatchInlineSnapshot(` +"# + +**Usage:** \` [options] [command]\` + +# CLI Documentation + + + +## Setup + +## test3 + +test3 + +**Usage:** \`test3 [options]\` + +## Test + +## test2 + +test2 + +**Usage:** \`test2 [options]\` + +## Other Commands + +## test + +test + +**Usage:** \`test [options]\` + +### Options + +| Option | Description | Required | Default | +|--------|-------------|----------|--------| +| \`-t, --test\` | test | No | | + +" +`); + }); +});