diff --git a/.circleci/config.yml b/.circleci/config.yml index 85c8b33f..c8a2ad13 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -65,6 +65,16 @@ workflows: context: - na40-auth-url - na40-jwt + - release-management/run-command-keyword: + name: sandbox-nuts + git_key_word: '[sb-nuts]' + timeout: 1.5h + command: test:nuts:sandbox + requires: + - release-management/test-nut + context: + - na40-auth-url + - na40-jwt - release-management/release-package: context: - AWS diff --git a/command-snapshot.json b/command-snapshot.json index 892b5382..d3b90dd0 100644 --- a/command-snapshot.json +++ b/command-snapshot.json @@ -1,10 +1,30 @@ [ + { + "command": "env:create:sandbox", + "plugin": "@salesforce/plugin-env", + "flags": [ + "alias", + "async", + "clone", + "definition-file", + "json", + "license-type", + "name", + "no-prompt", + "poll-interval", + "set-default", + "target-org", + "wait" + ], + "alias": [] + }, { "command": "env:create:scratch", "plugin": "@salesforce/plugin-env", "flags": [ "alias", "api-version", + "async", "client-id", "definition-file", "duration-days", @@ -47,5 +67,17 @@ "plugin": "@salesforce/plugin-env", "flags": ["browser", "json", "path", "target-env", "url-only"], "alias": [] + }, + { + "command": "env:resume:sandbox", + "plugin": "@salesforce/plugin-env", + "flags": ["job-id", "json", "name", "target-org", "use-most-recent", "wait"], + "alias": [] + }, + { + "command": "env:resume:scratch", + "plugin": "@salesforce/plugin-env", + "flags": ["job-id", "json", "use-most-recent"], + "alias": [] } ] diff --git a/messages/create.sandbox.md b/messages/create.sandbox.md new file mode 100644 index 00000000..e8a2705f --- /dev/null +++ b/messages/create.sandbox.md @@ -0,0 +1,129 @@ +# summary + +Create a sandbox org. + +# description + +There are two ways to create a sandbox org: specify a definition file that contains the sandbox options or use the --name and --license-type flags to specify the two required options. If you want to set an option other than name or license type, such as apexClassId, you must use a definition file. + +# examples + +- Create a sandbox org using a definition file and give it the alias "MyDevSandbox". The production org that contains the sandbox license has the alias "prodOrg". + + <%= config.bin %> <%= command.id %> -f config/dev-sandbox-def.json --alias MyDevSandbox --target-org prodOrg + +- Create a sandbox org by directly specifying its name and type of license (Developer) instead of using a definition file. Set the sandbox org as your default. + + <%= config.bin %> <%= command.id %> --name mysandbox --license-type Developer --alias MyDevSandbox --target-org prodOrg --set-default + +# flags.setDefault.summary + +Set the sandbox org as your default org. + +# flags.alias.summary + +Alias for the sandbox org. + +# flags.alias.description + +When you create a sandbox, the generated usernames are based on the usernames present in the production org. To ensure uniqueness, the new usernames are appended with the name of the sandbox. For example, the username "user@example.com" in the production org results in the username "user@example.com.mysandbox" in a sandbox named "mysandbox". When you set an alias for a sandbox org, it's assigned to the resulting username of the user running this command. + +# flags.targetOrg.summary + +Username or alias of the production org that contains the sandbox license. + +# flags.targetOrg.description + +When it creates the sandbox org, Salesforce copies the metadata, and optionally data, from your production org to the new sandbox org. + +# flags.definitionFile.summary + +Path to a sandbox definition file. + +# flags.definitionFile.description + +The sandbox definition file is a blueprint for the sandbox. You can create different definition files for each sandbox type that you use in the development process. See https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_sandbox_definition.htm for all the options you can specify in the defintion file. + +# flags.name.summary + +Name of the sandbox org. + +# flags.name.description + +The name must be a unique alphanumeric string (10 or fewer characters) to identify the sandbox. You can’t reuse a name while a sandbox is in the process of being deleted. + +# flags.clone.summary + +Name of the sandbox org to clone. + +# flags.clone.description + +The value of clone must be an existing sandbox in the same target-org. + +# flags.licenseType.summary + +Type of sandbox license. + +# flags.wait.summary + +Number of minutes to wait for the sandbox org to be ready. + +# flags.wait.description + +If the command continues to run after the wait period, the CLI returns control of the terminal to you and displays the "sf env resume sandbox" command you run to check the status of the create. The displayed command includes the job ID for the running sandbox creation. + +# flags.poll-interval.summary + +Number of seconds to wait between retries. + +# flags.async.summary + +Request the sandbox creation, but don't wait for it to complete. + +# flags.async.description + +The command immediately displays the job ID and returns control of the terminal to you. This way, you can continue to use the CLI. To check the status of the sandbox creation, run "sf env resume sandbox". + +# flags.noPrompt.summary + +Don't prompt for confirmation about the sandbox configuration. + +# isConfigurationOk + +Is the configuration correct? + +# warning.NoSandboxNameDefined + +No SandboxName defined, generating new SandboxName: %s. + +# error.RequiresTargetOrg + +This command requires a target-org. Specify it with the --target-org flag or by running the "sf config set target-org=" command. + +# error.MissingLicenseType + +The sandbox license type is required, but you didn't provide a value. Specify the license type in the sandbox definition file with the "licenseType" option, or specify the --license-type and --name flags at the command-line. See https://developer.salesforce.com/docs/atlas.en-us.sf_dev.meta/sf_dev/sf_dev_sandbox_definition.htm for more information. + +# error.NoConfig + +Specify either a sandbox definition file or the --name and --license-type flags together. + +# error.SandboxNameLength + +The sandbox name "%s" should be 10 or fewer characters. + +# error.UserNotSatisfiedWithSandboxConfig + +The sandbox request configuration isn't acceptable. + +# error.pollIntervalGreaterThanWait + +The poll interval (%d seconds) can't be larger that wait (%d in seconds). + +# error.CreateTimeout + +The wait timeout (%d minutes) has expired. Check command results for more information. + +# error.noCloneSource + +Could not find the clone sandbox name "%s" in the target org. diff --git a/messages/create_scratch.md b/messages/create_scratch.md index e9684171..08208a4f 100644 --- a/messages/create_scratch.md +++ b/messages/create_scratch.md @@ -46,6 +46,14 @@ Don't include second-generation managed package (2GP) ancestors in the scratch o Salesforce edition of the scratch org. +# flags.async.summary + +Request the org, but don't wait for it to complete. + +# flags.async.description + +The command immediately displays the job ID and returns control of the terminal to you. This way, you can continue to use the CLI. To resume the scratch org creation, run "sf env resume scratch". + # flags.edition.description The editions that begin with "partner-" are available only if the Dev Hub org is a Partner Business Org. @@ -66,6 +74,10 @@ Consumer key of the Dev Hub connected app. Number of minutes to wait for the scratch org to be ready. +# flags.wait.description + +If the command continues to run after the wait period, the CLI returns control of the terminal to you and displays the job ID. To resume the scratch org creation, run the env resume scratch command and pass it the job ID. + # flags.track-source.description We recommend you enable source tracking in scratch orgs, which is why it's the default behavior. Source tracking allows you to track the changes you make to your metadata, both in your local project and in the scratch org, and to detect any conflicts between the two. @@ -87,3 +99,7 @@ OAuth client secret of your personal connected app # success Your scratch org is ready. + +# action.resume + +Resume scratch org creation by running sf env resume scratch --job-id %s diff --git a/messages/delete_sandbox.md b/messages/delete_sandbox.md index 309d1db0..7d5edfe8 100644 --- a/messages/delete_sandbox.md +++ b/messages/delete_sandbox.md @@ -35,3 +35,7 @@ Are you sure you want to delete the sandbox with name: %s? # success Successfully marked sandbox %s for deletion. + +# error.isNotSandbox + +The target org, %s, is not a sandbox. diff --git a/messages/resume.sandbox.md b/messages/resume.sandbox.md new file mode 100644 index 00000000..9552806e --- /dev/null +++ b/messages/resume.sandbox.md @@ -0,0 +1,87 @@ +# summary + +Check the status of a sandbox creation, and log in to it if it's ready. + +# description + +Sandbox creation can take a long time. If the original "sf env create sandbox" command either times out, or you specified the --async flag, the command displays a job ID. Use this job ID to check whether the sandbox creation is complete, and if it is, the command then logs into it. + +You can also use the sandbox name to check the status or the --use-most-recent flag to use the job ID of the most recent sandbox creation. + +# examples + +- Check the status of a sandbox creation using its name and specify a production org with alias "prodOrg": + + <%= config.bin %> <%= command.id %> --name mysandbox --target-org prodOrg + +- Check the status using the job ID: + + <%= config.bin %> <%= command.id %> --job-id 0GRxxxxxxxx + +- Check the status of the most recent sandbox create request: + + <%= config.bin %> <%= command.id %> --use-most-recent + +# flags.setDefault.summary + +Set the sandbox org as your default org. + +# flags.id.summary + +Job ID of the incomplete sandbox creation that you want to check the status of. + +# flags.id.description + +The job ID is valid for 24 hours after you start the sandbox creation. + +# flags.alias.summary + +Alias for the sandbox org. + +# flags.alias.description + +When you create a sandbox, the generated usernames are based on the usernames present in the production org. To ensure uniqueness, the new usernames are appended with the name of the sandbox. For example, the username "user@example.com" in the production org results in the username "user@example.com.mysandbox" in a sandbox named "mysandbox". When you set an alias for a sandbox org, it's assigned to the resulting username of the user running this command. + +# flags.targetOrg.summary + +Username or alias of the production org that contains the sandbox license. + +# flags.targetOrg.description + +When it creates the sandbox org, Salesforce copies the metadata, and optionally data, from your production org to the new sandbox org. + +# flags.name.summary + +Name of the sandbox org. + +# flags.wait.summary + +Number of minutes to wait for the sandbox org to be ready. + +# flags.wait.description + +If the command continues to run after the wait period, the CLI returns control of the terminal window to you and returns the job ID. To resume checking the sandbox creation, rerun this command. + +# flags.use-most-recent.summary + +Use the most recent sandbox create request. + +# error.pollIntervalGreaterThanWait + +The poll interval (%d seconds) can't be larger that wait (%d in seconds). + +# error.NoSandboxNameOrJobId + +No sandbox name or job ID were provided. + +# error.LatestSandboxRequestNotFound + +Retry the command using either the --name or --job-id flags. + +#error.NoSandboxRequestFound + +Couldn't find a sandbox creation request using the provided sandbox name or job ID. + +# error.ResumeTimeout + +The wait timeout (%d minutes) has expired. Check command results for more information. diff --git a/messages/resume_scratch.md b/messages/resume_scratch.md new file mode 100644 index 00000000..3381d00a --- /dev/null +++ b/messages/resume_scratch.md @@ -0,0 +1,41 @@ +# summary + +Resume the creation of an incomplete scratch org. + +# description + +When the original "sf env create scratch" command either times out or is run with the --async flag, it displays a job ID. + +Run this command by either passing it a job ID or using the --use-most-recent flag to specify the most recent incomplete scratch org. + +# examples + +- Resume a scratch org create with a job ID: + + <%= config.bin %> <%= command.id %> --job-id 2SR3u0000008fBDGAY + +- Resume your most recent incomplete scratch org: + + <%= config.bin %> <%= command.id %> --use-most-recent + +# flags.job-id.summary + +Job ID of the incomplete scratch org create that you want to resume. + +# flags.job-id.description + +The job ID is the same as the record ID of the incomplete scratch org in the ScratchOrgInfo object of the Dev Hub. + +The job ID is valid for 24 hours after you start the scratch org creation. + +# flags.use-most-recent.summary + +Use the job ID of the most recent incomplete scratch org. + +# error.NoRecentJobId + +There are no recent job IDs (ScratchOrgInfo requests) in your cache. Maybe it completed or already resumed? + +# success + +Your scratch org is ready. diff --git a/messages/sandboxbase.md b/messages/sandboxbase.md new file mode 100644 index 00000000..b76a7c55 --- /dev/null +++ b/messages/sandboxbase.md @@ -0,0 +1,63 @@ +# sandboxSuccess + +The sandbox org creation was successful. + +# sandboxSuccess.actions + +The username for the sandbox is %s. +You can open the org by running "sf env open -e %s" + +# checkSandboxStatus + +Run "sf env resume sandbox --job-id %s -o %s" to check for status. +If the org is ready, checking the status also authorizes the org for use with Salesforce CLI. + +# isConfigurationOk + +Is the configuration correct? + +# warning.NoSandboxNameDefined + +No SandboxName defined, generating new SandboxName: %s. + +# warning.ClientTimeoutWaitingForSandboxCreate + +The wait time for the sandbox creation has been exhausted. Please see the results below for more information. + +# error.RequiresTargetOrg + +This command requires a target-org. Specify it with the --target-org flag or by running the "sf config set target-org=" command. + +# error.MissingLicenseType + +The sandbox license type is required, but you didn't provide a value. Specify the license type in the sandbox definition file with the "licenseType" option, or specify the --license-type and --name flags at the command-line. See https://developer.salesforce.com/docs/atlas.en-us.sf_dev.meta/sf_dev/sf_dev_sandbox_definition.htm for more information. + +# error.DnsTimeout + +The sandbox creation failed because the DNS name resolution timed out. + +# error.DnsTimeout.actions + +- Run "sf env resume sandbox --job-id %s -o %s" to check for status. + If the org is ready, checking the status also authorizes the org for use with Salesforce CLI. + +# error.PartialSuccess + +If you specified the -a or -s flags, but the sandbox wasn't immediately available, the "env create sandbox" command may not have finished setting the alias or target-org. +If so, set the alias manually with "sf alias set" and the target-org with "sf config set". + +# error.NoConfig + +Specify either a sandbox definition file or the --name and --license-type flags together. + +# error.SandboxNameLength + +The sandbox name "%s" should be 10 or fewer characters. + +# error.UserNotSatisfiedWithSandboxConfig + +The sandbox request configuration isn't acceptable. + +# error.pollIntervalGreaterThanWait + +The poll interval (%d seconds) can't be larger that wait (%d in seconds). diff --git a/package.json b/package.json index 15e80a0a..32bf5b1d 100644 --- a/package.json +++ b/package.json @@ -5,9 +5,9 @@ "author": "Salesforce", "bugs": "https://github.com/forcedotcom/cli/issues", "dependencies": { - "@oclif/core": "^1.6.3", - "@salesforce/core": "^3.10.1", - "@salesforce/sf-plugins-core": "^1.9.0", + "@oclif/core": "^1.7.0", + "@salesforce/core": "^3.15.0", + "@salesforce/sf-plugins-core": "^1.12.1", "chalk": "^4", "change-case": "^4.1.2", "open": "^8.4.0", @@ -16,17 +16,17 @@ "devDependencies": { "@oclif/plugin-command-snapshot": "^3.1.3", "@oclif/test": "^2.1.0", - "@salesforce/cli-plugins-testkit": "^1.5.20", + "@salesforce/cli-plugins-testkit": "^1.5.28", "@salesforce/dev-config": "^3.0.0", - "@salesforce/dev-scripts": "^2.0.1", - "@salesforce/plugin-command-reference": "^2.2.1", - "@salesforce/plugin-config": "^2.3.1", + "@salesforce/dev-scripts": "^2.0.2", + "@salesforce/plugin-command-reference": "^2.2.8", + "@salesforce/plugin-config": "^2.3.2", "@salesforce/plugin-functions": "^1.7.1", "@salesforce/prettier-config": "^0.0.2", "@salesforce/ts-sinon": "1.3.21", "@types/shelljs": "^0.8.11", - "@typescript-eslint/eslint-plugin": "^4.33.0", - "@typescript-eslint/parser": "^4.33.0", + "@typescript-eslint/eslint-plugin": "^5.19.0", + "@typescript-eslint/parser": "^5.19.0", "chai": "^4.3.6", "cz-conventional-changelog": "^3.3.0", "eslint": "^7.32.0", @@ -40,9 +40,9 @@ "eslint-plugin-prettier": "^3.4.1", "husky": "^7.0.4", "lint-staged": "^11.2.6", - "mocha": "^9.1.3", + "mocha": "^9.2.2", "nyc": "^15.1.0", - "oclif": "^2.6.0", + "oclif": "^2.6.3", "prettier": "^2.6.0", "pretty-quick": "^3.1.3", "shelljs": "^0.8.5", @@ -57,7 +57,7 @@ } }, "engines": { - "node": ">=12.0.0" + "node": ">=14.0.0" }, "files": [ "/lib", @@ -114,6 +114,7 @@ "test:deprecation-policy": "./bin/dev snapshot:compare", "test:json-schema": "./bin/dev schema:compare", "test:nuts": "nyc mocha \"**/*.nut.ts\" --slow 4500 --timeout 600000 --parallel", + "test:nuts:sandbox": "nyc mocha \"**/*.sandboxNut.ts\" --slow 450000 --timeout 7200000", "version": "oclif readme" }, "publishConfig": { diff --git a/schemas/env-create-sandbox.json b/schemas/env-create-sandbox.json new file mode 100644 index 00000000..3356486e --- /dev/null +++ b/schemas/env-create-sandbox.json @@ -0,0 +1,49 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$ref": "#/definitions/SandboxProcessObject", + "definitions": { + "SandboxProcessObject": { + "type": "object", + "properties": { + "Id": { + "type": "string" + }, + "Status": { + "type": "string" + }, + "SandboxName": { + "type": "string" + }, + "SandboxInfoId": { + "type": "string" + }, + "LicenseType": { + "type": "string" + }, + "CreatedDate": { + "type": "string" + }, + "SandboxOrganization": { + "type": "string" + }, + "CopyProgress": { + "type": "number" + }, + "SourceId": { + "type": "string" + }, + "Description": { + "type": "string" + }, + "ApexClassId": { + "type": "string" + }, + "EndDate": { + "type": "string" + } + }, + "required": ["Id", "Status", "SandboxName", "SandboxInfoId", "LicenseType", "CreatedDate"], + "additionalProperties": false + } + } +} diff --git a/schemas/env-resume-sandbox.json b/schemas/env-resume-sandbox.json new file mode 100644 index 00000000..3356486e --- /dev/null +++ b/schemas/env-resume-sandbox.json @@ -0,0 +1,49 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$ref": "#/definitions/SandboxProcessObject", + "definitions": { + "SandboxProcessObject": { + "type": "object", + "properties": { + "Id": { + "type": "string" + }, + "Status": { + "type": "string" + }, + "SandboxName": { + "type": "string" + }, + "SandboxInfoId": { + "type": "string" + }, + "LicenseType": { + "type": "string" + }, + "CreatedDate": { + "type": "string" + }, + "SandboxOrganization": { + "type": "string" + }, + "CopyProgress": { + "type": "number" + }, + "SourceId": { + "type": "string" + }, + "Description": { + "type": "string" + }, + "ApexClassId": { + "type": "string" + }, + "EndDate": { + "type": "string" + } + }, + "required": ["Id", "Status", "SandboxName", "SandboxInfoId", "LicenseType", "CreatedDate"], + "additionalProperties": false + } + } +} diff --git a/schemas/env-resume-scratch.json b/schemas/env-resume-scratch.json new file mode 100644 index 00000000..5fcc7e9c --- /dev/null +++ b/schemas/env-resume-scratch.json @@ -0,0 +1,310 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$ref": "#/definitions/ScratchCreateResponse", + "definitions": { + "ScratchCreateResponse": { + "type": "object", + "properties": { + "username": { + "type": "string" + }, + "scratchOrgInfo": { + "$ref": "#/definitions/ScratchOrgInfo" + }, + "authFields": { + "$ref": "#/definitions/AuthFields" + }, + "warnings": { + "type": "array", + "items": { + "type": "string" + } + }, + "orgId": { + "type": "string" + } + }, + "required": ["scratchOrgInfo", "warnings", "orgId"], + "additionalProperties": false + }, + "ScratchOrgInfo": { + "type": "object", + "properties": { + "AdminEmail": { + "type": "string" + }, + "CreatedDate": { + "type": "string" + }, + "ConnectedAppCallbackUrl": { + "type": "string" + }, + "ConnectedAppConsumerKey": { + "type": "string" + }, + "Country": { + "type": "string" + }, + "Description": { + "type": "string" + }, + "DurationDays": { + "type": "string" + }, + "Edition": { + "type": "string" + }, + "ErrorCode": { + "type": "string" + }, + "ExpirationDate": { + "type": "string" + }, + "Features": { + "type": "string" + }, + "HasSampleData": { + "type": "boolean" + }, + "Id": { + "type": "string" + }, + "Language": { + "type": "string" + }, + "LoginUrl": { + "type": "string" + }, + "Name": { + "type": "string" + }, + "Namespace": { + "type": "string" + }, + "OrgName": { + "type": "string" + }, + "Release": { + "type": "string", + "enum": ["Current", "Previous", "Preview"] + }, + "ScratchOrg": { + "type": "string" + }, + "SourceOrg": { + "type": "string" + }, + "AuthCode": { + "type": "string" + }, + "Snapshot": { + "type": "string" + }, + "Status": { + "type": "string", + "enum": ["New", "Creating", "Active", "Error", "Deleted"] + }, + "SignupEmail": { + "type": "string" + }, + "SignupUsername": { + "type": "string" + }, + "SignupInstance": { + "type": "string" + }, + "Username": { + "type": "string" + }, + "settings": { + "type": "object", + "additionalProperties": {} + }, + "objectSettings": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/ObjectSetting" + } + }, + "orgPreferences": { + "type": "object", + "properties": { + "enabled": { + "type": "array", + "items": { + "type": "string" + } + }, + "disabled": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": ["enabled", "disabled"], + "additionalProperties": false + } + }, + "required": [ + "LoginUrl", + "AuthCode", + "Snapshot", + "Status", + "SignupEmail", + "SignupUsername", + "SignupInstance", + "Username" + ], + "additionalProperties": false + }, + "ObjectSetting": { + "type": "object", + "properties": { + "sharingModel": { + "type": "string" + }, + "defaultRecordType": { + "type": "string" + } + }, + "additionalProperties": { + "$ref": "#/definitions/Optional%3CAnyJson%3E" + } + }, + "Optional": { + "anyOf": [ + { + "$ref": "#/definitions/AnyJson" + }, + { + "not": {} + } + ], + "description": "A union type for either the parameterized type `T` or `undefined` -- the opposite of {@link NonOptional } ." + }, + "AnyJson": { + "anyOf": [ + { + "$ref": "#/definitions/JsonPrimitive" + }, + { + "$ref": "#/definitions/JsonCollection" + } + ], + "description": "Any valid JSON value." + }, + "JsonPrimitive": { + "type": ["null", "boolean", "number", "string"], + "description": "Any valid JSON primitive value." + }, + "JsonCollection": { + "anyOf": [ + { + "$ref": "#/definitions/JsonMap" + }, + { + "$ref": "#/definitions/JsonArray" + } + ], + "description": "Any valid JSON collection value." + }, + "JsonMap": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/Optional%3CAnyJson%3E" + }, + "properties": {}, + "description": "Any JSON-compatible object." + }, + "JsonArray": { + "type": "array", + "items": { + "$ref": "#/definitions/AnyJson" + }, + "description": "Any JSON-compatible array." + }, + "AuthFields": { + "type": "object", + "properties": { + "accessToken": { + "type": "string" + }, + "alias": { + "type": "string" + }, + "authCode": { + "type": "string" + }, + "clientId": { + "type": "string" + }, + "clientSecret": { + "type": "string" + }, + "created": { + "type": "string" + }, + "createdOrgInstance": { + "type": "string" + }, + "devHubUsername": { + "type": "string" + }, + "instanceUrl": { + "type": "string" + }, + "instanceApiVersion": { + "type": "string" + }, + "instanceApiVersionLastRetrieved": { + "type": "string" + }, + "isDevHub": { + "type": "boolean" + }, + "loginUrl": { + "type": "string" + }, + "orgId": { + "type": "string" + }, + "password": { + "type": "string" + }, + "privateKey": { + "type": "string" + }, + "refreshToken": { + "type": "string" + }, + "scratchAdminUsername": { + "type": "string" + }, + "snapshot": { + "type": "string" + }, + "userId": { + "type": "string" + }, + "username": { + "type": "string" + }, + "usernames": { + "type": "array", + "items": { + "type": "string" + } + }, + "userProfileName": { + "type": "string" + }, + "expirationDate": { + "type": "string" + } + }, + "additionalProperties": false, + "description": "Fields for authorization, org, and local information." + } + } +} diff --git a/schemas/hooks/sf-env-display.json b/schemas/hooks/sf-env-display.json index 0c56e3fd..a33024e5 100644 --- a/schemas/hooks/sf-env-display.json +++ b/schemas/hooks/sf-env-display.json @@ -47,6 +47,9 @@ "isDevHub": { "type": "boolean" }, + "isSandbox": { + "type": "boolean" + }, "instanceUrl": { "type": "string" }, diff --git a/src/commands/env/create/sandbox.ts b/src/commands/env/create/sandbox.ts new file mode 100644 index 00000000..e6453ca1 --- /dev/null +++ b/src/commands/env/create/sandbox.ts @@ -0,0 +1,304 @@ +/* + * Copyright (c) 2022, salesforce.com, inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ + +import * as fs from 'fs'; +import { Flags } from '@salesforce/sf-plugins-core'; +import { + Lifecycle, + Messages, + Org, + SandboxEvents, + SandboxProcessObject, + SandboxRequest, + SfError, +} from '@salesforce/core'; +import { Duration } from '@salesforce/kit'; +import { Ux } from '@salesforce/sf-plugins-core/lib/ux'; +import * as Interfaces from '@oclif/core/lib/interfaces'; +import { SandboxCommandBase } from '../../../shared/sandboxCommandBase'; + +Messages.importMessagesDirectory(__dirname); +const messages = Messages.loadMessages('@salesforce/plugin-env', 'create.sandbox'); + +type CmdFlags = { + 'definition-file': string; + 'set-default': boolean; + alias: string; + async: boolean; + 'poll-interval': Duration; + wait: Duration; + name: string; + 'license-type': string; + 'no-prompt': boolean; + 'target-org': Org; + clone: string; + json: boolean; +}; + +export enum SandboxLicenseType { + developer = 'Developer', + developerPro = 'Developer_Pro', + partial = 'Partial', + full = 'Full', +} + +const getLicenseTypes = (): string[] => Object.values(SandboxLicenseType); + +type SandboxConfirmData = SandboxRequest & { CloneSource?: string }; + +export default class CreateSandbox extends SandboxCommandBase { + public static summary = messages.getMessage('summary'); + public static description = messages.getMessage('description'); + public static examples = messages.getMessages('examples'); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public static flags: Interfaces.FlagInput> = { + // needs to change when new flags are available + 'definition-file': Flags.file({ + exists: true, + char: 'f', + summary: messages.getMessage('flags.definitionFile.summary'), + description: messages.getMessage('flags.definitionFile.description'), + exclusive: ['name', 'license-type'], + }), + 'set-default': Flags.boolean({ + char: 's', + summary: messages.getMessage('flags.setDefault.summary'), + }), + alias: Flags.string({ + char: 'a', + summary: messages.getMessage('flags.alias.summary'), + description: messages.getMessage('flags.alias.description'), + }), + wait: Flags.duration({ + char: 'w', + summary: messages.getMessage('flags.wait.summary'), + description: messages.getMessage('flags.wait.description'), + min: 1, + unit: 'minutes', + defaultValue: 30, + helpValue: '', + exclusive: ['async'], + }), + 'poll-interval': Flags.duration({ + char: 'i', + summary: messages.getMessage('flags.poll-interval.summary'), + min: 15, + unit: 'seconds', + defaultValue: 30, + helpValue: '', + exclusive: ['async'], + }), + async: Flags.boolean({ + summary: messages.getMessage('flags.async.summary'), + description: messages.getMessage('flags.async.description'), + exclusive: ['wait', 'poll-interval'], + }), + name: Flags.string({ + char: 'n', + summary: messages.getMessage('flags.name.summary'), + description: messages.getMessage('flags.name.description'), + exclusive: ['definition-file'], + parse: (name: string): Promise => { + if (name.length > 10) { + throw messages.createError('error.SandboxNameLength', [name]); + } + return Promise.resolve(name); + }, + }), + clone: Flags.string({ + char: 'c', + summary: messages.getMessage('flags.clone.summary'), + description: messages.getMessage('flags.clone.description'), + exclusive: ['license-type'], + }), + 'license-type': Flags.enum({ + char: 'l', + summary: messages.getMessage('flags.licenseType.summary'), + exclusive: ['definition-file', 'clone'], + options: getLicenseTypes(), + default: SandboxLicenseType.developer, + }), + 'target-org': Flags.requiredOrg({ + char: 'o', + summary: messages.getMessage('flags.targetOrg.summary'), + description: messages.getMessage('flags.targetOrg.description'), + }), + 'no-prompt': Flags.boolean({ + summary: messages.getMessage('flags.noPrompt.summary'), + }), + }; + public static readonly state = 'beta'; + protected readonly lifecycleEventNames = ['postorgcreate']; + private flags: CmdFlags; + + public async run(): Promise { + this.sandboxRequestConfig = await this.getSandboxRequestConfig(); + this.flags = (await this.parse(CreateSandbox)).flags as CmdFlags; + this.debug('Create started with args %s ', this.flags); + this.validateFlags(); + return await this.createSandbox(); + } + + protected getCheckSandboxStatusParams(): string[] { + return [this.latestSandboxProgressObj.Id, this.flags['target-org'].getUsername()]; + } + + private lowerToUpper(object: Record): Record { + return Object.fromEntries(Object.entries(object).map(([k, v]) => [`${k.charAt(0).toUpperCase()}${k.slice(1)}`, v])); + } + + private async createSandboxRequest(prodOrg: Org): Promise { + let sandboxDefFileContents = this.readJsonDefFile() || {}; + + if (sandboxDefFileContents) { + sandboxDefFileContents = this.lowerToUpper(sandboxDefFileContents); + } + + // build sandbox request from data provided + const sandboxReq: SandboxRequest = { + SandboxName: undefined, + ...sandboxDefFileContents, + ...Object.assign({}, this.flags.name ? { SandboxName: this.flags.name } : {}), + ...Object.assign( + {}, + sandboxDefFileContents['LicenseType'] + ? { LicenseType: sandboxDefFileContents['LicenseType'] as string } + : { LicenseType: this.flags['license-type'] } + ), + }; + + if (!sandboxReq.SandboxName) { + // sandbox names are 10 chars or less, a radix of 36 = [a-z][0-9] + // see https://help.salesforce.com/s/articleView?id=sf.data_sandbox_create.htm&type=5 for sandbox naming criteria + // technically without querying the production org, the generated name could already exist, + // but the chances of that are lower than the perf penalty of querying and verifying + sandboxReq.SandboxName = `sbx${Date.now().toString(36).slice(-7)}`; + this.info(messages.createWarning('warning.NoSandboxNameDefined', [sandboxReq.SandboxName])); + } + + const sourceId = await this.getSourceId(prodOrg); + if (sourceId) { + sandboxReq.SourceId = sourceId; + delete sandboxReq.LicenseType; + } + return sandboxReq; + } + + private async createSandbox(): Promise { + const prodOrg = this.flags['target-org']; + const lifecycle = Lifecycle.getInstance(); + + this.registerLifecycleListeners(lifecycle, { + isAsync: this.flags.async, + setDefault: this.flags['set-default'], + alias: this.flags.alias, + prodOrg, + }); + const sandboxReq = await this.createSandboxRequest(prodOrg); + await this.confirmSandboxReq({ ...sandboxReq, ...(this.flags.clone ? { CloneSource: this.flags.clone } : {}) }); + this.initSandboxProcessData(prodOrg, sandboxReq); + + if (!this.flags.async) { + this.spinner.start('Sandbox Create'); + } + + this.debug('Calling create with SandboxRequest: %s ', sandboxReq); + + try { + const sandboxProcessObject = await prodOrg.createSandbox(sandboxReq, { + wait: this.flags.wait, + interval: this.flags['poll-interval'], + async: this.flags.async, + }); + this.latestSandboxProgressObj = sandboxProcessObject; + this.saveSandboxProgressConfig(); + if (this.flags.async) { + process.exitCode = 68; + } + return sandboxProcessObject; + } catch (err) { + this.spinner.stop(); + const error = err as SfError; + if (this.pollingTimeOut) { + void lifecycle.emit(SandboxEvents.EVENT_ASYNC_RESULT, undefined); + process.exitCode = 68; + return this.latestSandboxProgressObj; + } else if (error.name === 'SandboxCreateNotCompleteError') { + void lifecycle.emit(SandboxEvents.EVENT_ASYNC_RESULT, undefined); + process.exitCode = 68; + return this.latestSandboxProgressObj; + } + throw err; + } + } + + private initSandboxProcessData(prodOrg: Org, sandboxReq: SandboxRequest): void { + this.sandboxRequestData.alias = this.flags.alias; + this.sandboxRequestData.setDefault = this.flags['set-default']; + this.sandboxRequestData.prodOrgUsername = prodOrg.getUsername(); + this.sandboxRequestData.sandboxProcessObject.SandboxName = sandboxReq.SandboxName; + this.sandboxRequestData.sandboxRequest = sandboxReq; + this.saveSandboxProgressConfig(); + } + + private readJsonDefFile(): Record { + // the -f option + if (this.flags['definition-file']) { + this.debug('Reading JSON DefFile %s ', this.flags['definition-file']); + return JSON.parse(fs.readFileSync(this.flags['definition-file'], 'utf-8')) as Record; + } + } + + private async confirmSandboxReq(sandboxReq: SandboxConfirmData): Promise { + if (this.flags['no-prompt'] || this.jsonEnabled()) return; + + const columns: Ux.Table.Columns<{ key: string; value: unknown }> = { + key: { header: 'Field' }, + value: { header: 'Value' }, + }; + + const data = Object.entries(sandboxReq).map(([key, value]) => ({ key, value })); + this.styledHeader('Config Sandbox Request'); + this.table(data, columns, {}); + + const configurationCorrect = await this.timedPrompt<{ continue: boolean }>( + [ + { + name: 'continue', + type: 'confirm', + message: messages.getMessage('isConfigurationOk'), + }, + ], + 10_000 + ); + if (!configurationCorrect.continue) { + throw messages.createError('error.UserNotSatisfiedWithSandboxConfig'); + } + } + + private validateFlags(): void { + if (this.flags['poll-interval'].seconds > this.flags.wait.seconds) { + throw messages.createError('error.pollIntervalGreaterThanWait', [ + this.flags['poll-interval'].seconds, + this.flags.wait.seconds, + ]); + } + } + + private async getSourceId(prodOrg: Org): Promise { + if (!this.flags.clone) { + return undefined; + } + try { + const sourceOrg = await prodOrg.querySandboxProcessBySandboxName(this.flags.clone); + return sourceOrg.SandboxInfoId; + } catch (err) { + throw messages.createError('error.noCloneSource', [this.flags.clone], [], err as Error); + } + } +} diff --git a/src/commands/env/create/scratch.ts b/src/commands/env/create/scratch.ts index bc7e0b4f..9df19b7d 100644 --- a/src/commands/env/create/scratch.ts +++ b/src/commands/env/create/scratch.ts @@ -6,62 +6,41 @@ */ import * as fs from 'fs'; +import { Duration } from '@salesforce/kit'; import { Messages, - ScratchOrgRequest, - ScratchOrgInfo, + ScratchOrgCreateOptions, Lifecycle, - AuthFields, ScratchOrgLifecycleEvent, scratchOrgLifecycleEventName, - AuthInfo, Org, + scratchOrgCreate, + SfError, } from '@salesforce/core'; import { SfCommand, Flags } from '@salesforce/sf-plugins-core'; -import * as chalk from 'chalk'; -import { buildStatus } from '../../../scratchOrgOutput'; - +import { buildStatus } from '../../../shared/scratchOrgOutput'; +import { ScratchCreateResponse } from '../../../types'; Messages.importMessagesDirectory(__dirname); const messages = Messages.loadMessages('@salesforce/plugin-env', 'create_scratch'); -export interface ScratchCreateResponse { - username?: string; - scratchOrgInfo: ScratchOrgInfo; - authFields?: AuthFields; - warnings: string[]; - orgId: string; -} - -export const postOrgCreateHookFields = [ - 'accessToken', - 'clientId', - 'created', - 'createdOrgInstance', - 'devHubUsername', - 'expirationDate', - 'instanceUrl', - 'loginUrl', - 'orgId', - 'username', -] as const; - -const isHookField = (key: string): key is typeof postOrgCreateHookFields[number] => { - return postOrgCreateHookFields.includes(key as typeof postOrgCreateHookFields[number]); -}; - export const secretTimeout = 60000; -export type PostOrgCreateHook = Pick; export default class EnvCreateScratch extends SfCommand { public static readonly summary = messages.getMessage('summary'); public static readonly description = messages.getMessage('description'); public static readonly examples = messages.getMessages('examples'); + public static readonly state = 'beta'; + public static flags = { alias: Flags.string({ char: 'a', summary: messages.getMessage('flags.alias.summary'), description: messages.getMessage('flags.alias.description'), }), + async: Flags.boolean({ + summary: messages.getMessage('flags.async.summary'), + description: messages.getMessage('flags.async.description'), + }), 'set-default': Flags.boolean({ char: 'd', summary: messages.getMessage('flags.set-default.summary'), @@ -110,6 +89,7 @@ export default class EnvCreateScratch extends SfCommand { min: 1, max: 30, char: 'y', + helpValue: '', summary: messages.getMessage('flags.duration-days.summary'), }), wait: Flags.duration({ @@ -117,7 +97,9 @@ export default class EnvCreateScratch extends SfCommand { defaultValue: 5, min: 2, char: 'w', + helpValue: '', summary: messages.getMessage('flags.wait.summary'), + description: messages.getMessage('flags.wait.description'), }), 'api-version': Flags.orgApiVersion(), 'client-id': Flags.string({ @@ -125,49 +107,66 @@ export default class EnvCreateScratch extends SfCommand { summary: messages.getMessage('flags.client-id.summary'), }), }; - public static readonly state = 'beta'; public async run(): Promise { const lifecycle = Lifecycle.getInstance(); - const { flags } = await this.parse(EnvCreateScratch); + const baseUrl = flags['target-dev-hub'].getField(Org.Fields.INSTANCE_URL).toString(); + const orgConfig = flags['definition-file'] + ? (JSON.parse(await fs.promises.readFile(flags['definition-file'], 'utf-8')) as Record) + : { edition: flags.edition }; - const createCommandOptions: ScratchOrgRequest = { + const createCommandOptions: ScratchOrgCreateOptions = { + hubOrg: flags['target-dev-hub'], clientSecret: flags['client-id'] ? await this.clientSecretPrompt() : undefined, connectedAppConsumerKey: flags['client-id'], durationDays: flags['duration-days'].days, nonamespace: flags['no-namespace'], noancestors: flags['no-ancestors'], - wait: flags.wait, + wait: flags.async ? Duration.minutes(0) : flags.wait, apiversion: flags['api-version'], - definitionjson: flags['definition-file'] - ? await fs.promises.readFile(flags['definition-file'], 'utf-8') - : JSON.stringify({ edition: flags.edition }), + orgConfig, + alias: flags.alias, + setDefault: flags['set-default'], }; let lastStatus: string; - const baseUrl = flags['target-dev-hub'].getField(Org.Fields.INSTANCE_URL).toString(); - - // eslint-disable-next-line @typescript-eslint/require-await - lifecycle.on(scratchOrgLifecycleEventName, async (data): Promise => { - lastStatus = buildStatus(data, baseUrl); - this.spinner.status = lastStatus; - }); + if (!flags.async) { + // eslint-disable-next-line @typescript-eslint/require-await + lifecycle.on(scratchOrgLifecycleEventName, async (data): Promise => { + lastStatus = buildStatus(data, baseUrl); + this.spinner.status = lastStatus; + }); + } this.log(); - this.spinner.start('Creating Scratch Org'); - const { username, scratchOrgInfo, authFields, warnings } = await flags['target-dev-hub'].scratchOrgCreate( - createCommandOptions + this.spinner.start( + flags.async ? 'Requesting Scratch Org (will not wait for completion because --async)' : 'Creating Scratch Org' ); - this.spinner.stop(lastStatus); - await this.maybeSetAliasAndDefault(username, flags['set-default'], flags.alias); - await lifecycle.emit( - 'postorgcreate', - Object.fromEntries(Object.entries(authFields).filter(([key]) => isHookField(key))) as PostOrgCreateHook - ); - this.log(); - this.log(chalk.green(messages.getMessage('success'))); - return { username, scratchOrgInfo, authFields, warnings, orgId: scratchOrgInfo.Id }; + try { + const { username, scratchOrgInfo, authFields, warnings } = await scratchOrgCreate(createCommandOptions); + + this.spinner.stop(lastStatus); + this.log(); + if (flags.async) { + this.info(messages.getMessage('action.resume', [scratchOrgInfo.Id])); + } else { + this.logSuccess(messages.getMessage('success')); + } + + return { username, scratchOrgInfo, authFields, warnings, orgId: scratchOrgInfo.Id }; + } catch (error) { + if (error instanceof SfError && error.name === 'ScratchOrgInfoTimeoutError') { + this.spinner.stop(lastStatus); + const scratchOrgInfoId = (error.data as { scratchOrgInfoId: string }).scratchOrgInfoId; + const resumeMessage = messages.getMessage('action.resume', [scratchOrgInfoId]); + + this.info(resumeMessage); + this.error('The scratch org did not complete within your wait time', { code: '69', exit: 69 }); + } else { + throw error; + } + } } private async clientSecretPrompt(): Promise { @@ -183,16 +182,4 @@ export default class EnvCreateScratch extends SfCommand { ); return secret; } - - private async maybeSetAliasAndDefault(username: string, setDefault: boolean, alias?: string): Promise { - if (!setDefault && !alias) { - return; - } - const authInfo = await AuthInfo.create({ username }); - return authInfo.handleAliasAndDefaultSettings({ - alias, - setDefault, - setDefaultDevHub: false, - }); - } } diff --git a/src/commands/env/delete/sandbox.ts b/src/commands/env/delete/sandbox.ts index e63fcb28..b8930255 100644 --- a/src/commands/env/delete/sandbox.ts +++ b/src/commands/env/delete/sandbox.ts @@ -31,16 +31,20 @@ export default class EnvDeleteSandbox extends SfCommand { public static readonly state = 'beta'; public async run(): Promise { - const { flags } = await this.parse(EnvDeleteSandbox); + const flags = (await this.parse(EnvDeleteSandbox)).flags; const org = flags['target-org']; + if (!(await org.isSandbox())) { + throw messages.createError('error.isNotSandbox', [org.getUsername()]); + } + if (flags['no-prompt'] || (await this.confirm(messages.getMessage('prompt.confirm', [org.getUsername()])))) { try { await org.delete(); - this.log(messages.getMessage('success', [org.getUsername()])); + this.logSuccess(messages.getMessage('success', [org.getUsername()])); } catch (e) { if (e instanceof Error && e.name === 'SandboxNotFound') { - this.log(messages.getMessage('success.Idempotent', [org.getUsername()])); + this.logSuccess(messages.getMessage('success.Idempotent', [org.getUsername()])); } else { throw e; } diff --git a/src/commands/env/delete/scratch.ts b/src/commands/env/delete/scratch.ts index 412bba87..cf1a6829 100644 --- a/src/commands/env/delete/scratch.ts +++ b/src/commands/env/delete/scratch.ts @@ -20,6 +20,7 @@ export default class EnvDeleteScratch extends SfCommand { public static readonly summary = messages.getMessage('summary'); public static readonly description = messages.getMessage('description'); public static readonly examples = messages.getMessages('examples'); + // eslint-disable-next-line @typescript-eslint/no-explicit-any public static flags = { 'target-org': Flags.requiredOrg({ char: 'o', @@ -33,16 +34,16 @@ export default class EnvDeleteScratch extends SfCommand { public static readonly state = 'beta'; public async run(): Promise { - const { flags } = await this.parse(EnvDeleteScratch); + const flags = (await this.parse(EnvDeleteScratch)).flags; const org = flags['target-org']; if (flags['no-prompt'] || (await this.confirm(messages.getMessage('prompt.confirm', [org.getUsername()])))) { try { await org.delete(); - this.log(messages.getMessage('success', [org.getUsername()])); + this.logSuccess(messages.getMessage('success', [org.getUsername()])); } catch (e) { if (e instanceof Error && e.name === 'ScratchOrgNotFound') { - this.log(messages.getMessage('success.Idempotent', [org.getUsername()])); + this.logSuccess(messages.getMessage('success.Idempotent', [org.getUsername()])); } else { throw e; } diff --git a/src/commands/env/open.ts b/src/commands/env/open.ts index 442979cd..8d9cc853 100644 --- a/src/commands/env/open.ts +++ b/src/commands/env/open.ts @@ -82,7 +82,7 @@ export default class EnvOpen extends SfCommand { const browserName = browser ? browser : 'the default browser'; await this.open(url, browser); - this.log(`Opening ${nameOrAlias} in ${browserName}.`); + this.logSuccess(`Opening ${nameOrAlias} in ${browserName}.`); } } else { throw messages.createError('error.EnvironmentNotSupported', [nameOrAlias]); diff --git a/src/commands/env/resume/sandbox.ts b/src/commands/env/resume/sandbox.ts new file mode 100644 index 00000000..b5d7a21e --- /dev/null +++ b/src/commands/env/resume/sandbox.ts @@ -0,0 +1,237 @@ +/* + * Copyright (c) 2022, salesforce.com, inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ + +import { Flags } from '@salesforce/sf-plugins-core'; +import { + GlobalInfo, + Lifecycle, + Messages, + Org, + ResultEvent, + SandboxEvents, + SandboxProcessObject, + SandboxRequestCacheEntry, + ResumeSandboxRequest, + SandboxUserAuthResponse, + SfError, +} from '@salesforce/core'; +import { Duration } from '@salesforce/kit'; +import * as Interfaces from '@oclif/core/lib/interfaces'; +import { SandboxCommandBase } from '../../../shared/sandboxCommandBase'; + +Messages.importMessagesDirectory(__dirname); +const messages = Messages.loadMessages('@salesforce/plugin-env', 'resume.sandbox'); + +type CmdFlags = { + wait: Duration; + name: string; + 'job-id': string; + 'target-org': Org; + 'use-most-recent': boolean; +}; + +export default class ResumeSandbox extends SandboxCommandBase { + public static summary = messages.getMessage('summary'); + public static description = messages.getMessage('description'); + public static examples = messages.getMessages('examples'); + + public static flags: Interfaces.FlagInput = { + wait: Flags.duration({ + char: 'w', + summary: messages.getMessage('flags.wait.summary'), + description: messages.getMessage('flags.wait.description'), + min: 0, + unit: 'minutes', + helpValue: '', + defaultValue: 0, + }), + name: Flags.string({ + char: 'n', + summary: messages.getMessage('flags.name.summary'), + parse: (name: string): Promise => { + if (name.length > 10) { + throw messages.createError('error.SandboxNameLength', [name]); + } + return Promise.resolve(name); + }, + exclusive: ['job-id'], + }), + 'job-id': Flags.salesforceId({ + startsWith: '0GR', + char: 'i', + summary: messages.getMessage('flags.id.summary'), + description: messages.getMessage('flags.id.description'), + exclusive: ['name'], + }), + 'use-most-recent': Flags.boolean({ + char: 'l', + summary: messages.getMessage('flags.use-most-recent.summary'), + }), + 'target-org': Flags.optionalOrg({ + char: 'o', + summary: messages.getMessage('flags.targetOrg.summary'), + description: messages.getMessage('flags.targetOrg.description'), + }), + }; + public static readonly state = 'beta'; + protected readonly lifecycleEventNames = ['postorgcreate']; + private flags: CmdFlags; + + public async run(): Promise { + this.sandboxRequestConfig = await this.getSandboxRequestConfig(); + this.flags = (await this.parse(ResumeSandbox)).flags; + this.debug('Resume started with args %s ', this.flags); + return await this.resumeSandbox(); + } + + protected getCheckSandboxStatusParams(): string[] { + return [this.latestSandboxProgressObj.Id, this.flags['target-org'].getUsername()]; + } + + private createResumeSandboxRequest(): ResumeSandboxRequest { + if (this.flags['use-most-recent']) { + const [, sandboxRequestData] = this.sandboxRequestConfig.getLatestEntry(); + if (sandboxRequestData) { + return { SandboxName: sandboxRequestData.sandboxProcessObject?.SandboxName }; + } + } + // build resume sandbox request from data provided + return { + ...Object.assign({}, this.flags.name ? { SandboxName: this.flags.name } : {}), + ...Object.assign({}, this.flags['job-id'] ? { SandboxProcessObjId: this.flags['job-id'] } : {}), + }; + } + + private async resumeSandbox(): Promise { + this.sandboxRequestData = this.buildSandboxRequestCacheEntry(); + const prodOrgUsername: string = this.sandboxRequestData.prodOrgUsername; + + if (!this.sandboxRequestData.sandboxProcessObject.SandboxName) { + if (!this.flags['name'] && !this.flags['job-id']) { + throw messages.createError('error.NoSandboxNameOrJobId'); + } + } + const prodOrg = await Org.create({ aliasOrUsername: prodOrgUsername }); + this.flags['target-org'] = prodOrg; + const lifecycle = Lifecycle.getInstance(); + + this.registerLifecycleListeners(lifecycle, { + isAsync: false, + alias: this.sandboxRequestData.alias, + setDefault: this.sandboxRequestData.setDefault, + prodOrg, + }); + + if ( + await this.verifyIfAuthExists( + prodOrg, + this.sandboxRequestData.sandboxProcessObject.SandboxName, + this.flags['job-id'], + lifecycle + ) + ) { + return this.latestSandboxProgressObj; + } + + const sandboxReq = this.createResumeSandboxRequest(); + + if (this.flags.wait?.seconds > 0) { + this.spinner.start('Resume Create'); + } + + this.debug('Calling create with ResumeSandboxRequest: %s ', sandboxReq); + + try { + return await prodOrg.resumeSandbox(sandboxReq, { + wait: this.flags.wait ?? Duration.seconds(0), + interval: Duration.seconds(30), + }); + } catch (err) { + this.spinner.stop(); + const error = err as SfError; + if (this.pollingTimeOut) { + void lifecycle.emit(SandboxEvents.EVENT_ASYNC_RESULT, undefined); + process.exitCode = 68; + return this.latestSandboxProgressObj; + } else if (error.name === 'SandboxCreateNotCompleteError') { + process.exitCode = 68; + return this.latestSandboxProgressObj; + } + throw err; + } + } + + private buildSandboxRequestCacheEntry(): SandboxRequestCacheEntry { + let sandboxRequestCacheEntry = { + alias: undefined, + setDefault: undefined, + prodOrgUsername: undefined, + sandboxProcessObject: {}, + sandboxRequest: {}, + } as SandboxRequestCacheEntry; + + let name: string | undefined; + let entry: SandboxRequestCacheEntry | undefined; + + if (this.flags['use-most-recent']) { + [name, entry] = this.sandboxRequestConfig.getLatestEntry() || [undefined, undefined]; + if (!name) { + throw messages.createError('error.LatestSandboxRequestNotFound'); + } + sandboxRequestCacheEntry = entry; + } else if (this.flags.name) { + sandboxRequestCacheEntry = this.sandboxRequestConfig.get(this.flags.name) || sandboxRequestCacheEntry; + } else if (this.flags['job-id']) { + const sce: SandboxRequestCacheEntry = this.sandboxRequestConfig + .entries() + .find( + ([, e]) => (e as SandboxRequestCacheEntry).sandboxProcessObject.Id === this.flags['job-id'] + )[1] as SandboxRequestCacheEntry; + sandboxRequestCacheEntry = sce || sandboxRequestCacheEntry; + } + sandboxRequestCacheEntry.prodOrgUsername ??= this.flags['target-org']?.getUsername(); + sandboxRequestCacheEntry.sandboxProcessObject.SandboxName ??= this.flags.name; + return sandboxRequestCacheEntry; + } + + private async verifyIfAuthExists( + prodOrg: Org, + sandboxName: string, + jobId: string, + lifecycle: Lifecycle + ): Promise { + const sandboxProcessObject: SandboxProcessObject = await this.getSandboxProcessObject(prodOrg, sandboxName, jobId); + const sandboxUsername = `${prodOrg.getUsername()}.${sandboxProcessObject.SandboxName}`; + + if ((await GlobalInfo.getInstance()).orgs.has(sandboxUsername)) { + this.latestSandboxProgressObj = sandboxProcessObject; + const resultEvent = { + sandboxProcessObj: this.latestSandboxProgressObj, + sandboxRes: { authUserName: sandboxUsername } as Partial, + } as ResultEvent; + await lifecycle.emit(SandboxEvents.EVENT_RESULT, resultEvent as Partial); + return true; + } + return false; + } + + private async getSandboxProcessObject( + prodOrg: Org, + sandboxName?: string, + jobId?: string + ): Promise { + const where = sandboxName ? `SandboxName='${sandboxName}'` : `Id='${jobId}'`; + const queryStr = `SELECT Id, Status, SandboxName, SandboxInfoId, LicenseType, CreatedDate, CopyProgress, SandboxOrganization, SourceId, Description, EndDate FROM SandboxProcess WHERE ${where} AND Status != 'D'`; + try { + return await prodOrg.getConnection().singleRecordQuery(queryStr, { + tooling: true, + }); + } catch (err) { + throw messages.createError('error.NoSandboxRequestFound'); + } + } +} diff --git a/src/commands/env/resume/scratch.ts b/src/commands/env/resume/scratch.ts new file mode 100644 index 00000000..48e9566a --- /dev/null +++ b/src/commands/env/resume/scratch.ts @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2020, salesforce.com, inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ + +import { SfCommand, Flags } from '@salesforce/sf-plugins-core'; +import { + Messages, + scratchOrgResume, + ScratchOrgCache, + Lifecycle, + ScratchOrgLifecycleEvent, + scratchOrgLifecycleEventName, +} from '@salesforce/core'; +import { ScratchCreateResponse } from '../../../types'; +import { buildStatus } from '../../../shared/scratchOrgOutput'; + +Messages.importMessagesDirectory(__dirname); +const messages = Messages.loadMessages('@salesforce/plugin-env', 'resume_scratch'); + +export default class EnvResumeScratch extends SfCommand { + public static readonly summary = messages.getMessage('summary'); + public static readonly description = messages.getMessage('description'); + public static readonly examples = messages.getMessages('examples'); + public static flags = { + 'job-id': Flags.salesforceId({ + char: 'i', + summary: messages.getMessage('flags.job-id.summary'), + description: messages.getMessage('flags.job-id.description'), + exactlyOne: ['use-most-recent', 'job-id'], + startsWith: '2SR', + }), + 'use-most-recent': Flags.boolean({ + char: 'r', + summary: messages.getMessage('flags.use-most-recent.summary'), + exactlyOne: ['use-most-recent', 'job-id'], + }), + }; + + public async run(): Promise { + const { flags } = await this.parse(EnvResumeScratch); + const cache = await ScratchOrgCache.create(); + const lifecycle = Lifecycle.getInstance(); + + const jobId = flags['use-most-recent'] ? cache.getLatestKey() : flags['job-id']; + if (!jobId && flags['use-most-recent']) throw messages.createError('error.NoRecentJobId'); + + const { hubBaseUrl } = cache.get(jobId); + let lastStatus: string; + + // eslint-disable-next-line @typescript-eslint/require-await + lifecycle.on(scratchOrgLifecycleEventName, async (data): Promise => { + lastStatus = buildStatus(data, hubBaseUrl); + this.spinner.status = lastStatus; + }); + + this.log(); + this.spinner.start('Creating Scratch Org'); + + const { username, scratchOrgInfo, authFields, warnings } = await scratchOrgResume(jobId); + this.spinner.stop(lastStatus); + + this.log(); + this.logSuccess(messages.getMessage('success')); + return { username, scratchOrgInfo, authFields, warnings, orgId: scratchOrgInfo.Id }; + } +} diff --git a/src/shared/orgHooks.ts b/src/shared/orgHooks.ts new file mode 100644 index 00000000..ea9ae76f --- /dev/null +++ b/src/shared/orgHooks.ts @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2021, salesforce.com, inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ + +import { Optional } from '@salesforce/ts-types'; +import { AuthFields } from '@salesforce/core'; +import { Command, Hook, Hooks } from '@oclif/core/lib/interfaces'; + +type HookOpts = { + options: { Command: Command.Class; argv: string[]; commandId: string }; + return: Optional; +}; + +export type OrgCreateResult = Pick< + AuthFields, + | 'accessToken' + | 'clientId' + | 'created' + | 'createdOrgInstance' + | 'devHubUsername' + | 'expirationDate' + | 'instanceUrl' + | 'loginUrl' + | 'orgId' + | 'username' +>; + +type PostOrgCreateOpts = HookOpts; + +/** + * Extends OCLIF's Hooks interface to add types for hooks that run on org commands + */ +export interface OrgHooks extends Hooks { + postorgcreate: PostOrgCreateOpts; +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type OrgHook = (this: Hook.Context, options: T extends keyof Hooks ? OrgHooks[T] : T) => any; + +// eslint-disable-next-line no-redeclare +export declare namespace OrgHook { + // TODO get rid of the ts-ignore + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + export type PostOrgCreate = Hook; +} diff --git a/src/shared/sandboxCommandBase.ts b/src/shared/sandboxCommandBase.ts new file mode 100644 index 00000000..9f18054a --- /dev/null +++ b/src/shared/sandboxCommandBase.ts @@ -0,0 +1,181 @@ +/* + * Copyright (c) 2020, salesforce.com, inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ +import * as os from 'os'; +import { SfCommand } from '@salesforce/sf-plugins-core'; +import { Config } from '@oclif/core'; +import { + AuthInfo, + Lifecycle, + Messages, + Org, + ResultEvent, + SandboxEvents, + SandboxProcessObject, + SandboxRequestCache, + SandboxRequestCacheEntry, + SandboxUserAuthResponse, + StatusEvent, +} from '@salesforce/core'; +import { SandboxProgress, SandboxStatusData } from './sandboxProgress'; +import { State } from './stagedProgress'; + +Messages.importMessagesDirectory(__dirname); +const messages = Messages.loadMessages('@salesforce/plugin-env', 'sandboxbase'); +export abstract class SandboxCommandBase extends SfCommand { + protected sandboxProgress: SandboxProgress; + protected latestSandboxProgressObj: SandboxProcessObject; + protected sandboxAuth?: SandboxUserAuthResponse; + protected prodOrg: Org; + protected pollingTimeOut = false; + protected sandboxRequestConfig: SandboxRequestCache; + protected sandboxRequestData: SandboxRequestCacheEntry = { + alias: '', + setDefault: false, + prodOrgUsername: '', + sandboxProcessObject: {}, + sandboxRequest: {}, + }; + public constructor(argv: string[], config: Config) { + super(argv, config); + this.sandboxProgress = new SandboxProgress(); + } + protected async getSandboxRequestConfig(): Promise { + if (!this.sandboxRequestConfig) { + this.sandboxRequestConfig = await SandboxRequestCache.create(); + } + return this.sandboxRequestConfig; + } + + protected registerLifecycleListeners( + lifecycle: Lifecycle, + options: { isAsync: boolean; alias: string; setDefault: boolean; prodOrg?: Org } + ): void { + // eslint-disable-next-line @typescript-eslint/require-await + lifecycle.on('POLLING_TIME_OUT', async () => { + this.pollingTimeOut = true; + this.updateSandboxRequestData(); + }); + + // eslint-disable-next-line @typescript-eslint/require-await + lifecycle.on(SandboxEvents.EVENT_RESUME, async (results: SandboxProcessObject) => { + this.latestSandboxProgressObj = results; + this.sandboxProgress.markPreviousStagesAsCompleted( + results.Status !== 'Completed' ? results.Status : 'Authenticating' + ); + this.updateSandboxRequestData(); + }); + + // eslint-disable-next-line @typescript-eslint/require-await + lifecycle.on(SandboxEvents.EVENT_ASYNC_RESULT, async (results?: SandboxProcessObject) => { + this.latestSandboxProgressObj = results || this.latestSandboxProgressObj; + this.updateSandboxRequestData(); + if (!options.isAsync) { + this.spinner.stop(); + } + const progress = this.sandboxProgress.getSandboxProgress({ + sandboxProcessObj: this.latestSandboxProgressObj, + sandboxRes: undefined, + }); + const currentStage = progress.status; + this.sandboxProgress.markPreviousStagesAsCompleted(currentStage); + this.updateStage(currentStage, State.inProgress); + this.updateProgress({ sandboxProcessObj: this.latestSandboxProgressObj, sandboxRes: undefined }, options.isAsync); + if (this.pollingTimeOut) { + this.warn(messages.getMessage('warning.ClientTimeoutWaitingForSandboxCreate')); + } + this.log(this.sandboxProgress.formatProgressStatus(false)); + this.info(messages.getMessage('checkSandboxStatus', this.getCheckSandboxStatusParams())); + }); + + // eslint-disable-next-line @typescript-eslint/require-await + lifecycle.on(SandboxEvents.EVENT_STATUS, async (results: StatusEvent) => { + this.latestSandboxProgressObj = results.sandboxProcessObj; + this.updateSandboxRequestData(); + const progress = this.sandboxProgress.getSandboxProgress(results); + const currentStage = progress.status; + this.updateStage(currentStage, State.inProgress); + this.updateProgress(results, options.isAsync); + }); + + // eslint-disable-next-line @typescript-eslint/require-await + lifecycle.on(SandboxEvents.EVENT_AUTH, async (results: SandboxUserAuthResponse) => { + this.sandboxAuth = results; + }); + + lifecycle.on(SandboxEvents.EVENT_RESULT, async (results: ResultEvent) => { + this.latestSandboxProgressObj = results.sandboxProcessObj; + this.updateSandboxRequestData(); + this.sandboxProgress.markPreviousStagesAsCompleted(); + this.updateProgress(results, options.isAsync); + if (!options.isAsync) { + this.progress.stop(); + } + if (results.sandboxRes?.authUserName) { + const authInfo = await AuthInfo.create({ username: results.sandboxRes?.authUserName }); + await authInfo.handleAliasAndDefaultSettings({ + alias: options.alias, + setDefault: options.setDefault, + setDefaultDevHub: undefined, + }); + } + this.removeSandboxProgressConfig(); + this.updateProgress(results, options.isAsync); + this.reportResults(results); + }); + } + + protected reportResults(results: ResultEvent): void { + this.log(); + this.styledHeader('Sandbox Org Creation Status'); + this.log(this.sandboxProgress.formatProgressStatus(false)); + this.logSuccess( + [ + messages.getMessage('sandboxSuccess'), + messages.getMessages('sandboxSuccess.actions', [ + results.sandboxRes?.authUserName, + results.sandboxRes?.authUserName, + ]), + ].join(os.EOL) + ); + } + + protected updateProgress(event: ResultEvent | StatusEvent, isAsync: boolean): void { + const sandboxProgress = this.sandboxProgress.getSandboxProgress(event); + const sandboxData = { + sandboxUsername: (event as ResultEvent).sandboxRes?.authUserName, + sandboxProgress, + sandboxProcessObj: event.sandboxProcessObj, + } as SandboxStatusData; + this.sandboxProgress.statusData = sandboxData; + if (!isAsync) { + this.spinner.status = this.sandboxProgress.formatProgressStatus(); + } + } + + protected updateStage(stage: string | undefined, state: State): void { + if (stage) { + this.sandboxProgress.transitionStages(stage, state); + } + } + + protected updateSandboxRequestData(): void { + this.sandboxRequestData.sandboxProcessObject = this.latestSandboxProgressObj; + this.saveSandboxProgressConfig(); + } + + protected saveSandboxProgressConfig(): void { + this.sandboxRequestConfig.set(this.sandboxRequestData.sandboxProcessObject.SandboxName, this.sandboxRequestData); + this.sandboxRequestConfig.writeSync(); + } + + private removeSandboxProgressConfig(): void { + this.sandboxRequestConfig.unset(this.latestSandboxProgressObj.SandboxName); + this.sandboxRequestConfig.writeSync(); + } + + protected abstract getCheckSandboxStatusParams(): string[]; +} diff --git a/src/shared/sandboxProgress.ts b/src/shared/sandboxProgress.ts new file mode 100644 index 00000000..20cc39e6 --- /dev/null +++ b/src/shared/sandboxProgress.ts @@ -0,0 +1,121 @@ +/* + * Copyright (c) 2020, salesforce.com, inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ +import * as os from 'os'; +import { StatusEvent, ResultEvent, SandboxProcessObject } from '@salesforce/core'; +import { Ux } from '@salesforce/sf-plugins-core/lib/ux'; +import { CliUx } from '@oclif/core'; +import { getClockForSeconds } from '../utils/timeUtils'; +import { StagedProgress } from './stagedProgress'; + +const columns: Ux.Table.Columns<{ key: string; value: string }> = { + key: { header: 'Field' }, + value: { header: 'Value' }, +}; + +export type SandboxProgressData = { + id: string; + status: string; + percentComplete: number; + remainingWaitTime: number; + remainingWaitTimeHuman: string; +}; + +export type SandboxStatusData = { + sandboxUsername: string; + sandboxProgress: SandboxProgressData; + sandboxProcessObj?: SandboxProcessObject | undefined; +}; + +export class SandboxProgress extends StagedProgress { + public constructor(stageNames: string[] = ['Pending', 'Processing', 'Activating', 'Authenticating']) { + super(stageNames); + } + public getLogSandboxProcessResult(result: ResultEvent): string { + const { sandboxProcessObj } = result; + const sandboxReadyForUse = `Sandbox ${sandboxProcessObj.SandboxName}(${sandboxProcessObj.Id}) is ready for use.`; + return sandboxReadyForUse; + } + + public getTableDataFromProcessObj( + authUserName: string, + sandboxProcessObj: SandboxProcessObject + ): Array<{ key: string; value: string | number }> { + return [ + { key: 'Id', value: sandboxProcessObj.Id }, + { key: 'SandboxName', value: sandboxProcessObj.SandboxName }, + { key: 'Status', value: sandboxProcessObj.Status }, + { key: 'CopyProgress', value: `${sandboxProcessObj.CopyProgress}%` }, + { key: 'Description', value: sandboxProcessObj.Description }, + { key: 'LicenseType', value: sandboxProcessObj.LicenseType }, + { key: 'SandboxInfoId', value: sandboxProcessObj.SandboxInfoId }, + { key: 'SourceId', value: sandboxProcessObj.SourceId }, + { key: 'SandboxOrg', value: sandboxProcessObj.SandboxOrganization }, + { key: 'Created Date', value: sandboxProcessObj.CreatedDate }, + { key: 'ApexClassId', value: sandboxProcessObj.ApexClassId }, + { key: 'Authorized Sandbox Username', value: authUserName }, + ].filter((v) => !!v.value); + } + + public getSandboxProgress(event: StatusEvent | ResultEvent): SandboxProgressData { + const statusUpdate = event as StatusEvent; + const waitingOnAuth = statusUpdate.waitingOnAuth ?? false; + const { sandboxProcessObj } = event; + const waitTimeInSec = statusUpdate.remainingWait ?? 0; + + const sandboxIdentifierMsg = `${sandboxProcessObj.SandboxName}(${sandboxProcessObj.Id})`; + + return { + id: sandboxIdentifierMsg, + status: waitingOnAuth || sandboxProcessObj.Status === 'Completed' ? 'Authenticating' : sandboxProcessObj.Status, + percentComplete: sandboxProcessObj.CopyProgress, + remainingWaitTime: waitTimeInSec, + remainingWaitTimeHuman: waitTimeInSec === 0 ? '' : `${getClockForSeconds(waitTimeInSec)} until timeout.`, + }; + } + + public getSandboxTableAsText(sandboxUsername: string, sandboxProgress?: SandboxProcessObject): string[] { + if (!sandboxProgress) { + return []; + } + const tableRows: string[] = []; + CliUx.ux.table(this.getTableDataFromProcessObj(sandboxUsername, sandboxProgress), columns, { + printLine: (s: string): void => { + tableRows.push(s); + }, + }); + return tableRows; + } + + public formatProgressStatus(withClock = true): string { + const table = this.getSandboxTableAsText(undefined, this.statusData.sandboxProcessObj).join(os.EOL); + return [ + withClock + ? `${getClockForSeconds(this.statusData.sandboxProgress.remainingWaitTime)} until timeout. ${ + this.statusData.sandboxProgress.percentComplete + }%` + : undefined, + table, + '---------------------', + 'Sandbox Create Stages', + this.formatStages(), + ] + .filter((line) => line) + .join(os.EOL); + } + protected mapCurrentStage(currentStage: string): string { + switch (currentStage) { + case 'Pending Remote Creation': + return 'Pending'; + case 'Remote Sandbox Created': + return 'Pending'; + case 'Completed': + return 'Authenticating'; + default: + return currentStage; + } + } +} diff --git a/src/scratchOrgOutput.ts b/src/shared/scratchOrgOutput.ts similarity index 91% rename from src/scratchOrgOutput.ts rename to src/shared/scratchOrgOutput.ts index 36e393d7..a26df50b 100644 --- a/src/scratchOrgOutput.ts +++ b/src/shared/scratchOrgOutput.ts @@ -7,6 +7,7 @@ import { ScratchOrgLifecycleEvent, scratchOrgLifecycleStages } from '@salesforce/core'; import * as chalk from 'chalk'; import { capitalCase } from 'change-case'; +import { StandardColors } from '@salesforce/sf-plugins-core'; const boldBlue = (input: string): string => chalk.rgb(81, 176, 235).bold(input); const boldPurple = (input: string): string => chalk.rgb(157, 129, 221).bold(input); @@ -39,8 +40,8 @@ export const formatCurrentStage = (stage: string): string => { return boldPurple(capitalCase(stage)); }; export const formatCompletedStage = (stage: string): string => { - return chalk.bold.green(`✓ ${capitalCase(stage)}`); + return StandardColors.success.bold(`✓ ${capitalCase(stage)}`); }; export const formatFutureStage = (stage: string): string => { - return chalk.dim(capitalCase(stage)); + return StandardColors.info(capitalCase(stage)); }; diff --git a/src/shared/stagedProgress.ts b/src/shared/stagedProgress.ts new file mode 100644 index 00000000..1c348eda --- /dev/null +++ b/src/shared/stagedProgress.ts @@ -0,0 +1,128 @@ +/* + * Copyright (c) 2020, salesforce.com, inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ +import * as os from 'os'; +import * as chalk from 'chalk'; +import { StandardColors } from '@salesforce/sf-plugins-core'; +const compareStages = ([, aValue], [, bValue]): number => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + return aValue.index - bValue.index; +}; + +export const boldPurple = chalk.rgb(157, 129, 221).bold; + +export enum State { + 'inProgress' = 'inProgress', + 'completed' = 'completed', + 'failed' = 'failed', + 'unknown' = 'unknown', +} + +export type StageAttributes = { + state: State; + char: string; + color: chalk.Chalk; + index?: number; + visited: boolean; +}; + +export const StateConstants: { [stage: string]: StageAttributes } = { + inProgress: { color: boldPurple, char: '…', visited: false, state: State.inProgress }, + completed: { color: StandardColors.success, char: '✓', visited: false, state: State.completed }, + failed: { color: chalk.bold.red, char: '✖', visited: false, state: State.failed }, + unknown: { color: chalk.dim, char: '…', visited: false, state: State.unknown }, +}; + +export type Stage = { + [stage: string]: StageAttributes; +}; + +export abstract class StagedProgress { + private dataForTheStatus: T; + private theStages: Stage; + private currentStage: string; + private previousStage: string; + public constructor(stages: string[]) { + this.theStages = stages + .map((stage, index) => { + return { + [stage]: { ...StateConstants[State.unknown], index: (index + 1) * 10 }, + }; + }) + .reduce((m, b) => Object.assign(m, b), {} as Stage); + } + + public get statusData(): T { + return this.dataForTheStatus; + } + public set statusData(statusData: T) { + this.dataForTheStatus = statusData; + } + public formatStages(): string { + return Object.entries(this.theStages) + .sort(compareStages) + .map(([stage, stageState]) => { + return stageState.color(`${stageState.char} - ${stage}`); + }) + .join(os.EOL); + } + + public transitionStages(currentStage: string, newState?: State): void { + currentStage = this.mapCurrentStage(currentStage); + if (this.previousStage && this.previousStage !== currentStage) { + this.updateStages(this.previousStage, State.completed); + } + + // mark all previous stages as visited and completed + this.markPreviousStagesAsCompleted(currentStage); + + this.previousStage = currentStage; + this.currentStage = currentStage; + this.updateStages(currentStage, newState); + } + + public markPreviousStagesAsCompleted(currentStage?: string): void { + currentStage = this.mapCurrentStage(currentStage); + Object.entries(this.theStages).forEach(([stage, stageState]) => { + if (!currentStage || stageState.index < this.theStages[currentStage].index) { + this.updateStages(stage, State.completed); + } + }); + } + + public updateCurrentStage(newState: State): void { + this.updateStages(this.currentStage, newState); + } + + public updateStages(currentStage: string, newState?: State): void { + currentStage = this.mapCurrentStage(currentStage); + if (!this.theStages[currentStage]) { + const sortedEntries = Object.entries(this.theStages).sort(compareStages); + const visitedEntries = sortedEntries.filter(([, stageState]) => stageState.visited); + const [, lastState] = visitedEntries.length + ? visitedEntries[visitedEntries.length - 1] + : ['', { state: StateConstants.unknown.state, index: 0, visited: true }]; + const newEntry = { + [currentStage]: { state: StateConstants.unknown.state, visited: true, index: lastState.index + 1 }, + }; + this.theStages = Object.assign(this.theStages, newEntry); + } + this.theStages[currentStage].visited = true; + this.theStages[currentStage].state = newState || State.inProgress; + this.theStages[currentStage].char = StateConstants[this.theStages[currentStage].state].char; + this.theStages[currentStage].color = StateConstants[newState.toString()].color; + } + + public getStages(): Stage { + return this.theStages; + } + + protected mapCurrentStage(currentStage: string): string { + return currentStage; + } + + public abstract formatProgressStatus(withClock: boolean): string; +} diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 00000000..dfc9fd3a --- /dev/null +++ b/src/types.ts @@ -0,0 +1,16 @@ +/* + * Copyright (c) 2020, salesforce.com, inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ + +import { ScratchOrgInfo, AuthFields } from '@salesforce/core'; + +export interface ScratchCreateResponse { + username?: string; + scratchOrgInfo: ScratchOrgInfo; + authFields?: AuthFields; + warnings: string[]; + orgId: string; +} diff --git a/src/utils/timeUtils.ts b/src/utils/timeUtils.ts new file mode 100644 index 00000000..72bd0815 --- /dev/null +++ b/src/utils/timeUtils.ts @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2022, salesforce.com, inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ +import { Duration } from '@salesforce/kit'; + +export type TimeComponents = { + days: Duration; + hours: Duration; + minutes: Duration; + seconds: Duration; +}; + +export const getClockForSeconds = (timeInSec: number): string => { + const tc = getTimeComponentsFromSeconds(timeInSec); + + const dDisplay: string = tc.days.days > 0 ? `${tc.days.days.toString()}:` : ''; + const hDisplay: string = tc.hours.hours.toString().padStart(2, '0'); + const mDisplay: string = tc.minutes.minutes.toString().padStart(2, '0'); + const sDisplay: string = tc.seconds.seconds.toString().padStart(2, '0'); + + return `${dDisplay}${hDisplay}:${mDisplay}:${sDisplay}`; +}; +export const getTimeComponentsFromSeconds = (timeInSec: number): TimeComponents => { + const days = Duration.days(Math.floor(timeInSec / 86_400)); + const hours = Duration.hours(Math.floor((timeInSec % 86_400) / 3_600)); + const minutes = Duration.minutes(Math.floor((timeInSec % 3_600) / 60)); + const seconds = Duration.seconds(Math.floor(timeInSec % 60)); + + return { days, hours, minutes, seconds }; +}; +export const getSecondsToHuman = (timeInSec: number): string => { + const tc = getTimeComponentsFromSeconds(timeInSec); + + const dDisplay: string = tc.days.days > 0 ? tc.days.toString() + ' ' : ''; + const hDisplay: string = tc.hours.hours > 0 ? tc.hours.toString() + ' ' : ''; + const mDisplay: string = tc.minutes.minutes > 0 ? tc.minutes.toString() + ' ' : ''; + const sDisplay: string = tc.seconds.seconds > 0 ? tc.seconds.toString() : ''; + + return (dDisplay + hDisplay + mDisplay + sDisplay).trim(); +}; diff --git a/test/commands/env/create/async-create-resume.nut.ts b/test/commands/env/create/async-create-resume.nut.ts new file mode 100644 index 00000000..157d2070 --- /dev/null +++ b/test/commands/env/create/async-create-resume.nut.ts @@ -0,0 +1,129 @@ +/* + * Copyright (c) 2020, salesforce.com, inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ + +import * as fs from 'fs'; +import * as path from 'path'; +import { execCmd, TestSession } from '@salesforce/cli-plugins-testkit'; +import { expect } from 'chai'; +import { ScratchOrgCache } from '@salesforce/core'; +import { JsonMap } from '@salesforce/ts-types'; +import { CachedOptions } from '@salesforce/core/lib/org/scratchOrgCache'; +import { ScratchCreateResponse } from '../../../../src/types'; + +describe('env create scratch async/resume', () => { + let session: TestSession; + let cacheFilePath: string; + let sfJsonPath: string; + let soiId: string; + let username: string; + + const asyncKeys = ['username', 'orgId', 'scratchOrgInfo', 'warnings']; + const completeKeys = [...asyncKeys, 'authFields']; + + const readCacheFile = async (): Promise> => { + return JSON.parse(await fs.promises.readFile(cacheFilePath, 'utf8')) as unknown as Record; + }; + + before(async () => { + session = await TestSession.create({ + project: { + name: 'testProject', + }, + }); + cacheFilePath = path.join(session.dir, '.sf', ScratchOrgCache.getFileName()); + sfJsonPath = path.join(session.dir, '.sf', 'sf.json'); + }); + + after(async () => { + await session?.clean(); + }); + + describe('just edition', () => { + it('requests org', () => { + const resp = execCmd('env create scratch --edition developer --json --async', { + ensureExitCode: 0, + }).jsonOutput.result; + expect(resp).to.have.all.keys(asyncKeys); + soiId = resp.scratchOrgInfo.Id; + username = resp.username; + }); + it('is present in cache', async () => { + expect(fs.existsSync(cacheFilePath)).to.be.true; + const cache = await readCacheFile(); + expect(cache[soiId]).to.include.keys(['hubBaseUrl', 'definitionjson', 'hubUsername']); + expect(cache[soiId].definitionjson).to.deep.equal({ edition: 'developer' }); + }); + it('resumes org using id', () => { + const resp = execCmd(`env resume scratch --job-id ${soiId} --json`, { + ensureExitCode: 0, + }).jsonOutput.result; + expect(resp).to.have.all.keys(completeKeys); + }); + it('org is authenticated', async () => { + const sfJson = JSON.parse(await fs.promises.readFile(sfJsonPath, 'utf8')) as unknown as { orgs: JsonMap }; + expect(sfJson.orgs[username]).to.include.keys(['orgId', 'devHubUsername', 'accessToken']); + }); + it('is NOT present in cache', async () => { + const cache = await readCacheFile(); + expect(cache).to.not.have.property(soiId); + }); + }); + + describe('alias, set-default, username, config file, use-most-recent', () => { + const testAlias = 'testAlias'; + it('requests org', () => { + const resp = execCmd( + `env create scratch --json --async -f ${path.join( + 'config', + 'project-scratch-def.json' + )} --set-default --alias ${testAlias}`, + { + ensureExitCode: 0, + } + ).jsonOutput.result; + expect(resp).to.have.all.keys(asyncKeys); + soiId = resp.scratchOrgInfo.Id; + username = resp.username; + }); + it('is present in cache', async () => { + expect(fs.existsSync(cacheFilePath)).to.be.true; + const cache = await readCacheFile(); + + expect(cache[soiId]).to.include.keys(['hubBaseUrl', 'definitionjson', 'hubUsername']); + expect(cache[soiId]).to.have.property('setDefault', true); + expect(cache[soiId].definitionjson).to.deep.equal( + JSON.parse( + await fs.promises.readFile(path.join(session.project.dir, 'config', 'project-scratch-def.json'), 'utf8') + ) as unknown as JsonMap + ); + }); + it('resumes org using latest', () => { + const resp = execCmd('env resume scratch --use-most-recent --json', { + ensureExitCode: 0, + }).jsonOutput.result; + expect(resp).to.have.all.keys(completeKeys); + }); + it('org is authenticated with alias and config', async () => { + const sfJson = JSON.parse(await fs.promises.readFile(sfJsonPath, 'utf8')) as unknown as { + orgs: JsonMap; + aliases: JsonMap; + }; + expect(sfJson.orgs[username]).to.include.keys(['orgId', 'devHubUsername', 'accessToken']); + expect(sfJson.aliases[testAlias]).to.equal(username); + + const config = JSON.parse( + await fs.promises.readFile(path.join(session.project.dir, '.sf', 'config.json'), 'utf8') + ) as unknown as Record; + + expect(config['target-org']).to.equal(testAlias); + }); + it('is NOT present in cache', async () => { + const cache = await readCacheFile(); + expect(cache).to.not.have.property(soiId); + }); + }); +}); diff --git a/test/commands/env/create/createSandbox.test.ts b/test/commands/env/create/createSandbox.test.ts new file mode 100644 index 00000000..614d3940 --- /dev/null +++ b/test/commands/env/create/createSandbox.test.ts @@ -0,0 +1,146 @@ +/* + * Copyright (c) 2020, salesforce.com, inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ +import { + GlobalInfo, + Lifecycle, + Org, + SandboxEvents, + SandboxProcessObject, + SfInfo, + SfInfoKeys, + SfOrg, + SfOrgs, + SfProject, +} from '@salesforce/core'; +import { Spinner } from '@salesforce/sf-plugins-core/lib/ux'; +import { Config as IConfig } from '@oclif/core/lib/interfaces'; +import { fromStub, stubInterface, stubMethod } from '@salesforce/ts-sinon'; +import * as sinon from 'sinon'; +import { expect } from 'chai'; +import { Ux } from '@salesforce/sf-plugins-core/lib/ux'; +import CreateSandbox from '../../../../src/commands/env/create/sandbox'; +import { SandboxProgress } from '../../../../lib/shared/sandboxProgress'; + +const sandboxProcessObj: SandboxProcessObject = { + Id: '0GR4p000000U8EMXXX', + Status: 'Completed', + SandboxName: 'TestSandbox', + SandboxInfoId: '0GQ4p000000U6sKXXX', + LicenseType: 'DEVELOPER', + CreatedDate: '2021-12-07T16:20:21.000+0000', + CopyProgress: 100, + SandboxOrganization: '00D2f0000008XXX', + SourceId: '123', + Description: 'sandbox description', + ApexClassId: '123', + EndDate: '2021-12-07T16:38:47.000+0000', +}; + +const fakeOrg: SfOrg = { + orgId: '00Dsomefakeorg1', + instanceUrl: 'https://some.fake.org', + username: 'somefake.org', +}; + +const fakeSfInfo: SfInfo = { + [SfInfoKeys.ORGS]: { [fakeOrg.username]: fakeOrg } as SfOrgs, + [SfInfoKeys.TOKENS]: {}, + [SfInfoKeys.ALIASES]: { testProdOrg: fakeOrg.username }, + [SfInfoKeys.SANDBOXES]: {}, +}; + +describe('env:create:sandbox', () => { + beforeEach(() => { + GlobalInfo.clearInstance(); + stubMethod(sandbox, GlobalInfo.prototype, 'read').callsFake(async (): Promise => { + return fakeSfInfo; + }); + stubMethod(sandbox, GlobalInfo.prototype, 'write').callsFake(async (): Promise => { + return fakeSfInfo; + }); + }); + + const sandbox = sinon.createSandbox(); + const oclifConfigStub = fromStub(stubInterface(sandbox)); + // stubs + let resolveProjectConfigStub: sinon.SinonStub; + let createSandboxStub: sinon.SinonStub; + let uxLogStub: sinon.SinonStub; + let cmd: TestCreate; + + class TestCreate extends CreateSandbox { + public async runIt() { + await this.init(); + return this.run(); + } + public setProject(project: SfProject) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + this.project = project; + } + } + + const createCommand = async (params: string[]) => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + cmd = new TestCreate(params, oclifConfigStub); + stubMethod(sandbox, cmd, 'assignProject').callsFake(() => { + const sfProjectStub = fromStub( + stubInterface(sandbox, { + resolveProjectConfig: resolveProjectConfigStub, + }) + ); + cmd.setProject(sfProjectStub); + }); + + stubMethod(sandbox, TestCreate.prototype, 'warn'); + uxLogStub = stubMethod(sandbox, TestCreate.prototype, 'log'); + stubMethod(sandbox, Ux.prototype, 'styledHeader'); + stubMethod(sandbox, TestCreate.prototype, 'table'); + stubMethod(sandbox, SandboxProgress.prototype, 'getSandboxProgress'); + stubMethod(sandbox, SandboxProgress.prototype, 'formatProgressStatus'); + stubMethod(sandbox, Spinner.prototype, 'start'); + stubMethod(sandbox, Spinner.prototype, 'stop'); + stubMethod(sandbox, Spinner.prototype, 'status'); + return cmd; + }; + + describe('sandbox', () => { + it('will print the correct message for asyncResult lifecycle event', async () => { + const command = await createCommand(['-o', 'testProdOrg', '--name', 'mysandboxx', '--no-prompt']); + + stubMethod(sandbox, cmd, 'readJsonDefFile').returns({ + licenseType: 'licenseFromJon', + }); + stubMethod(sandbox, Org, 'create').resolves(Org.prototype); + stubMethod(sandbox, Org.prototype, 'getUsername').returns('testProdOrg'); + const createStub = stubMethod(sandbox, Org.prototype, 'createSandbox').callsFake(async () => { + return (async () => {})().catch(); + }); + + await command.runIt(); + + // no SandboxName defined, so we should generate one that starts with sbx + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + expect(createStub.firstCall.args[0].SandboxName).includes('mysandboxx'); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + expect(createStub.firstCall.args[0].SandboxName.length).equals(10); + + Lifecycle.getInstance().on(SandboxEvents.EVENT_ASYNC_RESULT, async (result) => { + expect(result).to.deep.equal(sandboxProcessObj); + expect(uxLogStub.firstCall.args[0]).to.includes('0GR4p000000U8EMXXX'); + }); + + await Lifecycle.getInstance().emit(SandboxEvents.EVENT_ASYNC_RESULT, sandboxProcessObj); + }); + }); + + afterEach(() => { + sandbox.restore(); + createSandboxStub?.restore(); + }); +}); diff --git a/test/commands/env/create/sandbox.sandboxNut.ts b/test/commands/env/create/sandbox.sandboxNut.ts new file mode 100644 index 00000000..633eea4e --- /dev/null +++ b/test/commands/env/create/sandbox.sandboxNut.ts @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2022, salesforce.com, inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ +import { execCmd, TestSession } from '@salesforce/cli-plugins-testkit'; +import { Lifecycle, Messages, SandboxEvents, SandboxProcessObject, StatusEvent } from '@salesforce/core'; +import { expect } from 'chai'; + +Messages.importMessagesDirectory(__dirname); + +describe('Sandbox Orgs', () => { + let session: TestSession; + + before(async () => { + session = await TestSession.create({ + project: { name: 'sandboxCreate' }, + }); + }); + + it('will create a sandbox, verify it can be opened, and then attempt to delete it', async () => { + let result: SandboxProcessObject; + try { + Lifecycle.getInstance().on(SandboxEvents.EVENT_STATUS, async (results: StatusEvent) => { + // eslint-disable-next-line no-console + console.log('sandbox copy progress', results.sandboxProcessObj.CopyProgress); + }); + let rawResult = execCmd( + `env:create:sandbox -a mySandbox -s -l Developer -o ${process.env.TESTKIT_HUB_USERNAME} --no-prompt --json --async`, + { timeout: 3600000 } + ); + result = rawResult.jsonOutput.result as SandboxProcessObject; + // autogenerated sandbox names start with 'sbx' + expect(result).to.be.ok; + expect(result.SandboxName.startsWith('sbx'), 'env:create:sandbox').to.be.true; + rawResult = execCmd( + `env:resume:sandbox --name ${result.SandboxName} -o ${process.env.TESTKIT_HUB_USERNAME} -w 60 --json`, + { timeout: 3600000 } + ); + result = rawResult.jsonOutput.result as SandboxProcessObject; + expect(result).to.be.ok; + } catch (e) { + expect(false).to.be.true(JSON.stringify(e)); + } + + const sandboxUsername = `${process.env.TESTKIT_HUB_USERNAME}.${result.SandboxName}`; + // even if a DNS issue occurred, the sandbox should still be present and available. + const openResult = execCmd<{ url: string }>('env:open -e mySandbox --url-only --json', { + ensureExitCode: 0, + }).jsonOutput.result; + expect(openResult, 'env:open').to.be.ok; + expect(openResult.url, 'env:open').to.ok; + + const deleteResult = execCmd<{ username: string }>('env:delete:sandbox --target-org mySandbox --no-prompt --json', { + ensureExitCode: 0, + }).jsonOutput.result; + expect(deleteResult, 'env:delete:sandbox').to.be.ok; + expect(deleteResult.username, 'env:delete:sandbox').to.equal(sandboxUsername); + }); + + after(async () => { + try { + await session?.clean(); + } catch (e) { + // do nothing + } + }); +}); diff --git a/test/commands/env/create/scratch.nut.ts b/test/commands/env/create/scratch.nut.ts index 9fe578d9..64293870 100644 --- a/test/commands/env/create/scratch.nut.ts +++ b/test/commands/env/create/scratch.nut.ts @@ -9,7 +9,8 @@ import * as path from 'path'; import { execCmd, TestSession } from '@salesforce/cli-plugins-testkit'; import { expect } from 'chai'; import { Messages } from '@salesforce/core'; -import { ScratchCreateResponse, secretTimeout } from '../../../../src/commands/env/create/scratch'; +import { secretTimeout } from '../../../../src/commands/env/create/scratch'; +import { ScratchCreateResponse } from '../../../../src/types'; Messages.importMessagesDirectory(__dirname); const messages = Messages.load('@salesforce/plugin-env', 'create_scratch', ['prompt.secret']); diff --git a/test/commands/env/create/scratchOrgOutput.test.ts b/test/commands/env/create/scratchOrgOutput.test.ts index 6b989820..14a3102b 100644 --- a/test/commands/env/create/scratchOrgOutput.test.ts +++ b/test/commands/env/create/scratchOrgOutput.test.ts @@ -15,7 +15,7 @@ import { formatRequest, formatStage, formatUsername, -} from '../../../../src/scratchOrgOutput'; +} from '../../../../src/shared/scratchOrgOutput'; describe('human output', () => { describe('stage formatter', () => { diff --git a/test/commands/env/delete/scratch.nut.ts b/test/commands/env/delete/scratch.nut.ts index 45a84e1c..8737ac36 100644 --- a/test/commands/env/delete/scratch.nut.ts +++ b/test/commands/env/delete/scratch.nut.ts @@ -29,12 +29,12 @@ describe('env delete scratch NUTs', () => { }); after(async () => { - // clean restores sinon, but will throw when it tries to delete the alread-deleted orgs. + // clean restores sinon, but will throw when it tries to delete the already-deleted orgs. // so catch that and delete the dir manually try { - session?.clean(); + await session?.clean(); } catch { - await fs.promises.rmdir(session.dir, { recursive: true }); + await fs.promises.rmdir(session.dir, { recursive: true }).catch(() => {}); } }); diff --git a/test/shared/sandboxProgress.test.ts b/test/shared/sandboxProgress.test.ts new file mode 100644 index 00000000..e1d4240a --- /dev/null +++ b/test/shared/sandboxProgress.test.ts @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2020, salesforce.com, inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ +import { expect } from 'chai'; +import { Duration } from '@salesforce/cli-plugins-testkit'; +import { SandboxProcessObject, StatusEvent } from '@salesforce/core'; +import { SandboxProgress } from '../../src/shared/sandboxProgress'; + +const sandboxProcessObj: SandboxProcessObject = { + Id: '0GR4p000000U8EMXXX', + Status: 'Completed', + SandboxName: 'TestSandbox', + SandboxInfoId: '0GQ4p000000U6sKXXX', + LicenseType: 'DEVELOPER', + CreatedDate: '2021-12-07T16:20:21.000+0000', + CopyProgress: 100, + SandboxOrganization: '00D2f0000008XXX', + SourceId: '123', + Description: 'sandbox description', + ApexClassId: '123', + EndDate: '2021-12-07T16:38:47.000+0000', +}; + +describe('sandbox progress', () => { + let sandboxProgress: SandboxProgress; + beforeEach(() => { + sandboxProgress = new SandboxProgress(); + }); + describe('getSandboxProgress', () => { + it('will calculate the correct human readable message (1h 33min 00seconds seconds left)', async () => { + const data: StatusEvent = { + // 186*30 = 5580 = 1 hour, 33 min, 0 seconds. so 186 attempts left, at a 30 second polling interval + sandboxProcessObj, + interval: 30, + remainingWait: Duration.minutes(93).seconds, + waitingOnAuth: false, + }; + const res = sandboxProgress.getSandboxProgress(data); + expect(res).to.have.property('id', 'TestSandbox(0GR4p000000U8EMXXX)'); + expect(res).to.have.property('status', 'Authenticating'); + expect(res).to.have.property('percentComplete', 100); + expect(res).to.have.property('remainingWaitTimeHuman', '01:33:00 until timeout.'); + }); + + it('will calculate the correct human readable message (5 min 30seconds seconds left)', async () => { + const data: StatusEvent = { + sandboxProcessObj, + interval: 30, + remainingWait: Duration.minutes(5).seconds + Duration.seconds(30).seconds, + waitingOnAuth: false, + }; + const res = sandboxProgress.getSandboxProgress(data); + expect(res).to.have.property('id', 'TestSandbox(0GR4p000000U8EMXXX)'); + expect(res).to.have.property('status', 'Authenticating'); + expect(res).to.have.property('percentComplete', 100); + expect(res).to.have.property('remainingWaitTimeHuman', '00:05:30 until timeout.'); + }); + }); + + describe('getTableDataFromProcessObj', () => { + it('getTableDataFromProcessObj should work', () => { + const tableData = sandboxProgress.getTableDataFromProcessObj('admin@prod.org.sandbox', sandboxProcessObj); + expect(tableData.find((r) => r.key === 'Authorized Sandbox Username')).to.have.property( + 'value', + 'admin@prod.org.sandbox' + ); + expect(tableData.find((r) => r.key === 'SandboxInfoId')).to.have.property('value', '0GQ4p000000U6sKXXX'); + }); + }); + describe('getSandboxTableAsText', () => { + it('getSandboxTableAsText should work', () => { + const tableData = sandboxProgress.getSandboxTableAsText('admin@prod.org.sandbox', sandboxProcessObj); + expect(tableData.find((r) => r.includes('Authorized Sandbox Username') && r.includes('admin@prod.org.sandbox'))) + .to.be.ok; + }); + }); +}); diff --git a/test/shared/stagedProgress.test.ts b/test/shared/stagedProgress.test.ts new file mode 100644 index 00000000..516eacf5 --- /dev/null +++ b/test/shared/stagedProgress.test.ts @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2020, salesforce.com, inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ +import { expect } from 'chai'; +import * as chalk from 'chalk'; +import { StagedProgress, State, StateConstants } from '../../src/shared/stagedProgress'; +import { SandboxStatusData } from '../../src/shared/sandboxProgress'; + +class TestStagedProgress extends StagedProgress { + public formatProgressStatus(): string { + return ''; + } +} + +describe('stagedProgress', () => { + let stagedProgress: TestStagedProgress; + describe('updateStages', () => { + beforeEach(() => { + stagedProgress = new TestStagedProgress(['Pending', 'Processing', 'Activating', 'Completed', 'Authenticating']); + }); + it('should update existing stage', () => { + stagedProgress.updateStages('Pending', State.failed); + const pendingStage = stagedProgress.getStages()['Pending']; + expect(pendingStage).to.be.ok; + expect(pendingStage.state).to.equal(State.failed); + }); + it('should insert new stage at beginning of stages', () => { + stagedProgress.updateStages('Creating', State.inProgress); + const creatingStage = stagedProgress.getStages()['Creating']; + const pendingStage = stagedProgress.getStages()['Pending']; + expect(creatingStage).to.be.ok; + expect(creatingStage.state).to.equal(State.inProgress); + expect(creatingStage.index).to.be.lessThan(pendingStage.index); + }); + it('should insert new stage at end of stages', () => { + const stages = stagedProgress.getStages(); + Object.keys(stages).forEach((stage) => { + stages[stage].visited = true; + stages[stage].state = State.completed; + }); + stagedProgress.updateStages('Past the End', State.inProgress); + const pastTheEnd = stagedProgress.getStages()['Past the End']; + const authenticatingStage = stagedProgress.getStages()['Authenticating']; + expect(pastTheEnd).to.be.ok; + expect(pastTheEnd.state).to.equal(State.inProgress); + expect(pastTheEnd.index).to.be.greaterThan(authenticatingStage.index); + }); + it('should insert new stage after Processing', () => { + const stages = stagedProgress.getStages(); + Object.keys(stages).forEach((stage) => { + if (['Pending', 'Processing'].includes(stage)) { + stages[stage].visited = true; + stages[stage].state = State.completed; + } + }); + stagedProgress.updateStages('After Processing', State.inProgress); + const afterProcessStage = stagedProgress.getStages()['After Processing']; + const processingStage = stagedProgress.getStages()['Processing']; + expect(afterProcessStage).to.be.ok; + expect(afterProcessStage.state).to.equal(State.inProgress); + expect(afterProcessStage.index).to.be.equal(processingStage.index + 1); + }); + }); + describe('getFormattedStages', () => { + beforeEach(() => { + stagedProgress = new TestStagedProgress(['Pending', 'Processing', 'Activating', 'Completed', 'Authenticating']); + }); + it('should get formatted stages - all unknown', () => { + const formattedStages = stagedProgress.formatStages(); + expect(formattedStages).to.be.ok; + expect(formattedStages).to.include(StateConstants.unknown.char); + expect(formattedStages).to.include(chalk.dim('')); + }); + it('should get formatted stages - pending in progress', () => { + stagedProgress.updateStages('Pending', State.inProgress); + const formattedStages = stagedProgress.formatStages(); + expect(formattedStages).to.be.ok; + expect(formattedStages).to.include(StateConstants.unknown.char); + // eslint-disable-next-line @typescript-eslint/no-unsafe-call + expect(formattedStages).to.include( + StateConstants.inProgress.color(`${StateConstants.inProgress.char} - Pending`) + ); + }); + it('should get formatted stages - pending successful', () => { + stagedProgress.updateStages('Pending', State.completed); + stagedProgress.updateStages('Processing', State.inProgress); + const formattedStages = stagedProgress.formatStages(); + expect(formattedStages).to.be.ok; + expect(formattedStages).to.include(StateConstants.unknown.char); + expect(formattedStages).to.include(StateConstants.completed.color(`${StateConstants.completed.char} - Pending`)); + }); + it('should get formatted stages - processing failed', () => { + stagedProgress.updateStages('Pending', State.completed); + stagedProgress.updateStages('Processing', State.failed); + const formattedStages = stagedProgress.formatStages(); + expect(formattedStages).to.be.ok; + expect(formattedStages).to.include(StateConstants.unknown.char); + expect(formattedStages).to.include(StateConstants.failed.color(`${StateConstants.failed.char} - Processing`)); + }); + }); +}); diff --git a/test/tsconfig.json b/test/tsconfig.json index 3fee52bd..456e2f32 100644 --- a/test/tsconfig.json +++ b/test/tsconfig.json @@ -2,6 +2,8 @@ "extends": "@salesforce/dev-config/tsconfig-test", "include": ["./**/*.ts"], "compilerOptions": { + "target": "es2021", + "lib": ["es2021"], "skipLibCheck": true } } diff --git a/test/utils/timeUtils.test.ts b/test/utils/timeUtils.test.ts new file mode 100644 index 00000000..e6e963df --- /dev/null +++ b/test/utils/timeUtils.test.ts @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2020, salesforce.com, inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ +import { expect } from 'chai'; +import { Duration } from '@salesforce/cli-plugins-testkit'; +import { getClockForSeconds, getSecondsToHuman } from '../../src/utils/timeUtils'; + +describe('timeUtils', () => { + describe('getSecondsToHuman', () => { + it('should build time string with 10 seconds', () => { + expect(getSecondsToHuman(10)).to.includes('10 seconds'); + }); + it('should build time string 1 minute', () => { + expect(getSecondsToHuman(60)).to.includes('1 minute'); + }); + it('should build time string 1 hour', () => { + expect(getSecondsToHuman(Duration.hours(1).seconds)).to.includes('1 hour'); + }); + it('should build time string 1 day', () => { + expect(getSecondsToHuman(Duration.days(1).seconds)).to.includes('1 day'); + }); + it('should build time string 1 day 12 hours', () => { + expect(getSecondsToHuman(Duration.days(1).seconds + Duration.hours(12).seconds)).to.includes('1 day 12 hour'); + }); + }); + describe('getClockForSeconds', () => { + it('should build time string with 10 seconds', () => { + expect(getClockForSeconds(10)).to.be.equal('00:00:10'); + }); + it('should build time string 1 minute', () => { + expect(getClockForSeconds(60)).to.be.equal('00:01:00'); + }); + it('should build time string 1 hour', () => { + expect(getClockForSeconds(Duration.hours(1).seconds)).to.be.equal('01:00:00'); + }); + it('should build time string 1 day', () => { + expect(getClockForSeconds(Duration.days(1).seconds)).to.be.equal('1:00:00:00'); + }); + it('should build time string 1 day 12 hours', () => { + expect(getClockForSeconds(Duration.days(1).seconds + Duration.hours(12).seconds)).to.be.equal('1:12:00:00'); + }); + }); +}); diff --git a/yarn.lock b/yarn.lock index 73f04ebc..62c18296 100644 --- a/yarn.lock +++ b/yarn.lock @@ -848,10 +848,10 @@ is-wsl "^2.1.1" tslib "^2.3.1" -"@oclif/core@^1.0.8", "@oclif/core@^1.2.1", "@oclif/core@^1.3.1", "@oclif/core@^1.3.4", "@oclif/core@^1.3.6", "@oclif/core@^1.6.0", "@oclif/core@^1.6.3", "@oclif/core@^1.6.4": - version "1.6.4" - resolved "https://registry.yarnpkg.com/@oclif/core/-/core-1.6.4.tgz#fd213265199a8ef93adb358e15a04d74927eb522" - integrity sha512-eUnh03MWxs3PHQUdZbo43ceLqmeOgGegsfimeqd6xfhcKwSv8dqarRyAoMCjg7P6Qm5qCjaNFZ6eS4ao349xvQ== +"@oclif/core@^1.0.8", "@oclif/core@^1.2.1", "@oclif/core@^1.3.1", "@oclif/core@^1.3.4", "@oclif/core@^1.3.6", "@oclif/core@^1.6.0", "@oclif/core@^1.6.3", "@oclif/core@^1.6.4", "@oclif/core@^1.7.0": + version "1.7.0" + resolved "https://registry.yarnpkg.com/@oclif/core/-/core-1.7.0.tgz#3b763b53eafa9afbb13cf7cdfeac0f8ddd8ab1af" + integrity sha512-I4q4qgtnNG7ef4sBDrJhwADdi7RExQV7LnflnXaWZZDiUoqF9AdOjC4jjx40MK0rRrCehCUtPNZBcr3WmxuS4Q== dependencies: "@oclif/linewrap" "^1.0.0" "@oclif/screen" "^3.0.2" @@ -1165,10 +1165,10 @@ mv "~2" safe-json-stringify "~1" -"@salesforce/cli-plugins-testkit@^1.5.20": - version "1.5.22" - resolved "https://registry.yarnpkg.com/@salesforce/cli-plugins-testkit/-/cli-plugins-testkit-1.5.22.tgz#eb7ffbf17fca20b5498d5adb6e0e887051014d74" - integrity sha512-P7tEkt2XTsEFwVqWZbmBLYMlfzBlO0xul3iy6piOcjp2A/VjzOf0WVnemdpcfjWpbsiRXDBbc8ZiQZZbEz43hA== +"@salesforce/cli-plugins-testkit@^1.5.28": + version "1.5.28" + resolved "https://registry.yarnpkg.com/@salesforce/cli-plugins-testkit/-/cli-plugins-testkit-1.5.28.tgz#3c3ed8e99fc6365f149d4c5f2690b02166661d4b" + integrity sha512-BC/M24sg7IjiE/5aD7RazOVWNVG8kStYF3ze/zvKGIdRGjTjgBDwYR7LkT4VWIqH0y48ohSc9aQc0kJ3HsiC2Q== dependencies: "@salesforce/core" "^2.24.0" "@salesforce/kit" "^1.5.13" @@ -1241,10 +1241,10 @@ semver "^7.3.5" ts-retry-promise "^0.6.0" -"@salesforce/core@^3.10.1", "@salesforce/core@^3.11.0", "@salesforce/core@^3.8.0": - version "3.11.1" - resolved "https://registry.yarnpkg.com/@salesforce/core/-/core-3.11.1.tgz#56ab3178444d0d4c6b7fc54747057dc6c2496cb4" - integrity sha512-aQZQTuto6kHFIzRiz6XNCL/xaY6yYYhIy7O65q0bkuVpZSdrTmi42hp2XY/eseDdWMvTwwy9yjt/623jhxz8Cg== +"@salesforce/core@^3.11.0", "@salesforce/core@^3.15.0", "@salesforce/core@^3.8.0": + version "3.15.0" + resolved "https://registry.yarnpkg.com/@salesforce/core/-/core-3.15.0.tgz#1fbee2cd31aae28bf283869a53927026e4297554" + integrity sha512-7GbNMpLUzc1Y7u1Ouuz+KGZlj4sV6ix51OPUFJA7QbMc7sbhFz7/hoiLToU6NIbHDzq5dyAK9Q0f1M3AR1ZOAg== dependencies: "@salesforce/bunyan" "^2.0.0" "@salesforce/kit" "^1.5.34" @@ -1271,10 +1271,10 @@ resolved "https://registry.yarnpkg.com/@salesforce/dev-config/-/dev-config-3.0.1.tgz#631a952abfd69e7cdb0fb312ba4b1656ae632b90" integrity sha512-hkH8g7/bQZvtOfKTb3AmTPo1KopUli31legtb84nF9Y6mKj27TRzWUvIRuaRRd86ma19C7lPA4ycUjydX4QCcQ== -"@salesforce/dev-scripts@^2.0.1": - version "2.0.1" - resolved "https://registry.yarnpkg.com/@salesforce/dev-scripts/-/dev-scripts-2.0.1.tgz#d3670a2ae9d8f26b17ec5d96c8808419e881dbe6" - integrity sha512-FmPYlnQul1duTglZflCxmBuyPxBbIagGBM74jozZC4/Tge/eLn/kVfu+Sc95ls/Xtd7vtIj3Oed8huK8J01kUA== +"@salesforce/dev-scripts@^2.0.2": + version "2.0.2" + resolved "https://registry.yarnpkg.com/@salesforce/dev-scripts/-/dev-scripts-2.0.2.tgz#7a174082806ffd3d0c078d83dfb97bc603c798c4" + integrity sha512-LApwttqOMTAX0xB8owluwMUClDzafjQY3C6TCKCFgV2R4qCy1QUtG3jrBAtrhK1FxuV7qBvp8S+uqA21TELOGQ== dependencies: "@commitlint/cli" "^15.0.0" "@commitlint/config-conventional" "^15.0.0" @@ -1307,7 +1307,7 @@ sinon "10.0.0" source-map-support "~0.5.19" ts-node "^10.0.0" - typedoc "0.22.11" + typedoc "0.22.13" typedoc-plugin-missing-exports "0.22.6" typescript "^4.1.3" @@ -1320,7 +1320,7 @@ shx "^0.3.3" tslib "^2.2.0" -"@salesforce/plugin-command-reference@^2.2.1": +"@salesforce/plugin-command-reference@^2.2.8": version "2.2.8" resolved "https://registry.yarnpkg.com/@salesforce/plugin-command-reference/-/plugin-command-reference-2.2.8.tgz#b75571df46ad5210ce5170cb42de57cd6965ff84" integrity sha512-vyJzYDOGDmF3NyVhdRwLNkM+lVj3pzwbusHq4eGJDsxrlgWALeg0IVbEeEI+qUk32BK/wpwcJNbPRTc4Bem36w== @@ -1337,7 +1337,7 @@ mkdirp "^1.0.4" tslib "^2" -"@salesforce/plugin-config@^2.3.1": +"@salesforce/plugin-config@^2.3.2": version "2.3.2" resolved "https://registry.yarnpkg.com/@salesforce/plugin-config/-/plugin-config-2.3.2.tgz#382b34762bda8dfe84840b5656e9a7f35c3548a9" integrity sha512-KRVxJOxcjHx8cMxSC8UsYhpVwcK9pkIBJO+AjKr0fuXnMXi9KkEhV9yyQJcDbQMm2N9kg2OXEGmiIdTLzuBZGg== @@ -1405,16 +1405,16 @@ resolved "https://registry.yarnpkg.com/@salesforce/schemas/-/schemas-1.1.0.tgz#bbf94a11ee036f2b0ec6ba82306cd9565a6ba26b" integrity sha512-6D7DvE6nFxpLyyTnrOIbbAeCJw2r/EpinFAcMh6gU0gA/CGfSbwV/8uR3uHLYL2zCyCZLH8jJ4dZ3BzCMqc+Eg== -"@salesforce/sf-plugins-core@^1.7.2", "@salesforce/sf-plugins-core@^1.9.0": - version "1.11.0" - resolved "https://registry.yarnpkg.com/@salesforce/sf-plugins-core/-/sf-plugins-core-1.11.0.tgz#bb263218481553920c8e37311c511c069b56d67d" - integrity sha512-7w+o3TYW/MX726KNaONAnqxlxFEuCOzTsGcUyNPUBRqBtlgOM5qwG5yqeT+P1KIy64RgxH8zBD800patmaQX6A== +"@salesforce/sf-plugins-core@^1.12.1", "@salesforce/sf-plugins-core@^1.7.2", "@salesforce/sf-plugins-core@^1.9.0": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@salesforce/sf-plugins-core/-/sf-plugins-core-1.12.1.tgz#8952455e15cd0bbbfbffa5fc7655e291ef757e5a" + integrity sha512-cXT2VBeyQvYR7iqp9WD8e5UGkp+V9Ui6zYBZM4RTqsWmb6+wUMbYy5s6Fm+V7LVs+nSh4m8p74KCGnXUmIZOBw== dependencies: "@oclif/core" "^1.6.3" "@salesforce/core" "^3.11.0" "@salesforce/kit" "^1.5.34" "@salesforce/ts-types" "^1.5.20" - chalk "^2.4.2" + chalk "^4" inquirer "^8.2.0" "@salesforce/templates@^52.0.0": @@ -1617,7 +1617,7 @@ dependencies: "@types/node" "*" -"@types/json-schema@^7.0.7", "@types/json-schema@^7.0.9": +"@types/json-schema@^7.0.9": version "7.0.10" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.10.tgz#9b05b7896166cd00e9cbd59864853abf65d9ac23" integrity sha512-BLO9bBq59vW3fxCpD4o0N4U+DXsvwvIcl+jofw0frQo/GrBFC+/jRZj1E7kgp6dvTyNmA4y6JCV5Id/r3mNP5A== @@ -1731,75 +1731,85 @@ "@types/expect" "^1.20.4" "@types/node" "*" -"@typescript-eslint/eslint-plugin@^4.33.0": - version "4.33.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.33.0.tgz#c24dc7c8069c7706bc40d99f6fa87edcb2005276" - integrity sha512-aINiAxGVdOl1eJyVjaWn/YcVAq4Gi/Yo35qHGCnqbWVz61g39D0h23veY/MA0rFFGfxK7TySg2uwDeNv+JgVpg== +"@typescript-eslint/eslint-plugin@^5.19.0": + version "5.20.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.20.0.tgz#022531a639640ff3faafaf251d1ce00a2ef000a1" + integrity sha512-fapGzoxilCn3sBtC6NtXZX6+P/Hef7VDbyfGqTTpzYydwhlkevB+0vE0EnmHPVTVSy68GUncyJ/2PcrFBeCo5Q== dependencies: - "@typescript-eslint/experimental-utils" "4.33.0" - "@typescript-eslint/scope-manager" "4.33.0" - debug "^4.3.1" + "@typescript-eslint/scope-manager" "5.20.0" + "@typescript-eslint/type-utils" "5.20.0" + "@typescript-eslint/utils" "5.20.0" + debug "^4.3.2" functional-red-black-tree "^1.0.1" ignore "^5.1.8" - regexpp "^3.1.0" + regexpp "^3.2.0" semver "^7.3.5" tsutils "^3.21.0" -"@typescript-eslint/experimental-utils@4.33.0": - version "4.33.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.33.0.tgz#6f2a786a4209fa2222989e9380b5331b2810f7fd" - integrity sha512-zeQjOoES5JFjTnAhI5QY7ZviczMzDptls15GFsI6jyUOq0kOf9+WonkhtlIhh0RgHRnqj5gdNxW5j1EvAyYg6Q== +"@typescript-eslint/parser@^5.19.0": + version "5.20.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.20.0.tgz#4991c4ee0344315c2afc2a62f156565f689c8d0b" + integrity sha512-UWKibrCZQCYvobmu3/N8TWbEeo/EPQbS41Ux1F9XqPzGuV7pfg6n50ZrFo6hryynD8qOTTfLHtHjjdQtxJ0h/w== dependencies: - "@types/json-schema" "^7.0.7" - "@typescript-eslint/scope-manager" "4.33.0" - "@typescript-eslint/types" "4.33.0" - "@typescript-eslint/typescript-estree" "4.33.0" - eslint-scope "^5.1.1" - eslint-utils "^3.0.0" + "@typescript-eslint/scope-manager" "5.20.0" + "@typescript-eslint/types" "5.20.0" + "@typescript-eslint/typescript-estree" "5.20.0" + debug "^4.3.2" -"@typescript-eslint/parser@^4.33.0": - version "4.33.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.33.0.tgz#dfe797570d9694e560528d18eecad86c8c744899" - integrity sha512-ZohdsbXadjGBSK0/r+d87X0SBmKzOq4/S5nzK6SBgJspFo9/CUDJ7hjayuze+JK7CZQLDMroqytp7pOcFKTxZA== +"@typescript-eslint/scope-manager@5.20.0": + version "5.20.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.20.0.tgz#79c7fb8598d2942e45b3c881ced95319818c7980" + integrity sha512-h9KtuPZ4D/JuX7rpp1iKg3zOH0WNEa+ZIXwpW/KWmEFDxlA/HSfCMhiyF1HS/drTICjIbpA6OqkAhrP/zkCStg== dependencies: - "@typescript-eslint/scope-manager" "4.33.0" - "@typescript-eslint/types" "4.33.0" - "@typescript-eslint/typescript-estree" "4.33.0" - debug "^4.3.1" + "@typescript-eslint/types" "5.20.0" + "@typescript-eslint/visitor-keys" "5.20.0" -"@typescript-eslint/scope-manager@4.33.0": - version "4.33.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.33.0.tgz#d38e49280d983e8772e29121cf8c6e9221f280a3" - integrity sha512-5IfJHpgTsTZuONKbODctL4kKuQje/bzBRkwHE8UOZ4f89Zeddg+EGZs8PD8NcN4LdM3ygHWYB3ukPAYjvl/qbQ== +"@typescript-eslint/type-utils@5.20.0": + version "5.20.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.20.0.tgz#151c21cbe9a378a34685735036e5ddfc00223be3" + integrity sha512-WxNrCwYB3N/m8ceyoGCgbLmuZwupvzN0rE8NBuwnl7APgjv24ZJIjkNzoFBXPRCGzLNkoU/WfanW0exvp/+3Iw== dependencies: - "@typescript-eslint/types" "4.33.0" - "@typescript-eslint/visitor-keys" "4.33.0" + "@typescript-eslint/utils" "5.20.0" + debug "^4.3.2" + tsutils "^3.21.0" -"@typescript-eslint/types@4.33.0": - version "4.33.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.33.0.tgz#a1e59036a3b53ae8430ceebf2a919dc7f9af6d72" - integrity sha512-zKp7CjQzLQImXEpLt2BUw1tvOMPfNoTAfb8l51evhYbOEEzdWyQNmHWWGPR6hwKJDAi+1VXSBmnhL9kyVTTOuQ== +"@typescript-eslint/types@5.20.0": + version "5.20.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.20.0.tgz#fa39c3c2aa786568302318f1cb51fcf64258c20c" + integrity sha512-+d8wprF9GyvPwtoB4CxBAR/s0rpP25XKgnOvMf/gMXYDvlUC3rPFHupdTQ/ow9vn7UDe5rX02ovGYQbv/IUCbg== -"@typescript-eslint/typescript-estree@4.33.0": - version "4.33.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.33.0.tgz#0dfb51c2908f68c5c08d82aefeaf166a17c24609" - integrity sha512-rkWRY1MPFzjwnEVHsxGemDzqqddw2QbTJlICPD9p9I9LfsO8fdmfQPOX3uKfUaGRDFJbfrtm/sXhVXN4E+bzCA== +"@typescript-eslint/typescript-estree@5.20.0": + version "5.20.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.20.0.tgz#ab73686ab18c8781bbf249c9459a55dc9417d6b0" + integrity sha512-36xLjP/+bXusLMrT9fMMYy1KJAGgHhlER2TqpUVDYUQg4w0q/NW/sg4UGAgVwAqb8V4zYg43KMUpM8vV2lve6w== dependencies: - "@typescript-eslint/types" "4.33.0" - "@typescript-eslint/visitor-keys" "4.33.0" - debug "^4.3.1" - globby "^11.0.3" - is-glob "^4.0.1" + "@typescript-eslint/types" "5.20.0" + "@typescript-eslint/visitor-keys" "5.20.0" + debug "^4.3.2" + globby "^11.0.4" + is-glob "^4.0.3" semver "^7.3.5" tsutils "^3.21.0" -"@typescript-eslint/visitor-keys@4.33.0": - version "4.33.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.33.0.tgz#2a22f77a41604289b7a186586e9ec48ca92ef1dd" - integrity sha512-uqi/2aSz9g2ftcHWf8uLPJA70rUv6yuMW5Bohw+bwcuzaxQIHaKFZCKGoGXIrc9vkTJ3+0txM73K0Hq3d5wgIg== +"@typescript-eslint/utils@5.20.0": + version "5.20.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.20.0.tgz#b8e959ed11eca1b2d5414e12417fd94cae3517a5" + integrity sha512-lHONGJL1LIO12Ujyx8L8xKbwWSkoUKFSO+0wDAqGXiudWB2EO7WEUT+YZLtVbmOmSllAjLb9tpoIPwpRe5Tn6w== dependencies: - "@typescript-eslint/types" "4.33.0" - eslint-visitor-keys "^2.0.0" + "@types/json-schema" "^7.0.9" + "@typescript-eslint/scope-manager" "5.20.0" + "@typescript-eslint/types" "5.20.0" + "@typescript-eslint/typescript-estree" "5.20.0" + eslint-scope "^5.1.1" + eslint-utils "^3.0.0" + +"@typescript-eslint/visitor-keys@5.20.0": + version "5.20.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.20.0.tgz#70236b5c6b67fbaf8b2f58bf3414b76c1e826c2a" + integrity sha512-1flRpNF+0CAQkMNlTJ6L/Z5jiODG/e5+7mk6XwtPOUS3UrTz3UOiAg9jG2VtKsWI6rZQfy4C6a232QNRZTRGlg== + dependencies: + "@typescript-eslint/types" "5.20.0" + eslint-visitor-keys "^3.0.0" "@ungap/promise-all-settled@1.1.2": version "1.1.2" @@ -3943,6 +3953,11 @@ eslint-visitor-keys@^2.0.0: resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz#f65328259305927392c938ed44eb0a5c9b2bd303" integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw== +eslint-visitor-keys@^3.0.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz#f6480fa6b1f30efe2d1968aa8ac745b862469826" + integrity sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA== + eslint@^7.27.0, eslint@^7.32.0: version "7.32.0" resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.32.0.tgz#c6d328a14be3fb08c8d1d21e12c02fdb7a2a812d" @@ -4882,7 +4897,7 @@ globby@^10.0.1: merge2 "^1.2.3" slash "^3.0.0" -globby@^11.0.1, globby@^11.0.3, globby@^11.1.0: +globby@^11.0.1, globby@^11.0.3, globby@^11.0.4, globby@^11.1.0: version "11.1.0" resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== @@ -6609,10 +6624,10 @@ map-visit@^1.0.0: dependencies: object-visit "^1.0.0" -marked@^4.0.10: - version "4.0.12" - resolved "https://registry.yarnpkg.com/marked/-/marked-4.0.12.tgz#2262a4e6fd1afd2f13557726238b69a48b982f7d" - integrity sha512-hgibXWrEDNBWgGiK18j/4lkS6ihTe9sxtV4Q1OQppb/0zzyPSzoFANBa5MfsG/zgsWklmNnhm0XACZOH/0HBiQ== +marked@^4.0.12: + version "4.0.14" + resolved "https://registry.yarnpkg.com/marked/-/marked-4.0.14.tgz#7a3a5fa5c80580bac78c1ed2e3b84d7bd6fc3870" + integrity sha512-HL5sSPE/LP6U9qKgngIIPTthuxC0jrfxpYMZ3LdGDD3vTnLs59m2Z7r6+LNDR3ToqEQdkKd6YaaEfJhodJmijQ== matcher@^3.0.0: version "3.0.0" @@ -6933,7 +6948,7 @@ mkdirp@^0.5.0, mkdirp@~0.5.1: dependencies: minimist "^1.2.5" -mocha@^9.1.3: +mocha@^9.1.3, mocha@^9.2.2: version "9.2.2" resolved "https://registry.yarnpkg.com/mocha/-/mocha-9.2.2.tgz#d70db46bdb93ca57402c809333e5a84977a88fb9" integrity sha512-L6XC3EdwT6YrIk0yXpavvLkn8h+EU+Y5UcCHKECyMbdUIxyMuZj4bX4U9e1nvnvUUvQVsV2VHQr5zLdcUkhW/g== @@ -7526,7 +7541,7 @@ object.values@^1.1.5: define-properties "^1.1.3" es-abstract "^1.19.1" -oclif@^2.6.0: +oclif@^2.6.3: version "2.6.3" resolved "https://registry.yarnpkg.com/oclif/-/oclif-2.6.3.tgz#77295d8342a41bfebcf13f7eba260d26ff5d92ef" integrity sha512-t3w1GnDrZ9GnlO2WxdTRIlb5EmfLo7fkcpKo95q+03HoL4GVUNneraVymyUTYEBsvMyWcX8PbVkcMai/HhBydA== @@ -8362,7 +8377,7 @@ regexp.prototype.flags@^1.2.0: call-bind "^1.0.2" define-properties "^1.1.3" -regexpp@^3.1.0: +regexpp@^3.1.0, regexpp@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2" integrity sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg== @@ -8765,7 +8780,7 @@ shelljs@^0.8.3, shelljs@^0.8.4, shelljs@^0.8.5, shelljs@~0.8.4: interpret "^1.0.0" rechoir "^0.6.2" -shiki@^0.10.0: +shiki@^0.10.1: version "0.10.1" resolved "https://registry.yarnpkg.com/shiki/-/shiki-0.10.1.tgz#6f9a16205a823b56c072d0f1a0bcd0f2646bef14" integrity sha512-VsY7QJVzU51j5o1+DguUd+6vmCmZ5v/6gYu4vyYAhzjuNQU6P/vmSy4uQaOhvje031qQMiW0d2BwgMH52vqMng== @@ -9664,16 +9679,16 @@ typedoc-plugin-missing-exports@0.22.6: resolved "https://registry.yarnpkg.com/typedoc-plugin-missing-exports/-/typedoc-plugin-missing-exports-0.22.6.tgz#7467c60f1cd26507124103f0b9bca271d5aa8d71" integrity sha512-1uguGQqa+c5f33nWS3v1mm0uAx4Ii1lw4Kx2zQksmYFKNEWTmrmMXbMNBoBg4wu0p4dFCNC7JIWPoRzpNS6pFA== -typedoc@0.22.11: - version "0.22.11" - resolved "https://registry.yarnpkg.com/typedoc/-/typedoc-0.22.11.tgz#a3d7f4577eef9fc82dd2e8f4e2915e69f884c250" - integrity sha512-pVr3hh6dkS3lPPaZz1fNpvcrqLdtEvXmXayN55czlamSgvEjh+57GUqfhAI1Xsuu/hNHUT1KNSx8LH2wBP/7SA== +typedoc@0.22.13: + version "0.22.13" + resolved "https://registry.yarnpkg.com/typedoc/-/typedoc-0.22.13.tgz#d061f8f0fb7c9d686e48814f245bddeea4564e66" + integrity sha512-NHNI7Dr6JHa/I3+c62gdRNXBIyX7P33O9TafGLd07ur3MqzcKgwTvpg18EtvCLHJyfeSthAtCLpM7WkStUmDuQ== dependencies: glob "^7.2.0" lunr "^2.3.9" - marked "^4.0.10" - minimatch "^3.0.4" - shiki "^0.10.0" + marked "^4.0.12" + minimatch "^5.0.1" + shiki "^0.10.1" typescript@^4.1.3, typescript@^4.4.3, typescript@^4.6.2: version "4.6.2"