diff --git a/.azurepipelines/build-code-push-1es.yml b/.azurepipelines/build-code-push-1es.yml new file mode 100644 index 00000000..b5051b82 --- /dev/null +++ b/.azurepipelines/build-code-push-1es.yml @@ -0,0 +1,102 @@ +trigger: +- master + +pr: +- master + +resources: + repositories: + - repository: 1ESPipelineTemplates + type: git + name: 1ESPipelineTemplates/1ESPipelineTemplates + ref: refs/tags/release +name: $(Build.SourceBranchName)_$(date:yyyyMMdd)$(rev:.r) + +extends: + ${{ if eq(variables['Build.SourceBranch'], 'refs/heads/master') }}: + template: v1/1ES.Official.PipelineTemplate.yml@1ESPipelineTemplates + ${{ else }}: + template: v1/1ES.Unofficial.PipelineTemplate.yml@1ESPipelineTemplates + parameters: + pool: + name: 1ES-PT-CBL-Mariner-2.0-Gen2 + os: linux + customBuildTags: + - ES365AIMigrationTooling-BulkMigrated + sdl: + sourceAnalysisPool: 1ES-PT-Windows-2022 + stages: + - stage: Stage + jobs: + - job: HostJob + templateContext: + outputs: + - output: pipelineArtifact + displayName: "Publish Artifact: artifacts" + path: '$(Build.ArtifactStagingDirectory)/npm' + artifactName: npm + + steps: + - task: NodeTool@0 + inputs: + versionSpec: '14.x' + displayName: 'Install Node.js' + + - script: | + npm pack + npm install -g code-push*.tgz + displayName: 'Package code-push' + workingDirectory: $(Build.SourcesDirectory) + + - task: DeleteFiles@1 + inputs: + contents: node_modules + displayName: 'Delete node_modules' + + - task: ArchiveFiles@2 + inputs: + rootFolderOrFile: '$(Build.SourcesDirectory)' + includeRootFolder: false + archiveType: 'tar' + archiveFile: '$(Build.ArtifactStagingDirectory)/npm/$(Build.BuildId).tgz' + replaceExistingArchive: true + verbose: true + displayName: 'Prepare npm artifact' + + - stage: APIScan + dependsOn: Stage + pool: + name: 1ES-PT-Windows-2022 + os: windows + variables: + "agent.source.skip": true + jobs: + - job: APIScan + steps: + - task: DownloadPipelineArtifact@2 + displayName: Download Pipeline Artifacts for APIScan + inputs: + artifactName: npm + targetPath: '$(Agent.BuildDirectory)/npm' + - task: ExtractFiles@1 + inputs: + archiveFilePatterns: '$(Agent.BuildDirectory)/npm/*.tgz' + destinationFolder: '$(Agent.BuildDirectory)/npm_extracted' + - task: AzureKeyVault@2 + inputs: + azureSubscription: 'AC - Dev Infra & Build Pool' + KeyVaultName: 'mobile-center-sdk' + SecretsFilter: 'appcenter-sdk-managed-identity-clientid' + RunAsPreJob: false + - task: APIScan@2 + displayName: 'Run APIScan' + inputs: + softwareFolder: '$(Agent.BuildDirectory)\npm_extracted' + softwareName: 'code-push' + softwareVersionNum: '$(Build.BuildId)' + isLargeApp: false + toolVersion: 'Latest' + verbosityLevel: verbose + condition: and(succeeded(), ne(variables['DisableAPIScan'], 'true')) + env: + AzureServicesAuthConnectionString: 'runAs=App;AppId=$(appcenter-sdk-managed-identity-clientid)' \ No newline at end of file diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 00000000..ba411809 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @microsoft/appcenter-fte diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 9c7fe28e..fa9037ba 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -2,5 +2,4 @@ Thanks so much for filing an issue or feature request! We will address it as soo 1. This repository is for the CodePush CLI and management SDK. For issues relating to the CodePush client SDK's, please see: * react-native-code-push: https://github.com/Microsoft/react-native-code-push - * cordova-plugin-code-push: https://github.com/Microsoft/cordova-plugin-code-push 2. In your description, please include the version of `code-push-cli` or `code-push` that you are using. diff --git a/.github/policies/resourceManagement.yml b/.github/policies/resourceManagement.yml new file mode 100644 index 00000000..9fc1c5e2 --- /dev/null +++ b/.github/policies/resourceManagement.yml @@ -0,0 +1,64 @@ +id: +name: GitOps.PullRequestIssueManagement +description: GitOps.PullRequestIssueManagement primitive +owner: +resource: repository +disabled: false +where: +configuration: + resourceManagementConfiguration: + scheduledSearches: + - description: + frequencies: + - hourly: + hour: 4 + filters: + - isOpen + - isNotLabeledWith: + label: bug + - isNotLabeledWith: + label: security + - isNotLabeledWith: + label: Stale + - isNotLabeledWith: + label: do not close + - noActivitySince: + days: 60 + - isIssue + - isNotAssigned + actions: + - addLabel: + label: Stale + - addReply: + reply: This issue has been automatically marked as stale because it has not had any activity for 60 days. It will be closed if no further activity occurs within 15 days of this comment. + - description: + frequencies: + - hourly: + hour: 6 + filters: + - isOpen + - isIssue + - hasLabel: + label: Stale + - isNotLabeledWith: + label: bug + - isNotLabeledWith: + label: do not close + - isNotAssigned + - noActivitySince: + days: 15 + actions: + - addReply: + reply: This issue will now be closed because it hasn't had any activity for 15 days after stale. Please feel free to open a new issue if you still have a question/issue or suggestion. + - closeIssue + eventResponderTasks: + - if: + - payloadType: Issue_Comment + - hasLabel: + label: Stale + then: + - removeLabel: + label: Stale + description: +onFailure: +onSuccess: diff --git a/.github/scripts/check-for-declaration.ts b/.github/scripts/check-for-declaration.ts new file mode 100755 index 00000000..46c34a93 --- /dev/null +++ b/.github/scripts/check-for-declaration.ts @@ -0,0 +1,45 @@ +import fs from "fs"; +import path from "path"; + +type ResultType = { + js : Record + ts : Record +} + +const result: ResultType = {js:{} , ts:{}} + +const readThroughDirectory = (directory: string): void => { + const __directoryPath = directory + const files = fs.readdirSync(__directoryPath); + files.forEach((file) => { + const filePath = path.join(__directoryPath, file); + const stats = fs.statSync(filePath); + if (stats.isDirectory()) { + readThroughDirectory(filePath); + return + } + + if(filePath.endsWith('.js')){ + const name = filePath.split('.') + name.pop() + result.js[name.join('.')] = true + } + + if(filePath.endsWith('.d.ts')){ + const name = filePath.split('.') + name.pop() + name.pop() + result.ts[name.join('.')] = true + } + + }); + + Object.keys(result.js).forEach(file => { + if(!result.ts[file]){ + throw new Error(`Declaration File Missing for ${file}.js`) + } + }) + +}; + +readThroughDirectory(path.join(process.env.INIT_CWD ?? '', './bin')) \ No newline at end of file diff --git a/.github/workflows/code-push-ci.yml b/.github/workflows/code-push-ci.yml new file mode 100644 index 00000000..aef064ce --- /dev/null +++ b/.github/workflows/code-push-ci.yml @@ -0,0 +1,24 @@ +name: Сode-push CI + +on: + pull_request: + branches: + - master + +jobs: + Run-tests: + name: Test code-push-sdk + runs-on: macos-13 + steps: + - name: Checkout + uses: actions/checkout@v1 + - name: Setup NodeJs + uses: actions/setup-node@v1 + with: + node-version: "14.x" + - name: Setup dependencies + run: npm run setup + - name: Build + run: npm run build + - name: Run tests + run: npm run test diff --git a/.gitignore b/.gitignore index 34e3b5db..cbd13c11 100644 --- a/.gitignore +++ b/.gitignore @@ -29,8 +29,6 @@ node_modules # Build specific exclusions bin/ -definitions/external/ -definitions/generated/ # Environment variables *.env @@ -43,3 +41,4 @@ definitions/generated/ # JSON Storage persisted file JsonStorage.json +.idea/ diff --git a/sdk/.npmignore b/.npmignore similarity index 72% rename from sdk/.npmignore rename to .npmignore index c4a04633..9a646a1e 100644 --- a/sdk/.npmignore +++ b/.npmignore @@ -1,4 +1,4 @@ .npmignore .gitignore node_modules/* -definitions/* +test/* diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..6e6f1f86 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,23 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Tests", + "type": "node", + "request": "launch", + "preLaunchTask": "Build", + "runtimeExecutable": "npm", + "runtimeArgs": [ + "run", + "test:debugger" + ], + "port": 9229, + "stopOnEntry": false, + "sourceMaps": true, + "console": "internalConsole", + "internalConsoleOptions": "openOnSessionStart", + "autoAttachChildProcesses": true, + "timeout": 100000 + } + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..9bbf37f4 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,4 @@ +// Place your settings in this file to overwrite default and user settings. +{ + "typescript.tsdk": "node_modules/typescript/lib" +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 00000000..242cc3e3 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,21 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "type": "shell", + "label": "Build", + "command": "npm", + "args": [ + "run", + "build" + ], + "presentation": { + "echo": false, + "focus": false + }, + "problemMatcher": [ + "$tsc" + ] + } + ] +} diff --git a/Gulpfile.js b/Gulpfile.js deleted file mode 100644 index 3e7558e3..00000000 --- a/Gulpfile.js +++ /dev/null @@ -1,6 +0,0 @@ -'use strict'; - -var gulp = require("gulp"); -var plugins = require("gulp-load-plugins")(); - -require("require-all")(__dirname + "/gulp"); diff --git a/README.md b/README.md index e3167241..8c839a73 100644 --- a/README.md +++ b/README.md @@ -1,28 +1,39 @@ -# CodePush +# Archiving this repository -[CodePush](https://microsoft.github.io/code-push) is a cloud service that enables Cordova and React Native developers to deploy mobile app updates directly to their users' devices. It works by acting as a central repository that developers can publish updates to (JS, HTML, CSS and images), and that apps can query for updates from (using provided client SDKs for [Cordova](https://github.com/Microsoft/cordova-plugin-code-push) and [React Native](https://github.com/Microsoft/react-native-code-push)). This allows you to have a more deterministic and direct engagement model with your userbase, when addressing bugs and/or adding small features that don't require you to re-build a binary and re-distribute it through the respective app stores. +Visual Studio App Center was retired on March 31, 2025, except for its Analytics and Diagnostics features. You can learn more about the retirement and the Analytics and Diagnostics extension [here](https://aka.ms/appcenter/retire). CodePush, along with other App Center features, was also retired on March 31, 2025. Consequently, we are archiving this repository. -This repo includes the [management CLI](https://github.com/Microsoft/code-push/tree/master/cli) and [Node.js management SDK](https://github.com/Microsoft/code-push/tree/master/sdk), which allows you to manage and automate the needs of your Cordova and React Native apps. To get started using CodePush, refer to our [documentation](http://microsoft.github.io/code-push/index.html#getting_started), otherwise, read the following steps if you'd like to build/contribute to the project from source. +--- + +[![appcenterbanner](https://user-images.githubusercontent.com/31293287/32969262-3cc5d48a-cb99-11e7-91bf-fa57c67a371c.png)](http://microsoft.github.io/code-push/) + +#### [Sign up With App Center](https://appcenter.ms/signup?utm_source=CodePush&utm_medium=Azure) to use CodePush + +## CodePush SDK + +CodePush SDK enables seamless in-app updates and serves as a core component of the [CodePush React Native SDK](https://github.com/Microsoft/react-native-code-push). + +To start integrating CodePush into your project, visit our [documentation](https://docs.microsoft.com/en-us/appcenter/distribution/codepush/). If you're interested in contributing or building the SDK from source, follow the steps below. + +## Visual Studio App Center CodePush Standalone Version + +For teams or organizations looking to self-host CodePush, we now offer the [CodePush Standalone Version](https://github.com/microsoft/code-push-server) which is compatible with this SDK. It allows you to set up and manage CodePush as a self-hosted service, giving you more control over your infrastructure and data. Visit the repository for installation instructions, usage guides, and more. ## Dev Setup * Install [Node.js](https://nodejs.org/) * Install [Git](http://www.git-scm.com/) -* Install Gulp: `npm install -g gulp` * Clone the Repository: `git clone https://github.com/Microsoft/code-push.git` ### Building -* Run `npm install` from the root of the repository. -* Run `gulp install` to install the NPM dependencies of each module within the project. -* Run `gulp link` to link CLI and SDK for local development. It is advisable to do this step if you are making changes to the SDK and want the CLI to pick those changes. -* Run `gulp build` to build all of the modules. To build just one of the modules (e.g. cli or sdk), run `gulp build-cli` or `gulp build-sdk`. +* Run `npm run setup` to install the NPM dependencies of management SDK. +* Run `npm run build` to build the management SDK for testing. +* Run `npm run build:release` to build the release version of management SDK. ### Running Tests -To run all tests, run `gulp test` script from the root of the project. - -To test just one of the projects (e.g. cli or sdk), run `gulp test-cli` or `gulp test-sdk` +* To run tests, run `npm run test` from the root of the project. +* You can use debug mode for tests with `.vscode/launch.json` file. ### Coding Conventions @@ -31,3 +42,114 @@ To test just one of the projects (e.g. cli or sdk), run `gulp test-cli` or `gulp * Use `camelCase` for local variables and imported modules, `PascalCase` for types, and `dash-case` for file names This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. + +# CodePush Management SDK (Node.js) + +A JavaScript library for programmatically managing your CodePush account (e.g. creating apps, promoting releases), which allows authoring Node.js-based build and/or deployment scripts, without needing to shell out to the [App Center CLI](https://github.com/microsoft/appcenter-cli). + +## Getting Started + +1. Create a token to authenticate with the CodePush server using the following [App Center CLI](https://github.com/microsoft/appcenter-cli) command: + + ```shell + appcenter tokens create -d "DESCRIPTION_OF_THE_TOKEN" + ``` + + Please copy your `API Token` and keep it secret. You won't be able to see it again. + +2. Install the management SDK by running `npm install code-push --save` + +3. Import it using one of the following statement: (using ES6 syntax as applicable): + * On commonjs environments: + + ```javascript + const CodePush = require("code-push"); + ``` + + * Using ES6 syntax with tsconfig.json: + + ```javascript + import CodePush from "code-push"; + ``` + +4. Create an instance of the `CodePush` class, passing it the `API Token` you created or retrieved in step #1: + + ```javascript + const codePush = new CodePush("YOUR_API_TOKEN"); + ``` + +5. Begin automating the management of your account! For more details on what you can do with this `codePush` object, refer to the API reference section below. + +## API Reference + +The `code-push` module exports a single class (typically referred to as `CodePush`), which represents a proxy to the CodePush account management REST API. This class has a single constructor for authenticating with the CodePush service, and a collection of instance methods that correspond to the commands in the [App Center CLI](https://github.com/microsoft/appcenter-cli), which allow you to programmatically control every aspect of your CodePush account. + +### Constructors + +* __CodePush(accessKey: string)__ - Creates a new instance of the CodePush management SDK, using the specified access key to authenticated with the server. + +### Methods + +*Note: `access key` here refers to an AppCenter API Token.* + +* __addAccessKey(description: string): Promise<AccessKey>__ - Creates a new access key with the specified description (e.g. "VSTS CI"). + +* __addApp(name: string, os: string, platform: string, manuallyProvisionDeployments: boolean = false): Promise<App>__ - Creates a new CodePush app with the specified name, os, and platform. If the default deployments of "Staging" and "Production" are not desired, pass a value of true for the manuallyProvisionDeployments parameter. + +* __addCollaborator(appName: string, email: string): Promise<void>__ - Adds the specified CodePush user as a collaborator to the specified CodePush app. + +* __addDeployment(appName: string, deploymentName: string): Promise<Deployment>__ - Creates a new deployment with the specified name, and associated with the specified app. + +* __clearDeploymentHistory(appName: string, deploymentName: string): Promise<void>__ - Clears the release history associated with the specified app deployment. + +* __getAccessKey(accessKey: string): Promise<AccessKey>__ - Retrieves the metadata about the specific access key. + +* __getAccessKeys(): Promise<AccessKey[]>__ - Retrieves the list of access keys associated with your CodePush account. + +* __getApp(appName: string): Promise<App>__ - Retrieves the metadata about the specified app. + +* __getApps(): Promise<App[]>__ - Retrieves the list of apps associated with your CodePush account. + +* __getCollaborators(appName: string): Promise<CollaboratorMap>__ - Retrieves the list of collaborators associated with the specified app. + +* __getDeployment(appName: string, deploymentName: string): Promise<Deployment>__ - Retrieves the metadata for the specified app deployment. + +* __getDeploymentHistory(appName: string, deploymentName: string): Promise<Package[]>__ - Retrieves the list of releases that have been made to the specified app deployment. + +* __getDeploymentMetrics(appName: string, deploymentName: string): Promise<DeploymentMetrics>__ - Retrieves the installation metrics for the specified app deployment. + +* __getDeployments(appName: string): Promise<Deployment[]>__ - Retrieves the list of deployments associated with the specified app. + +* __patchRelease(appName: string, deploymentName: string, label: string, updateMetadata: PackageInfo): Promise<void>__ - Updates the specified release's metadata with the given information. + +* __promote(appName: string, sourceDeploymentName: string, destinationDeploymentName: string, updateMetadata: PackageInfo): Promise<Package>__ - Promotes the latest release from one deployment to another for the specified app and updates the release with the given metadata. + +* __release(appName: string, deploymentName: string, updateContentsPath: string, targetBinaryVersion: string, updateMetadata: PackageInfo): Promise<Package>__ - Releases a new update to the specified deployment with the given metadata. + +* __removeAccessKey(accessKey: string): Promise<void>__ - Removes the specified access key from your CodePush account. + +* __removeApp(appName: string): Promise<void>__ - Deletes the specified CodePush app from your account. + +* __removeCollaborator(appName: string, email: string): Promise<void>__ - Removes the specified account as a collaborator from the specified app. + +* __removeDeployment(appName: string, deploymentName: string): Promise<void>__ - Removes the specified deployment from the specified app. + +* __renameApp(oldAppName: string, newAppName: string): Promise<void>__ - Renames an existing app. + +* __renameDeployment(appName: string, oldDeploymentName: string, newDeploymentName: string): Promise<void>__ - Renames an existing deployment within the specified app. + +* __rollback(appName: string, deploymentName: string, targetRelease?: string): Promise<void>__ - Rolls back the latest release within the specified deployment. Optionally allows you to target a specific release in the deployment's history, as opposed to rolling to the previous release. + +* __transferApp(appName: string, email: string): Promise<void>__ - Transfers the ownership of the specified app to the specified account. + +### Error Handling + +When an error occurs in any of the methods, the promise will be rejected with a CodePushError object with the following properties: + +* __message__: A user-friendly message that describes the error. +* __statusCode__: An HTTP response code that identifies the category of error: + * __CodePush.ERROR_GATEWAY_TIMEOUT__: A network error prevented you from connecting to the CodePush server. + * __CodePush.ERROR_INTERNAL_SERVER__: An error occurred internally on the CodePush server. + * __CodePush.ERROR_NOT_FOUND__: The resource you are attempting to retrieve does not exist. + * __CodePush.ERROR_CONFLICT__: The resource you are attempting to create already exists. + * __CodePush.ERROR_UNAUTHORIZED__: The access key you configured is invalid or expired. diff --git a/sdk/SDK.njsproj b/SDK.njsproj similarity index 100% rename from sdk/SDK.njsproj rename to SDK.njsproj diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..46bf9ec0 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,41 @@ + + +## Security + +Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). + +If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://docs.microsoft.com/en-us/previous-versions/tn-archive/cc751383(v=technet.10)), please report it to us as described below. + +## Reporting Security Issues + +**Please do not report security vulnerabilities through public GitHub issues.** + +Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://msrc.microsoft.com/create-report). + +If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://www.microsoft.com/en-us/msrc/pgp-key-msrc). + +You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc). + +Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: + +* Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) +* Full paths of source file(s) related to the manifestation of the issue +* The location of the affected source code (tag/branch/commit or direct URL) +* Any special configuration required to reproduce the issue +* Step-by-step instructions to reproduce the issue +* Proof-of-concept or exploit code (if possible) +* Impact of the issue, including how an attacker might exploit the issue + +This information will help us triage your report more quickly. + +If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://microsoft.com/msrc/bounty) page for more details about our active programs. + +## Preferred Languages + +We prefer all communications to be in English. + +## Policy + +Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://www.microsoft.com/en-us/msrc/cvd). + + \ No newline at end of file diff --git a/cli/.npmignore b/cli/.npmignore deleted file mode 100644 index a57287c2..00000000 --- a/cli/.npmignore +++ /dev/null @@ -1,5 +0,0 @@ -.npmignore -.gitignore -node_modules/* -definitions/* -!cli.js diff --git a/cli/README-cn.md b/cli/README-cn.md deleted file mode 100644 index dc3c009b..00000000 --- a/cli/README-cn.md +++ /dev/null @@ -1,721 +0,0 @@ -# CodePush 命令行 - -#### Note: This translated document is community contributed and maintained, it will not be kept updated or in sync with the [original](./README.md) by the main contributors. Please send a pull request if you find any issues. -#### 注:本翻译文档是由社区贡献和维护,不受[原文](./README.md)的作者持续维护并更新。如果您发现任何问题,请发送pull请求。 - -CodePush是一个云服务,它能让Cordova和React Native的开发者将手机应用的更新直接部署到用户的设备上。 -它担任类似中间仓库的角色,开发者可以把更新(JS,HTML,CSS和图片)发布到这个仓库上,然后那些Apps就能查询到更新了(那些集成了CodePush SDKs的[Cordova](http://github.com/Microsoft/cordova-plugin-code-push)和[React Native](http://github.com/Microsoft/react-native-code-push) 应用)。 - -这就让你可以与你的用户群有一个更确定且直接的交互模式,当你定位到Bug或添加小功能时,就不需要重新构建二进制文件再在各AppStore里重新发布了。 - -![CodePush CLI](https://cloud.githubusercontent.com/assets/116461/14505396/c97bdc78-016d-11e6-89da-3f3557f8b33d.png) - -* [安装](#安装) -* [快速开始](#快速开始) -* [创建账号](#创建账号) -* [身份认证](#身份认证) -* [应用管理](#应用管理) -* [应用合作](#应用合作) -* [部署管理](#部署管理) -* [发布更新](#发布更新) - * [发布更新 (General)](#发布更新-general) - * [发布更新 (React Native)](#发布更新-react-native) - * [发布更新 (Cordova)](#发布更新-cordova) -* [补丁更新](#补丁更新) -* [促进更新](#促进更新) -* [回滚更新](#回滚更新) -* [查看发布历史](#查看发布历史) -* [清除发布历史](#清除发布历史) - -[[English Version]](./README.md) - -## 安装 - -* 安装 [Node.js](https://nodejs.org/) -* 安装 CodePush CLI: `npm install -g code-push-cli` - -## 快速开始 - -1. 使用CodePush CLI创建一个[CodePush 账号](#创建账号) -2. 注册你的CodePush[应用](#应用管理), 并[分享](#应用合作)给你团队的其它开发者 -3. 用[Cordova插件](http://github.com/Microsoft/cordova-plugin-code-push) 或 [React Native插件](http://github.com/Microsoft/react-native-code-push)配置好CodePush并指向你希望的部署环境 -4. [发布](#发布更新)更新 -5. 活的长而成功![详细资料](https://en.wikipedia.org/wiki/Vulcan_salute) - -## 创建账号 - -在你发布应用更新之前,你需要创建一个CodePush帐号。一旦你安装了Cli你就可以简单的使用如下命令来注册: - -``` -code-push register -``` - -这将会启动浏览器,要求验证你的Github或微软帐号。一旦验证成功,它将创建一个CodePush帐号跟你的Github或MSA相连,并生成一个访问密钥(Access Key),你可以拷贝/粘贴到CLI以便登录。 - -*注意:注册成功后,你就已经自动登录了。所以除非你明确登出了,否则你不需要在此机器上再次登录。* - -如果你已有一个帐号,那你还可以把你的帐号跟另一个身份认证提供商关联起来,通过运行: - -(我个人理解:CodePush提供商有Github和Mircosoft,它允许你可以把两个帐号关联起来。) - -``` -code-push link -``` - -注意:为了实现这个(关联)的目的,你在另一个身份认证供应商那边用的Email地址必须与你现存的帐号一致。 - -## 身份认证 - -在CodePush CLI里大多数命令需要身份认证,所以在你开始管理你的帐号之前,你需要使用GitHub或者微软帐号注册和登录。你可以通过执行如下命令做到这些: - -``` -code-push login -``` - -这将会启动浏览器,要求验证你的Github或微软帐号。这将生成一个访问密钥(Access Key),然后你可以拷贝/粘贴到CLI(它会提示你这样做)。这时你就认证成功了,并且可以关掉你的浏览器了。 - - -如果在任何时候你想确认你是否已经登录了,你可以运行如下命令来显示与你当前认证会话相关的e-mail帐号,而且这个身份提供者是连接到(如:GitHub)的。 - -```shell -code-push whoami -``` - -当你从CLI登录后,你的访问密钥(Access Key)就一直保存在你本地磁盘上,所以你不必每次使用帐号是都需要登录。为了终止会话或删除AccessKey,你可以简单的运行如下命令: - -``` -code-push logout -``` - -如果你在一台机器上忘记注销(比如:你朋友的电脑上),你可以使用如下命令列出和删除任何“激活中”的Access Keys。Access Keys列表将显示创建Key的机器名和发生登录的时间。这让你可以简单的认出那些你不想要保存的Keys。 - -``` -code-push access-key ls -code-push access-key rm -``` - -如果你需要额外的Keys,被用来验证CodePush服务而不需要给你的GitHub和/或访问微软凭证,您可以运行下面的命令来创建一个持久的Access Key(连同一个描述): - -``` -code-push access-key add "VSTS Integration" -``` -在创建新的密钥之后,您可以在`login`命令后使用`--accessKey`标志并指定其值,它允许您执行“无头”身份验证,而不是启动一个浏览器。 - -``` -code-push login --accessKey -``` -当使用这种方式登录时,密钥(Access Key)在注销时不会自动失效,它可以一直被使用,除非它从CodePush服务端明确被移除掉。然而,仍然建议一旦你完成了会话就注销掉,以便从本地磁盘移除掉你的授权证书。 - -## 应用管理 - -在你发布更新前,你需要用如下命令在CodePush服务上注册一个App: - -``` -code-push app add -``` - -如果你的App既有IOS又有Android,我们推荐你创建单独的App。一个平台一个。这样你可以单独的管理和发布更新,从长远来看这会让事情更简单。大部分人的命名约定会在App名加后缀`-IOS`和`-Android`。例如: - -``` -code-push app add MyApp-Android -code-push app add MyApp-iOS -``` - -所有新的Apps自动会出现有个部署环境(`Staging`和`Production`),为了你可以开始发布更新到不同的渠道而不需要做任何其它的事(参考下面的部署指南)。你创建一个App之后,CLI将显示`Staging`和`Production`环境的开发密钥,你就可以使用不同的SDKs(详细请看[Cordova](http://github.com/Microsoft/cordova-plugin-code-push) 和 [React Native](http://github.com/Microsoft/react-native-code-push))来配置你的手机端App了。 - -如果你不喜欢你之前取的名字,你还可以随时重命名它,使用如下命令: - -``` -code-push app rename -``` - -应用的名字从管理方面看只是为了能辨识,因此,必要时可以随时重命名它。它其实不会影响正在运行的应用程序,因为更新的查询都是通过部署密钥的。 - -如果你不想要一个App,你可以从服务端上移除它,命令如下: - -``` -code-push app rm -``` - -做这个移除请务必小心,因为任何配置了它的App都将停止收到更新了。 - -最后,如果你想列出你在CodePush服务上注册的所有Apps,你可以运行如下命令: - -``` -code-push app ls -``` - -## 应用合作 - -如果你讲和其它开发者在一起合作同一个CodePush应用,你可以把他们添加为合作者,使用如下命令: - -```shell -code-push collaborator add -``` - -*注意: 这个期待开发者已经用e-mail[注册](#创建账号)了CodePush,所以在打算分享应用之前确保他们已经准备好了一切。* - -一旦添加了,所有的合作者将立即拥有了最新分享App的如下权限: - -1. 查看App,它的合作者,[部署管理](#部署管理)和[查看发布历史](#查看发布历史)。 -1. [发布](#发布更新)更新到任何应用的部署环境。 -1. [促进](#促进更新)更新在任何应用部署环境之间。 -1. [回滚](#回滚更新)任何应用部署。 -1. [打补丁](#补丁更新)在任何应用部署里。 - -相反的,这就意味着一个合作者不能做任何如下的事情: - -1. 重命名或删除应用。 -1. 转让应用的所有权。 -1. 创建,重命名或删除新的部署环境。 -1. 清除一个部署历史。 -1. 添加或删除合作者。 - -*注意:一个合作的开发者可以移除他/她自己。* - -随着时间的推移,如果有人不再和你一起合作,那你可以解除合作者关系,使用如下命令: - -```shell -code-push collaborator rm -``` - -如果你想列出应用的所有合作者,你可以简单的运行如下命令: - -```shell -code-push collaborator ls -``` - -最后,如果在某刻你(作为App的拥有者)将不再开发App了,你想转让给其他开发者(或客户),你可以运行如下命令: - -```shell -code-push app transfer -``` - -*注意:就像`code-push collaborator add`命令一样,这期望新的拥有者已经用指定的e-mail注册了CodePush。* - -一经确认,该指定的开发者成为App的拥有者,而且立即接收到该角色的相关权限。除了拥有权转移外,其它的任何都没有被修改(比如:部署环境,发布历史,合作者)。这意味着你还仍然是该App的一个合作者,所以如果你想移除你自己,那你可以在成功转让拥有全后简单的运行`code-push collaborator rm`命令。 - -## 部署管理 - -从CodePush的角度来看,一个应用把一个或更多的东西简单命名分组称为“部署(环境)”。 - -While the app represents a conceptual "namespace" or "scope" for a platform-specific version of an app (e.g. the iOS port of Foo app), its deployments represent the actual target for releasing updates (for developers) and synchronizing updates (for end-users). Deployments allow you to have multiple "environments" for each app in-flight at any given time, and help model the reality that apps typically move from a dev's personal environment to a testing/QA/staging environment, before finally making their way into production. - -*注意: 正如你将在下面看到的`release`(发布),`promote`(提升),`rollback`(回滚)命令需要应用名字和部署名字,因为这两个组成一个独特的发布标识(例如:我想发布更新到我的IOS应用给beta环境的测试者们)。* - -当一个用CodePush服务注册的应用,它默认包含两个部署环境:`Staging`和`Production`。这让你可以理解发布更新到一个内部的环境,你可以在推送到终端用户之前彻底的测试每个更新。这个工作流是至关重要的,以确保你的版本准备好给大众,而且这是一个在Web上实践很久的惯例。 - -如果你的App有`Staging`和`Production`环境其实已经满足了你的需求,然后你不需要做任何事情。不过,如果你需要alpha,dev等部署环境,那你可以简单的使用如下命令创建: - -``` -code-push deployment add -``` - -就像Apps一样,你也可以删除或重命名部署环境,分别使用如下命令: - -``` -code-push deployment rm -code-push deployment rename -``` - -你可以在任何时候查看特定应用包含的部署环境列表,你可以简单的运行下面的命令: - -``` -code-push deployment ls [--displayKeys|-k] -``` - -这将不仅显示部署环境列表,而且还有元数据(例如:强制性属性,描述)和最新版本的安装指标: - -![Deployment list](https://cloud.githubusercontent.com/assets/116461/12526883/7730991c-c127-11e5-9196-98e9ceec758f.png) - -*注意: 因为他们很少用和需要屏幕,部署密钥默认是不显示的。如果你需要查看它们,只要在`deployment ls`命令后面加上`-k`标识即可。* - -安装指标有如下意义: - -* **Active(激活)** - 成功安装的数量目前运行这个版本。这个数字将会随着用户更新到或离开这个版本分别增加或减少。 - -* **Total** - 该版本更新收到的所有成功安装的总数。这个数字只会随新用户/设备安装它而增加,所以它是__激活__的超集。 - -* **Pending** - 更新被下载了但还没安装的数量。This would only apply to updates that aren't installed immediately, and helps provide the broader picture of release adoption for apps that rely on app resume and restart to apply an update. - -* **Rollbacks** - 该版本被自动回滚的次数。理想情况下这个数应该为0,而且在这种情况下这个量是不会显示的。然而,如果你发布了一个包含严重问题(Crash)的更新,CodePush插件将在安装时回滚到上一个版本,同时把问题反馈到服务端。这可以让终端用户依旧能用,不被损坏的版本阻塞住,而且能够在CLI里看到这些,你可以鉴定错误的版本并且能在服务器上做出[回滚](#回滚更新)的响应。 - -* **Rollout** - 显示有资格接收更新的百分比。这个属性只会被显示在那些`激活的`的首次展示的版本,所以,首次展示百分比是小于100%。此外, 因为一个部署任何时候只能有一个激活的首次展示,这个标签只会被显示在最新的一次部署里。 - -* **Disabled** - 标示是否该版本被标记成失效的,因此用户是否可下载。这个属性只有在版本真实失效时才显示。 - -当度量(metrics)单元格统计为`No installs recorded`(无安装记录),那是表示这个版本在服务器上没有任何活动记录。这可能要么是因为被插件阻止了,或者用户还没有跟CodePush服务器同步。一旦发生了安装,你将在CLI里看到该版本的度量。 - -## 发布更新 - -一旦你的App被配置了从CodePush服务器查询版本更新,你就可以向它开始发布。为了简易性和灵活性,CodePush CLI包含三种不同的发布命令: - -1. [通用](#发布更新-general) - 使用外部的工具或构建脚本(如:Gulp任务,`react-native bundle`命令)像CodePush服务器发布一个更新。这对装配进目前的工作流而言提供最灵活的方式,因为它严格按CodePush特性的步骤处理,而把App特性的编译过程留给你。 - -2. [React Native](#发布更新-react-native) - 跟通用发布命令一样执行相同的功能,但是还会为你生成的应用更新内容(JS包和资源),而不需要你运行`react-native bundle`,然后执行`code-push release`。 - -3. [Cordova](#发布更新-cordova) - 跟通用发布命令一样执行相同的功能,但也会为你处理准备应用更新的任务,而不需要你运行`cordova prepare`,然后执行`code-push release`。 - -你应该使用哪个命令主要是一种需求或偏好的事。然而,我们通常推荐使用相关的特定平台的命令开始(因为它大大简化了体验),然后当有更大控制必要时用通用的`release`命令。 - -### 发布更新 (General) - -``` -code-push release -[--deploymentName ] -[--description ] -[--disabled ] -[--mandatory] -[--rollout ] -``` - -#### App name (应用名)参数 - -指定将发布更新的CodePush 应用名。这个与最初你调用`code-push app add`(如:"MyApp-Android")的名字保持一致。如果你想查一下,可以运行`code-push app ls`命令看看应用的列表。 - -#### Update contents (更新内容)参数 - -指定应用更新的代码和资源位置。你可以提供要么一个单独文件(如:React Native的JS bundle文件),或者一个文件夹路径(如:Cordova应用的`/platforms/ios/www`文件夹)。注意你不需要为了部署更新而对文件或文件夹进行Zip压缩,因为CLI会帮你自动ZIP压缩。 - -重要的是你指定的路径是跟特定平台相关的,准备/打包你的应用。下面表格概括了在发布前你应该运行哪个命令,以及你以后可以参考的`updateContents` 参数路径: - -| 平台 | 准备命令(Prepare command ) | 包的路径 (相对项目的根目录) | -|-------------------------------------|--------------------------------------------|-------------------------------------------------------------------------------------------------------------| -| Cordova (Android) | `cordova prepare android` | `./platforms/android/assets/www` 目录 | -| Cordova (iOS) | `cordova prepare ios` | `./platforms/ios/www ` 目录 | -| React Native wo/assets (Android) | `react-native bundle --platform android --entry-file --bundle-output --dev false` | `--bundle-output` 参数的值 | -| React Native w/assets (Android) | `react-native bundle --platform android --entry-file --bundle-output / --assets-dest --dev false` | `--assets-dest` 参数的值,应该是一个包含资源和JS bundle的新创建的目录。| -| React Native wo/assets (iOS) | `react-native bundle --platform ios --entry-file --bundle-output --dev false` | `--bundle-output` 参数的值 | -| React Native w/assets (iOS) | `react-native bundle --platform ios --entry-file --bundle-output / --assets-dest --dev false` | `--assets-dest` 参数的值,应该是一个包含资源和JS bundle的新创建的目录。| - -#### Target binary version (目标二进制版本)参数 - -这是你想发布更新的特定仓库/二进制版本,这样只有那个版本上的用户才会接收到更新,而那些运行较老/新版本用户则不会。这样很有用,原因如下: - -1. 如果有用户运行一个很老的版本,有可能在CodePush的更新里有个破坏性的更新,这跟他们现在运行的版本不兼容。 - -2. 如果用户正在运行一个新的二进制版本,那么假定,他们正在运行并更新CodePush 更新(可能不兼容)。 - -如果你想更新应用商店里二进制文件的多个版本,我们允许你指定参数像这样[语义版本范围表达式](https://github.com/npm/node-semver#advanced-range-syntax)。这样, 任何在版本号范围内(如:`semver.satisfies(version, range)` returns `true`)的客户端设备都能获得更新。 - -如下是有效的版本号范围表达式的例子: - -| 范围表达式 | 谁获得更新 | -|------------------|----------------------------------------------------------------------------------------| -| `1.2.3` | 只有`1.2.3`版本 | -| `*` | 所有版本 | -| `1.2.x` | 主版本为1,小版本为2的任何版本 | -| `1.2.3 - 1.2.7` | 在 `1.2.3` (包含) 和 `1.2.7` (包含) 之间的版本 | -| `>=1.2.3 <1.2.7` | 在 `1.2.3` (包含) 和 `1.2.7` (不包含)之间的版本 | -| `~1.2.3` | 相当于`>=1.2.3 <1.3.0` | -| `^1.2.3` | 相当于`>=1.2.3 <2.0.0` | - -*注意:如果语义表达式以特殊字符开始如`>`,`^`或***,如果你没有用引号括起来的话命令可能执行不对,因为shell在CLI里不支持右边的值。所以,当调用`release`命令时最好能把你的`targetBinaryVersion`参数用双引号括起来,如:`code-push release MyApp updateContents ">1.2.3"`。* - -如下表格分别概括了每个应用类型的CodePush更新的语义版本范围的版本值: - -| 平台 | 应用商店版本来源 | -|------------------------|------------------------------------------------------------------------------| -| Cordova | 在`config.xml`文件里的`` 属性 | -| React Native (Android) | 在`build.gradle`文件里 `android.defaultConfig.versionName` 属性 | -| React Native (iOS) | 在`Info.plist`文件里的`CFBundleShortVersionString` 键 | -| React Native (Windows) | 在`Package.appxmanifest`文件的 `` 键 | - -*注意:如果在元数据文件里的应用版本号漏掉补丁版本值,如`2.0`,它将被当成补丁版本值为`0`,如:`2.0 当成 2.0.0`.* - -#### Deployment name (部署环境名)参数 - -这是你想发布更新到的那个指定部署环境名。默认为`Staging`(临时环境),但是当你准备部署到`Production`(生产环境)或一个你自定义的部署环境时,你只要指明设置这个参数即可。 - -*注意:这个参数可以用"--deploymentName" 或 "-d"来设置。* - -#### Description (描述)参数 - -给部署提供一个可选的"更新日志"。当被检测到有更新时这个值就会完整的传到客户端,所以你的应用可以选择显示给终端用户(如:通过一个`哪些新东西?`的对话框)。这个字符串可以接受控制字符如`\n` 和 `\t`,以便你可以包含空白格式在你的描述里来提高可读性。 - -*注意:这个参数可以用"--description" 或 "-desc"来设置。* - -#### Mandatory (强制性)参数 - -这个标识该更新是否是强制性的(如:包含一个严重的安全修复)。这个属性简单的传到客户端,然后客户端决定是否要强制更新。 - -*注意: 这个参数是简单的一个"标记",所以,没有该标记表示版本更新可选,如果有标记则表示版本是强制更新的。你可以给它赋值(如:`--mandatory true`),但其实简单的`--mandatory`就已能标识强制更新了。* - -强制属性是唯一的,因为服务端必要时将动态修改它,为了确保你对终端用户的版本更新语义上的维护。例如:设想你的应用有如下3个更新: - -| 版本 | 强制? | -|---------|------------| -| v1 | No | -| v2 | Yes | -| v3 | No | - -如果用户当前是`v1`版本,然后从服务端查询更新,将以`v3`(因为这是最新的)响应,但是它将动态将这个版本转变成强制的,因为中间有一个强制更新的版本。这个行为很重要因为`v3`的代码是在`v2`上增加的,所以任何没有获取`v2`版本的都会不管`v2`的强制,而继续让`v3`变成强制更新版本。 - -如果用户当前是`v2`版本,然后从服务器查询更新,响应结果为`v3`,但会留着这个版本作为可选的。这个因为他们已经接受了强制更新,所以没有必要去修改`v3`。这样的行为就是为什么我们说服务器会"动态改变"强制标签,因为随着版本的迭代,新版本的强制属性总会保存你设置的这个值。当有一个版本更新检查要响应给用户时,它只会在相邻的版本上改变。 - -如果你从没发布一个强制的更新,那么上面的行为不会应用到你,因为服务器从不改变一个可选的版本为强制版本,除非有像上面阐述的那样掺杂了强制版本。此外,如果一个版本标记成强制了,它决不会被转变成可选的,因为那没有任何意义。为了尊重上面描述的语义,服务器将只会把一个可选的发布改变为强制的。 - -*注意:这个参数可以用`--mandatory` 或 `-m`来设置* - -#### Rollout 参数 - -**重要:为了使这个参数有效,终端用户需要运行CodePush插件的`1.6.0-beta+`版本 (Cordova) 或 `1.9.0-beta+`版本 (React Native)。如果你发布了一个指明了首次展示(Rollout)属性的更新,那么运行老版本的Cordova或ReactNative用户不会更新。因此,直到你已经采取了必要CodePush SDK的版本,否则我们不建议设置一个首次展示(rollout)版本,因为没有人会接受它。** - -这指定了可以接收这次更新的用户百分比(在`1`到`100`之间的数字)。这会是有帮助的,假如你想在每个人广泛获取之前,"飞行"一个新版本给部分的受众(如:25%) ,并且得到异常/崩溃的反馈观察。如果没有设置这个参数,它会设置为`100%`,所以,你只需要在你想实际限制多少用户能接收时去设置它。 - -当借用首次展现(rollout)能力,要记住一些额外注意事项: - -1. 你不可以在最新版本的首次展示是"有效的"(如:首次展示值非空)的部署环境上发布新更新。在你在部署环境上发布进一步更新之前,首次展示属性需要是"完全的"(如:设置`roullout`属性为`100`)。 - -2. 如果你回滚部署环境,它的最新版本的首次展示是"有效的",那首次展示的值将被清除,实际上"禁止"首次展示行为。 - -3. 不像`mandatory`和`description`字段,当从一个部署环境中促进发布时,它将不会传送`rollout`属性,所以,如果你想新的发布(在目标部署环境里)有首次展示的值,那么你需要在调用`promote`命令时明确的设置它。 - -*注意:这个参数可以用 `--rollout` or `-r` 来设置* - -#### Disabled 参数 - -这个指明一个版本更新是否可以被用户下载。如果没有指定,版本更新不会是无效的(如:用户将要下载的那一刻你的应用称为`同步`)。如果你想发布一个更新但不是立即生效,那么这个参数是有价值的,直到你明确用[补丁](#补丁更新)发布,当你要让用户能够下载(如:公告博客上线)。 - -*注意:这个参数可以用 "--disabled" or "-x"来设置* - -### 发布更新 (React Native) - -```shell -code-push release-react -[--bundleName ] -[--deploymentName ] -[--description ] -[--development ] -[--disabled ] -[--entryFile ] -[--mandatory] -[--sourcemapOutput ] -[--targetBinaryVersion ] -[--rollout ] -``` -`release-react`命令是React Native特有的[`发布`](#发布更新)命令,支持相同的所有参数(如:`--mandatory`,`--description`),然而通过如下额外的动作简化了发布更新过程: - -1. 运行`react-native bundle`命令去生成将要发布到CodePush服务的[更新](#update-contents-params)(JS Bundle和资源)。它尽可能使用合理的默认值(如:创建一个non-dev构建,假设一个iOS入口文件被命名为“index.ios.js”),但也暴露了有关`react-native bundle`参数使得灵活(如:`--sourcemapOutput`)。 - -2. 通过使用定义在项目文件`info.plist`(IOS)和`build.gradle`(Android)里的版本名,推断[`targetBinaryVersion`](#target-binary-version-目标二进制版本-参数)的值。 - -为了阐述`release-react`命令产生的差异,如下的例子是你可能如何生成和发布一个React Native应用版本更新,通过使用`release`命令: - -```shell -mkdir ./CodePush - -react-native bundle --platform ios \ ---entry-file index.ios.js \ ---bundle-output ./CodePush/main.jsbundle \ ---assets-dest ./CodePush \ ---dev false - -code-push release MyApp ./CodePush 1.0.0 -``` - -用`release-react`命令实现等效的行为只需简单的如下的命令,这个通常更,这是通常更少出错: - -```shell -code-push release-react MyApp ios -``` - -*注意:我们相信`release-react`命令对大多数React Native的开发者是有价值的,所以如果你发现它不够灵活或者缺少关键功能,不要犹豫请[让我们知道](mailto:codepushfeed@microsoft.com),以便我们可以提高它。* - -#### App name 参数 - -这个参数跟[上面章节](#App-name-应用名-参数)描述的一样。 - -#### Platform 参数 - -指定当前的更新是哪个平台的,可以是`android`, `ios`, 或`windows`(不区分大小写)。 - -#### Deployment name 参数 - -相同的参数跟 [上面的章节](#deployment-name-参数)描述一样。 - -#### Description 参数 - -相同的参数跟 [上面的章节](#description-参数)描述一样。 - -#### Mandatory 参数 - -相同的参数跟 [上面的章节](#mandatory-参数)描述一样。 - -#### Rollout 参数 - -相同的参数跟 [上面的章节](#rollout-参数)描述一样。如果没有指定,版本将对所有用户有效可下载。 - -#### Target binary version 参数 - -相同的参数跟 [上面的章节](#target-binary-version-参数)描述一样。如果没有指定,默认使用`Info.plist` (iOS) and `build.gradle` (Android)文件里指定的精确版本号。 - -#### Disabled 参数 - -相同的参数跟 [上面的章节](#disabled-参数))描述一样。 - -#### Development 参数 - -这个指明是否要生成一个非最小化,开发的JS bundle文件。如果没有指明,默认是`false`,禁用警告提示并且bundle文件是最小化的。 - -*注意:这个参数可以配置成`--development` 或`--dev`* - -#### Entry file 参数 - -指明相对应用根目录的路径入口JavaScript 文件。如果没有指定,默认是:如果存在`index.ios.js`(IOS), `index.android.js`(Android), 或者`index.windows.bundle`(Windows),否则`index.js`。 - -*注意:参数可以配置成`--entryFile`或`-e`* - -#### Bundle name 参数 - -指明生成JS Bundle的文件名。如果没有指定,特定平台将会用的标准bundle名字:`main.jsbundle` (iOS), `index.android.bundle` (Android) and `index.windows.bundle` (Windows). - -*注意:参数可以配置成`--bundleName`或`-b`* - -#### Sourcemap output 参数 - -指明生成的JS bundle 的sourcemap写入的相对路径。如果没有指定,sourcemaps文件不会生成。 - -*注意:参数可以配置成`--sourcemapOutput`或`-s`* - -### 发布更新 (Cordova) - -```shell -code-push release-cordova -[--deploymentName ] -[--description ] -[--mandatory] -[--targetBinaryVersion ] -[--rollout ] -[--build] -``` - -这个`release-cordova` 命令是Cordova特有的[`发布`](#发布更新)命令,支持相同的所有参数(如:`--mandatory`,`--description`),然而通过如下额外的动作简化了发布更新过程: - -1. 运行`cordova prepare`命令去生成将要发布到CodePush服务的[更新内容](#update-contents-更新内容-参数) (`www` 文件夹) 。 - -2. 通过使用定义在项目文件`config.xml`文件里的版本名,推断[`targetBinaryVersion`](#target-binary-version-目标二进制版本-参数) 的值。 - -为了阐述`release-cordova`命令产生的差异,如下的例子是你可能如何生成和发布一个Cordova应用版本更新,通过使用`release`命令: - - -```shell -cordova prepare ios -code-push release MyApp ./platforms/ios/www 1.0.0 -``` - -用`release-cordova`命令实现等效的行为只需简单的如下的命令,这个通常更,这是通常更少出错: - -```shell -code-push release-cordova MyApp ios -``` - -注意:我们相信`release-cordova`命令对大多数Cordova的开发者是有价值的,所以如果你发现它不够灵活或者缺少关键功能,不要犹豫请[让我们知道](mailto:codepushfeed@microsoft.com),以便我们可以提高它。 - -#### App name 参数 - -这个参数跟[上面章节](#App-name-应用名-参数)描述的一样。 - -#### Platform 参数 - -指定当前的更新是哪个平台的,可以是`ios`或`android`(不区分大小写)。 - -#### Deployment name 参数 - -相同的参数跟 [上面的章节](#deployment-name-参数)描述一样。 - -#### Description 参数 - -相同的参数跟 [上面的章节](#description-参数)描述一样。 - -#### Mandatory 参数 - -相同的参数跟 [上面的章节](#mandatory-参数)描述一样。 - -#### Rollout 参数 - -相同的参数跟 [上面的章节](#rollout-参数)描述一样。如果没有指定,版本将对所有用户有效可下载。 - -#### Target binary version 参数 - -相同的参数跟 [上面的章节](#target-binary-version-参数)描述一样。如果没有指定,默认使用项目元数据里指定的(`Info.plist` (iOS) and `build.gradle` (Android)版本号。 - -#### Disabled 参数 - -相同的参数跟 [上面的章节](#disabled-参数))描述一样。 - -#### Build 参数 - -当你生成版本更新的web资源时,指定是否想用`cordova build`来取代`cordova prepare`(默认行为)。这是有价值的,假设你的项目包含构建钩子(如:转换TypeScript),所以CodePush简单的运行`cordova prepare`不够充分的创建和发布更新。如果没有指定,它默认是`false`。 - -*注意:这个参数可以用`--build`或`-b`来设置* - -## 补丁更新 - -在发布更新之后,可能有这样的场景,你需要修改一个或多个相关的属性(如:你忘记给一个严重的Bug修复打上强制标记了,你想增加更新的首次展示百分比)。你可以很容易的用下面的命令行来实现: - -```shell -code-push patch -[--label ] -[--mandatory ] -[--description ] -[--rollout ] -[--disabled ] -[--targetBinaryVersion ] -``` - -抛开`appName` 和 `deploymentName`,所有参数是可选的,所以,你可以用这个命令一次性更新单个或者所有属性。调用`patch`命令而不指定任何属性将不产生任何操作结果。 - -```shell -# Mark the latest production release as mandatory -code-push patch MyApp Production -m - -# Increase the rollout for v23 to 50% -code-push patch MyApp Production -l v23 -rollout 50% -``` - -### Label 参数 - -表明你想在指定的部署环境里更新哪个发布版本(如:`v23`)。如果省略了,那要求的变化将应用到指定的部署环境的最新版本上。为了查看你想更新的版本标签,你可以运行`code-push deployment history`命令并参见`Label`列。 - -*注意:这个参数可以设置成`--label`或`-l`* - -### Mandatory 参数 - -同样的参数跟[上面的章节](#mandatory-参数)描述一致,简单的允许你更改这个版本是否考虑强制更新。注意`--mandatory`和`--mandatory true`是同等的,但是缺少这个标记不等于`--mandatory false`。所以,如果参数省略了,对目标版本的强制性属性来说不会产生任何改变。你需要设置`--mandatory false`去明确的标识版本是可选的。 - -### Description 参数 - -同样的参数跟[上面的章节](#description-参数)描述一致,简单的允许你更改关联版本的描述(如:你在发布时写了个错别字,或者你完全忘记添加一个描述了)。如果参数省略掉了,那么对于目标版本的描述属性来说不会有任何改动。 - -### Disabled 参数 - -同样的参数跟[上面的章节](#disabled-参数)描述一致,简单的允许你去更改发布的版本是否无效。注意`--disabled`和`--disabled true`是等同的,但是缺省这个标识不等于`--disabled false`。所以,如果忽略了该参数,并不会对目标版本的无效(disabled)属性有修改。你需要设置`--disabled false`去明确标识一个以前无效的版本有效。 - -### Rollout 参数 - -同样的参数跟[上面的章节](#rollout-参数)描述一致,简单的允许你去__增加__目标版本首次展示的百分比。这个参数只能设成一个比当前首次展示值要大的数字。此外,如果你想"完全的"首次展示,因此,让版本对每个人有效,你可以简单的设置参数`--rollout 100`,如果省略了这个参数,目标版本的首次展示(rollout)属性不会有任何改动。 - -此外,上面提到的,当你发布版本时没有指定首次展示(rollout)的值时,它相当于是被设置成了`100`。因此,如果你发布一个没有首次展示的更新,那你不可以通过`patch`命令改变rollout属性,因为那样是被认为在降低首次展示(rolltout)百分比。 - -### Target binary version 参数 - -同样的参数跟[上面的章节](#target-binary-version-参数)描述一致,简单的允许你去更改语义版本范围表明兼容哪个版本版本。这个可以很有用,如果你在最初发布时犯了个错(如:你指定`1.0.0`但本意`1.1.0`)或你想增加或减少版本支持的版本范围(如:你发现一个版本总是不能在`1.1.2`版本上正常运行)。如果省略了这个参数,目标版本的版本号属性不会有任何改动。 - -```shell -# 给意境存在的版本添加一个"最大二进制版本"范围 -# by scoping its eligibility to users running >= 1.0.5 -code-push patch MyApp Staging -t "1.0.0 - 1.0.5" -``` - -## 促进更新 - -一旦测试完指定部署环境的版本更新(如:`Staging`),你想把它向下游推进(如:dev->staging, staging->production),你可以简单的用如下命令去从一个部署环境拷贝到另一个: - -``` -code-push promote -[--description ] -[--disabled ] -[--mandatory] -[--rollout ] -[--targetBinaryVersion -code-push rollback MyApp Production -``` - -这个的影响是在部署环境里创建一个包含**精确的代码和资源**的新版本,比最新版本更优先的一个版本。举个例子,想象你发布了如下更新: - -| 版本 | 描述 | 强制 | -|---------|-------------------|-----------| -| v1 | 初始化版本! | Yes | -| v2 | 添加新功能 | No | -| v3 | 修复Bugs | Yes | - -如果你在部署环境里运行`rollback`命令,一个包含`v2`版本内容的新的版本(`v4`)将会被创建。 - -| 版本 | 描述 | 强制 | -|---------|-------------------|-----------| -| v1 | 初始化版本! | Yes | -| v2 | 添加新功能 | No | -| v3 | 修复Bugs | Yes | -| v4 (从v3回滚到v2) | 添加新功能 | No | - -当app执行版本检查时,已经获得`v3`版本的用户现在被"回滚"到`v2`版本。此外,任何仍运行在`v2`版本的用户,因而将不会捕获到`v3`版本,因为他/她们已经在运行最新的版本(这就是为什么我们使用附加在版本标签里的包的hash来做版本检查)。 - - -如果你想回滚部署环境到一个版本而不是前一个版本(如:`v3` -> `v2`),你可以指定一个可选的`--targetRelease`参数: - -``` -code-push rollback MyApp Production --targetRelease v34 -``` - -*注意:由回滚产生的版本将会在`deployment history`命令的输出里被注释,以便助于更容易被辨识出来。* - -## 查看发布历史 - -你可以使用如下命令查看某个应用的部署环境里最多50条最新的发布历史: - -``` -code-push deployment history -``` - -这个历史纪录将显示每个版本的所有的属性(如:标签,强制性),也会标明任何版本是否由提升(promotion)或是回滚操作而来。 - -![Deployment History](https://cloud.githubusercontent.com/assets/696206/11605068/14e440d0-9aab-11e5-8837-69ab09bfb66c.PNG) - -此外,历史记录显示每个版本的安装指标。你可以在文档的上面`deployment ls`命令处查看指标数据的解释明细。 - -默认情况下,历史纪录不会显示各个版本的作者,但是如果你是和其它开发者合作的,而且想看每个更新是谁发布的,那你可以给历史命令传额外的`--displayAuthor`(或`-a`)标记。 - -*注意:历史命令可以使用"h"别名来运行* - -## 清除发布历史 - -你可以用如下命令清除相关的发布历史: - -``` -code-push deployment clear -``` - -运行此命令后,那些已经配置了使用关联的部署密钥的客户端设备将不再接收被清除掉的更新。这个命令是不可逆的,因此不应该使用在生产部署。 diff --git a/cli/README.md b/cli/README.md deleted file mode 100644 index 6b4181c3..00000000 --- a/cli/README.md +++ /dev/null @@ -1,829 +0,0 @@ -# CodePush Management CLI - -CodePush is a cloud service that enables Cordova and React Native developers to deploy mobile app updates directly to their users' devices. It works by acting as a central repository that developers can publish updates to (JS, HTML, CSS and images), and that apps can query for updates from (using the provided client SDKs for [Cordova](http://github.com/Microsoft/cordova-plugin-code-push) and [React Native](http://github.com/Microsoft/react-native-code-push)). This allows you to have a more deterministic and direct engagement model with your user base, when addressing bugs and/or adding small features that don't require you to re-build a binary and re-distribute it through the respective app stores. - -![CodePush CLI](https://cloud.githubusercontent.com/assets/116461/16246693/2e7df77c-37bb-11e6-9456-e392af7f7b84.png) - -* [Installation](#installation) -* [Getting Started](#getting-started) -* [Account Management](#account-management) - * [Authentication](#authentication) - * [Access Keys](#access-keys) - * [Proxy Support](#proxy-support) -* [App Management](#app-management) - * [App Collaboration](#app-collaboration) - * [Deployment Management](#deployment-management) -* [Releasing Updates](#releasing-updates) - * [Releasing Updates (General)](#releasing-updates-general) - * [Releasing Updates (React Native)](#releasing-updates-react-native) - * [Releasing Updates (Cordova)](#releasing-updates-cordova) -* [Debugging CodePush Integration](#debugging-codepush-integration) -* [Patching Update Metadata](#patching-update-metadata) -* [Promoting Updates](#promoting-updates) -* [Rolling Back Updates](#rolling-back-updates) -* [Viewing Release History](#viewing-release-history) -* [Clearing Release History](#clearing-release-history) - -[[Chinese version 中文版]](./README-cn.md) - -## Installation - -* Install [Node.js](https://nodejs.org/) -* Install the CodePush CLI: `npm install -g code-push-cli` - -## Getting Started - -1. Create a [CodePush account](#account-creation) push using the CodePush CLI -2. Register your [app](#app-management) with CodePush, and optionally [share it](#app-collaboration) with other developers on your team -3. CodePush-ify your app and point it at the deployment you wish to use ([Cordova](http://github.com/Microsoft/cordova-plugin-code-push) and [React Native](http://github.com/Microsoft/react-native-code-push)) -4. [Release](#releasing-updates) an update for your app -5. Check out the [debug logs](#debugging-codepush-integration) to ensure everything is working as expected -6. Live long and prosper! ([details](https://en.wikipedia.org/wiki/Vulcan_salute)) - -## Account Management - -Before you can begin releasing app updates, you need to create a CodePush account. You can do this by simply running the following command once you've installed the CLI: - -``` -code-push register -``` - -This will launch a browser, asking you to authenticate with either your GitHub or Microsoft account. Once authenticated, it will create a CodePush account "linked" to your GitHub/MSA identity, and generate an access key you can copy/paste into the CLI in order to login. - -*Note: After registering, you are automatically logged-in with the CLI, so until you explicitly log out, you don't need to login again from the same machine.* - -If you have an existing account, you may also link your account to another identity provider (e.g. Microsoft, GitHub) by running: - -``` -code-push link -``` - -*Note: In order to link multiple accounts, the email address associated with each provider must match.* - -### Authentication - -Most commands within the CodePush CLI require authentication, and therefore, before you can begin managing your account, you need to login using the GitHub or Microsoft account you used when registering. You can do this by running the following command: - -```shell -code-push login -``` - -This will launch a browser, asking you to authenticate with either your GitHub or Microsoft account. This will generate an access key that you need to copy/paste into the CLI (it will prompt you for it). You are now successfully authenticated and can safely close your browser window. - -If at any time you want to determine if you're already logged in, you can run the following command to display the e-mail address associated with your current authentication session, which identity providers your account is linked to (e.g. GitHub) and any previously set proxy: - -```shell -code-push whoami -``` - -When you login from the CLI, your access key is persisted to disk for the duration of your session so that you don't have to login every time you attempt to access your account. In order to end your session and delete this access key, simply run the following command: - -```shell -code-push logout -``` - -If you forget to logout from a machine you'd prefer not to leave a running session on (e.g. your friend's laptop), you can use the following commands to list and remove any current login sessions. - -```shell -code-push session ls -code-push session rm -``` - -### Access Keys - -If you need to be able to authenticate against the CodePush service without launching a browser and/or without needing to use your GitHub and/or Microsoft credentials (e.g. in a CI environment), you can run the following command to create an "access key" (along with a name describing what it is for): - -```shell -code-push access-key add "VSTS Integration" -``` - -By default, access keys expire in 60 days. You can specify a different expiry duration by using the `--ttl` option and passing in a [human readable duration string](https://github.com/jkroso/parse-duration#parsestr) (e.g. "2d" => 2 days, "1h 15 min" => 1 hour and 15 minutes). For security, the key will only be shown once on creation, so remember to save it somewhere if needed! - -After creating the new key, you can specify its value using the `--accessKey` flag of the `login` command, which allows you to perform "headless" authentication, as opposed to launching a browser. - -```shell -code-push login --accessKey -``` - -When logging in via this method, the access key will not be automatically invalidated on logout, and can be used in future sessions until it is explicitly removed from the CodePush server or expires. However, it is still recommended that you log out once your session is complete, in order to remove your credentials from disk. - -Finally, if at any point you need to change a key's name and/or expiration date, you can use the following command: - -```shell -code-push access-key patch --name "new name" --ttl 10d -``` - -*NOTE: When patching the TTL of an existing access key, its expiration date will be set relative to the current time, with no regard for its previous value.* - -### Proxy Support - -By default, the `login` command will automatically look for a system-wide proxy, specified via an `HTTPS_PROXY` or `HTTP_PROXY` environment variable, and use that to connect to the CodePush server. If you'd like to disable this behavior, and have the CLI establish a direct connection to CodePush, simply specify the `--noProxy` parameter when logging in: - -```shell -code-push login --noProxy -``` - -I'd you like to explicitly specify a proxy server that the CodePush CLI should use, without relying on system-wide settings, you can instead pass the `--proxy` parameter when logging in: - -```shell -code-push login --proxy https://foo.com:3454 -``` - -Once you've logged in, any inferred and/or specified proxy settings are persisted along with your user session. This allows you to continue using the CodePush CLI without needing to re-authenticate or re-specify your preferred proxy. If at any time you want to start or stop using a proxy, simply logout, and then log back in with the newly desired settings. - -Additionally, if at any time you want to see what proxy settings (if any) are being used for your current login setting, simply run the `code-push whoami` command, which will display whether you're using, or explicitly bypassing a proxy. - -![ignoredproxy](https://cloud.githubusercontent.com/assets/116461/16537275/5166abf8-3fb3-11e6-930b-fb6a8164c65d.PNG) - -## App Management - -Before you can deploy any updates, you need to register an app with the CodePush service using the following command: - -``` -code-push app add -``` - -If your app targets both iOS and Android, we recommend creating separate apps with CodePush. One for each platform. This way, you can manage and release updates to them separately, which in the long run, tends to make things simpler. The naming convention that most folks use is to suffix the app name with `-iOS` and `-Android`. For example: - -``` -code-push app add MyApp-Android -code-push app add MyApp-iOS -``` - -All new apps automatically come with two deployments (`Staging` and `Production`) so that you can begin distributing updates to multiple channels without needing to do anything extra (see deployment instructions below). After you create an app, the CLI will output the deployment keys for the `Staging` and `Production` deployments, which you can begin using to configure your mobile clients via their respective SDKs (details for [Cordova](http://github.com/Microsoft/cordova-plugin-code-push) and [React Native](http://github.com/Microsoft/react-native-code-push)). - -If you decide that you don't like the name you gave to an app, you can rename it at any time using the following command: - -``` -code-push app rename -``` - -The app's name is only meant to be recognizable from the management side, and therefore, you can feel free to rename it as necessary. It won't actually impact the running app, since update queries are made via deployment keys. - -If at some point you no longer need an app, you can remove it from the server using the following command: - -``` -code-push app rm -``` - -Do this with caution since any apps that have been configured to use it will obviously stop receiving updates. - -Finally, if you want to list all apps that you've registered with the CodePush server, -you can run the following command: - -``` -code-push app ls -``` - -### App Collaboration - -If you will be working with other developers on the same CodePush app, you can add them as collaborators using the following command: - -```shell -code-push collaborator add -``` - -*NOTE: This expects the developer to have already [registered](#account-creation) with CodePush using the specified e-mail address, so ensure that they have done that before attempting to share the app with them.* - -Once added, all collaborators will immediately have the following permissions with regards to the newly shared app: - -1. View the app, its collaborators, [deployments](#deployment-management) and [release history](#viewing-release-history) -1. [Release](#releasing-updates) updates to any of the app's deployments -1. [Promote](#promoting-updates) an update between any of the app's deployments -1. [Rollback](#rolling-back-undesired-updates) any of the app's deployments -1. [Patch](#updating-existing-releases) any releases within any of the app's deployments - -Inversely, that means that an app collaborator cannot do any of the following: - -1. Rename or delete the app -1. Transfer ownership of the app -1. Create, rename or delete new deployments within the app -1. Clear a deployment's release history -1. Add or remove collaborators from the app (*) - -*NOTE: A developer can remove him/herself as a collaborator from an app that was shared with them.* - -Over time, if someone is no longer working on an app with you, you can remove them as a collaborator using the following command: - -```shell -code-push collaborator rm -``` - -If at any time you want to list all collaborators that have been added to an app, you can simply run the following command: - -```shell -code-push collaborator ls -``` - -Finally, if at some point, you (as the app owner) will no longer be working on the app, and you want to transfer it to another developer (or a client), you can run the following command: - -```shell -code-push app transfer -``` - -*NOTE: Just like with the `code-push collaborator add` command, this expects that the new owner has already registered with CodePush using the specified e-mail address.* - -Once confirmed, the specified developer becomes the app's owner and immediately receives the permissions associated with that role. Besides the transfer of ownership, nothing else about the app is modified (e.g. deployments, release history, collaborators). This means that you will still be a collaborator of the app, and therefore, if you want to remove yourself, you simply need to run the `code-push collaborator rm` command after successfully transferring ownership. - -### Deployment Management - -From the CodePush perspective, an app is simply a named grouping for one or more things called "deployments". While the app represents a conceptual "namespace" or "scope" for a platform-specific version of an app (e.g. the iOS port of Foo app), its deployments represent the actual target for releasing updates (for developers) and synchronizing updates (for end-users). Deployments allow you to have multiple "environments" for each app in-flight at any given time, and help model the reality that apps typically move from a dev's personal environment to a testing/QA/staging environment, before finally making their way into production. - -*NOTE: As you'll see below, the `release`, `promote` and `rollback` commands require both an app name and a deployment name is order to work, because it is the combination of the two that uniquely identifies a point of distribution (e.g. I want to release an update of my iOS app to my beta testers).* - -Whenever an app is registered with the CodePush service, it includes two deployments by default: `Staging` and `Production`. This allows you to immediately begin releasing updates to an internal environment, where you can thoroughly test each update before pushing them out to your end-users. This workflow is critical for ensuring your releases are ready for mass-consumption, and is a practice that has been established in the web for a long time. - -If having a staging and production version of your app is enough to meet your needs, then you don't need to do anything else. However, if you want an alpha, dev, etc. deployment, you can easily create them using the following command: - -``` -code-push deployment add -``` - -Just like with apps, you can remove and rename deployments as well, using the following commands respectively: - -``` -code-push deployment rm -code-push deployment rename -``` - -If at any time you'd like to view the list of deployments that a specific app includes, you can simply run the following command: - -``` -code-push deployment ls [--displayKeys|-k] -``` - -This will display not only the list of deployments, but also the update metadata (e.g. mandatory, description) and installation metrics for their latest release: - -![Deployment list](https://cloud.githubusercontent.com/assets/116461/12526883/7730991c-c127-11e5-9196-98e9ceec758f.png) - -*NOTE: Due to their infrequent use and needed screen real estate, deployment keys aren't displayed by default. If you need to view them, simply make sure to pass the `-k` flag to the `deployment ls` command.* - -The install metrics have the following meaning: - -* **Active** - The number of successful installs that are currently running this release (i.e. if the user opened your app, they would see/run this version). This number will increase and decrease as end-users upgrade to and away from this release, respectively. This metric shows both the total of active users, as well as what percentage of your overall audience that represents. This makes it easy to determine the distribution of updates that your users are currently running, as well as answer questions such as "How many of my users have received my latest update?". - -* **Total** - The total number of successful installations that this update has received overall. This number only ever increases as new users/devices install it, and therefore, this is always a superset of the total active count. An update is considered successful once `notifyApplicationReady` (or `sync`) is called after it was installed. Between the moment that an update is downloaded, and it is marked as being successful, it will be reported as a "pending" update (see below for details). - -* **Pending** - The number of times this release has been downloaded, but not yet installed (i.e. the app was restarted to apply the changes). Therefore, this metric increases as updates are downloaded, and decreases as those corresponding downloaded updates are installed. This metric primarily applies to updates that aren't configured to install immediately, and helps provide the broader picture of release adoption for apps that rely on app resume and/or restart to apply an update (e.g. I want to rollback an update and I'm curious if anyone has downloaded it yet). If you've configured updates to install immediately, and are still seeing pending updates being reported, then it's likely that you're not calling `notifyApplicationReady` (or `sync`) on app start, which is the method that initiates sending install reports and marks installed updates as being considered successful. - -* **Rollbacks** - The number of times that this release has been automatically rolled back on the client. Ideally this number should be zero, and in that case, this metric isn't even shown. However, if you released an update that includes a crash as part of the installation process, the CodePush plugin will roll the end-user back to the previous release, and report that issue back to the server. This allows your end-users to remain unblocked in the event of broken releases, and by being able to see this telemetry in the CLI, you can identify erroneous releases and respond to them by [rolling it back](#rolling-back-undesired-updates) on the server. - -* **Rollout** - Indicates the percentage of users that are eligible to receive this update. This property will only be displayed for releases that represent an "active" rollout, and therefore, have a rollout percentage that is less than 100%. Additionally, since a deployment can only have one active rollout at any given time, this label would only be present on the latest release within a deployment. - -* **Disabled** - Indicates whether the release has been marked as disabled or not, and therefore, is downloadable by end users. This property will only be displayed for releases that are actually disabled. - -When the metrics cell reports `No installs recorded`, that indicates that the server hasn't seen any activity for this release. This could either be because it precluded the plugin versions that included telemetry support, or no end-users have synchronized with the CodePush server yet. As soon as an install happens, you will begin to see metrics populate in the CLI for the release. - -## Releasing Updates - -Once your app has been configured to query for updates against the CodePush server, you can begin releasing updates to it. In order to provide both simplicity and flexibility, the CodePush CLI includes three different commands for releasing updates: - -1. [General](#releasing-updates-general) - Releases an update to the CodePush server that was generated by an external tool or build script (e.g. a Gulp task, the `react-native bundle` command). This provides the most flexibility in terms of fitting into existing workflows, since it strictly deals with CodePush-specific step, and leaves the app-specific compilation process to you. - -2. [React Native](#releasing-updates-react-native) - Performs the same functionality as the general release command, but also handles the task of generating the updated app contents for you (JS bundle and assets), instead of requiring you to run both `react-native bundle` and then `code-push release`. - -3. [Cordova](#releasing-updates-cordova) - Performs the same functionality as the general release command, but also handles the task of preparing the app update for you, instead of requiring you to run both `cordova prepare` (or `phonegap prepare`) and then `code-push release`. - -Which of these commands you should use is mostly a matter of requirements and/or preference. However, we generally recommend using the relevant platform-specific command to start (since it greatly simplifies the experience), and then leverage the general-purpose `release` command if/when greater control is needed. - -### Releasing Updates (General) - -``` -code-push release -[--deploymentName ] -[--description ] -[--disabled ] -[--mandatory] -[--noDuplicateReleaseError] -[--rollout ] -``` - -#### App name parameter - -This specifies the name of the CodePush app that this update is being released for. This value corresponds to the friendly name that you specified when originally calling `code-push app add` (e.g. "MyApp-Android"). If you need to look it up, you can run the `code-push app ls` command to see your list of apps. - -#### Update contents parameter - -This specifies the location of the updated app code and assets you want to release. You can provide either a single file (e.g. a JS bundle for a React Native app), or a path to a directory (e.g. the `/platforms/ios/www` folder for a Cordova app). Note that you don't need to ZIP up multiple files or directories in order to deploy those changes, since the CLI will automatically ZIP them for you. - -It's important that the path you specify refers to the platform-specific, prepared/bundled version of your app. The following table outlines which command you should run before releasing, as well as the location you can subsequently refer to using the `updateContents` parameter: - -| Platform | Prepare command | Package path (relative to project root) | -|----------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------| -| Cordova (Android) | `cordova prepare android` | `./platforms/android/assets/www` directory | -| Cordova (iOS) | `cordova prepare ios` | `./platforms/ios/www ` directory | -| React Native wo/assets (Android) | `react-native bundle --platform android --entry-file --bundle-output --dev false` | Value of the `--bundle-output` option | -| React Native w/assets (Android) | `react-native bundle --platform android --entry-file --bundle-output / --assets-dest --dev false` | Value of the `--assets-dest` option, which should represent a newly created directory that includes your assets and JS bundle | -| React Native wo/assets (iOS) | `react-native bundle --platform ios --entry-file --bundle-output --dev false` | Value of the `--bundle-output` option | -| React Native w/assets (iOS) | `react-native bundle --platform ios --entry-file --bundle-output / --assets-dest --dev false` | Value of the `--assets-dest` option, which should represent a newly created directory that includes your assets and JS bundle | - -#### Target binary version parameter - -This specifies the store/binary version of the application you are releasing the update for, so that only users running that version will receive the update, while users running an older and/or newer version of the app binary will not. This is useful for the following reasons: - -1. If a user is running an older binary version, it's possible that there are breaking changes in the CodePush update that wouldn't be compatible with what they're running. - -2. If a user is running a newer binary version, then it's presumed that what they are running is newer (and potentially incompatible) with the CodePush update. - -If you ever want an update to target multiple versions of the app store binary, we also allow you to specify the parameter as a [semver range expression](https://github.com/npm/node-semver#advanced-range-syntax). That way, any client device running a version of the binary that satisfies the range expression (i.e. `semver.satisfies(version, range)` returns `true`) will get the update. Examples of valid semver range expressions are as follows: - -| Range Expression | Who gets the update | -|------------------|----------------------------------------------------------------------------------------| -| `1.2.3` | Only devices running the specific binary app store version `1.2.3` of your app | -| `*` | Any device configured to consume updates from your CodePush app | -| `1.2.x` | Devices running major version 1, minor version 2 and any patch version of your app | -| `1.2.3 - 1.2.7` | Devices running any binary version between `1.2.3` (inclusive) and `1.2.7` (inclusive) | -| `>=1.2.3 <1.2.7` | Devices running any binary version between `1.2.3` (inclusive) and `1.2.7` (exclusive) | -| `~1.2.3` | Equivalent to `>=1.2.3 <1.3.0` | -| `^1.2.3` | Equivalent to `>=1.2.3 <2.0.0` | - -*NOTE: If your semver expression starts with a special shell character or operator such as `>`, `^`, or ** -*, the command may not execute correctly if you do not wrap the value in quotes as the shell will not supply the right values to our CLI process. Therefore, it is best to wrap your `targetBinaryVersion` parameter in double quotes when calling the `release` command, e.g. `code-push release MyApp-iOS updateContents ">1.2.3"`.* - -The following table outlines the version value that CodePush expects your update's semver range to satisfy for each respective app type: - -| Platform | Source of app store version | -|------------------------|------------------------------------------------------------------------------| -| Cordova | The `` attribute in the `config.xml` file | -| React Native (Android) | The `android.defaultConfig.versionName` property in your `build.gradle` file | -| React Native (iOS) | The `CFBundleShortVersionString` key in the `Info.plist` file | -| React Native (Windows) | The `` key in the `Package.appxmanifest` file | - -*NOTE: If the app store version in the metadata files are missing a patch version, e.g. `2.0`, it will be treated as having a patch version of `0`, i.e. `2.0 -> 2.0.0`.* - -#### Deployment name parameter - -This specifies which deployment you want to release the update to. This defaults to `Staging`, but when you're ready to deploy to `Production`, or one of your own custom deployments, just explicitly set this argument. - -*NOTE: The parameter can be set using either "--deploymentName" or "-d".* - -#### Description parameter - -This provides an optional "change log" for the deployment. The value is simply round tripped to the client so that when the update is detected, your app can choose to display it to the end-user (e.g. via a "What's new?" dialog). This string accepts control characters such as `\n` and `\t` so that you can include whitespace formatting within your descriptions for improved readability. - -*NOTE: This parameter can be set using either "--description" or "-desc"* - -#### Disabled parameter - -This specifies whether an update should be downloadable by end users or not. If left unspecified, the update will not be disabled (i.e. users will download it the moment your app calls `sync`). This parameter can be valuable if you want to release an update that isn't immediately available, until you expicitly [patch it](#patching-releases) when you want end users to be able to download it (e.g. an announcement blog post went live). - -*NOTE: This parameter can be set using either "--disabled" or "-x"* - -#### Mandatory parameter - -This specifies whether the update should be considered mandatory or not (e.g. it includes a critical security fix). This attribute is simply round tripped to the client, who can then decide if and how they would like to enforce it. - -*NOTE: This parameter is simply a "flag", and therefore, its absence indicates that the release is optional, and its presence indicates that it's mandatory. You can provide a value to it (e.g. `--mandatory true`), however, simply specifying `--mandatory` is sufficient for marking a release as mandatory.* - -The mandatory attribute is unique because the server will dynamically modify it as necessary in order to ensure that the semantics of your releases are maintained for your end-users. For example, imagine that you released the following three updates to your app: - -| Release | Mandatory? | -|---------|------------| -| v1 | No | -| v2 | Yes | -| v3 | No | - -If an end-user is currently running `v1`, and they query the server for an update, it will respond with `v3` (since that is the latest), but it will dynamically convert the release to mandatory, since a mandatory update was released in between. This behavior is important since the code contained in `v3` is incremental to that included in `v2`, and therefore, whatever made `v2` mandatory, continues to make `v3` mandatory for anyone that didn't already acquire `v2`. - -If an end-user is currently running `v2`, and they query the server for an update, it will respond with `v3`, but leave the release as optional. This is because they already received the mandatory update, and therefore, there isn't a need to modify the policy of `v3`. This behavior is why we say that the server will "dynamically convert" the mandatory flag, because as far as the release goes, its mandatory attribute will always be stored using the value you specified when releasing it. It is only changed on-the-fly as necessary when responding to an update check from an end-user. - -If you never release an update that is marked as mandatory, then the above behavior doesn't apply to you, since the server will never change an optional release to mandatory unless there were intermingled mandatory updates as illustrated above. Additionally, if a release is marked as mandatory, it will never be converted to optional, since that wouldn't make any sense. The server will only change an optional release to mandatory in order to respect the semantics described above. - -*NOTE: This parameter can be set using either `--mandatory` or `-m`* - -#### No duplicate release error parameter - -This specifies that if the update is identical to the latest release on the deployment, the CLI should generate a warning instead of an error. This is useful for continuous integration scenarios where it is expected that small modifications may trigger releases where no production code has changed. - -#### Rollout parameter - -**IMPORTANT: In order for this parameter to actually take affect, your end users need to be running version `1.6.0-beta+` (for Cordova) or `1.9.0-beta+` (for React Native) of the CodePush plugin. If you release an update that specifies a rollout property, no end user running an older version of the Cordova or React Native plugins will be eligible for the update. Therefore, until you have adopted the neccessary version of the platform-specific CodePush plugin (as previously mentioned), we would advise not setting a rollout value on your releases, since no one would end up receiving it.** - -This specifies the percentage of users (as an integer between `1` and `100`) that should be eligible to receive this update. It can be helpful if you want to "flight" new releases with a portion of your audience (e.g. 25%), and get feedback and/or watch for exceptions/crashes, before making it broadly available for everyone. If this parameter isn't set, it is set to `100%`, and therefore, you only need to set it if you want to actually limit how many users will receive it. - - When leveraging the rollout capability, there are a few additional considerations to keep in mind: - -1. You cannot release a new update to a deployment whose latest release is an "active" rollout (i.e. its rollout property is non-null). The rollout needs to be "completed" (i.e. setting the `rollout` property to `100`) before you can release further updates to the deployment. - -2. If you rollback a deployment whose latest release is an "active" rollout, the rollout value will be cleared, effectively "deactivating" the rollout behavior - -3. Unlike the `mandatory` and `description` fields, when you promote a release from one deployment to another, it will not propagate the `rollout` property, and therefore, if you want the new release (in the target deployment) to have a rollout value, you need to explicitly set it when you call the `promote` command. - -*NOTE: This parameter can be set using either `--rollout` or `-r`* - -### Releasing Updates (React Native) - -```shell -code-push release-react -[--bundleName ] -[--deploymentName ] -[--description ] -[--development ] -[--disabled ] -[--entryFile ] -[--gradleFile ] -[--mandatory] -[--noDuplicateReleaseError] -[--plistFile ] -[--plistFilePrefix ] -[--sourcemapOutput ] -[--targetBinaryVersion ] -[--rollout ] -``` - -The `release-react` command is a React Native-specific version of the "vanilla" [`release`](#releasing-app-updates) command, which supports all of the same parameters (e.g. `--mandatory`, `--description`), yet simplifies the process of releasing updates by performing the following additional behavior: - -1. Running the `react-native bundle` command in order to generate the [update contents](#update-contents-parameter) (JS bundle and assets) that will be released to the CodePush server. It uses sensible defaults as much as possible (e.g. creating a non-dev build, assuming an iOS entry file is named `index.ios.js`), but also exposes the relevant `react-native bundle` parameters to enable flexibility (e.g. `--sourcemapOutput`). - -2. Inferring the [`targetBinaryVersion`](#target-binary-version-parameter) of this release by using the version name that is specified in your project's `Info.plist` (for iOS) and `build.gradle` (for Android) files. - -To illustrate the difference that the `release-react` command can make, the following is an example of how you might generate and release an update for a React Native app using the "vanilla" `release` command: - -```shell -mkdir ./CodePush - -react-native bundle --platform ios \ ---entry-file index.ios.js \ ---bundle-output ./CodePush/main.jsbundle \ ---assets-dest ./CodePush \ ---dev false - -code-push release MyApp-iOS ./CodePush 1.0.0 -``` - -Achieving the equivalent behavior with the `release-react` command would simply require the following command, which is generally less error-prone: - -```shell -code-push release-react MyApp-iOS ios -``` - -*NOTE: We believe that the `release-react` command should be valuable for most React Native developers, so if you're finding that it isn't flexible enough or missing a key feature, please don't hesistate to [let us know](mailto:codepushfeed@microsoft.com), so that we can improve it!* - -#### App name parameter - -This is the same parameter as the one described in the [above section](#app-name-parameter). - -#### Platform parameter - -This specifies which platform the current update is targeting, and can be either `android`, `ios` or `windows` (case-insensitive). This value is only used to determine how to properly bundle your update contents and isn't actually sent to the server. - -#### Deployment name parameter - -This is the same parameter as the one described in the [above section](#deployment-name-parameter). - -#### Description parameter - -This is the same parameter as the one described in the [above section](#description-parameter). - -#### Mandatory parameter - -This is the same parameter as the one described in the [above section](#mandatory-parameter). - -#### No duplicate release error parameter - -This is the same parameter as the one described in the [above section](#no-duplicate-release-error-parameter). - -#### Rollout parameter - -This is the same parameter as the one described in the [above section](#rollout-parameter). If left unspecified, the release will be made available to all users. - -#### Target binary version parameter - -This is the same parameter as the one described in the [above section](#target-binary-version-parameter). If left unspecified, this defaults to targeting the exact version specified in the app's `Info.plist` (for iOS) and `build.gradle` (for Android) files. - -#### Bundle name parameter - -This specifies the file name that should be used for the generated JS bundle. If left unspecified, the standard bundle name will be used for the specified platform: `main.jsbundle` (iOS), `index.android.bundle` (Android) and `index.windows.bundle` (Windows). - -*NOTE: This parameter can be set using either --bundleName or -b* - -#### Development parameter - -This specifies whether to generate a unminified, development JS bundle. If left unspecified, this defaults to `false` where warnings are disabled and the bundle is minified. - -*NOTE: This parameter can be set using either --development or --dev* - -#### Disabled parameter - -This is the same parameter as the one described in the [above section](#disabled-parameter). - -#### Entry file parameter - -This specifies the relative path to the app's root/entry JavaScript file. If left unspecified, this defaults to `index.ios.js` (for iOS), `index.android.js` (for Android) or `index.windows.bundle` (for Windows) if that file exists, or `index.js` otherwise. - -*NOTE: This parameter can be set using either --entryFile or -e* - -#### Gradle file parameter (Android only) - -This specifies the relative path to the `build.gradle` file that the CLI should use when attempting to auto-detect the target binary version for the release. This parameter is only meant for advanced scenarios, since the CLI will automatically be able to find your `build.grade` file in "standard" React Native projects. However, if your gradle file is located in an arbitrary location, that the CLI can't discover, then using this parameter allows you to continue releasing CodePush updates, without needing to explicitly set the `--targetBinaryVersion` parameter. Since `build.gradle` is a required file name, specifying the path to the containing folder or the full path to the file itself will both achieve the same effect. - -```shell -code-push release-react MyApp-Android android -p "./foo/bar/" -code-push release-react MyApp-Android android -p "./foo/bar/build.gradle" -``` - -#### Plist file parameter (iOS only) - -This specifies the relative path to the `Info.plist` file that the CLI should use when attempting to auto-detect the target binary version for the release. This parameter is only meant for advanced scenarios, since the CLI will automatically be able to find your `Info.plist` file in "standard" React Native projects, and you can use the `--plistFilePrefix` parameter in order to support per-environment plist files (e.g. `STAGING-Info.plist`). However, if your plist is located in an arbitrary location, that the CLI can't discover, then using this parameter allows you to continue releasing CodePush updates, without needing to explicitly set the `--targetBinaryVersion` parameter. - -```shell -code-push release-react MyApp-iOS ios -p "./foo/bar/MyFile.plist" -``` - -*NOTE: This parameter can be set using either --plistFile or -p* - -#### Plist file prefix parameter (iOS only) - -This specifies the file name prefix of the `Info.plist` file that that CLI should use when attempting to auto-detect the target binary version for the release. This can be useful if you've created per-environment plist files (e.g. `DEV-Info.plist`, `STAGING-Info.plist`), and you want to be able to release CodePush updates without needing to explicity set the `--targetBinaryVersion` parameter. By specifying a `--plistFilePrefx`, the CLI will look for a file named `-Info.plist`, instead of simply `Info.plist` (which is the default behavior), in the following locations: `./ios` and `./ios/`. If your plist file isn't located in either of those directories (e.g. your app is a native iOS app with embedded RN views), or uses an entirely different file naming convention, then consider using the `--plistFile` parameter. - -```shell -# Auto-detect the target binary version of this release by looking up the -# app version within the STAGING-Info.plist file in either the ./ios or ./ios/ directories. -code-push release-react MyApp-iOS ios --pre "STAGING" - -# Tell the CLI to use your dev plist (`DEV-Info.plist`). -# Note that the hyphen separator can be explicitly stated. -code-push release-react MyApp-iOS ios --pre "DEV-" -``` - -*NOTE: This parameter can be set using either --plistFilePrefix or --pre* - -#### Sourcemap output parameter - -This specifies the relative path to where the generated JS bundle's sourcemap file should be written. If left unspecified, sourcemaps will not be generated. - -*NOTE: This parameter can be set using either --sourcemapOutput or -s* - -### Releasing Updates (Cordova) - -```shell -code-push release-cordova -[--deploymentName ] -[--description ] -[--mandatory] -[--noDuplicateReleaseError] -[--targetBinaryVersion ] -[--rollout ] -[--build] -``` - -The `release-cordova` command is a Cordova-specific version of the "vanilla" [`release`](#releasing-app-updates) command, which supports all of the same parameters (e.g. `--mandatory`, `--description`), yet simplifies the process of releasing updates by performing the following additional behavior: - -1. Running the `cordova prepare` (or `phonegap prepare`) command in order to generate the [update contents](#update-contents-parameter) (`www` folder) that will be released to the CodePush server. - -2. Inferring the [`targetBinaryVersion`](#target-binary-version-parameter) of this release by using the version name that is specified in your project's `config.xml` file. - -To illustrate the difference that the `release-cordova` command can make, the following is an example of how you might generate and release an update for a Cordova app using the "vanilla" `release` command: - -```shell -cordova prepare ios -code-push release MyApp-iOS ./platforms/ios/www 1.0.0 -``` - -Achieving the equivalent behavior with the `release-cordova` command would simply require the following command, which is generally less error-prone: - -```shell -code-push release-cordova MyApp-iOS ios -``` - -*NOTE: We believe that the `release-cordova` command should be valuable for most Cordova developers, so if you're finding that it isn't flexible enough or missing a key feature, please don't hesistate to [let us know](mailto:codepushfeed@microsoft.com), so that we can improve it.* - -#### App name parameter - -This is the same parameter as the one described in the [above section](#app-name-parameter). - -#### Platform parameter - -This specifies which platform the current update is targeting, and can be either `ios` or `android` (case-insensitive). - -#### Deployment name parameter - -This is the same parameter as the one described in the [above section](#deployment-name-parameter). - -#### Description parameter - -This is the same parameter as the one described in the [above section](#description-parameter). - -#### Mandatory parameter - -This is the same parameter as the one described in the [above section](#mandatory-parameter). - -#### No duplicate release error parameter - -This is the same parameter as the one described in the [above section](#no-duplicate-release-error-parameter). - -#### Rollout parameter - -This is the same parameter as the one described in the [above section](#rollout-parameter). If left unspecified, the release will be made available to all users. - -#### Target binary version parameter - -This is the same parameter as the one described in the [above section](#target-binary-version-parameter). If left unspecified, the command defaults to targeting only the specified version in the project's metadata (`Info.plist` if this update is for iOS clients, and `build.gradle` for Android clients). - -#### Disabled parameter - -This is the same parameter as the one described in the [above section](#disabled-parameter). - -#### Build parameter - -Specifies whether you want to run `cordova build` instead of `cordova prepare` (which is the default behavior), when generating your updated web assets. This is valuable if your project includes before and/or after build hooks (e.g. to transpile TypeScript), and therefore, having CodePush simply run `cordova prepare` isn't sufficient to create and release an update. If left unspecified, this defaults to `false`. - -*NOTE: This parameter can be set using either --build or -b* - -## Debugging CodePush Integration - -Once you've released an update, and the Cordova or React Native plugin has been integrated into your app, it can be helpful to diagnose how the plugin is behaving, especially if you run into an issue and want to understand why. In order to debug the CodePush update discovery experience, you can run the following command in order to easily view the diagnostic logs produced by the CodePush plugin within your app: - -```shell -code-push debug - -# View all CodePush logs from a running -# instace of the iOS simulator. -code-push debug ios - -# View all CodePush logs from a running -# Android emulator or attached device. -code-push debug android -``` - - - -Under the covers, this command simply automates the usage of the iOS system logs and ADB logcat, but provides a platform-agnostic, filtered view of all logs coming from the CodePush plugin, for both Cordova or React Native. This way, you don't need to learn and/or use another tool simply to be able to answer basic questions about how CodePush is behaving. - -*NOTE: The debug command supports both emulators and devices for Android, but currently only supports listening to logs from the iOS simulator. We hope to add device support soon.* - -## Patching Update Metadata - -After releasing an update, there may be scenarios where you need to modify one or more of the metadata attributes associated with it (e.g. you forgot to mark a critical bug fix as mandatory, you want to increase the rollout percentage of an update). You can easily do this by running the following command: - -```shell -code-push patch -[--label ] -[--mandatory ] -[--description ] -[--rollout ] -[--disabled ] -[--targetBinaryVersion ] -``` - -*NOTE: This command doesn't allow modifying the actual update contents of a release (e.g. `www` folder of a Cordova app). If you need to respond to a release that has been identified as being broken, you should use the [rollback](#rolling-back-updates) command to immediately roll it back, and then if necessary, release a new update with the approrpriate fix when it is available.* - -Aside from the `appName` and `deploymentName`, all parameters are optional, and therefore, you can use this command to update just a single attribute or all of them at once. Calling the `patch` command without specifying any attribute flag will result in a no-op. - -```shell -# Mark the latest production release as mandatory -code-push patch MyApp-iOS Production -m - -# Increase the rollout for v23 to 50% -code-push patch MyApp-iOS Production -l v23 -rollout 50% -``` - -### Label parameter - -Indicates which release (e.g. `v23`) you want to update within the specified deployment. If ommitted, the requested changes will be applied to the latest release in the specified deployment. In order to look up the label for the release you want to update, you can run the `code-push deployment history` command and refer to the `Label` column. - -*NOTE: This parameter can be set using either `--label` or `-l`* - -### Mandatory parameter - -This is the same parameter as the one described in the [above section](#mandatory-parameter), and simply allows you to update whether the release should be considered mandatory or not. Note that `--mandatory` and `--mandatory true` are equivalent, but the absence of this flag is not equivalent to `--mandatory false`. Therefore, if the parameter is ommitted, no change will be made to the value of the target release's mandatory property. You need to set this to `--mandatory false` to explicitly make a release optional. - -### Description parameter - -This is the same parameter as the one described in the [above section](#description-parameter), and simply allows you to update the description associated with the release (e.g. you made a typo when releasing, or you forgot to add a description at all). If this parameter is ommitted, no change will be made to the value of the target release's description property. - -### Disabled parameter - -This is the same parameter as the one described in the [above section](#disabled-parameter), and simply allows you to update whether the release should be disabled or not. Note that `--disabled` and `--disabled true` are equivalent, but the absence of this flag is not equivalent to `--disabled false`. Therefore, if the paremeter is ommitted, no change will be made to the value of the target release's disabled property. You need to set this to `--disabled false` to explicity make a release acquirable if it was previously disabled. - -### Rollout parameter - -This is the same parameter as the one described in the [above section](#rollout-parameter), and simply allows you to increase the rollout percentage of the target release. This parameter can only be set to an integer whose value is greater than the current rollout value. Additionally, if you want to "complete" the rollout, and therefore, make the release available to everyone, you can simply set this parameter to `--rollout 100`. If this parameter is ommitted, no change will be made to the value of the target release's rollout parameter. - -Additionally, as mentioned above, when you release an update without a rollout value, it is treated equivalently to setting the rollout to `100`. Therefore, if you released an update without a rollout, you cannot change the rollout property of it via the `patch` command since that would be considered lowering the rollout percentage. - -### Target binary version parameter - -This is the same parameter as the one described in the [above section](#target-binary-version-parameter), and simply allows you to update the semver range that indicates which binary version(s) a release is compatible with. This can be useful if you made a mistake when originally releasing an update (e.g. you specified `1.0.0` but meant `1.1.0`) or you want to increase or decrease the version range that a release supports (e.g. you discovered that a release doesn't work with `1.1.2` after all). If this paremeter is ommitted, no change will be made to the value of the target release's version property. - -```shell -# Add a "max binary version" to an existing release -# by scoping its eligibility to users running >= 1.0.5 -code-push patch MyApp-iOS Staging -t "1.0.0 - 1.0.5" -``` - -## Promoting Updates - -Once you've tested an update against a specific deployment (e.g. `Staging`), and you want to promote it "downstream" (e.g. dev->staging, staging->production), you can simply use the following command to copy the release from one deployment to another: - -``` -code-push promote -[--description ] -[--disabled ] -[--mandatory] -[--noDuplicateReleaseError] -[--rollout ] -[--targetBinaryVersion -code-push rollback MyApp-iOS Production -``` - -This has the effect of creating a new release for the deployment that includes the **exact same code and metadata** as the version prior to the latest one. For example, imagine that you released the following updates to your app: - -| Release | Description | Mandatory | -|---------|-------------------|-----------| -| v1 | Initial release! | Yes | -| v2 | Added new feature | No | -| v3 | Bug fixes | Yes | - -If you ran the `rollback` command on that deployment, a new release (`v4`) would be created that included the contents of the `v2` release. - -| Release | Description | Mandatory | -|-----------------------------|-------------------|-----------| -| v1 | Initial release! | Yes | -| v2 | Added new feature | No | -| v3 | Bug fixes | Yes | -| v4 (Rollback from v3 to v2) | Added new feature | No | - -End-users that had already acquired `v3` would now be "moved back" to `v2` when the app performs an update check. Additionally, any users that were still running `v2`, and therefore, had never acquired `v3`, wouldn't receive an update since they are already running the latest release (this is why our update check uses the package hash in addition to the release label). - -If you would like to rollback a deployment to a release other than the previous (e.g. `v3` -> `v2`), you can specify the optional `--targetRelease` parameter: - -``` -code-push rollback MyApp-iOS Production --targetRelease v34 -``` - -*NOTE: The release produced by a rollback will be annotated in the output of the `deployment history` command to help identify them more easily.* - -## Viewing Release History - -You can view a history of the 50 most recent releases for a specific app deployment using the following command: - -``` -code-push deployment history -``` - -The history will display all attributes about each release (e.g. label, mandatory) as well as indicate if any releases were made due to a promotion or a rollback operation. - -![Deployment History](https://cloud.githubusercontent.com/assets/696206/11605068/14e440d0-9aab-11e5-8837-69ab09bfb66c.PNG) - -Additionally, the history displays the install metrics for each release. You can view the details about how to interpret the metric data in the documentation for the `deployment ls` command above. - -By default, the history doesn't display the author of each release, but if you are collaborating on an app with other developers, and want to view who released each update, you can pass the additional `--displayAuthor` (or `-a`) flag to the history command. - -*NOTE: The history command can also be run using the "h" alias* - -## Clearing Release History - -You can clear the release history associated with a deployment using the following command: - -``` -code-push deployment clear -``` - -After running this command, client devices configured to receive updates using its associated deployment key will no longer receive the updates that have been cleared. This command is irreversible, and therefore should not be used in a production deployment. diff --git a/cli/definitions/backslash.d.ts b/cli/definitions/backslash.d.ts deleted file mode 100644 index 16080e18..00000000 --- a/cli/definitions/backslash.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -declare module "backslash" { - function backslash(str: string): string; - - export = backslash; -} \ No newline at end of file diff --git a/cli/definitions/cli.ts b/cli/definitions/cli.ts deleted file mode 100644 index d7ddf954..00000000 --- a/cli/definitions/cli.ts +++ /dev/null @@ -1,214 +0,0 @@ -export enum CommandType { - accessKeyAdd, - accessKeyPatch, - accessKeyList, - accessKeyRemove, - appAdd, - appList, - appRemove, - appRename, - appTransfer, - collaboratorAdd, - collaboratorList, - collaboratorRemove, - debug, - deploymentAdd, - deploymentHistory, - deploymentHistoryClear, - deploymentList, - deploymentMetrics, - deploymentRemove, - deploymentRename, - link, - login, - logout, - patch, - promote, - register, - release, - releaseCordova, - releaseReact, - rollback, - sessionList, - sessionRemove, - whoami -} - -export interface ICommand { - type: CommandType; -} - -export interface IAccessKeyAddCommand extends ICommand { - name: string; - ttl?: number; -} - -export interface IAccessKeyPatchCommand extends ICommand { - newName?: string; - oldName: string; - ttl?: number; -} - -export interface IAccessKeyListCommand extends ICommand { - format: string; -} - -export interface IAccessKeyRemoveCommand extends ICommand { - accessKey: string; -} - -export interface IAppAddCommand extends ICommand { - appName: string; -} - -export interface IAppListCommand extends ICommand { - format: string; -} - -export interface IAppRemoveCommand extends ICommand { - appName: string; -} - -export interface IAppRenameCommand extends ICommand { - currentAppName: string; - newAppName: string; -} - -export interface IAppTransferCommand extends ICommand { - appName: string; - email: string; -} - -export interface ICollaboratorAddCommand extends ICommand { - appName: string; - email: string; -} - -export interface ICollaboratorListCommand extends ICommand { - appName: string; - format: string; -} - -export interface ICollaboratorRemoveCommand extends ICommand { - appName: string; - email: string; -} - - -export interface IDebugCommand extends ICommand { - platform: string; -} - -export interface IDeploymentAddCommand extends ICommand { - appName: string; - deploymentName: string; -} - -export interface IDeploymentHistoryClearCommand extends ICommand { - appName: string; - deploymentName: string; -} - -export interface IDeploymentHistoryCommand extends ICommand { - appName: string; - deploymentName: string; - format: string; - displayAuthor: boolean; -} - -export interface IDeploymentListCommand extends ICommand { - appName: string; - format: string; - displayKeys: boolean; -} - -export interface IDeploymentRemoveCommand extends ICommand { - appName: string; - deploymentName: string; -} - -export interface IDeploymentRenameCommand extends ICommand { - appName: string; - currentDeploymentName: string; - newDeploymentName: string; -} - -export interface ILinkCommand extends ICommand { - serverUrl?: string; -} - -export interface ILoginCommand extends ICommand { - serverUrl?: string; - accessKey: string; - proxy?: string; - noProxy?: boolean; -} - -export interface IPackageInfo { - description?: string; - disabled?: boolean; - mandatory?: boolean; - rollout?: number; -} - -export interface IPatchCommand extends ICommand, IPackageInfo { - appName: string; - appStoreVersion?: string; - deploymentName: string; - label: string; -} - -export interface IPromoteCommand extends ICommand, IPackageInfo { - appName: string; - appStoreVersion?: string; - sourceDeploymentName: string; - destDeploymentName: string; - noDuplicateReleaseError?: boolean; -} - -export interface IRegisterCommand extends ICommand { - serverUrl?: string; - proxy?: string; - noProxy?: boolean; -} - -export interface IReleaseBaseCommand extends ICommand, IPackageInfo { - appName: string; - appStoreVersion: string; - deploymentName: string; - noDuplicateReleaseError?: boolean; -} - -export interface IReleaseCommand extends IReleaseBaseCommand { - package: string; -} - -export interface IReleaseCordovaCommand extends IReleaseBaseCommand { - build: boolean; - platform: string; -} - -export interface IReleaseReactCommand extends IReleaseBaseCommand { - bundleName?: string; - development?: boolean; - entryFile?: string; - gradleFile?: string; - platform: string; - plistFile?: string; - plistFilePrefix?: string; - sourcemapOutput?: string; -} - -export interface IRollbackCommand extends ICommand { - appName: string; - deploymentName: string; - targetRelease: string; -} - -export interface ISessionListCommand extends ICommand { - format: string; -} - -export interface ISessionRemoveCommand extends ICommand { - machineName: string; -} diff --git a/cli/definitions/parse-duration.d.ts b/cli/definitions/parse-duration.d.ts deleted file mode 100644 index a6985986..00000000 --- a/cli/definitions/parse-duration.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -declare module "parse-duration" { - function parseDuration(duration: string): number; - - export = parseDuration; -} \ No newline at end of file diff --git a/cli/definitions/wordwrap.d.ts b/cli/definitions/wordwrap.d.ts deleted file mode 100644 index 72e9ec6e..00000000 --- a/cli/definitions/wordwrap.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -declare module "wordwrap" { - function wordwrap(width: number): (text: string) => string; - export = wordwrap; -} \ No newline at end of file diff --git a/cli/package.json b/cli/package.json deleted file mode 100644 index ec977ce7..00000000 --- a/cli/package.json +++ /dev/null @@ -1,52 +0,0 @@ -{ - "name": "code-push-cli", - "version": "1000.0.0-beta", - "description": "Management CLI for the CodePush service", - "main": "script/cli.js", - "scripts": { - "test": "gulp test" - }, - "bin": { - "code-push": "script/cli.js" - }, - "repository": { - "type": "git", - "url": "https://github.com/Microsoft/code-push/" - }, - "keywords": [ - "code", - "push", - "cordova", - "react-native", - "react" - ], - "homepage": "https://microsoft.github.io/code-push", - "author": "Microsoft Corporation", - "license": "MIT", - "dependencies": { - "backslash": "^0.1.7", - "base-64": "^0.1.0", - "chalk": "^1.1.0", - "cli-table": "^0.3.1", - "code-push": "1.11.1-beta", - "email-validator": "^1.0.3", - "gradle-to-js": "0.1.1", - "moment": "^2.10.6", - "opener": "^1.4.1", - "parse-duration": "0.1.1", - "plist": "1.2.0", - "progress": "^1.1.8", - "prompt": "^0.2.14", - "properties": "^1.2.1", - "q": "~1.4.1", - "recursive-fs": "0.1.4", - "rimraf": "^2.5.1", - "semver": "4.3.6", - "simctl": "0.0.9", - "update-notifier": "^0.5.0", - "which": "^1.2.7", - "wordwrap": "1.0.0", - "xml2js": "^0.4.16", - "yargs": "^3.15.0" - } -} diff --git a/cli/script/cli.ts b/cli/script/cli.ts deleted file mode 100644 index 9507741a..00000000 --- a/cli/script/cli.ts +++ /dev/null @@ -1,22 +0,0 @@ -/// - -import { Promise } from "q"; -import * as parser from "./command-parser"; -import { execute } from "./command-executor"; -import * as chalk from "chalk"; - -function run(): void { - if (!parser.command) { - parser.showHelp(/*showRootDescription*/false); - return; - } - - execute(parser.command) - .catch((error: any): void => { - console.error(chalk.red("[Error] " + error.message)); - process.exit(1); - }) - .done(); -} - -run(); diff --git a/cli/script/command-executor.ts b/cli/script/command-executor.ts deleted file mode 100644 index 53c029f5..00000000 --- a/cli/script/command-executor.ts +++ /dev/null @@ -1,1492 +0,0 @@ -/// - -import AccountManager = require("code-push"); -import * as base64 from "base-64"; -import * as chalk from "chalk"; -var childProcess = require("child_process"); -import debugCommand from "./commands/debug"; -import * as fs from "fs"; -var g2js = require("gradle-to-js/lib/parser"); -import * as moment from "moment"; -var opener = require("opener"); -import * as os from "os"; -import * as path from "path"; -var plist = require("plist"); -var progress = require("progress"); -var prompt = require("prompt"); -import * as Q from "q"; -var rimraf = require("rimraf"); -import * as semver from "semver"; -var simctl = require("simctl"); -var Table = require("cli-table"); -var which = require("which"); -import wordwrap = require("wordwrap"); -import * as cli from "../definitions/cli"; -import { AccessKey, Account, App, CodePushError, CollaboratorMap, CollaboratorProperties, Deployment, DeploymentMetrics, Headers, Package, PackageInfo, Session, UpdateMetrics } from "code-push/script/types"; - -var configFilePath: string = path.join(process.env.LOCALAPPDATA || process.env.HOME, ".code-push.config"); -var emailValidator = require("email-validator"); -var packageJson = require("../package.json"); -var parseXml = Q.denodeify(require("xml2js").parseString); -var progress = require("progress"); -import Promise = Q.Promise; -var properties = require("properties"); - -const ACTIVE_METRICS_KEY: string = "Active"; -const CLI_HEADERS: Headers = { - "X-CodePush-CLI-Version": packageJson.version -}; -const DOWNLOADED_METRICS_KEY: string = "Downloaded"; - -interface NameToCountMap { - [name: string]: number; -} - -/** Deprecated */ -interface ILegacyLoginConnectionInfo { - accessKeyName: string; -} - -interface ILoginConnectionInfo { - accessKey: string; - customServerUrl?: string; // A custom serverUrl for internal debugging purposes - preserveAccessKeyOnLogout?: boolean; - proxy?: string; // To specify the proxy url explicitly, other than the environment var (HTTP_PROXY) - noProxy?: boolean; // To suppress the environment proxy setting, like HTTP_PROXY -} - - - -export interface UpdateMetricsWithTotalActive extends UpdateMetrics { - totalActive: number; -} - -export interface PackageWithMetrics { - metrics?: UpdateMetricsWithTotalActive; -} - -export var log = (message: string | Chalk.ChalkChain): void => console.log(message); -export var sdk: AccountManager; -export var spawn = childProcess.spawn; -export var execSync = childProcess.execSync; - -var connectionInfo: ILoginConnectionInfo; - -export var confirm = (): Promise => { - return Promise((resolve, reject, notify): void => { - prompt.message = ""; - prompt.delimiter = ""; - - prompt.start(); - - prompt.get({ - properties: { - response: { - description: chalk.cyan("Are you sure? (Y/n):") - } - } - }, (err: any, result: any): void => { - if (!result.response || result.response === "" || result.response === "Y") { - resolve(true); - } else { - if (result.response !== "n") console.log("Invalid response: \"" + result.response + "\""); - resolve(false); - } - }); - }); -} - -function accessKeyAdd(command: cli.IAccessKeyAddCommand): Promise { - return sdk.addAccessKey(command.name, command.ttl) - .then((accessKey: AccessKey) => { - log(`Successfully created the "${command.name}" access key: ${accessKey.key}`); - log("Make sure to save this key value somewhere safe, since you won't be able to view it from the CLI again!"); - }); -} - -function accessKeyPatch(command: cli.IAccessKeyPatchCommand): Promise { - const willUpdateName: boolean = isCommandOptionSpecified(command.newName) && command.oldName !== command.newName; - const willUpdateTtl: boolean = isCommandOptionSpecified(command.ttl); - - if (!willUpdateName && !willUpdateTtl) { - throw new Error("A new name and/or TTL must be provided."); - } - - return sdk.patchAccessKey(command.oldName, command.newName, command.ttl) - .then((accessKey: AccessKey) => { - let logMessage: string = "Successfully "; - if (willUpdateName) { - logMessage += `renamed the access key "${command.oldName}" to "${command.newName}"`; - } - - if (willUpdateTtl) { - const expirationDate = moment(accessKey.expires).format("LLLL"); - if (willUpdateName) { - logMessage += ` and changed its expiration date to ${expirationDate}`; - } else { - logMessage += `changed the expiration date of the "${command.oldName}" access key to ${expirationDate}`; - } - } - - log(`${logMessage}.`); - }); -} - -function accessKeyList(command: cli.IAccessKeyListCommand): Promise { - throwForInvalidOutputFormat(command.format); - - return sdk.getAccessKeys() - .then((accessKeys: AccessKey[]): void => { - printAccessKeys(command.format, accessKeys); - }); -} - -function accessKeyRemove(command: cli.IAccessKeyRemoveCommand): Promise { - return confirm() - .then((wasConfirmed: boolean): Promise => { - if (wasConfirmed) { - return sdk.removeAccessKey(command.accessKey) - .then((): void => { - log(`Successfully removed the "${command.accessKey}" access key.`); - }); - } - - log("Access key removal cancelled."); - }); -} - -function appAdd(command: cli.IAppAddCommand): Promise { - return sdk.addApp(command.appName) - .then((app: App): Promise => { - log("Successfully added the \"" + command.appName + "\" app, along with the following default deployments:"); - var deploymentListCommand: cli.IDeploymentListCommand = { - type: cli.CommandType.deploymentList, - appName: app.name, - format: "table", - displayKeys: true - }; - return deploymentList(deploymentListCommand, /*showPackage=*/ false); - }); -} - -function appList(command: cli.IAppListCommand): Promise { - throwForInvalidOutputFormat(command.format); - var apps: App[]; - return sdk.getApps() - .then((retrievedApps: App[]): void => { - printAppList(command.format, retrievedApps); - }); -} - -function appRemove(command: cli.IAppRemoveCommand): Promise { - return confirm() - .then((wasConfirmed: boolean): Promise => { - if (wasConfirmed) { - return sdk.removeApp(command.appName) - .then((): void => { - log("Successfully removed the \"" + command.appName + "\" app."); - }); - } - - log("App removal cancelled."); - }); -} - -function appRename(command: cli.IAppRenameCommand): Promise { - return sdk.renameApp(command.currentAppName, command.newAppName) - .then((): void => { - log("Successfully renamed the \"" + command.currentAppName + "\" app to \"" + command.newAppName + "\"."); - }); -} - -export var createEmptyTempReleaseFolder = (folderPath: string) => { - return deleteFolder(folderPath) - .then(() => { - fs.mkdirSync(folderPath); - }); -}; - -function appTransfer(command: cli.IAppTransferCommand): Promise { - throwForInvalidEmail(command.email); - - return confirm() - .then((wasConfirmed: boolean): Promise => { - if (wasConfirmed) { - return sdk.transferApp(command.appName, command.email) - .then((): void => { - log("Successfully transferred the ownership of app \"" + command.appName + "\" to the account with email \"" + command.email + "\"."); - }); - } - - log("App transfer cancelled."); - }); -} - -function addCollaborator(command: cli.ICollaboratorAddCommand): Promise { - throwForInvalidEmail(command.email); - - return sdk.addCollaborator(command.appName, command.email) - .then((): void => { - log("Successfully added \"" + command.email + "\" as a collaborator to the app \"" + command.appName + "\"."); - }); -} - -function listCollaborators(command: cli.ICollaboratorListCommand): Promise { - throwForInvalidOutputFormat(command.format); - - return sdk.getCollaborators(command.appName) - .then((retrievedCollaborators: CollaboratorMap): void => { - printCollaboratorsList(command.format, retrievedCollaborators); - }); -} - -function removeCollaborator(command: cli.ICollaboratorRemoveCommand): Promise { - throwForInvalidEmail(command.email); - - return confirm() - .then((wasConfirmed: boolean): Promise => { - if (wasConfirmed) { - return sdk.removeCollaborator(command.appName, command.email) - .then((): void => { - log("Successfully removed \"" + command.email + "\" as a collaborator from the app \"" + command.appName + "\"."); - }); - } - - log("App collaborator removal cancelled."); - }); -} - -function deleteConnectionInfoCache(printMessage: boolean = true): void { - try { - fs.unlinkSync(configFilePath); - - if (printMessage) { - log(`Successfully logged-out. The session file located at ${chalk.cyan(configFilePath)} has been deleted.\r\n`); - } - } catch (ex) { - } -} - -function deleteFolder(folderPath: string): Promise { - return Promise((resolve, reject, notify) => { - rimraf(folderPath, (err: any) => { - if (err) { - reject(err); - } else { - resolve(null); - } - }); - }); -} - -function deploymentAdd(command: cli.IDeploymentAddCommand): Promise { - return sdk.addDeployment(command.appName, command.deploymentName) - .then((deployment: Deployment): void => { - log("Successfully added the \"" + command.deploymentName + "\" deployment with key \"" + deployment.key + "\" to the \"" + command.appName + "\" app."); - }); -} - -function deploymentHistoryClear(command: cli.IDeploymentHistoryClearCommand): Promise { - return confirm() - .then((wasConfirmed: boolean): Promise => { - if (wasConfirmed) { - return sdk.clearDeploymentHistory(command.appName, command.deploymentName) - .then((): void => { - log("Successfully cleared the release history associated with the \"" + command.deploymentName + "\" deployment from the \"" + command.appName + "\" app."); - }) - } - - log("Clear deployment cancelled."); - }); -} - -export var deploymentList = (command: cli.IDeploymentListCommand, showPackage: boolean = true): Promise => { - throwForInvalidOutputFormat(command.format); - var deployments: Deployment[]; - - return sdk.getDeployments(command.appName) - .then((retrievedDeployments: Deployment[]) => { - deployments = retrievedDeployments; - if (showPackage) { - var metricsPromises: Promise[] = deployments.map((deployment: Deployment) => { - if (deployment.package) { - return sdk.getDeploymentMetrics(command.appName, deployment.name) - .then((metrics: DeploymentMetrics): void => { - if (metrics[deployment.package.label]) { - var totalActive: number = getTotalActiveFromDeploymentMetrics(metrics); - ((deployment.package)).metrics = { - active: metrics[deployment.package.label].active, - downloaded: metrics[deployment.package.label].downloaded, - failed: metrics[deployment.package.label].failed, - installed: metrics[deployment.package.label].installed, - totalActive: totalActive - }; - } - }); - } else { - return Q(null); - } - }); - - return Q.all(metricsPromises); - } - }) - .then(() => { - printDeploymentList(command, deployments, showPackage); - }); -} - -function deploymentRemove(command: cli.IDeploymentRemoveCommand): Promise { - return confirm() - .then((wasConfirmed: boolean): Promise => { - if (wasConfirmed) { - return sdk.removeDeployment(command.appName, command.deploymentName) - .then((): void => { - log("Successfully removed the \"" + command.deploymentName + "\" deployment from the \"" + command.appName + "\" app."); - }) - } - - log("Deployment removal cancelled."); - }); -} - -function deploymentRename(command: cli.IDeploymentRenameCommand): Promise { - return sdk.renameDeployment(command.appName, command.currentDeploymentName, command.newDeploymentName) - .then((): void => { - log("Successfully renamed the \"" + command.currentDeploymentName + "\" deployment to \"" + command.newDeploymentName + "\" for the \"" + command.appName + "\" app."); - }); -} - -function deploymentHistory(command: cli.IDeploymentHistoryCommand): Promise { - throwForInvalidOutputFormat(command.format); - - return Q.all([ - sdk.getAccountInfo(), - sdk.getDeploymentHistory(command.appName, command.deploymentName), - sdk.getDeploymentMetrics(command.appName, command.deploymentName) - ]) - .spread((account: Account, deploymentHistory: Package[], metrics: DeploymentMetrics): void => { - var totalActive: number = getTotalActiveFromDeploymentMetrics(metrics); - deploymentHistory.forEach((packageObject: Package) => { - if (metrics[packageObject.label]) { - (packageObject).metrics = { - active: metrics[packageObject.label].active, - downloaded: metrics[packageObject.label].downloaded, - failed: metrics[packageObject.label].failed, - installed: metrics[packageObject.label].installed, - totalActive: totalActive - }; - } - }); - printDeploymentHistory(command, deploymentHistory, account.email); - }); -} - -function deserializeConnectionInfo(): ILoginConnectionInfo { - try { - var savedConnection: string = fs.readFileSync(configFilePath, { encoding: "utf8" }); - var connectionInfo: ILegacyLoginConnectionInfo | ILoginConnectionInfo = JSON.parse(savedConnection); - - // If the connection info is in the legacy format, convert it to the modern format - if ((connectionInfo).accessKeyName) { - connectionInfo = { - accessKey: (connectionInfo).accessKeyName - }; - } - - var connInfo = connectionInfo; - - connInfo.proxy = getProxy(connInfo.proxy, connInfo.noProxy); - - return connInfo; - } catch (ex) { - return; - } -} - -export function execute(command: cli.ICommand): Promise { - connectionInfo = deserializeConnectionInfo(); - - return Q(null) - .then(() => { - switch (command.type) { - // Must not be logged in - case cli.CommandType.login: - case cli.CommandType.register: - if (connectionInfo) { - throw new Error("You are already logged in from this machine."); - } - break; - - // It does not matter whether you are logged in or not - case cli.CommandType.link: - break; - - // Must be logged in - default: - if (!!sdk) break; // Used by unit tests to skip authentication - - if (!connectionInfo) { - throw new Error("You are not currently logged in. Run the 'code-push login' command to authenticate with the CodePush server."); - } - - sdk = getSdk(connectionInfo.accessKey, CLI_HEADERS, connectionInfo.customServerUrl, connectionInfo.proxy); - break; - } - - switch (command.type) { - case cli.CommandType.accessKeyAdd: - return accessKeyAdd(command); - - case cli.CommandType.accessKeyPatch: - return accessKeyPatch(command); - - case cli.CommandType.accessKeyList: - return accessKeyList(command); - - case cli.CommandType.accessKeyRemove: - return accessKeyRemove(command); - - case cli.CommandType.appAdd: - return appAdd(command); - - case cli.CommandType.appList: - return appList(command); - - case cli.CommandType.appRemove: - return appRemove(command); - - case cli.CommandType.appRename: - return appRename(command); - - case cli.CommandType.appTransfer: - return appTransfer(command); - - case cli.CommandType.collaboratorAdd: - return addCollaborator(command); - - case cli.CommandType.collaboratorList: - return listCollaborators(command); - - case cli.CommandType.collaboratorRemove: - return removeCollaborator(command); - - case cli.CommandType.debug: - return debugCommand(command); - - case cli.CommandType.deploymentAdd: - return deploymentAdd(command); - - case cli.CommandType.deploymentHistoryClear: - return deploymentHistoryClear(command); - - case cli.CommandType.deploymentHistory: - return deploymentHistory(command); - - case cli.CommandType.deploymentList: - return deploymentList(command); - - case cli.CommandType.deploymentRemove: - return deploymentRemove(command); - - case cli.CommandType.deploymentRename: - return deploymentRename(command); - - case cli.CommandType.link: - return link(command); - - case cli.CommandType.login: - return login(command); - - case cli.CommandType.logout: - return logout(command); - - case cli.CommandType.patch: - return patch(command); - - case cli.CommandType.promote: - return promote(command); - - case cli.CommandType.register: - return register(command); - - case cli.CommandType.release: - return release(command); - - case cli.CommandType.releaseCordova: - return releaseCordova(command); - - case cli.CommandType.releaseReact: - return releaseReact(command); - - case cli.CommandType.rollback: - return rollback(command); - - case cli.CommandType.sessionList: - return sessionList(command); - - case cli.CommandType.sessionRemove: - return sessionRemove(command); - - case cli.CommandType.whoami: - return whoami(command); - - default: - // We should never see this message as invalid commands should be caught by the argument parser. - throw new Error("Invalid command: " + JSON.stringify(command)); - } - }); -} - -function fileDoesNotExistOrIsDirectory(filePath: string): boolean { - try { - return fs.lstatSync(filePath).isDirectory(); - } catch (error) { - return true; - } -} - -function getTotalActiveFromDeploymentMetrics(metrics: DeploymentMetrics): number { - var totalActive = 0; - Object.keys(metrics).forEach((label: string) => { - totalActive += metrics[label].active; - }); - - return totalActive; -} - -function initiateExternalAuthenticationAsync(action: string, serverUrl?: string): void { - var message: string = `A browser is being launched to authenticate your account. Follow the instructions ` + - `it displays to complete your ${action === "register" ? "registration" : action}.`; - - log(message); - var hostname: string = os.hostname(); - var url: string = `${serverUrl || AccountManager.SERVER_URL}/auth/${action}?hostname=${hostname}`; - opener(url); -} - -function link(command: cli.ILinkCommand): Promise { - initiateExternalAuthenticationAsync("link", command.serverUrl); - return Q(null); -} - -function login(command: cli.ILoginCommand): Promise { - // Check if one of the flags were provided. - if (command.accessKey) { - var proxy = getProxy(command.proxy, command.noProxy); - sdk = getSdk(command.accessKey, CLI_HEADERS, command.serverUrl, proxy); - return sdk.isAuthenticated() - .then((isAuthenticated: boolean): void => { - if (isAuthenticated) { - serializeConnectionInfo(command.accessKey, /*preserveAccessKeyOnLogout*/ true, command.serverUrl, command.proxy, command.noProxy); - } else { - throw new Error("Invalid access key."); - } - }); - } else { - return loginWithExternalAuthentication("login", command.serverUrl, command.proxy, command.noProxy); - } -} - -function loginWithExternalAuthentication(action: string, serverUrl?: string, proxy?: string, noProxy?: boolean): Promise { - initiateExternalAuthenticationAsync(action, serverUrl); - log(""); // Insert newline - - return requestAccessKey() - .then((accessKey: string): Promise => { - if (accessKey === null) { - // The user has aborted the synchronous prompt (e.g.: via [CTRL]+[C]). - return; - } - - sdk = getSdk(accessKey, CLI_HEADERS, serverUrl, getProxy(proxy, noProxy)); - - return sdk.isAuthenticated() - .then((isAuthenticated: boolean): void => { - if (isAuthenticated) { - serializeConnectionInfo(accessKey, /*preserveAccessKeyOnLogout*/ false, serverUrl, proxy, noProxy); - } else { - throw new Error("Invalid access key."); - } - }); - }); -} - -function logout(command: cli.ICommand): Promise { - return Q(null) - .then((): Promise => { - if (!connectionInfo.preserveAccessKeyOnLogout) { - var machineName: string = os.hostname(); - return sdk.removeSession(machineName) - .catch((error: CodePushError) => { - // If we are not authenticated or the session doesn't exist anymore, just swallow the error instead of displaying it - if (error.statusCode !== AccountManager.ERROR_UNAUTHORIZED && error.statusCode !== AccountManager.ERROR_NOT_FOUND) { - throw error; - } - }); - } - }) - .then((): void => { - sdk = null; - deleteConnectionInfoCache(); - }); -} - -function formatDate(unixOffset: number): string { - var date: moment.Moment = moment(unixOffset); - var now: moment.Moment = moment(); - if (Math.abs(now.diff(date, "days")) < 30) { - return date.fromNow(); // "2 hours ago" - } else if (now.year() === date.year()) { - return date.format("MMM D"); // "Nov 6" - } else { - return date.format("MMM D, YYYY"); // "Nov 6, 2014" - } -} - -function printAppList(format: string, apps: App[]): void { - if (format === "json") { - printJson(apps); - } else if (format === "table") { - var headers = ["Name", "Deployments"]; - printTable(headers, (dataSource: any[]): void => { - apps.forEach((app: App, index: number): void => { - var row = [app.name, wordwrap(50)(app.deployments.join(", "))]; - dataSource.push(row); - }); - }); - } -} - -function getCollaboratorDisplayName(email: string, collaboratorProperties: CollaboratorProperties): string { - return (collaboratorProperties.permission === AccountManager.AppPermission.OWNER) ? email + chalk.magenta(" (Owner)") : email; -} - -function printCollaboratorsList(format: string, collaborators: CollaboratorMap): void { - if (format === "json") { - var dataSource = { "collaborators": collaborators }; - printJson(dataSource); - } else if (format === "table") { - var headers = ["E-mail Address"]; - printTable(headers, (dataSource: any[]): void => { - Object.keys(collaborators).forEach((email: string): void => { - var row = [getCollaboratorDisplayName(email, collaborators[email])]; - dataSource.push(row); - }); - }); - } -} - -function printDeploymentList(command: cli.IDeploymentListCommand, deployments: Deployment[], showPackage: boolean = true): void { - if (command.format === "json") { - printJson(deployments); - } else if (command.format === "table") { - var headers = ["Name"]; - if (command.displayKeys) { - headers.push("Deployment Key"); - } - - if (showPackage) { - headers.push("Update Metadata"); - headers.push("Install Metrics"); - } - - printTable(headers, (dataSource: any[]): void => { - deployments.forEach((deployment: Deployment): void => { - var row = [deployment.name]; - if (command.displayKeys) { - row.push(deployment.key); - } - - if (showPackage) { - row.push(getPackageString(deployment.package)); - row.push(getPackageMetricsString(deployment.package)); - } - - dataSource.push(row); - }); - }); - } -} - -function printDeploymentHistory(command: cli.IDeploymentHistoryCommand, deploymentHistory: PackageWithMetrics[], currentUserEmail: string): void { - if (command.format === "json") { - printJson(deploymentHistory); - } else if (command.format === "table") { - var headers = ["Label", "Release Time", "App Version", "Mandatory"]; - if (command.displayAuthor) { - headers.push("Released By"); - } - - headers.push("Description", "Install Metrics"); - - printTable(headers, (dataSource: any[]) => { - deploymentHistory.forEach((packageObject: Package) => { - var releaseTime: string = formatDate(packageObject.uploadTime); - var releaseSource: string; - if (packageObject.releaseMethod === "Promote") { - releaseSource = `Promoted ${packageObject.originalLabel} from "${packageObject.originalDeployment}"`; - } else if (packageObject.releaseMethod === "Rollback") { - var labelNumber: number = parseInt(packageObject.label.substring(1)); - var lastLabel: string = "v" + (labelNumber - 1); - releaseSource = `Rolled back ${lastLabel} to ${packageObject.originalLabel}`; - } - - if (releaseSource) { - releaseTime += "\n" + chalk.magenta(`(${releaseSource})`).toString(); - } - - var row: string[] = [packageObject.label, releaseTime, packageObject.appVersion, packageObject.isMandatory ? "Yes" : "No"]; - if (command.displayAuthor) { - var releasedBy: string = packageObject.releasedBy ? packageObject.releasedBy : ""; - if (currentUserEmail && releasedBy === currentUserEmail) { - releasedBy = "You"; - } - - row.push(releasedBy); - } - - row.push(packageObject.description ? wordwrap(30)(packageObject.description) : ""); - row.push(getPackageMetricsString(packageObject) + (packageObject.isDisabled ? `\n${chalk.green("Disabled:")} Yes` : "")); - if (packageObject.isDisabled) { - row = row.map((cellContents: string) => applyChalkSkippingLineBreaks(cellContents, (chalk).dim)); - } - - dataSource.push(row); - }); - }); - } -} - -function applyChalkSkippingLineBreaks(applyString: string, chalkMethod: (string: string) => Chalk.ChalkChain): string { - // Used to prevent "chalk" from applying styles to linebreaks which - // causes table border chars to have the style applied as well. - return applyString - .split("\n") - .map((token: string) => chalkMethod(token)) - .join("\n"); -} - -function getPackageString(packageObject: Package): string { - if (!packageObject) { - return chalk.magenta("No updates released").toString(); - } - - var packageString: string = chalk.green("Label: ") + packageObject.label + "\n" + - chalk.green("App Version: ") + packageObject.appVersion + "\n" + - chalk.green("Mandatory: ") + (packageObject.isMandatory ? "Yes" : "No") + "\n" + - chalk.green("Release Time: ") + formatDate(packageObject.uploadTime) + "\n" + - chalk.green("Released By: ") + (packageObject.releasedBy ? packageObject.releasedBy : "") + - (packageObject.description ? wordwrap(70)("\n" + chalk.green("Description: ") + packageObject.description) : ""); - - if (packageObject.isDisabled) { - packageString += `\n${chalk.green("Disabled:")} Yes`; - } - - return packageString; -} - -function getPackageMetricsString(obj: Package): string { - var packageObject = obj; - var rolloutString: string = (obj && obj.rollout && obj.rollout !== 100) ? `\n${chalk.green("Rollout:")} ${obj.rollout.toLocaleString()}%` : ""; - - if (!packageObject || !packageObject.metrics) { - return chalk.magenta("No installs recorded").toString() + (rolloutString || ""); - } - - var activePercent: number = packageObject.metrics.totalActive - ? packageObject.metrics.active / packageObject.metrics.totalActive * 100 - : 0.0; - var percentString: string; - if (activePercent === 100.0) { - percentString = "100%"; - } else if (activePercent === 0.0) { - percentString = "0%"; - } else { - percentString = activePercent.toPrecision(2) + "%"; - } - - var numPending: number = packageObject.metrics.downloaded - packageObject.metrics.installed - packageObject.metrics.failed; - var returnString: string = chalk.green("Active: ") + percentString + " (" + packageObject.metrics.active.toLocaleString() + " of " + packageObject.metrics.totalActive.toLocaleString() + ")\n" + - chalk.green("Total: ") + packageObject.metrics.installed.toLocaleString(); - - if (numPending > 0) { - returnString += " (" + numPending.toLocaleString() + " pending)"; - } - - if (packageObject.metrics.failed) { - returnString += "\n" + chalk.green("Rollbacks: ") + chalk.red(packageObject.metrics.failed.toLocaleString() + ""); - } - - if (rolloutString) { - returnString += rolloutString; - } - - return returnString; -} - -function getReactNativeProjectAppVersion(command: cli.IReleaseReactCommand, projectName: string): Promise { - const fileExists = (file: string): boolean => { - try { return fs.statSync(file).isFile() } - catch (e) { return false } - }; - - const isValidVersion = (version: string): boolean => !!semver.valid(version) || /^\d+\.\d+$/.test(version); - - log(chalk.cyan(`Detecting ${command.platform} app version:\n`)); - - if (command.platform === "ios") { - let resolvedPlistFile: string = command.plistFile; - if (resolvedPlistFile) { - // If a plist file path is explicitly provided, then we don't - // need to attempt to "resolve" it within the well-known locations. - if (!fileExists(resolvedPlistFile)) { - throw new Error("The specified plist file doesn't exist. Please check that the provided path is correct."); - } - } else { - // Allow the plist prefix to be specified with or without a trailing - // separator character, but prescribe the use of a hyphen when omitted, - // since this is the most commonly used convetion for plist files. - if (command.plistFilePrefix && /.+[^-.]$/.test(command.plistFilePrefix)) { - command.plistFilePrefix += "-"; - } - - const iOSDirectory: string = "ios"; - const plistFileName = `${command.plistFilePrefix || ""}Info.plist`; - - const knownLocations = [ - path.join(iOSDirectory, projectName, plistFileName), - path.join(iOSDirectory, plistFileName) - ]; - - resolvedPlistFile = (knownLocations).find(fileExists); - - if (!resolvedPlistFile) { - throw new Error(`Unable to find either of the following plist files in order to infer your app's binary version: "${knownLocations.join("\", \"")}". If your plist has a different name, or is located in a different directory, consider using either the "--plistFile" or "--plistFilePrefix" parameters to help inform the CLI how to find it.`); - } - } - - const plistContents = fs.readFileSync(resolvedPlistFile).toString(); - - try { - var parsedPlist = plist.parse(plistContents); - } catch (e) { - throw new Error(`Unable to parse "${resolvedPlistFile}". Please ensure it is a well-formed plist file.`); - } - - if (parsedPlist && parsedPlist.CFBundleShortVersionString) { - if (isValidVersion(parsedPlist.CFBundleShortVersionString)) { - log(`Using the target binary version value "${parsedPlist.CFBundleShortVersionString}" from "${resolvedPlistFile}".\n`); - return Q(parsedPlist.CFBundleShortVersionString); - } else { - throw new Error(`The "CFBundleShortVersionString" key in the "${resolvedPlistFile}" file needs to specify a valid semver string, containing both a major and minor version (e.g. 1.3.2, 1.1).`); - } - } else { - throw new Error(`The "CFBundleShortVersionString" key doesn't exist within the "${resolvedPlistFile}" file.`); - } - } else if (command.platform === "android") { - let buildGradlePath: string = path.join("android", "app"); - if (command.gradleFile) { - buildGradlePath = command.gradleFile; - } - if (fs.lstatSync(buildGradlePath).isDirectory()) { - buildGradlePath = path.join(buildGradlePath, "build.gradle"); - } - - if (fileDoesNotExistOrIsDirectory(buildGradlePath)) { - throw new Error(`Unable to find gradle file "${buildGradlePath}".`); - } - - return g2js.parseFile(buildGradlePath) - .catch(() => { - throw new Error(`Unable to parse the "${buildGradlePath}" file. Please ensure it is a well-formed Gradle file.`); - }) - .then((buildGradle: any) => { - if (!buildGradle.android || !buildGradle.android.defaultConfig || !buildGradle.android.defaultConfig.versionName) { - throw new Error(`The "${buildGradlePath}" file doesn't specify a value for the "android.defaultConfig.versionName" property.`); - } - - if (typeof buildGradle.android.defaultConfig.versionName !== "string") { - throw new Error(`The "android.defaultConfig.versionName" property value in "${buildGradlePath}" is not a valid string. If this is expected, consider using the --targetBinaryVersion option to specify the value manually.`); - } - - let appVersion: string = buildGradle.android.defaultConfig.versionName.replace(/"/g, "").trim(); - - if (isValidVersion(appVersion)) { - // The versionName property is a valid semver string, - // so we can safely use that and move on. - log(`Using the target binary version value "${appVersion}" from "${buildGradlePath}".\n`); - return appVersion; - } else if (/^\d.*/.test(appVersion)) { - // The versionName property isn't a valid semver string, - // but it starts with a number, and therefore, it can't - // be a valid Gradle property reference. - throw new Error(`The "android.defaultConfig.versionName" property in the "${buildGradlePath}" file needs to specify a valid semver string, containing both a major and minor version (e.g. 1.3.2, 1.1).`); - } - - // The version property isn't a valid semver string - // so we assume it is a reference to a property variable. - const propertyName = appVersion.replace("project.", ""); - const propertiesFileName = "gradle.properties"; - - const knownLocations = [ - path.join("android", "app", propertiesFileName), - path.join("android", propertiesFileName) - ]; - - const propertiesFile: string = (knownLocations).find(fileExists); - const propertiesContent: string = fs.readFileSync(propertiesFile).toString(); - - try { - const parsedProperties: any = properties.parse(propertiesContent); - appVersion = parsedProperties[propertyName]; - } catch (e) { - throw new Error(`Unable to parse "${propertiesFile}". Please ensure it is a well-formed properties file.`); - } - - if (!appVersion) { - throw new Error(`No property named "${propertyName}" exists in the "${propertiesFile}" file.`); - } - - if (!isValidVersion(appVersion)) { - throw new Error(`The "${propertyName}" property in the "${propertiesFile}" file needs to specify a valid semver string, containing both a major and minor version (e.g. 1.3.2, 1.1).`); - } - - log(`Using the target binary version value "${appVersion}" from the "${propertyName}" key in the "${propertiesFile}" file.\n`); - return appVersion.toString(); - }); - } else { - var appxManifestFileName: string = "Package.appxmanifest"; - try { - var appxManifestContainingFolder: string = path.join("windows", projectName); - var appxManifestContents: string = fs.readFileSync(path.join(appxManifestContainingFolder, "Package.appxmanifest")).toString(); - } catch (err) { - throw new Error(`Unable to find or read "${appxManifestFileName}" in the "${path.join("windows", projectName)}" folder.`); - } - - return parseXml(appxManifestContents) - .catch((err: any) => { - throw new Error(`Unable to parse the "${path.join(appxManifestContainingFolder, appxManifestFileName)}" file, it could be malformed.`); - }) - .then((parsedAppxManifest: any) => { - try { - return parsedAppxManifest.Package.Identity[0]["$"].Version.match(/^\d+\.\d+\.\d+/)[0]; - } catch (e) { - throw new Error(`Unable to parse the package version from the "${path.join(appxManifestContainingFolder, appxManifestFileName)}" file.`); - } - }); - } -} - -function printJson(object: any): void { - log(JSON.stringify(object, /*replacer=*/ null, /*spacing=*/ 2)); -} - -function printAccessKeys(format: string, keys: AccessKey[]): void { - if (format === "json") { - printJson(keys); - } else if (format === "table") { - printTable(["Name", "Created", "Expires"], (dataSource: any[]): void => { - var now = new Date().getTime(); - - function isExpired(key: AccessKey): boolean { - return now >= key.expires; - } - - function keyToTableRow(key: AccessKey, dim: boolean): string[] { - var row: string[] = [ - key.name, - key.createdTime ? formatDate(key.createdTime) : "", - formatDate(key.expires) - ]; - - if (dim) { - row.forEach((col: string, index: number) => { - row[index] = (chalk).dim(col); - }); - } - - return row; - } - - keys.forEach((key: AccessKey) => - !isExpired(key) && dataSource.push(keyToTableRow(key, /*dim*/ false))); - keys.forEach((key: AccessKey) => - isExpired(key) && dataSource.push(keyToTableRow(key, /*dim*/ true))); - }); - } -} - -function printSessions(format: string, sessions: Session[]): void { - if (format === "json") { - printJson(sessions); - } else if (format === "table") { - printTable(["Machine", "Logged in"], (dataSource: any[]): void => { - sessions.forEach((session: Session) => - dataSource.push([session.machineName, formatDate(session.loggedInTime)])); - }); - } -} - -function printTable(columnNames: string[], readData: (dataSource: any[]) => void): void { - var table = new Table({ - head: columnNames, - style: { head: ["cyan"] } - }); - - readData(table); - - log(table.toString()); -} - -function register(command: cli.IRegisterCommand): Promise { - return loginWithExternalAuthentication("register", command.serverUrl, command.proxy, command.noProxy); -} - -function promote(command: cli.IPromoteCommand): Promise { - var packageInfo: PackageInfo = { - appVersion: command.appStoreVersion, - description: command.description, - isDisabled: getYargsBooleanOrNull(command.disabled), - isMandatory: getYargsBooleanOrNull(command.mandatory), - rollout: command.rollout - }; - - return sdk.promote(command.appName, command.sourceDeploymentName, command.destDeploymentName, packageInfo) - .then((): void => { - log("Successfully promoted the \"" + command.sourceDeploymentName + "\" deployment of the \"" + command.appName + "\" app to the \"" + command.destDeploymentName + "\" deployment."); - }) - .catch((err: CodePushError) => releaseErrorHandler(err, command)); -} - -function patch(command: cli.IPatchCommand): Promise { - var packageInfo: PackageInfo = { - appVersion: command.appStoreVersion, - description: command.description, - isMandatory: getYargsBooleanOrNull(command.mandatory), - isDisabled: getYargsBooleanOrNull(command.disabled), - rollout: command.rollout - }; - - for (var updateProperty in packageInfo) { - if ((packageInfo)[updateProperty] !== null) { - return sdk.patchRelease(command.appName, command.deploymentName, command.label, packageInfo) - .then((): void => { - log(`Successfully updated the "${command.label ? command.label : `latest`}" release of "${command.appName}" app's "${command.deploymentName}" deployment.`); - }); - } - } - - throw new Error("At least one property must be specified to patch a release."); -} - -export var release = (command: cli.IReleaseCommand): Promise => { - - if (isBinaryOrZip(command.package)) { - throw new Error("It is unnecessary to package releases in a .zip or binary file. Please specify the direct path to the update content's directory (e.g. /platforms/ios/www) or file (e.g. main.jsbundle)."); - } - - throwForInvalidSemverRange(command.appStoreVersion); - var filePath: string = command.package; - var isSingleFilePackage: boolean = true; - - if (fs.lstatSync(filePath).isDirectory()) { - isSingleFilePackage = false; - } - - var lastTotalProgress = 0; - var progressBar = new progress("Upload progress:[:bar] :percent :etas", { - complete: "=", - incomplete: " ", - width: 50, - total: 100 - }); - - var uploadProgress = (currentProgress: number): void => { - progressBar.tick(currentProgress - lastTotalProgress); - lastTotalProgress = currentProgress; - }; - - var updateMetadata: PackageInfo = { - description: command.description, - isDisabled: command.disabled, - isMandatory: command.mandatory, - rollout: command.rollout - }; - - return sdk.isAuthenticated(true) - .then((isAuth: boolean): Promise => { - return sdk.release(command.appName, command.deploymentName, filePath, command.appStoreVersion, updateMetadata, uploadProgress); - }) - .then((): void => { - log("Successfully released an update containing the \"" + command.package + "\" " + (isSingleFilePackage ? "file" : "directory") + " to the \"" + command.deploymentName + "\" deployment of the \"" + command.appName + "\" app."); - }) - .catch((err: CodePushError) => releaseErrorHandler(err, command)); -} - -export var releaseCordova = (command: cli.IReleaseCordovaCommand): Promise => { - var platform: string = command.platform.toLowerCase(); - var projectRoot: string = process.cwd(); - var platformFolder: string = path.join(projectRoot, "platforms", platform); - var platformCordova: string = path.join(platformFolder, "cordova"); - var outputFolder: string; - - if (platform === "ios") { - outputFolder = path.join(platformFolder, "www"); - } else if (platform === "android") { - outputFolder = path.join(platformFolder, "assets", "www"); - } else { - throw new Error("Platform must be either \"ios\" or \"android\"."); - } - - var cordovaCommand: string = command.build ? "build" : "prepare"; - var cordovaCLI: string = "cordova"; - - // Check whether the Cordova or PhoneGap CLIs are - // installed, and if not, fail early - try { - which.sync(cordovaCLI); - } catch (e) { - try { - cordovaCLI = "phonegap"; - which.sync(cordovaCLI); - } catch (e) { - throw new Error(`Unable to ${cordovaCommand} project. Please ensure that either the Cordova or PhoneGap CLI is installed.`); - } - } - - log(chalk.cyan(`Running "${cordovaCLI} ${cordovaCommand}" command:\n`)); - try { - execSync([cordovaCLI, cordovaCommand, platform, "--verbose"].join(" "), { stdio: "inherit" }); - } catch (error) { - throw new Error(`Unable to ${cordovaCommand} project. Please ensure that the CWD represents a Cordova project and that the "${platform}" platform was added by running "${cordovaCLI} platform add ${platform}".`); - } - - try { - var configString: string = fs.readFileSync(path.join(projectRoot, "config.xml"), { encoding: "utf8" }); - } catch (error) { - throw new Error(`Unable to find or read "config.xml" in the CWD. The "release-cordova" command must be executed in a Cordova project folder.`); - } - - var configPromise: Promise = parseXml(configString); - var releaseCommand: cli.IReleaseCommand = command; - - releaseCommand.package = outputFolder; - releaseCommand.type = cli.CommandType.release; - - return configPromise - .catch((err: any) => { - throw new Error(`Unable to parse "config.xml" in the CWD. Ensure that the contents of "config.xml" is valid XML.`); - }) - .then((parsedConfig: any) => { - var config: any = parsedConfig.widget; - - var releaseTargetVersion: string; - if (command.appStoreVersion) { - releaseTargetVersion = command.appStoreVersion; - } else { - releaseTargetVersion = config["$"].version; - } - - throwForInvalidSemverRange(releaseTargetVersion); - releaseCommand.appStoreVersion = releaseTargetVersion; - - log(chalk.cyan("\nReleasing update contents to CodePush:\n")); - return release(releaseCommand); - }); -} - -export var releaseReact = (command: cli.IReleaseReactCommand): Promise => { - var bundleName: string = command.bundleName; - var entryFile: string = command.entryFile; - var outputFolder: string = path.join(os.tmpdir(), "CodePush"); - var platform: string = command.platform = command.platform.toLowerCase(); - var releaseCommand: cli.IReleaseCommand = command; - releaseCommand.package = outputFolder; - - switch (platform) { - case "android": - case "ios": - case "windows": - if (!bundleName) { - bundleName = platform === "ios" - ? "main.jsbundle" - : `index.${platform}.bundle`; - } - - break; - default: - throw new Error("Platform must be either \"android\", \"ios\" or \"windows\"."); - } - - try { - var projectPackageJson: any = require(path.join(process.cwd(), "package.json")); - var projectName: string = projectPackageJson.name; - if (!projectName) { - throw new Error("The \"package.json\" file in the CWD does not have the \"name\" field set."); - } - - if (!projectPackageJson.dependencies["react-native"]) { - throw new Error("The project in the CWD is not a React Native project."); - } - } catch (error) { - throw new Error("Unable to find or read \"package.json\" in the CWD. The \"release-react\" command must be executed in a React Native project folder."); - } - - if (!entryFile) { - entryFile = `index.${platform}.js`; - if (fileDoesNotExistOrIsDirectory(entryFile)) { - entryFile = "index.js"; - } - - if (fileDoesNotExistOrIsDirectory(entryFile)) { - throw new Error(`Entry file "index.${platform}.js" or "index.js" does not exist.`); - } - } else { - if (fileDoesNotExistOrIsDirectory(entryFile)) { - throw new Error(`Entry file "${entryFile}" does not exist.`); - } - } - - if (command.appStoreVersion) { - throwForInvalidSemverRange(command.appStoreVersion); - } - - var appVersionPromise: Promise = command.appStoreVersion - ? Q(command.appStoreVersion) - : getReactNativeProjectAppVersion(command, projectName); - - return appVersionPromise - .then((appVersion: string) => { - releaseCommand.appStoreVersion = appVersion; - return createEmptyTempReleaseFolder(outputFolder); - }) - // This is needed to clear the react native bundler cache: - // https://github.com/facebook/react-native/issues/4289 - .then(() => deleteFolder(`${os.tmpdir()}/react-*`)) - .then(() => runReactNativeBundleCommand(bundleName, command.development || false, entryFile, outputFolder, platform, command.sourcemapOutput)) - .then(() => { - log(chalk.cyan("\nReleasing update contents to CodePush:\n")); - return release(releaseCommand); - }) - .then(() => deleteFolder(outputFolder)) - .catch((err: Error) => { - deleteFolder(outputFolder); - throw err; - }); -} - -function rollback(command: cli.IRollbackCommand): Promise { - return confirm() - .then((wasConfirmed: boolean) => { - if (!wasConfirmed) { - log("Rollback cancelled.") - return; - } - - return sdk.rollback(command.appName, command.deploymentName, command.targetRelease || undefined) - .then((): void => { - log("Successfully performed a rollback on the \"" + command.deploymentName + "\" deployment of the \"" + command.appName + "\" app."); - }); - }); -} - -function requestAccessKey(): Promise { - return Promise((resolve, reject, notify): void => { - prompt.message = ""; - prompt.delimiter = ""; - - prompt.start(); - - prompt.get({ - properties: { - response: { - description: chalk.cyan("Enter your access key: ") - } - } - }, (err: any, result: any): void => { - if (err) { - resolve(null); - } else { - resolve(result.response.trim()); - } - }); - }); -} - -export var runReactNativeBundleCommand = (bundleName: string, development: boolean, entryFile: string, outputFolder: string, platform: string, sourcemapOutput: string): Promise => { - var reactNativeBundleArgs = [ - path.join("node_modules", "react-native", "local-cli", "cli.js"), "bundle", - "--assets-dest", outputFolder, - "--bundle-output", path.join(outputFolder, bundleName), - "--dev", development, - "--entry-file", entryFile, - "--platform", platform, - ]; - - if (sourcemapOutput) { - reactNativeBundleArgs.push("--sourcemap-output", sourcemapOutput); - } - - log(chalk.cyan("Running \"react-native bundle\" command:\n")); - var reactNativeBundleProcess = spawn("node", reactNativeBundleArgs); - log(`node ${reactNativeBundleArgs.join(" ")}`); - - return Promise((resolve, reject, notify) => { - reactNativeBundleProcess.stdout.on("data", (data: Buffer) => { - log(data.toString().trim()); - }); - - reactNativeBundleProcess.stderr.on("data", (data: Buffer) => { - console.error(data.toString().trim()); - }); - - reactNativeBundleProcess.on("close", (exitCode: number) => { - if (exitCode) { - reject(new Error(`"react-native bundle" command exited with code ${exitCode}.`)); - } - - resolve(null); - }); - }); -} - -function serializeConnectionInfo(accessKey: string, preserveAccessKeyOnLogout: boolean, customServerUrl?: string, proxy?: string, noProxy?: boolean): void { - var connectionInfo: ILoginConnectionInfo = { accessKey: accessKey, preserveAccessKeyOnLogout: preserveAccessKeyOnLogout, proxy: proxy, noProxy: noProxy }; - if (customServerUrl) { - connectionInfo.customServerUrl = customServerUrl; - } - - var json: string = JSON.stringify(connectionInfo); - fs.writeFileSync(configFilePath, json, { encoding: "utf8" }); - - log(`\r\nSuccessfully logged-in. Your session file was written to ${chalk.cyan(configFilePath)}. You can run the ${chalk.cyan("code-push logout")} command at any time to delete this file and terminate your session.\r\n`); -} - -function sessionList(command: cli.ISessionListCommand): Promise { - throwForInvalidOutputFormat(command.format); - - return sdk.getSessions() - .then((sessions: Session[]): void => { - printSessions(command.format, sessions); - }); -} - -function sessionRemove(command: cli.ISessionRemoveCommand): Promise { - if (os.hostname() === command.machineName) { - throw new Error("Cannot remove the current login session via this command. Please run 'code-push logout' instead."); - } else { - return confirm() - .then((wasConfirmed: boolean): Promise => { - if (wasConfirmed) { - return sdk.removeSession(command.machineName) - .then((): void => { - log(`Successfully removed the login session for "${command.machineName}".`); - }); - } - - log("Session removal cancelled."); - }); - } -} - -function releaseErrorHandler(error: CodePushError, command: cli.ICommand): void { - if ((command).noDuplicateReleaseError && error.statusCode === AccountManager.ERROR_CONFLICT) { - console.warn(chalk.yellow("[Warning] " + error.message)); - } else { - throw error; - } -} - -function isBinaryOrZip(path: string): boolean { - return path.search(/\.zip$/i) !== -1 - || path.search(/\.apk$/i) !== -1 - || path.search(/\.ipa$/i) !== -1; -} - -function getYargsBooleanOrNull(value: any): boolean { - // Yargs treats a boolean argument as an array of size 2 for null, third is the value of boolean. - return value && value.length > 2 ? value[2] : null; -} - -function throwForInvalidEmail(email: string): void { - if (!emailValidator.validate(email)) { - throw new Error("\"" + email + "\" is an invalid e-mail address."); - } -} - -function throwForInvalidSemverRange(semverRange: string): void { - if (semver.validRange(semverRange) === null) { - throw new Error("Please use a semver-compliant target binary version range, for example \"1.0.0\", \"*\" or \"^1.2.3\"."); - } -} - -function throwForInvalidOutputFormat(format: string): void { - switch (format) { - case "json": - case "table": - break; - - default: - throw new Error("Invalid format: " + format + "."); - } -} - -function whoami(command: cli.ICommand): Promise { - return sdk.getAccountInfo() - .then((account): void => { - var accountInfo = `${account.email} (${account.linkedProviders.join(", ")})`; - - var connectionInfo = deserializeConnectionInfo(); - if (connectionInfo.noProxy || connectionInfo.proxy) { - log(chalk.green('Account: ') + accountInfo); - - var proxyInfo = chalk.green('Proxy: ') + (connectionInfo.noProxy ? 'Ignored' : connectionInfo.proxy); - log(proxyInfo); - } else { - log(accountInfo); - } - }); -} - -function getProxy(proxy?: string, noProxy?: boolean): string { - if (noProxy) return null; - if (!proxy) return process.env.HTTPS_PROXY || process.env.https_proxy || process.env.HTTP_PROXY || process.env.http_proxy; - else return proxy; -} - -function isCommandOptionSpecified(option: any): boolean { - return option !== undefined && option !== null; -} - -function getSdk(accessKey: string, headers: Headers, customServerUrl: string, proxy: string): AccountManager { - var sdk: any = new AccountManager(accessKey, CLI_HEADERS, customServerUrl, proxy); - /* - * If the server returns `Unauthorized`, it must be due to an invalid - * (or expired) access key. For convenience, we patch every SDK call - * to delete the cached connection so the user can simply - * login again instead of having to log out first. - */ - Object.getOwnPropertyNames(AccountManager.prototype).forEach((functionName: any) => { - if (typeof sdk[functionName] === "function") { - var originalFunction = sdk[functionName]; - sdk[functionName] = function() { - var maybePromise: Promise = originalFunction.apply(sdk, arguments); - if (maybePromise && maybePromise.then !== undefined) { - maybePromise = maybePromise - .catch((error: any) => { - if (error.statusCode && error.statusCode === AccountManager.ERROR_UNAUTHORIZED) { - deleteConnectionInfoCache(/* printMessage */ false); - } - - throw error; - }); - } - - return maybePromise; - }; - } - }); - - return sdk; -} diff --git a/cli/script/command-parser.ts b/cli/script/command-parser.ts deleted file mode 100644 index bbe2d505..00000000 --- a/cli/script/command-parser.ts +++ /dev/null @@ -1,915 +0,0 @@ -import * as yargs from "yargs"; -import * as cli from "../definitions/cli"; -import * as chalk from "chalk"; -import * as updateNotifier from "update-notifier"; -import backslash = require("backslash"); -import parseDuration = require("parse-duration"); - -var packageJson = require("../package.json"); -const ROLLOUT_PERCENTAGE_REGEX: RegExp = /^(100|[1-9][0-9]|[1-9])%?$/; -const USAGE_PREFIX = "Usage: code-push"; - -// Command categories are: access-key, app, release, deployment, deployment-key, login, logout, register -var isValidCommandCategory = false; -// Commands are the verb following the command category (e.g.: "add" in "app add"). -var isValidCommand = false; -var wasHelpShown = false; - -export function showHelp(showRootDescription?: boolean): void { - if (!wasHelpShown) { - if (showRootDescription) { - console.log(chalk.cyan(" _____ __ " + chalk.green(" ___ __ "))); - console.log(chalk.cyan(" / ___/__ ___/ /__" + chalk.green(" / _ \\__ _____ / / "))); - console.log(chalk.cyan("/ /__/ _ \\/ _ / -_)" + chalk.green(" ___/ // (_-") - .demand(/*count*/ 3, /*max*/ 3) // Require exactly three non-option arguments. - .example("access-key " + commandName + " \"VSTS Integration\"", "Creates a new access key with the name \"VSTS Integration\", which expires in 60 days") - .example("access-key " + commandName + " \"One time key\" --ttl 5m", "Creates a new access key with the name \"One time key\", which expires in 5 minutes") - .option("ttl", { default: "60d", demand: false, description: "Duration string which specifies the amount of time that the access key should remain valid for (e.g 5m, 60d, 1y)", type: "string" }); - - addCommonConfiguration(yargs); -} - -function accessKeyPatch(commandName: string, yargs: yargs.Argv): void { - isValidCommand = true; - yargs.usage(USAGE_PREFIX + " access-key " + commandName + " ") - .demand(/*count*/ 3, /*max*/ 3) // Require exactly three non-option arguments. - .example("access-key " + commandName + " \"Key for build server\" --name \"Key for CI machine\"", "Renames the access key named \"Key for build server\" to \"Key for CI machine\"") - .example("access-key " + commandName + " \"Key for build server\" --ttl 7d", "Updates the access key named \"Key for build server\" to expire in 7 days") - .option("name", { default: null, demand: false, description: "Display name for the access key", type: "string" }) - .option("ttl", { default: null, demand: false, description: "Duration string which specifies the amount of time that the access key should remain valid for (e.g 5m, 60d, 1y)", type: "string" }); - addCommonConfiguration(yargs); -} - -function accessKeyList(commandName: string, yargs: yargs.Argv): void { - isValidCommand = true; - yargs.usage(USAGE_PREFIX + " access-key " + commandName + " [options]") - .demand(/*count*/ 2, /*max*/ 2) // Require exactly two non-option arguments. - .example("access-key " + commandName, "Lists your access keys in tabular format") - .example("access-key " + commandName + " --format json", "Lists your access keys in JSON format") - .option("format", { default: "table", demand: false, description: "Output format to display your access keys with (\"json\" or \"table\")", type: "string" }); - - addCommonConfiguration(yargs); -} - -function accessKeyRemove(commandName: string, yargs: yargs.Argv): void { - isValidCommand = true; - yargs.usage(USAGE_PREFIX + " access-key " + commandName + " ") - .demand(/*count*/ 3, /*max*/ 3) // Require exactly three non-option arguments. - .example("access-key " + commandName + " \"VSTS Integration\"", "Removes the \"VSTS Integration\" access key"); - - addCommonConfiguration(yargs); -} - -function addCommonConfiguration(yargs: yargs.Argv): void { - yargs.wrap(/*columnLimit*/ null) - .string("_") // Interpret non-hyphenated arguments as strings (e.g. an app version of '1.10'). - .strict() // Validate hyphenated (named) arguments. - .fail((msg: string) => showHelp()); // Suppress the default error message. -} - -function appList(commandName: string, yargs: yargs.Argv): void { - isValidCommand = true; - yargs.usage(USAGE_PREFIX + " app " + commandName + " [options]") - .demand(/*count*/ 2, /*max*/ 2) // Require exactly two non-option arguments. - .example("app " + commandName, "List your apps in tabular format") - .example("app " + commandName + " --format json", "List your apps in JSON format") - .option("format", { default: "table", demand: false, description: "Output format to display your apps with (\"json\" or \"table\")", type: "string" }); - - addCommonConfiguration(yargs); -} - -function appRemove(commandName: string, yargs: yargs.Argv): void { - isValidCommand = true; - yargs.usage(USAGE_PREFIX + " app " + commandName + " ") - .demand(/*count*/ 3, /*max*/ 3) // Require exactly three non-option arguments. - .example("app " + commandName + " MyApp", "Removes app \"MyApp\""); - - addCommonConfiguration(yargs); -} - -function listCollaborators(commandName: string, yargs: yargs.Argv): void { - isValidCommand = true; - yargs.usage(USAGE_PREFIX + " collaborator " + commandName + " [options]") - .demand(/*count*/ 3, /*max*/ 3) // Require exactly three non-option arguments. - .example("collaborator " + commandName + " MyApp", "Lists the collaborators for app \"MyApp\" in tabular format") - .example("collaborator " + commandName + " MyApp --format json", "Lists the collaborators for app \"MyApp\" in JSON format") - .option("format", { default: "table", demand: false, description: "Output format to display collaborators with (\"json\" or \"table\")", type: "string" }); - - addCommonConfiguration(yargs); -} - -function removeCollaborator(commandName: string, yargs: yargs.Argv): void { - isValidCommand = true; - yargs.usage(USAGE_PREFIX + " collaborator " + commandName + " ") - .demand(/*count*/ 4, /*max*/ 4) // Require exactly four non-option arguments. - .example("collaborator " + commandName + " MyApp foo@bar.com", "Removes foo@bar.com as a collaborator from app \"MyApp\""); - - addCommonConfiguration(yargs); -} - -function sessionList(commandName: string, yargs: yargs.Argv): void { - isValidCommand = true; - yargs.usage(USAGE_PREFIX + " session " + commandName + " [options]") - .demand(/*count*/ 2, /*max*/ 2) // Require exactly two non-option arguments. - .example("session " + commandName, "Lists your sessions in tabular format") - .example("session " + commandName + " --format json", "Lists your login sessions in JSON format") - .option("format", { default: "table", demand: false, description: "Output format to display your login sessions with (\"json\" or \"table\")", type: "string" }); - - addCommonConfiguration(yargs); -} - -function sessionRemove(commandName: string, yargs: yargs.Argv): void { - isValidCommand = true; - yargs.usage(USAGE_PREFIX + " session " + commandName + " ") - .demand(/*count*/ 3, /*max*/ 3) // Require exactly three non-option arguments. - .example("session " + commandName + " \"John's PC\"", "Removes the existing login session from \"John's PC\""); - - addCommonConfiguration(yargs); -} - -function deploymentHistoryClear(commandName: string, yargs: yargs.Argv): void { - isValidCommand = true; - yargs.usage(USAGE_PREFIX + " deployment " + commandName + " ") - .demand(/*count*/ 4, /*max*/ 4) // Require exactly four non-option arguments. - .example("deployment " + commandName + " MyApp MyDeployment", "Clears the release history associated with deployment \"MyDeployment\" from app \"MyApp\""); - - addCommonConfiguration(yargs); -} - -function deploymentList(commandName: string, yargs: yargs.Argv): void { - isValidCommand = true; - yargs.usage(USAGE_PREFIX + " deployment " + commandName + " [options]") - .demand(/*count*/ 3, /*max*/ 3) // Require exactly three non-option arguments. - .example("deployment " + commandName + " MyApp", "Lists the deployments for app \"MyApp\" in tabular format") - .example("deployment " + commandName + " MyApp --format json", "Lists the deployments for app \"MyApp\" in JSON format") - .option("format", { default: "table", demand: false, description: "Output format to display your deployments with (\"json\" or \"table\")", type: "string" }) - .option("displayKeys", { alias: "k", default: false, demand: false, description: "Specifies whether to display the deployment keys", type: "boolean" }); - addCommonConfiguration(yargs); -} - -function deploymentRemove(commandName: string, yargs: yargs.Argv): void { - isValidCommand = true; - yargs.usage(USAGE_PREFIX + " deployment " + commandName + " ") - .demand(/*count*/ 4, /*max*/ 4) // Require exactly four non-option arguments. - .example("deployment " + commandName + " MyApp MyDeployment", "Removes deployment \"MyDeployment\" from app \"MyApp\""); - - addCommonConfiguration(yargs); -} - -function deploymentHistory(commandName: string, yargs: yargs.Argv): void { - isValidCommand = true; - yargs.usage(USAGE_PREFIX + " deployment " + commandName + " [options]") - .demand(/*count*/ 4, /*max*/ 4) // Require exactly four non-option arguments. - .example("deployment " + commandName + " MyApp MyDeployment", "Displays the release history for deployment \"MyDeployment\" from app \"MyApp\" in tabular format") - .example("deployment " + commandName + " MyApp MyDeployment --format json", "Displays the release history for deployment \"MyDeployment\" from app \"MyApp\" in JSON format") - .option("format", { default: "table", demand: false, description: "Output format to display the release history with (\"json\" or \"table\")", type: "string" }) - .option("displayAuthor", { alias: "a", default: false, demand: false, description: "Specifies whether to display the release author", type: "boolean" }); - - addCommonConfiguration(yargs); -} - -var argv = yargs.usage(USAGE_PREFIX + " ") - .demand(/*count*/ 1, /*max*/ 1) // Require exactly one non-option argument. - .command("access-key", "View and manage the access keys associated with your account", (yargs: yargs.Argv) => { - isValidCommandCategory = true; - yargs.usage(USAGE_PREFIX + " access-key ") - .demand(/*count*/ 2, /*max*/ 2) // Require exactly two non-option arguments. - .command("add", "Create a new access key associated with your account", (yargs: yargs.Argv) => accessKeyAdd("add", yargs)) - .command("patch", "Update the name and/or TTL of an existing access key", (yargs: yargs.Argv) => accessKeyPatch("patch", yargs)) - .command("remove", "Remove an existing access key", (yargs: yargs.Argv) => accessKeyRemove("remove", yargs)) - .command("rm", "Remove an existing access key", (yargs: yargs.Argv) => accessKeyRemove("rm", yargs)) - .command("list", "List the access keys associated with your account", (yargs: yargs.Argv) => accessKeyList("list", yargs)) - .command("ls", "List the access keys associated with your account", (yargs: yargs.Argv) => accessKeyList("ls", yargs)) - .check((argv: any, aliases: { [aliases: string]: string }): any => isValidCommand); // Report unrecognized, non-hyphenated command category. - - addCommonConfiguration(yargs); - }) - .command("app", "View and manage your CodePush apps", (yargs: yargs.Argv) => { - isValidCommandCategory = true; - yargs.usage(USAGE_PREFIX + " app ") - .demand(/*count*/ 2, /*max*/ 2) // Require exactly two non-option arguments. - .command("add", "Add a new app to your account", (yargs: yargs.Argv): void => { - isValidCommand = true; - yargs.usage(USAGE_PREFIX + " app add ") - .demand(/*count*/ 3, /*max*/ 3) // Require exactly three non-option arguments. - .example("app add MyApp", "Adds app \"MyApp\""); - - addCommonConfiguration(yargs); - }) - .command("remove", "Remove an app from your account", (yargs: yargs.Argv) => appRemove("remove", yargs)) - .command("rm", "Remove an app from your account", (yargs: yargs.Argv) => appRemove("rm", yargs)) - .command("rename", "Rename an existing app", (yargs: yargs.Argv) => { - isValidCommand = true; - yargs.usage(USAGE_PREFIX + " app rename ") - .demand(/*count*/ 4, /*max*/ 4) // Require exactly four non-option arguments. - .example("app rename CurrentName NewName", "Renames app \"CurrentName\" to \"NewName\""); - - addCommonConfiguration(yargs); - }) - .command("list", "Lists the apps associated with your account", (yargs: yargs.Argv) => appList("list", yargs)) - .command("ls", "Lists the apps associated with your account", (yargs: yargs.Argv) => appList("ls", yargs)) - .command("transfer", "Transfer the ownership of an app to another account", (yargs: yargs.Argv) => { - yargs.usage(USAGE_PREFIX + " app transfer ") - .demand(/*count*/ 4, /*max*/ 4) // Require exactly four non-option arguments. - .example("app transfer MyApp foo@bar.com", "Transfers the ownership of app \"MyApp\" to an account with email \"foo@bar.com\""); - - addCommonConfiguration(yargs); - }) - .check((argv: any, aliases: { [aliases: string]: string }): any => isValidCommand); // Report unrecognized, non-hyphenated command category. - - addCommonConfiguration(yargs); - }) - .command("collaborator", "View and manage app collaborators", (yargs: yargs.Argv) => { - isValidCommandCategory = true; - yargs.usage(USAGE_PREFIX + " collaborator ") - .demand(/*count*/ 2, /*max*/ 2) // Require exactly two non-option arguments. - .command("add", "Add a new collaborator to an app", (yargs: yargs.Argv): void => { - isValidCommand = true; - yargs.usage(USAGE_PREFIX + " collaborator add ") - .demand(/*count*/ 4, /*max*/ 4) // Require exactly four non-option arguments. - .example("collaborator add MyApp foo@bar.com", "Adds foo@bar.com as a collaborator to app \"MyApp\""); - - addCommonConfiguration(yargs); - }) - .command("remove", "Remove a collaborator from an app", (yargs: yargs.Argv) => removeCollaborator("remove", yargs)) - .command("rm", "Remove a collaborator from an app", (yargs: yargs.Argv) => removeCollaborator("rm", yargs)) - .command("list", "List the collaborators for an app", (yargs: yargs.Argv) => listCollaborators("list", yargs)) - .command("ls", "List the collaborators for an app", (yargs: yargs.Argv) => listCollaborators("ls", yargs)) - .check((argv: any, aliases: { [aliases: string]: string }): any => isValidCommand); // Report unrecognized, non-hyphenated command category. - - addCommonConfiguration(yargs); - }) - .command("debug", "View the CodePush debug logs for a running app", (yargs: yargs.Argv) => { - isValidCommandCategory = true; - isValidCommand = true; - yargs.usage(USAGE_PREFIX + " debug ") - .demand(/*count*/ 2, /*max*/ 2) - .example("debug android", "View the CodePush debug logs for an Android emulator or device") - .example("debug ios", "View the CodePush debug logs for the iOS simulator"); - - addCommonConfiguration(yargs); - }) - .command("deployment", "View and manage your app deployments", (yargs: yargs.Argv) => { - isValidCommandCategory = true; - yargs.usage(USAGE_PREFIX + " deployment ") - .demand(/*count*/ 2, /*max*/ 2) // Require exactly two non-option arguments. - .command("add", "Add a new deployment to an app", (yargs: yargs.Argv): void => { - isValidCommand = true; - yargs.usage(USAGE_PREFIX + " deployment add ") - .demand(/*count*/ 4, /*max*/ 4) // Require exactly four non-option arguments. - .example("deployment add MyApp MyDeployment", "Adds deployment \"MyDeployment\" to app \"MyApp\""); - - addCommonConfiguration(yargs); - }) - .command("clear", "Clear the release history associated with a deployment", (yargs: yargs.Argv) => deploymentHistoryClear("clear", yargs)) - .command("remove", "Remove a deployment from an app", (yargs: yargs.Argv) => deploymentRemove("remove", yargs)) - .command("rm", "Remove a deployment from an app", (yargs: yargs.Argv) => deploymentRemove("rm", yargs)) - .command("rename", "Rename an existing deployment", (yargs: yargs.Argv) => { - isValidCommand = true; - yargs.usage(USAGE_PREFIX + " deployment rename ") - .demand(/*count*/ 5, /*max*/ 5) // Require exactly five non-option arguments. - .example("deployment rename MyApp CurrentDeploymentName NewDeploymentName", "Renames deployment \"CurrentDeploymentName\" to \"NewDeploymentName\""); - - addCommonConfiguration(yargs); - }) - .command("list", "List the deployments associated with an app", (yargs: yargs.Argv) => deploymentList("list", yargs)) - .command("ls", "List the deployments associated with an app", (yargs: yargs.Argv) => deploymentList("ls", yargs)) - .command("history", "Display the release history for a deployment", (yargs: yargs.Argv) => deploymentHistory("history", yargs)) - .command("h", "Display the release history for a deployment", (yargs: yargs.Argv) => deploymentHistory("h", yargs)) - .check((argv: any, aliases: { [aliases: string]: string }): any => isValidCommand); // Report unrecognized, non-hyphenated command category. - - addCommonConfiguration(yargs); - }) - .command("link", "Link an additional authentication provider (e.g. GitHub) to an existing CodePush account", (yargs: yargs.Argv) => { - isValidCommandCategory = true; - isValidCommand = true; - yargs.usage(USAGE_PREFIX + " link") - .demand(/*count*/ 1, /*max*/ 2) // Require one non-optional and one optional argument. - .example("link", "Links an account on the CodePush server") - .check((argv: any, aliases: { [aliases: string]: string }): any => isValidCommand); // Report unrecognized, non-hyphenated command category. - - addCommonConfiguration(yargs); - }) - .command("login", "Authenticate with the CodePush server in order to begin managing your apps", (yargs: yargs.Argv) => { - isValidCommandCategory = true; - isValidCommand = true; - yargs.usage(USAGE_PREFIX + " login [options]") - .demand(/*count*/ 1, /*max*/ 2) // Require one non-optional and one optional argument. - .example("login", "Logs in to the CodePush server") - .example("login --accessKey mykey", "Logs in on behalf of the user who owns and created the access key \"mykey\"") - .example("login --proxy http://someproxy.com:455", "Logs in with the specified proxy url") - .option("accessKey", { alias: "key", default: null, demand: false, description: "Access key to authenticate against the CodePush server with, instead of providing your username and password credentials", type: "string" }) - .option("proxy", { default: null, demand: false, description: "URL of the proxy server to use", type: "string" }) - .option("noProxy", { default: false, demand: false, description: "Bypass the system-wide proxy settings", type: "boolean" }) - .check((argv: any, aliases: { [aliases: string]: string }): any => isValidCommand); // Report unrecognized, non-hyphenated command category. - - addCommonConfiguration(yargs); - }) - .command("logout", "Log out of the current session", (yargs: yargs.Argv) => { - isValidCommandCategory = true; - isValidCommand = true; - yargs.usage(USAGE_PREFIX + " logout") - .demand(/*count*/ 1, /*max*/ 1) // Require exactly one non-option argument. - .example("logout", "Logs out and ends your current session"); - addCommonConfiguration(yargs); - }) - .command("patch", "Update the metadata for an existing release", (yargs: yargs.Argv) => { - yargs.usage(USAGE_PREFIX + " patch [options]") - .demand(/*count*/ 3, /*max*/ 3) // Require exactly three non-option arguments. - .example("patch MyApp Production --des \"Updated description\" -r 50%", "Updates the description of the latest release for \"MyApp\" app's \"Production\" deployment and updates the rollout value to 50%") - .example("patch MyApp Production -l v3 --des \"Updated description for v3\"", "Updates the description of the release with label v3 for \"MyApp\" app's \"Production\" deployment") - .option("label", { alias: "l", default: null, demand: false, description: "Label of the release to update. Defaults to the latest release within the specified deployment", type: "string" }) - .option("description", { alias: "des", default: null, demand: false, description: "Description of the changes made to the app with this release", type: "string" }) - .option("disabled", { alias: "x", default: null, demand: false, description: "Specifies whether this release should be immediately downloadable", type: "boolean" }) - .option("mandatory", { alias: "m", default: null, demand: false, description: "Specifies whether this release should be considered mandatory", type: "boolean" }) - .option("rollout", { alias: "r", default: null, demand: false, description: "Percentage of users this release should be immediately available to. This attribute can only be increased from the current value.", type: "string" }) - .option("targetBinaryVersion", { alias: "t", default: null, demand: false, description: "Semver expression that specifies the binary app version(s) this release is targeting (e.g. 1.1.0, ~1.2.3).", type: "string" }) - .check((argv: any, aliases: { [aliases: string]: string }): any => { return isValidRollout(argv); }); - - addCommonConfiguration(yargs); - }) - .command("promote", "Promote the latest release from one app deployment to another", (yargs: yargs.Argv) => { - yargs.usage(USAGE_PREFIX + " promote [options]") - .demand(/*count*/ 4, /*max*/ 4) // Require exactly four non-option arguments. - .example("promote MyApp Staging Production", "Promotes the latest release within the \"Staging\" deployment of \"MyApp\" to \"Production\"") - .example("promote MyApp Staging Production --des \"Production rollout\" -r 25", "Promotes the latest release within the \"Staging\" deployment of \"MyApp\" to \"Production\", with an updated description, and targeting only 25% of the users") - .option("description", { alias: "des", default: null, demand: false, description: "Description of the changes made to the app with this release. If omitted, the description from the release being promoted will be used.", type: "string" }) - .option("disabled", { alias: "x", default: null, demand: false, description: "Specifies whether this release should be immediately downloadable. If omitted, the disabled attribute from the release being promoted will be used.", type: "boolean" }) - .option("mandatory", { alias: "m", default: null, demand: false, description: "Specifies whether this release should be considered mandatory. If omitted, the mandatory property from the release being promoted will be used.", type: "boolean" }) - .option("noDuplicateReleaseError", { default: false, demand: false, description: "When this flag is set, promoting a package that is identical to the latest release on the target deployment will produce a warning instead of an error", type: "boolean" }) - .option("rollout", { alias: "r", default: "100%", demand: false, description: "Percentage of users this update should be immediately available to", type: "string" }) - .option("targetBinaryVersion", { alias: "t", default: null, demand: false, description: "Semver expression that specifies the binary app version(s) this release is targeting (e.g. 1.1.0, ~1.2.3). If omitted, the target binary version property from the release being promoted will be used.", type: "string" }) - .check((argv: any, aliases: { [aliases: string]: string }): any => { return isValidRollout(argv); }); - - addCommonConfiguration(yargs); - }) - .command("register", "Register a new CodePush account", (yargs: yargs.Argv) => { - isValidCommandCategory = true; - isValidCommand = true; - yargs.usage(USAGE_PREFIX + " register") - .demand(/*count*/ 1, /*max*/ 2) // Require one non-optional and one optional argument. - .example("register", "Registers a new CodePush account") - .example("register --proxy http://someproxy.com:455", "Registers with the specified proxy url") - .option("proxy", { default: null, demand: false, description: "URL of the proxy server to use", type: "string" }) - .option("noProxy", { default: false, demand: false, description: "Bypass the system-wide proxy settings", type: "boolean" }) - .check((argv: any, aliases: { [aliases: string]: string }): any => isValidCommand); // Report unrecognized, non-hyphenated command category. - - addCommonConfiguration(yargs); - }) - .command("release", "Release an update to an app deployment", (yargs: yargs.Argv) => { - yargs.usage(USAGE_PREFIX + " release [options]") - .demand(/*count*/ 4, /*max*/ 4) // Require exactly four non-option arguments. - .example("release MyApp app.js \"*\"", "Releases the \"app.js\" file to the \"MyApp\" app's \"Staging\" deployment, targeting any binary version using the \"*\" wildcard range syntax.") - .example("release MyApp ./platforms/ios/www 1.0.3 -d Production", "Releases the \"./platforms/ios/www\" folder and all its contents to the \"MyApp\" app's \"Production\" deployment, targeting only the 1.0.3 binary version") - .example("release MyApp ./platforms/ios/www 1.0.3 -d Production -r 20", "Releases the \"./platforms/ios/www\" folder and all its contents to the \"MyApp\" app's \"Production\" deployment, targeting the 1.0.3 binary version and rolling out to about 20% of the users") - .option("deploymentName", { alias: "d", default: "Staging", demand: false, description: "Deployment to release the update to", type: "string" }) - .option("description", { alias: "des", default: null, demand: false, description: "Description of the changes made to the app in this release", type: "string" }) - .option("disabled", { alias: "x", default: false, demand: false, description: "Specifies whether this release should be immediately downloadable", type: "boolean" }) - .option("mandatory", { alias: "m", default: false, demand: false, description: "Specifies whether this release should be considered mandatory", type: "boolean" }) - .option("noDuplicateReleaseError", { default: false, demand: false, description: "When this flag is set, releasing a package that is identical to the latest release will produce a warning instead of an error", type: "boolean" }) - .option("rollout", { alias: "r", default: "100%", demand: false, description: "Percentage of users this release should be available to", type: "string" }) - .check((argv: any, aliases: { [aliases: string]: string }): any => { return checkValidReleaseOptions(argv); }); - - addCommonConfiguration(yargs); - }) - .command("release-cordova", "Release a Cordova update to an app deployment", (yargs: yargs.Argv) => { - yargs.usage(USAGE_PREFIX + " release-cordova [options]") - .demand(/*count*/ 3, /*max*/ 3) // Require exactly three non-option arguments. - .example("release-cordova MyApp ios", "Releases the Cordova iOS project in the current working directory to the \"MyApp\" app's \"Staging\" deployment") - .example("release-cordova MyApp android -d Production", "Releases the Cordova Android project in the current working directory to the \"MyApp\" app's \"Production\" deployment") - .option("build", { alias: "b", default: false, demand: false, description: "Invoke \"cordova build\" instead of \"cordova prepare\"", type: "boolean" }) - .option("deploymentName", { alias: "d", default: "Staging", demand: false, description: "Deployment to release the update to", type: "string" }) - .option("description", { alias: "des", default: null, demand: false, description: "Description of the changes made to the app in this release", type: "string" }) - .option("disabled", { alias: "x", default: false, demand: false, description: "Specifies whether this release should be immediately downloadable", type: "boolean" }) - .option("mandatory", { alias: "m", default: false, demand: false, description: "Specifies whether this release should be considered mandatory", type: "boolean" }) - .option("noDuplicateReleaseError", { default: false, demand: false, description: "When this flag is set, releasing a package that is identical to the latest release will produce a warning instead of an error", type: "boolean" }) - .option("rollout", { alias: "r", default: "100%", demand: false, description: "Percentage of users this release should be immediately available to", type: "string" }) - .option("targetBinaryVersion", { alias: "t", default: null, demand: false, description: "Semver expression that specifies the binary app version(s) this release is targeting (e.g. 1.1.0, ~1.2.3). If omitted, the release will target the exact version specified in the config.xml file.", type: "string" }) - .check((argv: any, aliases: { [aliases: string]: string }): any => { return checkValidReleaseOptions(argv); }); - - addCommonConfiguration(yargs); - }) - .command("release-react", "Release a React Native update to an app deployment", (yargs: yargs.Argv) => { - yargs.usage(USAGE_PREFIX + " release-react [options]") - .demand(/*count*/ 3, /*max*/ 3) // Require exactly three non-option arguments. - .example("release-react MyApp ios", "Releases the React Native iOS project in the current working directory to the \"MyApp\" app's \"Staging\" deployment") - .example("release-react MyApp android -d Production", "Releases the React Native Android project in the current working directory to the \"MyApp\" app's \"Production\" deployment") - .example("release-react MyApp windows --dev", "Releases the development bundle of the React Native Windows project in the current working directory to the \"MyApp\" app's \"Staging\" deployment") - .option("bundleName", { alias: "b", default: null, demand: false, description: "Name of the generated JS bundle file. If unspecified, the standard bundle name will be used, depending on the specified platform: \"main.jsbundle\" (iOS), \"index.android.bundle\" (Android) or \"index.windows.bundle\" (Windows)", type: "string" }) - .option("deploymentName", { alias: "d", default: "Staging", demand: false, description: "Deployment to release the update to", type: "string" }) - .option("description", { alias: "des", default: null, demand: false, description: "Description of the changes made to the app with this release", type: "string" }) - .option("development", { alias: "dev", default: false, demand: false, description: "Specifies whether to generate a dev or release build", type: "boolean" }) - .option("disabled", { alias: "x", default: false, demand: false, description: "Specifies whether this release should be immediately downloadable", type: "boolean" }) - .option("entryFile", { alias: "e", default: null, demand: false, description: "Path to the app's entry Javascript file. If omitted, \"index..js\" and then \"index.js\" will be used (if they exist)", type: "string" }) - .option("gradleFile", { alias: "g", default: null, demand: false, description: "Path to the gradle file which specifies the binary version you want to target this release at (android only)." }) - .option("mandatory", { alias: "m", default: false, demand: false, description: "Specifies whether this release should be considered mandatory", type: "boolean" }) - .option("noDuplicateReleaseError", { default: false, demand: false, description: "When this flag is set, releasing a package that is identical to the latest release will produce a warning instead of an error", type: "boolean" }) - .option("plistFile", { alias: "p", default: null, demand: false, description: "Path to the plist file which specifies the binary version you want to target this release at (iOS only)." }) - .option("plistFilePrefix", { alias: "pre", default: null, demand: false, description: "Prefix to append to the file name when attempting to find your app's Info.plist file (iOS only)." }) - .option("rollout", { alias: "r", default: "100%", demand: false, description: "Percentage of users this release should be immediately available to", type: "string" }) - .option("sourcemapOutput", { alias: "s", default: null, demand: false, description: "Path to where the sourcemap for the resulting bundle should be written. If omitted, a sourcemap will not be generated.", type: "string" }) - .option("targetBinaryVersion", { alias: "t", default: null, demand: false, description: "Semver expression that specifies the binary app version(s) this release is targeting (e.g. 1.1.0, ~1.2.3). If omitted, the release will target the exact version specified in the \"Info.plist\" (iOS), \"build.gradle\" (Android) or \"Package.appxmanifest\" (Windows) files.", type: "string" }) - .check((argv: any, aliases: { [aliases: string]: string }): any => { return checkValidReleaseOptions(argv); }); - - addCommonConfiguration(yargs); - }) - .command("rollback", "Rollback the latest release for an app deployment", (yargs: yargs.Argv) => { - yargs.usage(USAGE_PREFIX + " rollback [options]") - .demand(/*count*/ 3, /*max*/ 3) // Require exactly three non-option arguments. - .example("rollback MyApp Production", "Performs a rollback on the \"Production\" deployment of \"MyApp\"") - .example("rollback MyApp Production --targetRelease v4", "Performs a rollback on the \"Production\" deployment of \"MyApp\" to the v4 release") - .option("targetRelease", { alias: "r", default: null, demand: false, description: "Label of the release to roll the specified deployment back to (e.g. v4). If omitted, the deployment will roll back to the previous release.", type: "string" }); - - addCommonConfiguration(yargs); - }) - .command("session", "View and manage the current login sessions associated with your account", (yargs: yargs.Argv) => { - isValidCommandCategory = true; - yargs.usage(USAGE_PREFIX + " session ") - .demand(/*count*/ 2, /*max*/ 2) // Require exactly two non-option arguments. - .command("remove", "Remove an existing login session", (yargs: yargs.Argv) => sessionRemove("remove", yargs)) - .command("rm", "Remove an existing login session", (yargs: yargs.Argv) => sessionRemove("rm", yargs)) - .command("list", "List the current login sessions associated with your account", (yargs: yargs.Argv) => sessionList("list", yargs)) - .command("ls", "List the current login sessions associated with your account", (yargs: yargs.Argv) => sessionList("ls", yargs)) - .check((argv: any, aliases: { [aliases: string]: string }): any => isValidCommand); // Report unrecognized, non-hyphenated command category. - - addCommonConfiguration(yargs); - }) - .command("whoami", "Display the account info for the current login session", (yargs: yargs.Argv) => { - isValidCommandCategory = true; - isValidCommand = true; - yargs.usage(USAGE_PREFIX + " whoami") - .demand(/*count*/ 1, /*max*/ 1) // Require exactly one non-option argument. - .example("whoami", "Display the account info for the current login session"); - addCommonConfiguration(yargs); - }) - .alias("v", "version") - .version(packageJson.version) - .wrap(/*columnLimit*/ null) - .strict() // Validate hyphenated (named) arguments. - .check((argv: any, aliases: { [aliases: string]: string }): any => isValidCommandCategory) // Report unrecognized, non-hyphenated command category. - .fail((msg: string) => showHelp(/*showRootDescription*/ true)) // Suppress the default error message. - .argv; - -function createCommand(): cli.ICommand { - var cmd: cli.ICommand; - - if (!wasHelpShown && argv._ && argv._.length > 0) { - // Create a command object - var arg0: any = argv._[0]; - var arg1: any = argv._[1]; - var arg2: any = argv._[2]; - var arg3: any = argv._[3]; - var arg4: any = argv._[4]; - - switch (arg0) { - case "access-key": - switch (arg1) { - case "add": - if (arg2) { - cmd = { type: cli.CommandType.accessKeyAdd }; - var accessKeyAddCmd = cmd; - accessKeyAddCmd.name = arg2; - var ttlOption: string = argv["ttl"]; - if (isDefined(ttlOption)) { - accessKeyAddCmd.ttl = parseDurationMilliseconds(ttlOption); - } - } - break; - - case "patch": - if (arg2) { - cmd = { type: cli.CommandType.accessKeyPatch }; - var accessKeyPatchCmd = cmd; - accessKeyPatchCmd.oldName = arg2; - - var newNameOption: string = argv["name"]; - var ttlOption: string = argv["ttl"]; - if (isDefined(newNameOption)) { - accessKeyPatchCmd.newName = newNameOption; - } - - if (isDefined(ttlOption)) { - accessKeyPatchCmd.ttl = parseDurationMilliseconds(ttlOption); - } - } - break; - - case "list": - case "ls": - cmd = { type: cli.CommandType.accessKeyList }; - - (cmd).format = argv["format"]; - break; - - case "remove": - case "rm": - if (arg2) { - cmd = { type: cli.CommandType.accessKeyRemove }; - - (cmd).accessKey = arg2; - } - break; - } - break; - - case "app": - switch (arg1) { - case "add": - if (arg2) { - cmd = { type: cli.CommandType.appAdd }; - - (cmd).appName = arg2; - } - break; - - case "list": - case "ls": - cmd = { type: cli.CommandType.appList }; - - (cmd).format = argv["format"]; - break; - - case "remove": - case "rm": - if (arg2) { - cmd = { type: cli.CommandType.appRemove }; - - (cmd).appName = arg2; - } - break; - - case "rename": - if (arg2 && arg3) { - cmd = { type: cli.CommandType.appRename }; - - var appRenameCommand = cmd; - - appRenameCommand.currentAppName = arg2; - appRenameCommand.newAppName = arg3; - } - break; - - case "transfer": - if (arg2 && arg3) { - cmd = { type: cli.CommandType.appTransfer }; - - var appTransferCommand = cmd; - - appTransferCommand.appName = arg2; - appTransferCommand.email = arg3; - } - break; - } - break; - - case "collaborator": - switch (arg1) { - case "add": - if (arg2 && arg3) { - cmd = { type: cli.CommandType.collaboratorAdd }; - - (cmd).appName = arg2; - (cmd).email = arg3; - } - break; - - case "list": - case "ls": - if (arg2) { - cmd = { type: cli.CommandType.collaboratorList }; - - (cmd).appName = arg2; - (cmd).format = argv["format"]; - } - break; - - case "remove": - case "rm": - if (arg2 && arg3) { - cmd = { type: cli.CommandType.collaboratorRemove }; - - (cmd).appName = arg2; - (cmd).email = arg3; - } - break; - } - break; - - - case "debug": - cmd = { - type: cli.CommandType.debug, - platform: arg1 - }; - - break; - - case "deployment": - switch (arg1) { - case "add": - if (arg2 && arg3) { - cmd = { type: cli.CommandType.deploymentAdd }; - - var deploymentAddCommand = cmd; - - deploymentAddCommand.appName = arg2; - deploymentAddCommand.deploymentName = arg3; - } - break; - - case "clear": - if (arg2 && arg3) { - cmd = { type: cli.CommandType.deploymentHistoryClear }; - - var deploymentHistoryClearCommand = cmd; - - deploymentHistoryClearCommand.appName = arg2; - deploymentHistoryClearCommand.deploymentName = arg3; - } - break; - - case "list": - case "ls": - if (arg2) { - cmd = { type: cli.CommandType.deploymentList }; - - var deploymentListCommand = cmd; - - deploymentListCommand.appName = arg2; - deploymentListCommand.format = argv["format"]; - deploymentListCommand.displayKeys = argv["displayKeys"]; - } - break; - - case "remove": - case "rm": - if (arg2 && arg3) { - cmd = { type: cli.CommandType.deploymentRemove }; - - var deploymentRemoveCommand = cmd; - - deploymentRemoveCommand.appName = arg2; - deploymentRemoveCommand.deploymentName = arg3; - } - break; - - case "rename": - if (arg2 && arg3 && arg4) { - cmd = { type: cli.CommandType.deploymentRename }; - - var deploymentRenameCommand = cmd; - - deploymentRenameCommand.appName = arg2; - deploymentRenameCommand.currentDeploymentName = arg3; - deploymentRenameCommand.newDeploymentName = arg4; - } - break; - - case "history": - case "h": - if (arg2 && arg3) { - cmd = { type: cli.CommandType.deploymentHistory }; - - var deploymentHistoryCommand = cmd; - - deploymentHistoryCommand.appName = arg2; - deploymentHistoryCommand.deploymentName = arg3; - deploymentHistoryCommand.format = argv["format"]; - deploymentHistoryCommand.displayAuthor = argv["displayAuthor"]; - } - break; - } - break; - - case "link": - cmd = { type: cli.CommandType.link, serverUrl: getServerUrl(arg1) }; - break; - - case "login": - cmd = { type: cli.CommandType.login }; - - var loginCommand = cmd; - - loginCommand.serverUrl = getServerUrl(arg1); - loginCommand.accessKey = argv["accessKey"]; - loginCommand.proxy = argv["proxy"]; - loginCommand.noProxy = argv["noProxy"]; - break; - - case "logout": - cmd = { type: cli.CommandType.logout }; - break; - - case "patch": - if (arg1 && arg2) { - cmd = { type: cli.CommandType.patch }; - - var patchCommand = cmd; - - patchCommand.appName = arg1; - patchCommand.deploymentName = arg2; - patchCommand.label = argv["label"]; - // Description must be set to null to indicate that it is not being patched. - patchCommand.description = argv["description"] ? backslash(argv["description"]) : null; - patchCommand.disabled = argv["disabled"]; - patchCommand.mandatory = argv["mandatory"]; - patchCommand.rollout = getRolloutValue(argv["rollout"]); - patchCommand.appStoreVersion = argv["targetBinaryVersion"]; - } - break; - - case "promote": - if (arg1 && arg2 && arg3) { - cmd = { type: cli.CommandType.promote }; - - var deploymentPromoteCommand = cmd; - - deploymentPromoteCommand.appName = arg1; - deploymentPromoteCommand.sourceDeploymentName = arg2; - deploymentPromoteCommand.destDeploymentName = arg3; - deploymentPromoteCommand.description = argv["description"] ? backslash(argv["description"]) : ""; - deploymentPromoteCommand.disabled = argv["disabled"]; - deploymentPromoteCommand.mandatory = argv["mandatory"]; - deploymentPromoteCommand.noDuplicateReleaseError = argv["noDuplicateReleaseError"]; - deploymentPromoteCommand.rollout = getRolloutValue(argv["rollout"]); - deploymentPromoteCommand.appStoreVersion = argv["targetBinaryVersion"]; - } - break; - - case "register": - cmd = { type: cli.CommandType.register }; - - var registerCommand = cmd; - - registerCommand.serverUrl = getServerUrl(arg1); - registerCommand.proxy = argv["proxy"]; - registerCommand.noProxy = argv["noProxy"]; - break; - - case "release": - if (arg1 && arg2 && arg3) { - cmd = { type: cli.CommandType.release }; - - var releaseCommand = cmd; - - releaseCommand.appName = arg1; - releaseCommand.package = arg2; - releaseCommand.appStoreVersion = arg3; - releaseCommand.deploymentName = argv["deploymentName"]; - releaseCommand.description = argv["description"] ? backslash(argv["description"]) : ""; - releaseCommand.disabled = argv["disabled"]; - releaseCommand.mandatory = argv["mandatory"]; - releaseCommand.noDuplicateReleaseError = argv["noDuplicateReleaseError"]; - releaseCommand.rollout = getRolloutValue(argv["rollout"]); - } - break; - - case "release-cordova": - if (arg1 && arg2) { - cmd = { type: cli.CommandType.releaseCordova }; - - var releaseCordovaCommand = cmd; - - releaseCordovaCommand.appName = arg1; - releaseCordovaCommand.platform = arg2; - - releaseCordovaCommand.build = argv["build"]; - releaseCordovaCommand.deploymentName = argv["deploymentName"]; - releaseCordovaCommand.description = argv["description"] ? backslash(argv["description"]) : ""; - releaseCordovaCommand.disabled = argv["disabled"]; - releaseCordovaCommand.mandatory = argv["mandatory"]; - releaseCordovaCommand.noDuplicateReleaseError = argv["noDuplicateReleaseError"]; - releaseCordovaCommand.rollout = getRolloutValue(argv["rollout"]); - releaseCordovaCommand.appStoreVersion = argv["targetBinaryVersion"]; - } - break; - - case "release-react": - if (arg1 && arg2) { - cmd = { type: cli.CommandType.releaseReact }; - - var releaseReactCommand = cmd; - - releaseReactCommand.appName = arg1; - releaseReactCommand.platform = arg2; - - releaseReactCommand.appStoreVersion = argv["targetBinaryVersion"]; - releaseReactCommand.bundleName = argv["bundleName"]; - releaseReactCommand.deploymentName = argv["deploymentName"]; - releaseReactCommand.disabled = argv["disabled"]; - releaseReactCommand.description = argv["description"] ? backslash(argv["description"]) : ""; - releaseReactCommand.development = argv["development"]; - releaseReactCommand.entryFile = argv["entryFile"]; - releaseReactCommand.gradleFile = argv["gradleFile"]; - releaseReactCommand.mandatory = argv["mandatory"]; - releaseReactCommand.noDuplicateReleaseError = argv["noDuplicateReleaseError"]; - releaseReactCommand.plistFile = argv["plistFile"]; - releaseReactCommand.plistFilePrefix = argv["plistFilePrefix"]; - releaseReactCommand.rollout = getRolloutValue(argv["rollout"]); - releaseReactCommand.sourcemapOutput = argv["sourcemapOutput"]; - } - break; - - case "rollback": - if (arg1 && arg2) { - cmd = { type: cli.CommandType.rollback }; - - var rollbackCommand = cmd; - - rollbackCommand.appName = arg1; - rollbackCommand.deploymentName = arg2; - rollbackCommand.targetRelease = argv["targetRelease"]; - } - break; - - case "session": - switch (arg1) { - case "list": - case "ls": - cmd = { type: cli.CommandType.sessionList }; - - (cmd).format = argv["format"]; - break; - - case "remove": - case "rm": - if (arg2) { - cmd = { type: cli.CommandType.sessionRemove }; - - (cmd).machineName = arg2; - } - break; - } - break; - - case "whoami": - cmd = { type: cli.CommandType.whoami }; - break; - } - - return cmd; - } -} - -function isValidRollout(args: any): boolean { - var rollout: string = args["rollout"]; - if (rollout && !ROLLOUT_PERCENTAGE_REGEX.test(rollout)) { - return false; - } - - return true; -} - -function checkValidReleaseOptions(args: any): boolean { - return isValidRollout(args) && !!args["deploymentName"]; -} - -function getRolloutValue(input: string): number { - return input ? parseInt(input.replace("%", "")) : null; -} - -function getServerUrl(url: string): string { - if (!url) return null; - - // Trim whitespace and a trailing slash (/) character. - url = url.trim(); - if (url[url.length - 1] === "/") { - url = url.substring(0, url.length - 1); - } - - url = url.replace(/^(https?):\\/, "$1://"); // Replace 'http(s):\' with 'http(s)://' for Windows Git Bash - - return url; -} - -function isDefined(object: any): boolean { - return object !== undefined && object !== null; -} - -function parseDurationMilliseconds(durationString: string): number { - return Math.floor(parseDuration(durationString)); -} - -export var command = createCommand(); diff --git a/cli/script/commands/debug.ts b/cli/script/commands/debug.ts deleted file mode 100644 index 3d50d9bf..00000000 --- a/cli/script/commands/debug.ts +++ /dev/null @@ -1,130 +0,0 @@ -import * as childProcess from "child_process"; -import * as cli from "../../definitions/cli"; -import * as moment from "moment"; -import * as path from "path"; -import * as Q from "q"; - -const simctl = require("simctl"); -const which = require("which"); - -interface IDebugPlatform { - getLogProcess(): any; - normalizeLogMessage(message: string): string; -} - -class AndroidDebugPlatform implements IDebugPlatform { - public getLogProcess(): any { - try { - which.sync("adb"); - } catch (e) { - throw new Error("ADB command not found. Please ensure it is installed and available on your path."); - } - - if (!this.isDeviceAvailable()) { - throw new Error("No Android devices found. Re-run this command after starting one."); - } - - return childProcess.spawn("adb", ["logcat"]); - } - - // The following is an example of what the output looks - // like when running the "adb devices" command. - // - // List of devices attached - // emulator-5554 device - private isDeviceAvailable(): boolean { - const output = childProcess.execSync("adb devices").toString(); - return output.search(/^[\w-]+\s+device$/mi) > -1; - } - - public normalizeLogMessage(message: string): string { - // Check to see whether the message includes the source URL - // suffix, and if so, strip it. This can occur in Android Cordova apps. - const sourceURLIndex: number = message.indexOf("\", source: file:///"); - if (~sourceURLIndex) { - return message.substring(0, sourceURLIndex); - } else { - return message; - } - } -} - -class iOSDebugPlatform implements IDebugPlatform { - private getSimulatorID(): string { - const output: any = simctl.list({ devices: true, silent: true }); - const simulators: string[] = output.json.devices - .map((platform: any) => platform.devices) - .reduce((prev: any, next: any) => prev.concat(next)) - .filter((device: any) => device.state === "Booted") - .map((device: any) => device.id); - - return simulators[0]; - } - - public getLogProcess(): any { - if (process.platform !== "darwin") { - throw new Error("iOS debug logs can only be viewed on OS X."); - } - - const simulatorID: string = this.getSimulatorID(); - if (!simulatorID) { - throw new Error("No iOS simulators found. Re-run this command after starting one."); - } - - const logFilePath: string = path.join(process.env.HOME, "Library/Logs/CoreSimulator", simulatorID, "system.log"); - return childProcess.spawn("tail", ["-f", logFilePath]); - } - - public normalizeLogMessage(message: string): string { - return message; - } -} - -const logMessagePrefix = "[CodePush] "; -function processLogData(logData: Buffer) { - const content = logData.toString() - content.split("\n") - .filter((line: string) => line.indexOf(logMessagePrefix) > -1) - .map((line: string) => { - // Allow the current platform - // to normalize the message first. - line = this.normalizeLogMessage(line); - - // Strip the CodePush-specific, platform agnostic - // log message prefix that is added to each entry. - const message = line.substring(line.indexOf(logMessagePrefix) + logMessagePrefix.length); - - const timeStamp = moment().format("hh:mm:ss"); - return `[${timeStamp}] ${message}`; - }) - .forEach((line: string) => console.log(line)); -} - -const debugPlatforms: any = { - android: new AndroidDebugPlatform(), - ios: new iOSDebugPlatform() -}; - -export default function (command: cli.IDebugCommand): Q.Promise { - return Q.Promise((resolve, reject) => { - const platform: string = command.platform.toLowerCase(); - const debugPlatform: IDebugPlatform = debugPlatforms[platform]; - - if (!debugPlatform) { - const availablePlatforms = Object.getOwnPropertyNames(debugPlatforms); - return reject(new Error(`"${platform}" is an unsupported platform. Available options are ${availablePlatforms.join(", ")}.`)); - } - - try { - const logProcess = debugPlatform.getLogProcess(); - console.log(`Listening for ${platform} debug logs (Press CTRL+C to exit)`); - - logProcess.stdout.on("data", processLogData.bind(debugPlatform)); - logProcess.stderr.on("data", reject); - - logProcess.on("close", resolve); - } catch (e) { - reject(e); - } - }); -}; \ No newline at end of file diff --git a/cli/script/hash-utils.ts b/cli/script/hash-utils.ts deleted file mode 100644 index 324459b3..00000000 --- a/cli/script/hash-utils.ts +++ /dev/null @@ -1,247 +0,0 @@ -/** - * NOTE!!! This utility file is duplicated for use by the CodePush service (for server-driven hashing/ - * integrity checks) and Management SDK (for end-to-end code signing), please keep them in sync. - */ - -import * as crypto from "crypto"; -import * as fs from "fs"; -import * as path from "path"; -import * as q from "q"; -import * as stream from "stream"; - -// Do not throw an exception if either of these modules are missing, as they may not be needed by the -// consumer of this file. -// - recursiveFs: Only required for hashing of directories -// - yauzl: Only required for in-memory hashing of zip files -try { var recursiveFs = require("recursive-fs"); } catch (e) {} -try { var yauzl = require("yauzl"); } catch (e) {} - -import Promise = q.Promise; -const HASH_ALGORITHM = "sha256"; - -export function generatePackageHashFromDirectory(directoryPath: string, basePath: string): Promise { - if (!fs.lstatSync(directoryPath).isDirectory()) { - throw new Error("Not a directory. Please either create a directory, or use hashFile()."); - } - - return generatePackageManifestFromDirectory(directoryPath, basePath) - .then((manifest: PackageManifest) => { - return manifest.computePackageHash() - }); -} - -export function generatePackageManifestFromZip(filePath: string): Promise { - var deferred: q.Deferred = q.defer(); - var reject = (error: Error) => { - if (deferred.promise.isPending()) { - deferred.reject(error); - } - } - - var resolve = (manifest: PackageManifest) => { - if (deferred.promise.isPending()) { - deferred.resolve(manifest); - } - } - - var zipFile: any; - - yauzl.open(filePath, { lazyEntries: true }, (error?: any, openedZipFile?: any): void => { - if (error) { - // This is the first time we try to read the package as a .zip file; - // however, it may not be a .zip file. Handle this gracefully. - resolve(null); - return; - } - - zipFile = openedZipFile; - var fileHashesMap = new Map(); - var hashFilePromises: q.Promise[] = []; - - // Read each entry in the archive sequentially and generate a hash for it. - zipFile.readEntry(); - zipFile - .on("error", (error: any): void => { - reject(error); - }) - .on("entry", (entry: any): void => { - var fileName: string = PackageManifest.normalizePath(entry.fileName); - if (PackageManifest.isIgnored(fileName)) { - zipFile.readEntry(); - return; - } - - zipFile.openReadStream(entry, (error?: any, readStream?: stream.Readable): void => { - if (error) { - reject(error); - return; - } - - hashFilePromises.push( - hashStream(readStream) - .then((hash: string) => { - fileHashesMap.set(fileName, hash); - zipFile.readEntry(); - }, reject) - ); - }); - }) - .on("end", (): void => { - q.all(hashFilePromises).then( - () => resolve(new PackageManifest(fileHashesMap)), - reject - ); - }); - }); - - return deferred.promise - .finally(() => zipFile && zipFile.close()); -} - -export function generatePackageManifestFromDirectory(directoryPath: string, basePath: string): Promise { - var deferred: q.Deferred = q.defer(); - var fileHashesMap = new Map(); - - recursiveFs.readdirr(directoryPath, (error?: any, directories?: string[], files?: string[]): void => { - if (error) { - deferred.reject(error); - return; - } - - if (!files || files.length === 0) { - deferred.reject("Error: Can't sign the release because no files were found."); - return; - } - - // Hash the files sequentially, because streaming them in parallel is not necessarily faster - var generateManifestPromise: Promise = files.reduce((soFar: Promise, filePath: string) => { - return soFar - .then(() => { - var relativePath: string = PackageManifest.normalizePath(path.relative(basePath, filePath)); - if (!PackageManifest.isIgnored(relativePath)) { - return hashFile(filePath) - .then((hash: string) => { - fileHashesMap.set(relativePath, hash); - }); - } - }); - }, q(null)); - - generateManifestPromise - .then(() => { - deferred.resolve(new PackageManifest(fileHashesMap)); - }, deferred.reject) - .done(); - }); - - return deferred.promise; -} - -export function hashFile(filePath: string): Promise { - var readStream: fs.ReadStream = fs.createReadStream(filePath); - return hashStream(readStream); -} - -export function hashStream(readStream: stream.Readable): Promise { - var hashStream = crypto.createHash(HASH_ALGORITHM); - var deferred: q.Deferred = q.defer(); - - readStream - .on("error", (error: any): void => { - if (deferred.promise.isPending()) { - hashStream.end(); - deferred.reject(error); - } - }) - .on("end", (): void => { - if (deferred.promise.isPending()) { - hashStream.end(); - - var buffer = hashStream.read(); - var hash: string = buffer.toString("hex"); - - deferred.resolve(hash); - } - }); - - readStream.pipe(hashStream); - - return deferred.promise; -} - -export class PackageManifest { - private _map: Map; - - public constructor(map?: Map) { - if (!map) { - map = new Map(); - } - this._map = map; - } - - public toMap(): Map { - return this._map; - } - - public computePackageHash(): Promise { - var entries: string[] = []; - this._map.forEach((hash: string, name: string): void => { - entries.push(name + ":" + hash); - }); - - // Make sure this list is alphabetically ordered so that other clients - // can also compute this hash easily given the update contents. - entries = entries.sort(); - - return q( - crypto.createHash(HASH_ALGORITHM) - .update(JSON.stringify(entries)) - .digest("hex") - ); - } - - public serialize(): string { - var obj: any = {}; - - this._map.forEach(function(value, key) { - obj[key] = value; - }); - - return JSON.stringify(obj); - } - - public static deserialize(serializedContents: string): PackageManifest { - try { - var obj: any = JSON.parse(serializedContents); - var map = new Map(); - - for (var key of Object.keys(obj)) { - map.set(key, obj[key]); - } - - return new PackageManifest(map); - } catch (e) { - } - } - - public static normalizePath(filePath: string): string { - return filePath.replace("\\", "/"); - } - - public static isIgnored(relativeFilePath: string): boolean { - const __MACOSX = "__MACOSX/"; - const DS_STORE = ".DS_Store"; - - return startsWith(relativeFilePath, __MACOSX) - || relativeFilePath === DS_STORE - || endsWith(relativeFilePath, "/" + DS_STORE); - } -} - -function startsWith(str: string, prefix: string): boolean { - return str && str.substring(0, prefix.length) === prefix; -} - -function endsWith(str: string, suffix: string): boolean { - return str && str.indexOf(suffix, str.length - suffix.length) !== -1; -} diff --git a/cli/test/cli.ts b/cli/test/cli.ts deleted file mode 100644 index 7b7b7bca..00000000 --- a/cli/test/cli.ts +++ /dev/null @@ -1,1781 +0,0 @@ -import * as assert from "assert"; -import * as sinon from "sinon"; -import Q = require("q"); -import * as path from "path"; -import Promise = Q.Promise; -import * as codePush from "code-push/script/types"; -import * as cli from "../definitions/cli"; -import * as cmdexec from "../script/command-executor"; -import * as os from "os"; - -function assertJsonDescribesObject(json: string, object: Object): void { - // Make sure JSON is indented correctly - assert.equal(json, JSON.stringify(object, /*replacer=*/ null, /*spacing=*/ 2)); -} - -function clone(obj: T): T { - return JSON.parse(JSON.stringify(obj)); -} - -function ensureInTestAppDirectory(): void { - if (!~__dirname.indexOf("/resources/TestApp")) { - process.chdir(__dirname + "/resources/TestApp"); - } -} - -function isDefined(object: any): boolean { - return object !== undefined && object !== null; -} - -const NOW = 1471460856191; -const DEFAULT_ACCESS_KEY_MAX_AGE = 1000 * 60 * 60 * 24 * 60; // 60 days -const TEST_MACHINE_NAME = "Test machine"; - -export class SdkStub { - public getAccountInfo(): Promise { - return Q({ - email: "a@a.com" - }); - } - - public addAccessKey(name: string, ttl: number): Promise { - return Q({ - key: "key123", - createdTime: new Date().getTime(), - name, - expires: NOW + (isDefined(ttl) ? ttl : DEFAULT_ACCESS_KEY_MAX_AGE) - }); - } - - public patchAccessKey(oldName: string, newName?: string, newTtl?: number): Promise { - return Q({ - createdTime: new Date().getTime(), - name: newName, - expires: NOW + (isDefined(newTtl) ? newTtl : DEFAULT_ACCESS_KEY_MAX_AGE) - }); - } - - public addApp(name: string): Promise { - return Q({ - name: name - }); - } - - public addCollaborator(name: string, email: string): Promise { - return Q(null); - } - - public addDeployment(appId: string, name: string): Promise { - return Q({ - name: name, - key: "6" - }); - } - - public clearDeploymentHistory(appId: string, deployment: string): Promise { - return Q(null); - } - - public getAccessKeys(): Promise { - return Q([{ - createdTime: 0, - name: "Test name", - expires: NOW + DEFAULT_ACCESS_KEY_MAX_AGE - }]); - } - - public getSessions(): Promise { - return Q([{ - loggedInTime: 0, - machineName: TEST_MACHINE_NAME - }]); - } - - public getApps(): Promise { - return Q([{ - name: "a", - collaborators: { "a@a.com": { permission: "Owner", isCurrentAccount: true } }, - deployments: [ "Production", "Staging" ] - }, { - name: "b", - collaborators: { "a@a.com": { permission: "Owner", isCurrentAccount: true } }, - deployments: [ "Production", "Staging" ] - }]); - } - - public getDeployments(appId: string): Promise { - return Q([{ - name: "Production", - key: "6" - }, { - name: "Staging", - key: "6", - package: { - appVersion: "1.0.0", - description: "fgh", - label: "v2", - packageHash: "jkl", - isMandatory: true, - size: 10, - blobUrl: "http://mno.pqr", - uploadTime: 1000 - } - }]); - } - - public getDeploymentHistory(appId: string, deploymentId: string): Promise { - return Q([ - { - description: null, - appVersion: "1.0.0", - isMandatory: false, - packageHash: "463acc7d06adc9c46233481d87d9e8264b3e9ffe60fe98d721e6974209dc71a0", - blobUrl: "https://fakeblobstorage.net/storagev2/blobid1", - uploadTime: 1447113596270, - size: 1, - label: "v1" - }, - { - description: "New update - this update does a whole bunch of things, including testing linewrapping", - appVersion: "1.0.1", - isMandatory: false, - packageHash: "463acc7d06adc9c46233481d87d9e8264b3e9ffe60fe98d721e6974209dc71a0", - blobUrl: "https://fakeblobstorage.net/storagev2/blobid2", - uploadTime: 1447118476669, - size: 2, - label: "v2" - } - ]); - } - - public getDeploymentMetrics(appId: string, deploymentId: string): Promise { - return Q({ - "1.0.0": { - active: 123 - }, - "v1": { - active: 789, - downloaded: 456, - failed: 654, - installed: 987 - }, - "v2": { - active: 123, - downloaded: 321, - failed: 789, - installed: 456 - } - }); - } - - public getCollaborators(app: codePush.App): Promise { - return Q({ - "a@a.com": { - permission: "Owner", - isCurrentAccount: true - }, - "b@b.com": { - permission: "Collaborator", - isCurrentAccount: false - } - }); - } - - public patchRelease(appName: string, deployment: string, label: string, updateMetaData: codePush.PackageInfo): Promise { - return Q(null); - } - - public promote(appName: string, sourceDeployment: string, destinationDeployment: string, updateMetaData: codePush.PackageInfo): Promise { - return Q(null); - } - - public release(appId: string, deploymentId: string): Promise { - return Q("Successfully released"); - } - - public removeAccessKey(accessKeyId: string): Promise { - return Q(null); - } - - public removeApp(appId: string): Promise { - return Q(null); - } - - public removeCollaborator(name: string, email: string): Promise { - return Q(null); - } - - public removeDeployment(appId: string, deployment: string): Promise { - return Q(null); - } - - public removeSession(createdBy: string): Promise { - return Q(null); - } - - public renameApp(app: codePush.App): Promise { - return Q(null); - } - - public rollback(appName: string, deployment: string, targetRelease: string): Promise { - return Q(null); - } - - public transferApp(app: codePush.App): Promise { - return Q(null); - } - - public renameDeployment(appId: string, deployment: codePush.Deployment): Promise { - return Q(null); - } -} - -describe("CLI", () => { - var log: Sinon.SinonStub; - var sandbox: Sinon.SinonSandbox; - var spawn: Sinon.SinonStub; - var wasConfirmed = true; - const INVALID_RELEASE_FILE_ERROR_MESSAGE: string = "It is unnecessary to package releases in a .zip or binary file. Please specify the direct path to the update content's directory (e.g. /platforms/ios/www) or file (e.g. main.jsbundle)."; - - beforeEach((): void => { - wasConfirmed = true; - - sandbox = sinon.sandbox.create(); - - sandbox.stub(cmdexec, "confirm", (): Promise => Q(wasConfirmed)); - sandbox.stub(cmdexec, "createEmptyTempReleaseFolder", (): Promise => Q(null)); - log = sandbox.stub(cmdexec, "log", (message: string): void => { }); - spawn = sandbox.stub(cmdexec, "spawn", (command: string, commandArgs: string[]): any => { - return { - stdout: { on: () => { } }, - stderr: { on: () => { } }, - on: (event: string, callback: () => void) => { - callback(); - } - }; - }); - cmdexec.sdk = new SdkStub(); - }); - - afterEach((): void => { - sandbox.restore(); - }); - - it("accessKeyAdd creates access key with name and default ttl", (done: MochaDone): void => { - var command: cli.IAccessKeyAddCommand = { - type: cli.CommandType.accessKeyAdd, - name: "Test name" - }; - - cmdexec.execute(command) - .done((): void => { - sinon.assert.calledTwice(log); - assert.equal(log.args[0].length, 1); - - var actual: string = log.args[0][0]; - var expected = `Successfully created the "Test name" access key: key123`; - assert.equal(actual, expected); - - actual = log.args[1][0]; - expected = "Make sure to save this key value somewhere safe, since you won't be able to view it from the CLI again!"; - assert.equal(actual, expected); - - done(); - }); - }); - - it("accessKeyAdd creates access key with name and specified ttl", (done: MochaDone): void => { - var ttl = 10000; - var command: cli.IAccessKeyAddCommand = { - type: cli.CommandType.accessKeyAdd, - name: "Test name", - ttl - }; - - cmdexec.execute(command) - .done((): void => { - sinon.assert.calledTwice(log); - assert.equal(log.args[0].length, 1); - - var actual: string = log.args[0][0]; - var expected = `Successfully created the "Test name" access key: key123`; - assert.equal(actual, expected); - - actual = log.args[1][0]; - expected = "Make sure to save this key value somewhere safe, since you won't be able to view it from the CLI again!"; - assert.equal(actual, expected); - - done(); - }); - }); - - it("accessKeyPatch updates access key with new name", (done: MochaDone): void => { - var command: cli.IAccessKeyPatchCommand = { - type: cli.CommandType.accessKeyPatch, - oldName: "Test name", - newName: "Updated name" - }; - - cmdexec.execute(command) - .done((): void => { - sinon.assert.calledOnce(log); - assert.equal(log.args[0].length, 1); - - var actual: string = log.args[0][0]; - var expected = `Successfully renamed the access key "Test name" to "Updated name".`; - - assert.equal(actual, expected); - done(); - }); - }); - - - it("accessKeyPatch updates access key with new ttl", (done: MochaDone): void => { - var ttl = 10000; - var command: cli.IAccessKeyPatchCommand = { - type: cli.CommandType.accessKeyPatch, - oldName: "Test name", - ttl - }; - - cmdexec.execute(command) - .done((): void => { - sinon.assert.calledOnce(log); - assert.equal(log.args[0].length, 1); - - var actual: string = log.args[0][0]; - var expected = `Successfully changed the expiration date of the "Test name" access key to Wednesday, August 17, 2016 12:07 PM.`; - - assert.equal(actual, expected); - done(); - }); - }); - - it("accessKeyPatch updates access key with new name and ttl", (done: MochaDone): void => { - var ttl = 10000; - var command: cli.IAccessKeyPatchCommand = { - type: cli.CommandType.accessKeyPatch, - oldName: "Test name", - newName: "Updated name", - ttl - }; - - cmdexec.execute(command) - .done((): void => { - sinon.assert.calledOnce(log); - assert.equal(log.args[0].length, 1); - - var actual: string = log.args[0][0]; - var expected = `Successfully renamed the access key "Test name" to "Updated name" and changed its expiration date to Wednesday, August 17, 2016 12:07 PM.`; - - assert.equal(actual, expected); - done(); - }); - }); - - it("accessKeyList lists access key name and expires fields", (done: MochaDone): void => { - var command: cli.IAccessKeyListCommand = { - type: cli.CommandType.accessKeyList, - format: "json" - }; - - cmdexec.execute(command) - .done((): void => { - sinon.assert.calledOnce(log); - assert.equal(log.args[0].length, 1); - - var actual: string = log.args[0][0]; - var expected = [ - { - createdTime: 0, - name: "Test name", - expires: NOW + DEFAULT_ACCESS_KEY_MAX_AGE - } - ]; - - assertJsonDescribesObject(actual, expected); - done(); - }); - }); - - it("accessKeyRemove removes access key", (done: MochaDone): void => { - var command: cli.IAccessKeyRemoveCommand = { - type: cli.CommandType.accessKeyRemove, - accessKey: "8" - }; - - var removeAccessKey: Sinon.SinonSpy = sandbox.spy(cmdexec.sdk, "removeAccessKey"); - - cmdexec.execute(command) - .done((): void => { - sinon.assert.calledOnce(removeAccessKey); - sinon.assert.calledWithExactly(removeAccessKey, "8"); - sinon.assert.calledOnce(log); - sinon.assert.calledWithExactly(log, "Successfully removed the \"8\" access key."); - - done(); - }); - }); - - it("accessKeyRemove does not remove access key if cancelled", (done: MochaDone): void => { - var command: cli.IAccessKeyRemoveCommand = { - type: cli.CommandType.accessKeyRemove, - accessKey: "8" - }; - - var removeAccessKey: Sinon.SinonSpy = sandbox.spy(cmdexec.sdk, "removeAccessKey"); - - wasConfirmed = false; - - cmdexec.execute(command) - .done((): void => { - sinon.assert.notCalled(removeAccessKey); - sinon.assert.calledOnce(log); - sinon.assert.calledWithExactly(log, "Access key removal cancelled."); - - done(); - }); - }); - - it("appAdd reports new app name and ID", (done: MochaDone): void => { - var command: cli.IAppAddCommand = { - type: cli.CommandType.appAdd, - appName: "a" - }; - - var addApp: Sinon.SinonSpy = sandbox.spy(cmdexec.sdk, "addApp"); - var deploymentList: Sinon.SinonSpy = sandbox.spy(cmdexec, "deploymentList"); - - cmdexec.execute(command) - .done((): void => { - sinon.assert.calledOnce(addApp); - sinon.assert.calledTwice(log); - sinon.assert.calledWithExactly(log, "Successfully added the \"a\" app, along with the following default deployments:"); - sinon.assert.calledOnce(deploymentList); - done(); - }); - }); - - it("appList lists app names and ID's", (done: MochaDone): void => { - var command: cli.IAppListCommand = { - type: cli.CommandType.appList, - format: "json" - }; - - cmdexec.execute(command) - .done((): void => { - sinon.assert.calledOnce(log); - assert.equal(log.args[0].length, 1); - - var actual: string = log.args[0][0]; - var expected = [ - { - name: "a", - collaborators: { - "a@a.com": { - permission: "Owner", - isCurrentAccount: true - } - }, - deployments: ["Production", "Staging"] - }, - { - name: "b", - collaborators: { - "a@a.com": { - permission: "Owner", - isCurrentAccount: true - } - }, - deployments: ["Production", "Staging"] - } - ]; - - assertJsonDescribesObject(actual, expected); - done(); - }); - }); - - it("appRemove removes app", (done: MochaDone): void => { - var command: cli.IAppRemoveCommand = { - type: cli.CommandType.appRemove, - appName: "a" - }; - - var removeApp: Sinon.SinonSpy = sandbox.spy(cmdexec.sdk, "removeApp"); - - cmdexec.execute(command) - .done((): void => { - sinon.assert.calledOnce(removeApp); - sinon.assert.calledWithExactly(removeApp, "a"); - sinon.assert.calledOnce(log); - sinon.assert.calledWithExactly(log, "Successfully removed the \"a\" app."); - - done(); - }); - }); - - it("appRemove does not remove app if cancelled", (done: MochaDone): void => { - var command: cli.IAppRemoveCommand = { - type: cli.CommandType.appRemove, - appName: "a" - }; - - var removeApp: Sinon.SinonSpy = sandbox.spy(cmdexec.sdk, "removeApp"); - - wasConfirmed = false; - - cmdexec.execute(command) - .done((): void => { - sinon.assert.notCalled(removeApp); - sinon.assert.calledOnce(log); - sinon.assert.calledWithExactly(log, "App removal cancelled."); - - done(); - }); - }); - - it("appRename renames app", (done: MochaDone): void => { - var command: cli.IAppRenameCommand = { - type: cli.CommandType.appRename, - currentAppName: "a", - newAppName: "c" - }; - - var renameApp: Sinon.SinonSpy = sandbox.spy(cmdexec.sdk, "renameApp"); - - cmdexec.execute(command) - .done((): void => { - sinon.assert.calledOnce(renameApp); - sinon.assert.calledOnce(log); - sinon.assert.calledWithExactly(log, "Successfully renamed the \"a\" app to \"c\"."); - - done(); - }); - }); - - it("appTransfer transfers app", (done: MochaDone): void => { - var command: cli.IAppTransferCommand = { - type: cli.CommandType.appTransfer, - appName: "a", - email: "b@b.com" - }; - - var transferApp: Sinon.SinonSpy = sandbox.spy(cmdexec.sdk, "transferApp"); - - cmdexec.execute(command) - .done((): void => { - sinon.assert.calledOnce(transferApp); - sinon.assert.calledOnce(log); - sinon.assert.calledWithExactly(log, "Successfully transferred the ownership of app \"a\" to the account with email \"b@b.com\"."); - - done(); - }); - }); - - it("collaboratorAdd adds collaborator", (done: MochaDone): void => { - var command: cli.ICollaboratorAddCommand = { - type: cli.CommandType.collaboratorAdd, - appName: "a", - email: "b@b.com" - }; - - var addCollaborator: Sinon.SinonSpy = sandbox.spy(cmdexec.sdk, "addCollaborator"); - - cmdexec.execute(command) - .done((): void => { - sinon.assert.calledOnce(addCollaborator); - sinon.assert.calledOnce(log); - sinon.assert.calledWithExactly(log, "Successfully added \"b@b.com\" as a collaborator to the app \"a\"."); - - done(); - }); - }); - - it("collaboratorList lists collaborators email and properties", (done: MochaDone): void => { - var command: cli.ICollaboratorListCommand = { - type: cli.CommandType.collaboratorList, - appName: "a", - format: "json" - }; - - cmdexec.execute(command) - .done((): void => { - sinon.assert.calledOnce(log); - assert.equal(log.args[0].length, 1); - - var actual: string = log.args[0][0]; - var expected = { - "collaborators": - { - "a@a.com": { permission: "Owner", isCurrentAccount: true }, - "b@b.com": { permission: "Collaborator", isCurrentAccount: false } - } - }; - - assertJsonDescribesObject(actual, expected); - done(); - }); - }); - - it("collaboratorRemove removes collaborator", (done: MochaDone): void => { - var command: cli.ICollaboratorRemoveCommand = { - type: cli.CommandType.collaboratorRemove, - appName: "a", - email: "b@b.com" - }; - - var removeCollaborator: Sinon.SinonSpy = sandbox.spy(cmdexec.sdk, "removeCollaborator"); - - cmdexec.execute(command) - .done((): void => { - sinon.assert.calledOnce(removeCollaborator); - sinon.assert.calledOnce(log); - sinon.assert.calledWithExactly(log, "Successfully removed \"b@b.com\" as a collaborator from the app \"a\"."); - - done(); - }); - }); - - - it("deploymentAdd reports new app name and ID", (done: MochaDone): void => { - var command: cli.IDeploymentAddCommand = { - type: cli.CommandType.deploymentAdd, - appName: "a", - deploymentName: "b" - }; - - var addDeployment: Sinon.SinonSpy = sandbox.spy(cmdexec.sdk, "addDeployment"); - - cmdexec.execute(command) - .done((): void => { - sinon.assert.calledOnce(addDeployment); - sinon.assert.calledOnce(log); - sinon.assert.calledWithExactly(log, "Successfully added the \"b\" deployment with key \"6\" to the \"a\" app."); - done(); - }); - }); - - it("deploymentHistoryClear clears deployment", (done: MochaDone): void => { - var command: cli.IDeploymentHistoryClearCommand = { - type: cli.CommandType.deploymentHistoryClear, - appName: "a", - deploymentName: "Staging" - }; - - var clearDeployment: Sinon.SinonSpy = sandbox.spy(cmdexec.sdk, "clearDeploymentHistory"); - - cmdexec.execute(command) - .done((): void => { - sinon.assert.calledOnce(clearDeployment); - sinon.assert.calledWithExactly(clearDeployment, "a", "Staging"); - sinon.assert.calledOnce(log); - sinon.assert.calledWithExactly(log, "Successfully cleared the release history associated with the \"Staging\" deployment from the \"a\" app."); - - done(); - }); - }); - - it("deploymentHistoryClear does not clear deployment if cancelled", (done: MochaDone): void => { - var command: cli.IDeploymentHistoryClearCommand = { - type: cli.CommandType.deploymentHistoryClear, - appName: "a", - deploymentName: "Staging" - }; - - var clearDeployment: Sinon.SinonSpy = sandbox.spy(cmdexec.sdk, "clearDeploymentHistory"); - - wasConfirmed = false; - - cmdexec.execute(command) - .done((): void => { - sinon.assert.notCalled(clearDeployment); - sinon.assert.calledOnce(log); - sinon.assert.calledWithExactly(log, "Clear deployment cancelled."); - - done(); - }); - }); - - it("deploymentList lists deployment names, deployment keys, and package information", (done: MochaDone): void => { - var command: cli.IDeploymentListCommand = { - type: cli.CommandType.deploymentList, - appName: "a", - format: "json", - displayKeys: true - }; - - cmdexec.execute(command) - .done((): void => { - sinon.assert.calledOnce(log); - assert.equal(log.args[0].length, 1); - - var actual: string = log.args[0][0]; - var expected = [ - { - name: "Production", - key: "6" - }, - { - name: "Staging", - key: "6", - package: { - appVersion: "1.0.0", - description: "fgh", - label: "v2", - packageHash: "jkl", - isMandatory: true, - size: 10, - blobUrl: "http://mno.pqr", - uploadTime: 1000, - metrics: { - active: 123, - downloaded: 321, - failed: 789, - installed: 456, - totalActive: 1035 - } - } - } - ]; - - assertJsonDescribesObject(actual, expected); - done(); - }); - }); - - it("deploymentRemove removes deployment", (done: MochaDone): void => { - var command: cli.IDeploymentRemoveCommand = { - type: cli.CommandType.deploymentRemove, - appName: "a", - deploymentName: "Staging" - }; - - var removeDeployment: Sinon.SinonSpy = sandbox.spy(cmdexec.sdk, "removeDeployment"); - - cmdexec.execute(command) - .done((): void => { - sinon.assert.calledOnce(removeDeployment); - sinon.assert.calledWithExactly(removeDeployment, "a", "Staging"); - sinon.assert.calledOnce(log); - sinon.assert.calledWithExactly(log, "Successfully removed the \"Staging\" deployment from the \"a\" app."); - - done(); - }); - }); - - it("deploymentRemove does not remove deployment if cancelled", (done: MochaDone): void => { - var command: cli.IDeploymentRemoveCommand = { - type: cli.CommandType.deploymentRemove, - appName: "a", - deploymentName: "Staging" - }; - - var removeDeployment: Sinon.SinonSpy = sandbox.spy(cmdexec.sdk, "removeDeployment"); - - wasConfirmed = false; - - cmdexec.execute(command) - .done((): void => { - sinon.assert.notCalled(removeDeployment); - sinon.assert.calledOnce(log); - sinon.assert.calledWithExactly(log, "Deployment removal cancelled."); - - done(); - }); - }); - - it("deploymentRename renames deployment", (done: MochaDone): void => { - var command: cli.IDeploymentRenameCommand = { - type: cli.CommandType.deploymentRename, - appName: "a", - currentDeploymentName: "Staging", - newDeploymentName: "c" - }; - - var renameDeployment: Sinon.SinonSpy = sandbox.spy(cmdexec.sdk, "renameDeployment"); - - cmdexec.execute(command) - .done((): void => { - sinon.assert.calledOnce(renameDeployment); - sinon.assert.calledOnce(log); - sinon.assert.calledWithExactly(log, "Successfully renamed the \"Staging\" deployment to \"c\" for the \"a\" app."); - - done(); - }); - }); - - it("deploymentHistory lists package history information", (done: MochaDone): void => { - var command: cli.IDeploymentHistoryCommand = { - type: cli.CommandType.deploymentHistory, - appName: "a", - deploymentName: "Staging", - format: "json", - displayAuthor: false - }; - - var getDeploymentHistory: Sinon.SinonSpy = sandbox.spy(cmdexec.sdk, "getDeploymentHistory"); - - cmdexec.execute(command) - .done((): void => { - sinon.assert.calledOnce(getDeploymentHistory); - sinon.assert.calledOnce(log); - assert.equal(log.args[0].length, 1); - - var actual: string = log.args[0][0]; - var expected: codePush.Package[] = [ - { - description: null, - appVersion: "1.0.0", - isMandatory: false, - packageHash: "463acc7d06adc9c46233481d87d9e8264b3e9ffe60fe98d721e6974209dc71a0", - blobUrl: "https://fakeblobstorage.net/storagev2/blobid1", - uploadTime: 1447113596270, - size: 1, - label: "v1", - metrics: { - active: 789, - downloaded: 456, - failed: 654, - installed: 987, - totalActive: 1035 - } - }, - { - description: "New update - this update does a whole bunch of things, including testing linewrapping", - appVersion: "1.0.1", - isMandatory: false, - packageHash: "463acc7d06adc9c46233481d87d9e8264b3e9ffe60fe98d721e6974209dc71a0", - blobUrl: "https://fakeblobstorage.net/storagev2/blobid2", - uploadTime: 1447118476669, - size: 2, - label: "v2", - metrics: { - active: 123, - downloaded: 321, - failed: 789, - installed: 456, - totalActive: 1035 - } - } - ]; - - assertJsonDescribesObject(actual, expected); - done(); - }); - }); - - it("patch command successfully updates specific label", (done: MochaDone): void => { - var command: cli.IPatchCommand = { - type: cli.CommandType.patch, - appName: "a", - deploymentName: "Staging", - label: "v1", - disabled: false, - description: "Patched", - mandatory: true, - rollout: 25, - appStoreVersion: "1.0.1" - }; - - var patch: Sinon.SinonSpy = sandbox.spy(cmdexec.sdk, "patchRelease"); - - cmdexec.execute(command) - .done((): void => { - sinon.assert.calledOnce(patch); - sinon.assert.calledOnce(log); - sinon.assert.calledWithExactly(log, `Successfully updated the "v1" release of "a" app's "Staging" deployment.`); - - done(); - }); - }); - - it("patch command successfully updates latest release", (done: MochaDone): void => { - var command: cli.IPatchCommand = { - type: cli.CommandType.patch, - appName: "a", - deploymentName: "Staging", - label: null, - disabled: false, - description: "Patched", - mandatory: true, - rollout: 25, - appStoreVersion: "1.0.1" - }; - - var patch: Sinon.SinonSpy = sandbox.spy(cmdexec.sdk, "patchRelease"); - - cmdexec.execute(command) - .done((): void => { - sinon.assert.calledOnce(patch); - sinon.assert.calledOnce(log); - sinon.assert.calledWithExactly(log, `Successfully updated the "latest" release of "a" app's "Staging" deployment.`); - - done(); - }); - }); - - it("patch command successfully updates without appStoreVersion", (done: MochaDone): void => { - var command: cli.IPatchCommand = { - type: cli.CommandType.patch, - appName: "a", - deploymentName: "Staging", - label: null, - disabled: false, - description: "Patched", - mandatory: true, - rollout: 25, - appStoreVersion: null - }; - - var patch: Sinon.SinonSpy = sandbox.spy(cmdexec.sdk, "patchRelease"); - - cmdexec.execute(command) - .done((): void => { - sinon.assert.calledOnce(patch); - sinon.assert.calledOnce(log); - sinon.assert.calledWithExactly(log, `Successfully updated the "latest" release of "a" app's "Staging" deployment.`); - - done(); - }); - }); - - it("patch command fails if no properties were specified for update", (done: MochaDone): void => { - var command: cli.IPatchCommand = { - type: cli.CommandType.patch, - appName: "a", - deploymentName: "Staging", - label: null, - disabled: null, - description: null, - mandatory: null, - rollout: null, - appStoreVersion: null - }; - - var patch: Sinon.SinonSpy = sandbox.spy(cmdexec.sdk, "patchRelease"); - - cmdexec.execute(command) - .then(() => { - done(new Error("Did not throw error.")); - }) - .catch((err) => { - assert.equal(err.message, "At least one property must be specified to patch a release."); - sinon.assert.notCalled(patch); - done(); - }) - .done(); - }); - - it("promote works successfully", (done: MochaDone): void => { - var command: cli.IPromoteCommand = { - type: cli.CommandType.promote, - appName: "a", - sourceDeploymentName: "Staging", - destDeploymentName: "Production", - description: "Promoted", - mandatory: true, - rollout: 25, - appStoreVersion: "1.0.1" - }; - - var promote: Sinon.SinonSpy = sandbox.spy(cmdexec.sdk, "promote"); - - cmdexec.execute(command) - .done((): void => { - sinon.assert.calledOnce(promote); - sinon.assert.calledOnce(log); - sinon.assert.calledWithExactly(log, `Successfully promoted the "Staging" deployment of the "a" app to the "Production" deployment.`); - - done(); - }); - }); - - it("promote works successfully without appStoreVersion", (done: MochaDone): void => { - var command: cli.IPromoteCommand = { - type: cli.CommandType.promote, - appName: "a", - sourceDeploymentName: "Staging", - destDeploymentName: "Production", - description: "Promoted", - mandatory: true, - rollout: 25, - appStoreVersion: null - }; - - var promote: Sinon.SinonSpy = sandbox.spy(cmdexec.sdk, "promote"); - - cmdexec.execute(command) - .done((): void => { - sinon.assert.calledOnce(promote); - sinon.assert.calledOnce(log); - sinon.assert.calledWithExactly(log, `Successfully promoted the "Staging" deployment of the "a" app to the "Production" deployment.`); - - done(); - }); - }); - - it("rollback works successfully", (done: MochaDone): void => { - var command: cli.IRollbackCommand = { - type: cli.CommandType.rollback, - appName: "a", - deploymentName: "Staging", - targetRelease: "v2" - }; - - var rollback: Sinon.SinonSpy = sandbox.spy(cmdexec.sdk, "rollback"); - - cmdexec.execute(command) - .done((): void => { - sinon.assert.calledOnce(rollback); - sinon.assert.calledOnce(log); - sinon.assert.calledWithExactly(log, `Successfully performed a rollback on the "Staging" deployment of the "a" app.`); - - done(); - }); - }); - - it("release doesn't allow non valid semver ranges", (done: MochaDone): void => { - var command: cli.IReleaseCommand = { - type: cli.CommandType.release, - appName: "a", - deploymentName: "Staging", - description: "test releasing zip file", - mandatory: false, - rollout: null, - appStoreVersion: "not semver", - package: "./resources" - }; - - releaseHelperFunction(command, done, "Please use a semver-compliant target binary version range, for example \"1.0.0\", \"*\" or \"^1.2.3\"."); - }); - - it("release doesn't allow releasing .zip file", (done: MochaDone): void => { - var command: cli.IReleaseCommand = { - type: cli.CommandType.release, - appName: "a", - deploymentName: "Staging", - description: "test releasing zip file", - mandatory: false, - rollout: null, - appStoreVersion: "1.0.0", - package: "/fake/path/test/file.zip" - }; - - releaseHelperFunction(command, done, INVALID_RELEASE_FILE_ERROR_MESSAGE); - }); - - it("release doesn't allow releasing .ipa file", (done: MochaDone): void => { - var command: cli.IReleaseCommand = { - type: cli.CommandType.release, - appName: "a", - deploymentName: "Staging", - description: "test releasing ipa file", - mandatory: false, - rollout: null, - appStoreVersion: "1.0.0", - package: "/fake/path/test/file.ipa" - }; - - releaseHelperFunction(command, done, INVALID_RELEASE_FILE_ERROR_MESSAGE); - }); - - it("release doesn't allow releasing .apk file", (done: MochaDone): void => { - var command: cli.IReleaseCommand = { - type: cli.CommandType.release, - appName: "a", - deploymentName: "Staging", - description: "test releasing apk file", - mandatory: false, - rollout: null, - appStoreVersion: "1.0.0", - package: "/fake/path/test/file.apk" - }; - - releaseHelperFunction(command, done, INVALID_RELEASE_FILE_ERROR_MESSAGE); - }); - - it("release-cordova fails if Cordova project cannot be prepared", (done: MochaDone): void => { - testReleaseCordovaFailure(/*build*/ false, done); - }); - - it("release-cordova fails if Cordova project cannot be built", (done: MochaDone): void => { - testReleaseCordovaFailure(/*build*/ true, done); - }); - - function testReleaseCordovaFailure(build: boolean, done: MochaDone): void { - var command: cli.IReleaseCordovaCommand = { - type: cli.CommandType.releaseCordova, - appName: "a", - appStoreVersion: null, - build: build, - deploymentName: "Staging", - description: "Test invalid project", - mandatory: false, - rollout: null, - platform: "ios" - }; - - var cordovaCommand: string = build ? "build" : "prepare"; - var execSync: Sinon.SinonStub = sandbox.stub(cmdexec, "execSync", (command: string, options: any) => { throw `Failed ${cordovaCommand}`; }); - var release: Sinon.SinonSpy = sandbox.spy(cmdexec, "release"); - var releaseCordova: Sinon.SinonSpy = sandbox.spy(cmdexec, "releaseCordova"); - - cmdexec.execute(command) - .then(() => { - done(new Error("Did not throw error.")); - }) - .catch((err) => { - assert.equal(err.message, `Unable to ${cordovaCommand} project. Please ensure that the CWD represents a Cordova project and that the "${command.platform}" platform was added by running "cordova platform add ${command.platform}".`); - sinon.assert.notCalled(release); - sinon.assert.threw(releaseCordova, "Error"); - done(); - }) - .done(); - } - - it("release-cordova fails if CWD does not contain config.xml", (done: MochaDone): void => { - var command: cli.IReleaseCordovaCommand = { - type: cli.CommandType.releaseCordova, - appName: "a", - appStoreVersion: null, - build: false, - deploymentName: "Staging", - description: "Test missing config.xml", - mandatory: false, - rollout: null, - platform: "ios" - }; - - var execSync: Sinon.SinonStub = sandbox.stub(cmdexec, "execSync", (command: string, options: any) => { }); - var release: Sinon.SinonSpy = sandbox.spy(cmdexec, "release"); - var releaseCordova: Sinon.SinonSpy = sandbox.spy(cmdexec, "releaseCordova"); - - cmdexec.execute(command) - .then(() => { - done(new Error("Did not throw error.")); - }) - .catch((err) => { - assert.equal(err.message, `Unable to find or read "config.xml" in the CWD. The "release-cordova" command must be executed in a Cordova project folder.`); - sinon.assert.notCalled(release); - sinon.assert.threw(releaseCordova, "Error"); - sinon.assert.calledOnce(execSync); - done(); - }) - .done(); - }); - - it("release-cordova fails if platform is invalid", (done: MochaDone): void => { - var command: cli.IReleaseCordovaCommand = { - type: cli.CommandType.releaseCordova, - appName: "a", - appStoreVersion: null, - build: false, - deploymentName: "Staging", - description: "Test invalid platform", - mandatory: false, - rollout: null, - platform: "blackberry", - }; - - var release: Sinon.SinonSpy = sandbox.spy(cmdexec, "release"); - var releaseCordova: Sinon.SinonSpy = sandbox.spy(cmdexec, "releaseCordova"); - - cmdexec.execute(command) - .then(() => { - done(new Error("Did not throw error.")); - }) - .catch((err) => { - assert.equal(err.message, "Platform must be either \"ios\" or \"android\"."); - sinon.assert.notCalled(release); - sinon.assert.threw(releaseCordova, "Error"); - sinon.assert.notCalled(spawn); - done(); - }) - .done(); - }); - - it("release-cordova defaults appStoreVersion to value pulled from config.xml", (done: MochaDone): void => { - var command: cli.IReleaseCordovaCommand = { - type: cli.CommandType.releaseCordova, - appName: "a", - appStoreVersion: null, - build: false, - deploymentName: "Staging", - description: "Test config.xml app version read", - mandatory: false, - rollout: null, - platform: "ios" - }; - - var oldWd: string = process.cwd(); - ensureInTestAppDirectory(); - - var expectedReleaseCommand: any = { - type: cli.CommandType.release, - appName: "a", - appStoreVersion: "0.0.1", - build: false, - deploymentName: "Staging", - description: "Test config.xml app version read", - mandatory: false, - rollout: null, - package: path.join(process.cwd(), "platforms", "ios", "www"), - platform: "ios" - } - - var execSync: Sinon.SinonStub = sandbox.stub(cmdexec, "execSync", (command: string, options: any) => { }); - var release: Sinon.SinonSpy = sandbox.stub(cmdexec, "release"); - var releaseCordova: Sinon.SinonSpy = sandbox.spy(cmdexec, "releaseCordova"); - - cmdexec.execute(command) - .then((compiledReleaseCommand: any) => { - sinon.assert.calledOnce(execSync); - sinon.assert.calledWith(release, expectedReleaseCommand); - done(); - }) - .catch((err) => { - done(new Error("Threw error. " + err.message)); - }) - .done(() => { - process.chdir(oldWd); - }); - }); - - it("release-cordova points 'package' to the built folder for android", (done: MochaDone): void => { - var command: cli.IReleaseCordovaCommand = { - type: cli.CommandType.releaseCordova, - appName: "a", - appStoreVersion: null, - build: true, - deploymentName: "Staging", - description: "Test android package resolution", - mandatory: false, - rollout: null, - platform: "android" - }; - - var oldWd: string = process.cwd(); - ensureInTestAppDirectory(); - - var expectedReleaseCommand: any = { - type: cli.CommandType.release, - appName: "a", - appStoreVersion: "0.0.1", - build: true, - deploymentName: "Staging", - description: "Test android package resolution", - mandatory: false, - rollout: null, - package: path.join(process.cwd(), "platforms", "android", "assets", "www"), - platform: "android" - } - - var execSync: Sinon.SinonStub = sandbox.stub(cmdexec, "execSync", (command: string, options: any) => { }); - var release: Sinon.SinonSpy = sandbox.stub(cmdexec, "release"); - var releaseCordova: Sinon.SinonSpy = sandbox.spy(cmdexec, "releaseCordova"); - - cmdexec.execute(command) - .then((compiledReleaseCommand: any) => { - sinon.assert.calledOnce(execSync); - sinon.assert.calledWith(release, expectedReleaseCommand); - done(); - }) - .catch((err) => { - done(new Error("Threw error. " + err.message)); - }) - .done(() => { - process.chdir(oldWd); - }); - }); - - it("release-react fails if CWD does not contain package.json", (done: MochaDone): void => { - var command: cli.IReleaseReactCommand = { - type: cli.CommandType.releaseReact, - appName: "a", - appStoreVersion: null, - deploymentName: "Staging", - description: "Test invalid folder", - mandatory: false, - rollout: null, - platform: "ios" - }; - - var release: Sinon.SinonSpy = sandbox.spy(cmdexec, "release"); - var releaseReact: Sinon.SinonSpy = sandbox.spy(cmdexec, "releaseReact"); - - cmdexec.execute(command) - .then(() => { - done(new Error("Did not throw error.")); - }) - .catch((err) => { - assert.equal(err.message, "Unable to find or read \"package.json\" in the CWD. The \"release-react\" command must be executed in a React Native project folder."); - sinon.assert.notCalled(release); - sinon.assert.threw(releaseReact, "Error"); - sinon.assert.notCalled(spawn); - done(); - }) - .done(); - }); - - it("release-react fails if entryFile does not exist", (done: MochaDone): void => { - var command: cli.IReleaseReactCommand = { - type: cli.CommandType.releaseReact, - appName: "a", - appStoreVersion: null, - deploymentName: "Staging", - description: "Test invalid entryFile", - entryFile: "doesntexist.js", - mandatory: false, - rollout: null, - platform: "ios" - }; - - ensureInTestAppDirectory(); - - var release: Sinon.SinonSpy = sandbox.spy(cmdexec, "release"); - var releaseReact: Sinon.SinonSpy = sandbox.spy(cmdexec, "releaseReact"); - - cmdexec.execute(command) - .then(() => { - done(new Error("Did not throw error.")); - }) - .catch((err) => { - assert.equal(err.message, "Entry file \"doesntexist.js\" does not exist."); - sinon.assert.notCalled(release); - sinon.assert.threw(releaseReact, "Error"); - sinon.assert.notCalled(spawn); - done(); - }) - .done(); - }); - - it("release-react fails if platform is invalid", (done: MochaDone): void => { - var command: cli.IReleaseReactCommand = { - type: cli.CommandType.releaseReact, - appName: "a", - appStoreVersion: null, - deploymentName: "Staging", - description: "Test invalid platform", - mandatory: false, - rollout: null, - platform: "blackberry", - }; - - ensureInTestAppDirectory(); - - var release: Sinon.SinonSpy = sandbox.spy(cmdexec, "release"); - var releaseReact: Sinon.SinonSpy = sandbox.spy(cmdexec, "releaseReact"); - - cmdexec.execute(command) - .then(() => { - done(new Error("Did not throw error.")); - }) - .catch((err) => { - assert.equal(err.message, "Platform must be either \"android\", \"ios\" or \"windows\"."); - sinon.assert.notCalled(release); - sinon.assert.threw(releaseReact, "Error"); - sinon.assert.notCalled(spawn); - done(); - }) - .done(); - }); - - it("release-react fails if targetBinaryRange is not a valid semver range expression", (done: MochaDone): void => { - var bundleName = "bundle.js"; - var command: cli.IReleaseReactCommand = { - type: cli.CommandType.releaseReact, - appName: "a", - appStoreVersion: "notsemver", - bundleName: bundleName, - deploymentName: "Staging", - description: "Test uses targetBinaryRange", - mandatory: false, - rollout: null, - platform: "android", - sourcemapOutput: "index.android.js.map" - }; - - ensureInTestAppDirectory(); - - var release: Sinon.SinonSpy = sandbox.stub(cmdexec, "release", () => { return Q(null) }); - var releaseReact: Sinon.SinonSpy = sandbox.spy(cmdexec, "releaseReact"); - - cmdexec.execute(command) - .then(() => { - done(new Error("Did not throw error.")); - }) - .catch((err) => { - assert.equal(err.message, "Please use a semver-compliant target binary version range, for example \"1.0.0\", \"*\" or \"^1.2.3\"."); - sinon.assert.notCalled(release); - sinon.assert.threw(releaseReact, "Error"); - sinon.assert.notCalled(spawn); - done(); - }) - .done(); - }); - - it("release-react defaults entry file to index.{platform}.js if not provided", (done: MochaDone): void => { - var bundleName = "bundle.js"; - var command: cli.IReleaseReactCommand = { - type: cli.CommandType.releaseReact, - appName: "a", - appStoreVersion: null, - bundleName: bundleName, - deploymentName: "Staging", - description: "Test default entry file", - mandatory: false, - rollout: null, - platform: "ios" - }; - - ensureInTestAppDirectory(); - - var release: Sinon.SinonSpy = sandbox.stub(cmdexec, "release", () => { return Q(null) }); - - cmdexec.execute(command) - .then(() => { - var releaseCommand: cli.IReleaseCommand = command; - releaseCommand.package = path.join(os.tmpdir(), "CodePush"); - releaseCommand.appStoreVersion = "1.2.3"; - - sinon.assert.calledOnce(spawn); - var spawnCommand: string = spawn.args[0][0]; - var spawnCommandArgs: string = spawn.args[0][1].join(" "); - assert.equal(spawnCommand, "node"); - assert.equal( - spawnCommandArgs, - `${path.join("node_modules", "react-native", "local-cli", "cli.js")} bundle --assets-dest ${path.join(os.tmpdir(), "CodePush")} --bundle-output ${path.join(os.tmpdir(), "CodePush", bundleName)} --dev false --entry-file index.ios.js --platform ios` - ); - assertJsonDescribesObject(JSON.stringify(release.args[0][0], /*replacer=*/ null, /*spacing=*/ 2), releaseCommand); - done(); - }) - .done(); - }); - - it("release-react defaults bundle name to \"main.jsbundle\" if not provided and platform is \"ios\"", (done: MochaDone): void => { - var command: cli.IReleaseReactCommand = { - type: cli.CommandType.releaseReact, - appName: "a", - appStoreVersion: null, - deploymentName: "Staging", - description: "Test default entry file", - mandatory: false, - rollout: null, - platform: "ios" - }; - - ensureInTestAppDirectory(); - - var release: Sinon.SinonSpy = sandbox.stub(cmdexec, "release", () => { return Q(null) }); - - cmdexec.execute(command) - .then(() => { - var releaseCommand: cli.IReleaseCommand = clone(command); - var packagePath: string = path.join(os.tmpdir(), "CodePush"); - releaseCommand.package = packagePath; - releaseCommand.appStoreVersion = "1.2.3"; - - sinon.assert.calledOnce(spawn); - var spawnCommand: string = spawn.args[0][0]; - var spawnCommandArgs: string = spawn.args[0][1].join(" "); - assert.equal(spawnCommand, "node"); - assert.equal( - spawnCommandArgs, - `${path.join("node_modules", "react-native", "local-cli", "cli.js")} bundle --assets-dest ${packagePath} --bundle-output ${path.join(packagePath, "main.jsbundle")} --dev false --entry-file index.ios.js --platform ios` - ); - assertJsonDescribesObject(JSON.stringify(release.args[0][0], /*replacer=*/ null, /*spacing=*/ 2), releaseCommand); - done(); - }) - .done(); - }); - - it("release-react defaults bundle name to \"index.android.bundle\" if not provided and platform is \"android\"", (done: MochaDone): void => { - var command: cli.IReleaseReactCommand = { - type: cli.CommandType.releaseReact, - appName: "a", - appStoreVersion: null, - deploymentName: "Staging", - description: "Test default entry file", - mandatory: false, - rollout: null, - platform: "android" - }; - - ensureInTestAppDirectory(); - - var release: Sinon.SinonSpy = sandbox.stub(cmdexec, "release", () => { return Q(null) }); - - cmdexec.execute(command) - .then(() => { - var releaseCommand: cli.IReleaseCommand = clone(command); - var packagePath: string = path.join(os.tmpdir(), "CodePush"); - releaseCommand.package = packagePath; - releaseCommand.appStoreVersion = "1.0.0"; - - sinon.assert.calledOnce(spawn); - var spawnCommand: string = spawn.args[0][0]; - var spawnCommandArgs: string = spawn.args[0][1].join(" "); - assert.equal(spawnCommand, "node"); - assert.equal( - spawnCommandArgs, - `${path.join("node_modules", "react-native", "local-cli", "cli.js")} bundle --assets-dest ${packagePath} --bundle-output ${path.join(packagePath, "index.android.bundle")} --dev false --entry-file index.android.js --platform android` - ); - assertJsonDescribesObject(JSON.stringify(release.args[0][0], /*replacer=*/ null, /*spacing=*/ 2), releaseCommand); - done(); - }) - .done(); - }); - - it("release-react defaults bundle name to \"index.windows.bundle\" if not provided and platform is \"windows\"", (done: MochaDone): void => { - var command: cli.IReleaseReactCommand = { - type: cli.CommandType.releaseReact, - appName: "a", - appStoreVersion: null, - deploymentName: "Staging", - description: "Test default entry file", - mandatory: false, - rollout: null, - platform: "windows" - }; - - ensureInTestAppDirectory(); - - var release: Sinon.SinonSpy = sandbox.stub(cmdexec, "release", () => { return Q(null) }); - - cmdexec.execute(command) - .then(() => { - var releaseCommand: cli.IReleaseCommand = clone(command); - var packagePath = path.join(os.tmpdir(), "CodePush"); - releaseCommand.package = packagePath; - releaseCommand.appStoreVersion = "1.0.0"; - - sinon.assert.calledOnce(spawn); - var spawnCommand: string = spawn.args[0][0]; - var spawnCommandArgs: string = spawn.args[0][1].join(" "); - assert.equal(spawnCommand, "node"); - assert.equal( - spawnCommandArgs, - `${path.join("node_modules", "react-native", "local-cli", "cli.js")} bundle --assets-dest ${packagePath} --bundle-output ${path.join(packagePath, "index.windows.bundle")} --dev false --entry-file index.windows.js --platform windows` - ); - assertJsonDescribesObject(JSON.stringify(release.args[0][0], /*replacer=*/ null, /*spacing=*/ 2), releaseCommand); - done(); - }) - .done(); - }); - - it("release-react generates dev bundle", (done: MochaDone): void => { - var bundleName = "bundle.js"; - var command: cli.IReleaseReactCommand = { - type: cli.CommandType.releaseReact, - appName: "a", - appStoreVersion: null, - bundleName: bundleName, - deploymentName: "Staging", - development: true, - description: "Test generates dev bundle", - mandatory: false, - rollout: null, - platform: "android", - sourcemapOutput: "index.android.js.map" - }; - - ensureInTestAppDirectory(); - - var release: Sinon.SinonSpy = sandbox.stub(cmdexec, "release", () => { return Q(null) }); - - cmdexec.execute(command) - .then(() => { - var releaseCommand: cli.IReleaseCommand = command; - releaseCommand.package = path.join(os.tmpdir(), "CodePush"); - releaseCommand.appStoreVersion = "1.2.3"; - - sinon.assert.calledOnce(spawn); - var spawnCommand: string = spawn.args[0][0]; - var spawnCommandArgs: string = spawn.args[0][1].join(" "); - assert.equal(spawnCommand, "node"); - assert.equal( - spawnCommandArgs, - `${path.join("node_modules", "react-native", "local-cli", "cli.js")} bundle --assets-dest ${path.join(os.tmpdir(), "CodePush")} --bundle-output ${path.join(os.tmpdir(), "CodePush", bundleName)} --dev true --entry-file index.android.js --platform android --sourcemap-output index.android.js.map` - ); - assertJsonDescribesObject(JSON.stringify(release.args[0][0], /*replacer=*/ null, /*spacing=*/ 2), releaseCommand); - done(); - }) - .done(); - }); - - it("release-react generates sourcemaps", (done: MochaDone): void => { - var bundleName = "bundle.js"; - var command: cli.IReleaseReactCommand = { - type: cli.CommandType.releaseReact, - appName: "a", - appStoreVersion: null, - bundleName: bundleName, - deploymentName: "Staging", - description: "Test generates sourcemaps", - mandatory: false, - rollout: null, - platform: "android", - sourcemapOutput: "index.android.js.map" - }; - - ensureInTestAppDirectory(); - - var release: Sinon.SinonSpy = sandbox.stub(cmdexec, "release", () => { return Q(null) }); - - cmdexec.execute(command) - .then(() => { - var releaseCommand: cli.IReleaseCommand = command; - releaseCommand.package = path.join(os.tmpdir(), "CodePush"); - releaseCommand.appStoreVersion = "1.2.3"; - - sinon.assert.calledOnce(spawn); - var spawnCommand: string = spawn.args[0][0]; - var spawnCommandArgs: string = spawn.args[0][1].join(" "); - assert.equal(spawnCommand, "node"); - assert.equal( - spawnCommandArgs, - `${path.join("node_modules", "react-native", "local-cli", "cli.js")} bundle --assets-dest ${path.join(os.tmpdir(), "CodePush")} --bundle-output ${path.join(os.tmpdir(), "CodePush", bundleName)} --dev false --entry-file index.android.js --platform android --sourcemap-output index.android.js.map` - ); - assertJsonDescribesObject(JSON.stringify(release.args[0][0], /*replacer=*/ null, /*spacing=*/ 2), releaseCommand); - done(); - }) - .done(); - }); - - it("release-react uses specified targetBinaryRange option", (done: MochaDone): void => { - var bundleName = "bundle.js"; - var command: cli.IReleaseReactCommand = { - type: cli.CommandType.releaseReact, - appName: "a", - appStoreVersion: ">=1.0.0 <1.0.5", - bundleName: bundleName, - deploymentName: "Staging", - description: "Test uses targetBinaryRange", - mandatory: false, - rollout: null, - platform: "android", - sourcemapOutput: "index.android.js.map" - }; - - ensureInTestAppDirectory(); - - var release: Sinon.SinonSpy = sandbox.stub(cmdexec, "release", () => { return Q(null) }); - - cmdexec.execute(command) - .then(() => { - var releaseCommand: cli.IReleaseCommand = command; - releaseCommand.package = path.join(os.tmpdir(), "CodePush"); - - sinon.assert.calledOnce(spawn); - var spawnCommand: string = spawn.args[0][0]; - var spawnCommandArgs: string = spawn.args[0][1].join(" "); - assert.equal(spawnCommand, "node"); - assert.equal( - spawnCommandArgs, - `${path.join("node_modules", "react-native", "local-cli", "cli.js")} bundle --assets-dest ${path.join(os.tmpdir(), "CodePush")} --bundle-output ${path.join(os.tmpdir(), "CodePush", bundleName)} --dev false --entry-file index.android.js --platform android --sourcemap-output index.android.js.map` - ); - assertJsonDescribesObject(JSON.stringify(release.args[0][0], /*replacer=*/ null, /*spacing=*/ 2), releaseCommand); - done(); - }) - .done(); - }); - - it("sessionList lists session name and expires fields", (done: MochaDone): void => { - var command: cli.IAccessKeyListCommand = { - type: cli.CommandType.sessionList, - format: "json" - }; - - cmdexec.execute(command) - .done((): void => { - sinon.assert.calledOnce(log); - assert.equal(log.args[0].length, 1); - - var actual: string = log.args[0][0]; - var expected = [ - { - loggedInTime: 0, - machineName: TEST_MACHINE_NAME, - } - ]; - - assertJsonDescribesObject(actual, expected); - done(); - }); - }); - - it("sessionRemove removes session", (done: MochaDone): void => { - var machineName = TEST_MACHINE_NAME; - var command: cli.ISessionRemoveCommand = { - type: cli.CommandType.sessionRemove, - machineName: machineName - }; - - var removeSession: Sinon.SinonSpy = sandbox.spy(cmdexec.sdk, "removeSession"); - - cmdexec.execute(command) - .done((): void => { - sinon.assert.calledOnce(removeSession); - sinon.assert.calledWithExactly(removeSession, machineName); - sinon.assert.calledOnce(log); - sinon.assert.calledWithExactly(log, `Successfully removed the login session for "${machineName}".`); - - done(); - }); - }); - - it("sessionRemove does not remove session if cancelled", (done: MochaDone): void => { - var machineName = TEST_MACHINE_NAME; - var command: cli.ISessionRemoveCommand = { - type: cli.CommandType.sessionRemove, - machineName: machineName - }; - - var removeSession: Sinon.SinonSpy = sandbox.spy(cmdexec.sdk, "removeSession"); - - wasConfirmed = false; - - cmdexec.execute(command) - .done((): void => { - sinon.assert.notCalled(removeSession); - sinon.assert.calledOnce(log); - sinon.assert.calledWithExactly(log, "Session removal cancelled."); - - done(); - }); - }); - - it("sessionRemove does not remove current session", (done: MochaDone): void => { - var machineName = os.hostname(); - var command: cli.ISessionRemoveCommand = { - type: cli.CommandType.sessionRemove, - machineName: machineName - }; - - var removeSession: Sinon.SinonSpy = sandbox.spy(cmdexec.sdk, "removeSession"); - - wasConfirmed = false; - - cmdexec.execute(command) - .then(() => { - done(new Error("Did not throw error.")); - }) - .catch((err) => { - done(); - }) - .done(); - }); - - function releaseHelperFunction(command: cli.IReleaseCommand, done: MochaDone, expectedError: string): void { - var release: Sinon.SinonSpy = sandbox.spy(cmdexec.sdk, "release"); - cmdexec.execute(command) - .done((): void => { - throw "Error Expected"; - }, (error: any): void => { - assert(!!error); - assert.equal(error.message, expectedError); - done(); - }); - } -}); \ No newline at end of file diff --git a/cli/test/hash-utils.ts b/cli/test/hash-utils.ts deleted file mode 100644 index 8dc4abae..00000000 --- a/cli/test/hash-utils.ts +++ /dev/null @@ -1,169 +0,0 @@ -import * as assert from "assert"; -import * as crypto from "crypto"; -import * as fs from "fs"; -import * as hashUtils from "../script/hash-utils"; -var mkdirp = require("mkdirp"); -import * as os from "os"; -import * as path from "path"; -import * as q from "q"; -var yauzl = require("yauzl"); - -import PackageManifest = hashUtils.PackageManifest; -import Promise = q.Promise; - -function randomString(): string { - var stringLength = 10; - return crypto.randomBytes(Math.ceil(stringLength / 2)) - .toString('hex') // convert to hexadecimal format - .slice(0, stringLength); // return required number of characters -} - -function unzipToDirectory(zipPath: string, directoryPath: string): Promise { - var deferred: q.Deferred = q.defer(); - var originalCwd: string = process.cwd(); - - mkdirp(directoryPath, (err: Error) => { - if (err) throw err; - process.chdir(directoryPath); - - yauzl.open(zipPath, {lazyEntries: true}, function(err: Error, zipfile: any) { - if (err) throw err; - zipfile.readEntry(); - zipfile.on("entry", function(entry: any) { - if (/\/$/.test(entry.fileName)) { - // directory file names end with '/' - mkdirp(entry.fileName, function(err: Error) { - if (err) throw err; - zipfile.readEntry(); - }); - } else { - // file entry - zipfile.openReadStream(entry, function(err: Error, readStream: any) { - if (err) throw err; - // ensure parent directory exists - mkdirp(path.dirname(entry.fileName), function(err: Error) { - if (err) throw err; - readStream.pipe(fs.createWriteStream(entry.fileName)); - readStream.on("end", function() { - zipfile.readEntry(); - }); - }); - }); - } - }); - - zipfile.on("end", function(err: Error) { - if (err) deferred.reject(err); - else deferred.resolve(null); - }); - }); - }); - - return deferred.promise - .finally(() => { - process.chdir(originalCwd); - }); -} - -describe("Hashing utility", () => { - const TEST_DIRECTORY = path.join(os.tmpdir(), "codepushtests", randomString()); - - const TEST_ARCHIVE_FILE_PATH = path.join(__dirname, "resources", "test.zip"); - const TEST_ZIP_HASH = "540fed8df3553079e81d1353c5cc4e3cac7db9aea647a85d550f646e8620c317"; - const TEST_ZIP_MANIFEST_HASH = "9e0499ce7df5c04cb304c9deed684dc137fc603cb484a5b027478143c595d80b"; - const HASH_B = "3e23e8160039594a33894f6564e1b1348bbd7a0088d42c4acb73eeaed59c009d"; - const HASH_C = "2e7d2c03a9507ae265ecf5b5356885a53393a2029d241394997265a1a25aefc6"; - const HASH_D = "18ac3e7343f016890c510e93f935261169d9e3f565436429830faf0934f4f8e4"; - - const IGNORED_METADATA_ARCHIVE_FILE_PATH = path.join(__dirname, "resources", "ignoredMetadata.zip"); - const INDEX_HASH = "b0693dc92f76e08bf1485b3dd9b514a2e31dfd6f39422a6b60edb722671dc98f"; - - it("generates a package hash from file", (done) => { - hashUtils.hashFile(TEST_ARCHIVE_FILE_PATH) - .done((packageHash: string): void => { - assert.equal(packageHash, TEST_ZIP_HASH); - - done(); - }); - }); - - it("generates a package manifest for an archive", (done) => { - hashUtils.generatePackageManifestFromZip(TEST_ARCHIVE_FILE_PATH) - .done((manifest: PackageManifest): void => { - var fileHashesMap = manifest.toMap(); - assert.equal(fileHashesMap.size, 3); - - var hash: string = fileHashesMap.get("b.txt"); - assert.equal(hash, HASH_B); - - hash = fileHashesMap.get("c.txt"); - assert.equal(hash, HASH_C); - - hash = fileHashesMap.get("d.txt"); - assert.equal(hash, HASH_D); - - done(); - }); - }); - - it("generates a package manifest for a directory", (done) => { - var directory = path.join(TEST_DIRECTORY, "testZip"); - - unzipToDirectory(TEST_ARCHIVE_FILE_PATH, directory) - .then(() => { - return hashUtils.generatePackageManifestFromDirectory(/*directoryPath*/ directory, /*basePath*/ directory); - }) - .done((manifest: PackageManifest): void => { - var fileHashesMap = manifest.toMap(); - assert.equal(fileHashesMap.size, 3); - - var hash: string = fileHashesMap.get("b.txt"); - assert.equal(hash, HASH_B); - - hash = fileHashesMap.get("c.txt"); - assert.equal(hash, HASH_C); - - hash = fileHashesMap.get("d.txt"); - assert.equal(hash, HASH_D); - - done(); - }); - }); - - it("generates a package hash from manifest", (done) => { - hashUtils.generatePackageManifestFromZip(TEST_ARCHIVE_FILE_PATH) - .then((manifest: PackageManifest) => { - return manifest.computePackageHash(); - }) - .done((packageHash: string): void => { - assert.equal(packageHash, TEST_ZIP_MANIFEST_HASH); - - done(); - }); - }); - - it("generates a package manifest for an archive with ignorable metadata", (done) => { - hashUtils.generatePackageManifestFromZip(IGNORED_METADATA_ARCHIVE_FILE_PATH) - .done((manifest: PackageManifest): void => { - assert.equal(manifest.toMap().size, 1); - var hash: string = manifest.toMap().get("www/index.html"); - assert.equal(hash, INDEX_HASH); - done(); - }); - }); - - it("generates a package manifest for a directory with ignorable metadata", (done) => { - var directory = path.join(TEST_DIRECTORY, "ignorableMetadata"); - - unzipToDirectory(IGNORED_METADATA_ARCHIVE_FILE_PATH, directory) - .then(() => { - return hashUtils.generatePackageManifestFromDirectory(/*directoryPath*/ directory, /*basePath*/ directory); - }) - .done((manifest: PackageManifest): void => { - assert.equal(manifest.toMap().size, 1); - var hash: string = manifest.toMap().get("www/index.html"); - assert.equal(hash, INDEX_HASH); - done(); - }); - }); -}); diff --git a/cli/test/resources/TestApp/android/app/build.gradle b/cli/test/resources/TestApp/android/app/build.gradle deleted file mode 100644 index 0ff212b3..00000000 --- a/cli/test/resources/TestApp/android/app/build.gradle +++ /dev/null @@ -1,56 +0,0 @@ -apply plugin: "com.android.application" - -import com.android.build.OutputFile - -apply from: "react.gradle" - -def enableSeparateBuildPerCPUArchitecture = false -def enableProguardInReleaseBuilds = true - -android { - compileSdkVersion 23 - buildToolsVersion "23.0.1" - - defaultConfig { - applicationId "com.microsoft.testapp" - minSdkVersion 16 - targetSdkVersion 22 - versionCode 1 - versionName "1.0.0" - ndk { - abiFilters "armeabi-v7a", "x86" - } - } - splits { - abi { - enable enableSeparateBuildPerCPUArchitecture - universalApk true - reset() - include "armeabi-v7a", "x86" - } - } - buildTypes { - release { - minifyEnabled enableProguardInReleaseBuilds - proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro" - } - } - - applicationVariants.all { variant -> - variant.outputs.each { output -> - def versionCodes = ["armeabi-v7a":1, "x86":2] - def abi = output.getFilter(OutputFile.ABI) - if (abi != null) { - output.versionCodeOverride = - versionCodes.get(abi) * 1048576 + defaultConfig.versionCode - } - } - } -} - -dependencies { - compile fileTree(dir: "libs", include: ["*.jar"]) - compile "com.android.support:appcompat-v7:23.0.1" - compile "com.facebook.react:react-native:0.19.+" - compile project(":react-native-code-push") -} diff --git a/cli/test/resources/TestApp/config.xml b/cli/test/resources/TestApp/config.xml deleted file mode 100644 index 79318a1c..00000000 --- a/cli/test/resources/TestApp/config.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - HelloCordova - - A sample Apache Cordova application that responds to the deviceready event. - - - Apache Cordova Team - - - - - - - - - - \ No newline at end of file diff --git a/cli/test/resources/TestApp/iOS/TestApp/Info.plist b/cli/test/resources/TestApp/iOS/TestApp/Info.plist deleted file mode 100644 index e1033b9c..00000000 --- a/cli/test/resources/TestApp/iOS/TestApp/Info.plist +++ /dev/null @@ -1,49 +0,0 @@ - - - - - CFBundleDevelopmentRegion - en - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - APPL - CFBundleShortVersionString - 1.2.3 - CFBundleSignature - ???? - CFBundleVersion - 1 - LSRequiresIPhoneOS - - NSAppTransportSecurity - - NSAllowsArbitraryLoads - - - UILaunchStoryboardName - LaunchScreen - UIRequiredDeviceCapabilities - - armv7 - - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UIViewControllerBasedStatusBarAppearance - - NSLocationWhenInUseUsageDescription - - CodePushDeploymentKey - deployment-key-here - - diff --git a/cli/test/resources/TestApp/index.android.js b/cli/test/resources/TestApp/index.android.js deleted file mode 100644 index e69de29b..00000000 diff --git a/cli/test/resources/TestApp/index.ios.js b/cli/test/resources/TestApp/index.ios.js deleted file mode 100644 index e69de29b..00000000 diff --git a/cli/test/resources/TestApp/index.windows.js b/cli/test/resources/TestApp/index.windows.js deleted file mode 100644 index e69de29b..00000000 diff --git a/cli/test/resources/TestApp/package.json b/cli/test/resources/TestApp/package.json deleted file mode 100644 index 06c4003c..00000000 --- a/cli/test/resources/TestApp/package.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "TestApp", - "dependencies": { - "react-native": "0.19.0" - } -} diff --git a/cli/test/resources/TestApp/windows/TestApp/Package.appxmanifest b/cli/test/resources/TestApp/windows/TestApp/Package.appxmanifest deleted file mode 100644 index 04d415b6..00000000 --- a/cli/test/resources/TestApp/windows/TestApp/Package.appxmanifest +++ /dev/null @@ -1,46 +0,0 @@ - - - - - - - - - - TestApp - Assets\StoreLogo.png - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/cli/test/resources/ignoredMetadata.zip b/cli/test/resources/ignoredMetadata.zip deleted file mode 100644 index 4b0e0c87..00000000 Binary files a/cli/test/resources/ignoredMetadata.zip and /dev/null differ diff --git a/cli/test/resources/test.zip b/cli/test/resources/test.zip deleted file mode 100644 index aef9a20f..00000000 Binary files a/cli/test/resources/test.zip and /dev/null differ diff --git a/definitions/base-64.d.ts b/definitions/base-64.d.ts deleted file mode 100644 index 3360bee2..00000000 --- a/definitions/base-64.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -declare module "base-64" { - function decode(input: string): string; - function encode(input: string): string; -} \ No newline at end of file diff --git a/definitions/oauth2-server.d.ts b/definitions/oauth2-server.d.ts deleted file mode 100644 index f64a7049..00000000 --- a/definitions/oauth2-server.d.ts +++ /dev/null @@ -1,57 +0,0 @@ -declare module Express { - export interface Request { - user?: any; - } -} - -declare module "oauth2-server" { - import express = require('express'); - - module o { - interface Server { - authorise(): express.RequestHandler; - errorHandler(): express.ErrorRequestHandler; - grant(): express.RequestHandler; - } - - interface Model { - getAccessToken(bearerToken: string, callback: (error: any, accessToken?: AccessToken) => void): void; - getClient(clientId: string, clientSecret: string, callback: (error: any, client?: Client) => void): void; - grantTypeAllowed(clientId: string, grantType: string, callback: (error: any, allowed?: boolean) => void): void; - saveAccessToken(accessToken: string, clientId: string, expires: Date, user: any, callback: (error: any) => void): void; - getUserFromClient(clientId: string, clientSecret: string, callback: (error: any, user?: User) => void): void; - generateToken(type: string, req: Express.Request, callback: (error: any, token?: string) => void): void; - } - - interface AccessToken { - expires: Date; - user?: Object; - userId?: any; - } - - interface Client { - clientId: string; - redirectUri?: string; - } - - interface User { - id: any; - } - - interface ServerOptions { - model: Model; - grants?: string[]; - debug?: boolean; - accessTokenLifetime?: number; - refreshTokenLifetime?: number; - authCodeLifetime?: number; - clientIdRegex?: RegExp; - passthroughErrors?: boolean; - continueAfterResponse?: boolean; - } - } - - function o(options: o.ServerOptions): o.Server; - - export = o; -} diff --git a/definitions/rest-definitions.d.ts b/definitions/rest-definitions.d.ts deleted file mode 100644 index 0ac0fca4..00000000 --- a/definitions/rest-definitions.d.ts +++ /dev/null @@ -1,155 +0,0 @@ -declare module "rest-definitions" { - /** - * Annotations for properties on 'inout' interfaces: - * - generated: This property cannot be specified on any input requests (PUT/PATCH/POST). - * As a result, generated properties are always marked as optional. - * - key: This property is the identifier for an object, with certain uniqueness constraints. - */ - - interface AccessKeyBase { - createdBy?: string; - /*legacy*/ description?: string; - /*key*/ friendlyName?: string; - /*generated key*/ name?: string; - } - - /*out*/ - export interface AccessKey extends AccessKeyBase { - /*generated*/ createdTime?: number; - expires: number; - /*generated*/ isSession?: boolean; - } - - /*in*/ - export interface AccessKeyRequest extends AccessKeyBase { - ttl?: number; - } - - /*out*/ - export interface DeploymentMetrics { - [packageLabelOrAppVersion: string]: UpdateMetrics - } - - /*in*/ - export interface DeploymentStatusReport { - appVersion: string; - clientUniqueId?: string; - deploymentKey: string; - previousDeploymentKey?: string; - previousLabelOrAppVersion?: string; - label?: string; - status?: string; - } - - /*in*/ - export interface DownloadReport { - clientUniqueId: string; - deploymentKey: string; - label: string; - } - - /*inout*/ - export interface PackageInfo { - appVersion?: string; - description?: string; - isDisabled?: boolean; - isMandatory?: boolean; - /*generated*/ label?: string; - /*generated*/ packageHash?: string; - rollout?: number; - } - - /*out*/ - export interface UpdateCheckResponse extends PackageInfo { - downloadURL?: string; - isAvailable: boolean; - packageSize?: number; - shouldRunBinaryVersion?: boolean; - updateAppVersion?: boolean; - } - - /*out*/ - export interface UpdateCheckCacheResponse { - originalPackage: UpdateCheckResponse; - rollout?: number; - rolloutPackage?: UpdateCheckResponse; - } - - /*in*/ - export interface UpdateCheckRequest { - appVersion: string; - clientUniqueId?: string; - deploymentKey: string; - isCompanion?: boolean; - label?: string; - packageHash?: string; - } - - /*out*/ - export interface UpdateMetrics { - active: number; - downloaded?: number; - failed?: number; - installed?: number; - } - - /*out*/ - export interface Account { - /*key*/ email: string; - name: string; - linkedProviders: string[]; - } - - /*out*/ - export interface CollaboratorProperties { - isCurrentAccount?: boolean; - permission: string; - } - - /*out*/ - export interface CollaboratorMap { - [email: string]: CollaboratorProperties; - } - - /*inout*/ - export interface App { - /*generated*/ collaborators?: CollaboratorMap; - /*key*/ name: string; - /* generated */ deployments?: string[]; - } - - /*in*/ - export interface AppCreationRequest extends App { - manuallyProvisionDeployments?: boolean; - } - - /*inout*/ - export interface Deployment { - /*generated key*/ key?: string; - /*key*/ name: string; - /*generated*/ package?: Package - } - - /*out*/ - export interface BlobInfo { - size: number; - url: string; - } - - /*out*/ - export interface PackageHashToBlobInfoMap { - [packageHash: string]: BlobInfo; - } - - /*inout*/ - export interface Package extends PackageInfo { - /*generated*/ blobUrl: string; - /*generated*/ diffPackageMap?: PackageHashToBlobInfoMap; - /*generated*/ originalLabel?: string; // Set on "Promote" and "Rollback" - /*generated*/ originalDeployment?: string; // Set on "Promote" - /*generated*/ releasedBy?: string; // Set by commitPackage - /*generated*/ releaseMethod?: string; // "Upload", "Promote" or "Rollback". Unknown if unspecified - /*generated*/ size: number; - /*generated*/ uploadTime: number; - } -} \ No newline at end of file diff --git a/definitions/superagent.d.ts b/definitions/superagent.d.ts deleted file mode 100644 index 7e3a27a6..00000000 --- a/definitions/superagent.d.ts +++ /dev/null @@ -1,110 +0,0 @@ -// Type definitions for SuperAgent 0.15.4 -// Project: https://github.com/visionmedia/superagent -// Definitions by: Alex Varju -// Definitions: https://github.com/borisyankov/DefinitelyTyped - -/// - -declare module "superagent" { - import stream = require('stream'); - - type CallbackHandler = { (err: any, res: request.Response): void; }|{ (res: request.Response): void; }; - - var request: request.SuperAgentStatic; - - module request { - interface SuperAgentStatic extends SuperAgent { - (url: string): SuperAgentRequest; - (method: string, url: string): SuperAgentRequest; - - agent(): SuperAgent; - } - - interface SuperAgent> extends stream.Stream { - get(url: string, callback?: CallbackHandler): Req; - post(url: string, callback?: CallbackHandler): Req; - put(url: string, callback?: CallbackHandler): Req; - head(url: string, callback?: CallbackHandler): Req; - del(url: string, callback?: CallbackHandler): Req; - delete(url: string, callback?: CallbackHandler): Req; - options(url: string, callback?: CallbackHandler): Req; - trace(url: string, callback?: CallbackHandler): Req; - copy(url: string, callback?: CallbackHandler): Req; - lock(url: string, callback?: CallbackHandler): Req; - mkcol(url: string, callback?: CallbackHandler): Req; - move(url: string, callback?: CallbackHandler): Req; - purge(url: string, callback?: CallbackHandler): Req; - propfind(url: string, callback?: CallbackHandler): Req; - proppatch(url: string, callback?: CallbackHandler): Req; - unlock(url: string, callback?: CallbackHandler): Req; - report(url: string, callback?: CallbackHandler): Req; - mkactivity(url: string, callback?: CallbackHandler): Req; - checkout(url: string, callback?: CallbackHandler): Req; - merge(url: string, callback?: CallbackHandler): Req; - // m-search(url: string, callback?: CallbackHandler): Req; - notify(url: string, callback?: CallbackHandler): Req; - subscribe(url: string, callback?: CallbackHandler): Req; - unsubscribe(url: string, callback?: CallbackHandler): Req; - patch(url: string, callback?: CallbackHandler): Req; - search(url: string, callback?: CallbackHandler): Req; - connect(url: string, callback?: CallbackHandler): Req; - - parse(fn: Function): Req; - saveCookies(res: Response): void; - attachCookies(req: Req): void; - } - - interface Response extends NodeJS.ReadableStream { - text: string; - body: any; - files: any; - header: any; - type: string; - charset: string; - status: number; - statusType: number; - info: boolean; - ok: boolean; - redirect: boolean; - clientError: boolean; - serverError: boolean; - error: Error; - accepted: boolean; - noContent: boolean; - badRequest: boolean; - unauthorized: boolean; - notAcceptable: boolean; - notFound: boolean; - forbidden: boolean; - get(header: string): string; - toError(): Error; - } - - interface Request> /* extends NodeJS.WritableStream */ { - abort(): Req; - accept(type: string): Req; - attach(field: string, file: Blob|File, filename?: string): Req; - auth(user: string, pass: string): Req; - buffer(val: boolean): Req; - clearTimeout(): Req; - end(callback?: CallbackHandler): Req; - field(name: string, val: string|Blob|File): Req; - get(field: string): string; - on(name: string, handler: Function): Req; - pipe(stream: NodeJS.WritableStream, options?: Object): stream.Writable; - query(val: Object|string): Req; - send(data: Object|string): Req; - set(field: string, val: string): Req; - set(field: Object): Req; - timeout(ms: number): Req; - type(val: string): Req; - unset(field: string): Req; - withCredentials(): Req; - } - interface SuperAgentRequest extends Request>>> {} - - } - - export = request; -} - diff --git a/definitions/yargs.d.ts b/definitions/yargs.d.ts deleted file mode 100644 index 3251e21b..00000000 --- a/definitions/yargs.d.ts +++ /dev/null @@ -1,133 +0,0 @@ -/* -This project is licensed under the MIT license. -Copyrights are respective of each contributor listed at the beginning of each definition file. - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE -*/ - -// Type definitions for yargs -// Project: https://github.com/chevex/yargs -// Definitions by: Martin Poelstra -// Definitions: https://github.com/borisyankov/DefinitelyTyped - -// This file has been changed from the original. - -declare module "yargs" { - - module yargs { - interface Argv { - argv: any; - (...args: any[]): any; - parse(...args: any[]): any; - - alias(shortName: string, longName: string): Argv; - alias(aliases: { [shortName: string]: string }): Argv; - alias(aliases: { [shortName: string]: string[] }): Argv; - - default(key: string, value: any): Argv; - default(defaults: { [key: string]: any }): Argv; - - demand(key: string, msg: string): Argv; - demand(key: string, required?: boolean): Argv; - demand(keys: string[], msg: string): Argv; - demand(keys: string[], required?: boolean): Argv; - demand(positionals: number, required?: boolean): Argv; - demand(positionals: number, msg: string): Argv; - demand(count: number, max?: number, msg?: string): Argv; // new from original - - require(key: string, msg: string): Argv; - require(key: string, required: boolean): Argv; - require(keys: number[], msg: string): Argv; - require(keys: number[], required: boolean): Argv; - require(positionals: number, required: boolean): Argv; - require(positionals: number, msg: string): Argv; - - required(key: string, msg: string): Argv; - required(key: string, required: boolean): Argv; - required(keys: number[], msg: string): Argv; - required(keys: number[], required: boolean): Argv; - required(positionals: number, required: boolean): Argv; - required(positionals: number, msg: string): Argv; - - requiresArg(key: string): Argv; - requiresArg(keys: string[]): Argv; - - describe(key: string, description: string): Argv; - describe(descriptions: { [key: string]: string }): Argv; - - option(key: string, options: Options): Argv; - option(options: { [key: string]: Options }): Argv; - options(key: string, options: Options): Argv; - options(options: { [key: string]: Options }): Argv; - - usage(message: string, options?: { [key: string]: Options }): Argv; - usage(options?: { [key: string]: Options }): Argv; - - command(command: string, description: string, func?: (yargs: Argv) => any): Argv; // changed from original - - example(command: string, description: string): Argv; - - check(func: (argv: any, aliases: { [alias: string]: string }) => any): Argv; - - boolean(key: string): Argv; - boolean(keys: string[]): Argv; - - string(key: string): Argv; - string(keys: string[]): Argv; - - config(key: string): Argv; - config(keys: string[]): Argv; - - wrap(columns: number): Argv; - - strict(): Argv; - - help(): string; - help(option: string, description?: string): Argv; - - version(version: string, option?: string, description?: string): Argv; - - showHelpOnFail(enable: boolean, message?: string): Argv; - - showHelp(func?: (message: string) => any): Argv; - - terminalWidth(): number; // new from original - - /* Undocumented */ - - normalize(key: string): Argv; - normalize(keys: string[]): Argv; - - implies(key: string, value: string): Argv; - implies(implies: { [key: string]: string }): Argv; - - count(key: string): Argv; - count(keys: string[]): Argv; - - fail(func: (msg: string) => any): Argv; // changed from original - } - - interface Options { - type?: string; - alias?: any; - demand?: any; - required?: any; - require?: any; - default?: any; - boolean?: any; - string?: any; - count?: any; - describe?: any; - description?: any; - desc?: any; - requiresArg?: any; - } - } - - var yargs: yargs.Argv; - export = yargs; -} diff --git a/definitions/yazl.d.ts b/definitions/yazl.d.ts deleted file mode 100644 index d3ac8f1b..00000000 --- a/definitions/yazl.d.ts +++ /dev/null @@ -1,32 +0,0 @@ -declare module "yazl" { - import * as events from "events"; - import * as stream from "stream"; - - export interface IDosDateTime { - date: number; - time: number; - } - - export interface IFinalSizeCallback { - (finalSize: number): void; - } - - export interface IOptions { - compress?: boolean; - mode?: number; - mtime?: Date; - size?: number; - } - - export function dateToDosDateTime(date: Date): IDosDateTime; - - export class ZipFile extends events.EventEmitter { - outputStream: stream.Readable; - - public addBuffer(buffer: Buffer, metadataPath: string, options?: IOptions): void; - public addEmptyDirectory(metadataPath: string, options?: IOptions): void; - public addFile(realPath: string, metadataPath: string, options?: IOptions): void; - public addReadStream(readStream: stream.Readable, metadataPath: string, options?: IOptions): void; - public end(finalSizeCallback?: IFinalSizeCallback): void; - } -} \ No newline at end of file diff --git a/gulp/build.js b/gulp/build.js deleted file mode 100644 index 5e3599cd..00000000 --- a/gulp/build.js +++ /dev/null @@ -1,9 +0,0 @@ -"use strict"; - -var gulp = require("gulp"); - -gulp.task("build-sdk", ["content-sdk", "scripts-sdk"]); - -gulp.task("build-cli", ["content-cli", "scripts-cli"]); - -gulp.task("build", ["build-sdk", "build-cli"]); diff --git a/gulp/clean.js b/gulp/clean.js deleted file mode 100644 index 58d3a393..00000000 --- a/gulp/clean.js +++ /dev/null @@ -1,16 +0,0 @@ -"use strict"; - -var gulp = require("gulp"); -var del = require("del"); - -function deleteTask(glob, next) { - del(glob, null, next); -} - -var cliCleanList = ["cli/bin/**/*", "!cli/bin/.*"]; -var sdkCleanList = ["sdk/bin/**/*", "!sdk/bin/.*"]; - -gulp.task("clean-cli", function(next) { deleteTask(cliCleanList, next); }); -gulp.task("clean-sdk", function(next) { deleteTask(sdkCleanList, next); }); - -gulp.task("clean", ["clean-cli", "clean-sdk"]); diff --git a/gulp/content.js b/gulp/content.js deleted file mode 100644 index 12975909..00000000 --- a/gulp/content.js +++ /dev/null @@ -1,28 +0,0 @@ -"use strict"; - -var gulp = require("gulp"); -var merge = require("merge2"); -var plugins = require("gulp-load-plugins")(); - -function contentTask(cwd) { - var options = { - cwd: cwd, - base: "./" + cwd - }; - - return gulp.src([ - "{script,test}/**/*.{css,ejs,html,js,json,png,xml}", - "test/resources/**/*", - "*.{public,private}", - "package.json", - "plugin.xml", - "server.js", - "web.config", - ".npmignore", - "README.md" - ], options) - .pipe(gulp.dest("bin", options)); -} - -gulp.task("content-sdk", function() { return contentTask("sdk"); }); -gulp.task("content-cli", function() { return contentTask("cli"); }); diff --git a/gulp/default.js b/gulp/default.js deleted file mode 100644 index 77f87f0e..00000000 --- a/gulp/default.js +++ /dev/null @@ -1,5 +0,0 @@ -"use strict"; - -var gulp = require("gulp"); - -gulp.task("default", ["build"]); \ No newline at end of file diff --git a/gulp/install.js b/gulp/install.js deleted file mode 100644 index 6cfdb295..00000000 --- a/gulp/install.js +++ /dev/null @@ -1,13 +0,0 @@ -var gulp = require("gulp"); -var install = require("gulp-install"); -var path = require("path"); -var runSequence = require("run-sequence"); - -gulp.task("install", function(done) { - var packages = [ - path.join(__dirname, "..", "sdk", "package.json"), - path.join(__dirname, "..", "cli", "package.json") - ]; - return gulp.src(packages) - .pipe(install()); -}); diff --git a/gulp/link.js b/gulp/link.js deleted file mode 100644 index 0feec611..00000000 --- a/gulp/link.js +++ /dev/null @@ -1,61 +0,0 @@ -var gulp = require("gulp"); -var which = require("which"); -var path = require("path"); -var spawn = require("child_process").spawn; -var runSequence = require("run-sequence"); - -function linkPackage(folder, createBinLinks, callback) { - if (typeof createBinLinks === "function") { - callback = createBinLinks; - createBinLinks = false; - } - - which("npm", function(err, resolvedPath) { - if (err) return callback(err); - - var args = ["link"]; - if (!createBinLinks) { - args.push("--no-bin-links"); - } - - var link = spawn(resolvedPath, args, {cwd: folder}); - link.stdout.pipe(process.stdout); - link.stderr.pipe(process.stderr); - link.on("close", callback); - }); -} - -function linkDependency(folder, sourcePackage, callback) { - which("npm", function(err, resolvedPath) { - if (err) return callback(err); - - var link = spawn(resolvedPath, ["link", sourcePackage], {cwd: folder}); - link.stdout.pipe(process.stdout); - link.stderr.pipe(process.stderr); - link.on("close", callback); - }); -} - -gulp.task("link-sdk", ["content-sdk"], function(done) { - linkPackage(path.join(__dirname, "..", "sdk", "bin"), done); -}); - -gulp.task("link-cli", function(done) { - linkDependency(path.join(__dirname, "..", "cli"), "code-push", done); -}); - -gulp.task("link-cli-bin", function(done) { - linkDependency(path.join(__dirname, "..", "cli"), "code-push", function() { - runSequence("build-cli", function() { - linkPackage(path.join(__dirname, "..", "cli", "bin"), /*createBinLinks=*/ true, done); - }); - }); -}); - -gulp.task("link", function(done) { - runSequence("link-sdk", "link-cli", done); -}); - -gulp.task("link-bin", function(done) { - runSequence("link-sdk", "link-cli-bin", done); -}); diff --git a/gulp/scripts.js b/gulp/scripts.js deleted file mode 100644 index 2904eb60..00000000 --- a/gulp/scripts.js +++ /dev/null @@ -1,91 +0,0 @@ -"use strict"; - -var chmod = require("chmod"); -var fs = require("fs"); -var gulp = require("gulp"); -var plugins = require("gulp-load-plugins")(); -var through = require("through2"); -var tsc = require("typescript"); -var tsJsxLoader = require("ts-jsx-loader"); -var merge = require("merge2"); -var dtsGenerator = require("dts-generator"); - -var generatedDefinitionDependencies = { - sdk: [], - cli: ["code-push"] -}; - -function tsJsxPipe(file, enc, cb) { - var fileContent = file.contents.toString(); - file.contents = new Buffer(tsJsxLoader.call({cacheable: function() {} }, fileContent), enc); - cb(null, file); -} - -function scriptTask(cwd, jsx) { - var options = { - cwd: __dirname + "/../" + cwd, - base: __dirname + "/../" + cwd - }; - - var generatedDefinitions = [ - "definitions/external/**/*.d.ts", - "definitions/*.d.ts" - ].concat(generatedDefinitionDependencies[cwd].map(function(dep) { - return "definitions/generated/" + dep + ".d.ts"; - })); - - var tsProj = plugins.typescript.createProject("tsconfig.json", { typescript: tsc, declarationFiles: true }); - - var fullReporter = plugins.typescript.reporter.fullReporter(/*fullFileName=*/ true); - var errorCatch = through.obj(); - - var tsResult = merge([ - gulp.src("{script,test,definitions}/**/*.ts", options), - gulp.src(generatedDefinitions)]) - .pipe(plugins.if(jsx, through.obj(tsJsxPipe))) - .pipe(plugins.typescript(tsProj, /* filterSettings=*/ undefined, { - error: fullReporter.error, - finish: function(results) { - fullReporter.finish(results); - if (results.syntaxErrors || results.globalErrors || results.semanticErrors || results.emitErrors) { - if (!process.env.WATCHING) { - errorCatch.emit("error", new plugins.util.PluginError("gulp-typescript", "TypeScript compilation failed")); - } - } - } - })); - - return merge([ - tsResult.js.pipe(gulp.dest("bin", options)), - tsResult.dts.pipe(gulp.dest("bin/definitions", options)) - ]) - .pipe(errorCatch); -} - -function makeExecutable(path) { - var contents = fs.readFileSync(path); - fs.writeFileSync(path, "#!/usr/bin/env node\n" + contents); - chmod(path, {execute: true}); -} - -gulp.task("scripts-external", ["tsd"]); - -gulp.task("scripts-compile-sdk", ["scripts-external"], function() { return scriptTask("sdk"); }); -gulp.task("scripts-compile-cli", ["scripts-sdk", "scripts-external"], function() { return scriptTask("cli"); }); - -gulp.task("scripts-chmod-cli", ["scripts-compile-cli"], function() { - makeExecutable(__dirname + "/../cli/bin/script/cli.js"); -}); - -gulp.task("scripts-dtsbundle-sdk", ["scripts-compile-sdk"], function () { - dtsGenerator.generate({ - name: "code-push", - main: "code-push/script/index", - baseDir: "sdk/bin/definitions", - files: ["script/acquisition-sdk.d.ts", "script/index.d.ts"], - out: "definitions/generated/code-push.d.ts" - }); -}); - -gulp.task("scripts-sdk", ["scripts-dtsbundle-sdk"]); -gulp.task("scripts-cli", ["scripts-chmod-cli"]); diff --git a/gulp/test.js b/gulp/test.js deleted file mode 100644 index 79f05562..00000000 --- a/gulp/test.js +++ /dev/null @@ -1,55 +0,0 @@ -"use strict"; - -var gulp = require("gulp"); -var plugins = require("gulp-load-plugins")(); - -var mochaConfig = { - reporter: process.env.REPORTER || "spec", - timeout: parseInt(process.env.TIMEOUT) || 5000 -}; - -var projects = { - "sdk": ["build-sdk"], - "cli": ["build-cli"] -}; - -var projectNames = Object.keys(projects); - -var testPaths = projectNames.map(testPathFromName); -var sourcesPaths = projectNames.map(sourcePathFromName); - -function testPathFromName(name) { - return name + "/bin/test/**/*.js" -} - -function sourcePathFromName(name) { - return name + "/bin/script/**/*.js" -} - -function runTests(sources, tests, done) { - require("dotenv").config({ path: ".test.env", silent: true }); - gulp.src(sources) - .pipe(plugins.istanbul({includeUntested: true})) - .pipe(plugins.istanbul.hookRequire()) - .on("finish", function() { - gulp.src(tests, { read: false }) - .pipe(plugins.if(process.env.WATCHING, plugins.plumber())) - .pipe(plugins.mocha(mochaConfig)) - .pipe(plugins.istanbul.writeReports()) - .on("end", done); - }); -} - -function testTask(name, done) { - runTests([sourcePathFromName(name)], [testPathFromName(name)], done); -} - -projectNames.forEach(function(projectName) { - var projectDeps = projects[projectName]; - - gulp.task("test-" + projectName, projectDeps, function (done) { testTask(projectName, done); }); -}); - -gulp.task("test", ["build"], function(done) { - runTests(sourcesPaths, testPaths, done); -}); diff --git a/gulp/tsd.js b/gulp/tsd.js deleted file mode 100644 index 2911cf44..00000000 --- a/gulp/tsd.js +++ /dev/null @@ -1,11 +0,0 @@ -"use strict"; - -var gulp = require("gulp"); -var plugins = require("gulp-load-plugins")(); - -gulp.task("tsd", function(done) { - plugins.tsd({ - command: "reinstall", - config: "tsd.json" - }, done); -}); diff --git a/migration-notice.md b/migration-notice.md new file mode 100644 index 00000000..e5d399d4 --- /dev/null +++ b/migration-notice.md @@ -0,0 +1,74 @@ +# Migration notice + +## CodePush SDK + +CodePush SDK migrated to a new service. We recommend updating CodePush SDK to the latest version. Versions lower than **[4.0.0](https://github.com/microsoft/code-push/releases/tag/v4.0.0)** will not work in the near future. + +### Deprecated methods + +* getSessions +* patchAccessKey +* removeSession +* getAccessKey + +These methods are not supported in versions **[4.0.0](https://github.com/microsoft/code-push/releases/tag/v4.0.0)** and above. + +### Comparison + +* Server URL + + * versions **[4.0.0](https://github.com/microsoft/code-push/releases/tag/v4.0.0)** and above: + + `https://api.appcenter.ms/v0.1` + + * versions lower than **[4.0.0](https://github.com/microsoft/code-push/releases/tag/v4.0.0)**: + + `https://codepush-management.azurewebsites.net` + + `https://codepush.appcenter.ms/v0.1/legacy` + +* Request's path + + For example the path of `getDeployments` method: + + * versions **[4.0.0](https://github.com/microsoft/code-push/releases/tag/v4.0.0)** and above: + + `https://api.appcenter.ms/v0.1/apps/{owner_name}/{app_name}/deployments/` + + * versions lower than **[4.0.0](https://github.com/microsoft/code-push/releases/tag/v4.0.0)**: + + `https://codepush-management.azurewebsites.net/apps/{app_name}/deployments/` + +* Error handling + + Error messages differ. Status codes only differ when the application is not found for the owner: + + * versions **[4.0.0](https://github.com/microsoft/code-push/releases/tag/v4.0.0)** and above: + + ```javascript + { + message: 'Not found. Correlation ID: *****', + statusCode: 404 + } + ``` + + * versions lower than **[4.0.0](https://github.com/microsoft/code-push/releases/tag/v4.0.0)**: + + ```javascript + { + message: 'Internal Server Error', + statusCode: 500 + } + ``` + +* Method signature + + Methods signature was not changed. All methods work the same as in previous versions. + +## CodePush CLI + +The CodePush CLI **[3.0.0](https://www.npmjs.com/package/code-push-cli/v/3.0.0)** is the latest and last version for this CLI. We no longer update the CodePush CLI and recommend migrating to the App Center CLI (). + +## React-native-code-push + +React-native-code-push versions lower than **[5.7.0](https://github.com/microsoft/react-native-code-push/releases/tag/v5.7.0)** will stop working in the near future. diff --git a/mocha.opts b/mocha.opts new file mode 100644 index 00000000..9fbbfec4 --- /dev/null +++ b/mocha.opts @@ -0,0 +1,3 @@ +--exit +--reporter spec +--timeout 5000 diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..6f6bd5c2 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,3519 @@ +{ + "name": "code-push", + "version": "4.2.3", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "code-push", + "version": "4.2.3", + "license": "MIT", + "dependencies": { + "appcenter-file-upload-client": "0.1.0", + "proxy-agent": "^6.3.0", + "recursive-fs": "^2.1.0", + "slash": "^3.0.0", + "superagent": "^8.0.0", + "yazl": "^2.5.1" + }, + "devDependencies": { + "@types/mocha": "^9.0.0", + "@types/node": "^14.0.27", + "@types/slash": "^3.0.0", + "@types/superagent": "^4.1.13", + "@types/yazl": "^2.4.2", + "mocha": "^9.2.0", + "shx": "^0.3.4", + "superagent-mock": "^4.0.0", + "typescript": "^5.1.6" + } + }, + "node_modules/@tootallnate/quickjs-emscripten": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", + "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==" + }, + "node_modules/@types/cookiejar": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.1.tgz", + "integrity": "sha512-aRnpPa7ysx3aNW60hTiCtLHlQaIFsXFCgQlpakNgDNVFzbtusSY8PwjAQgRWfSk0ekNoBjO51eQRB6upA9uuyw==", + "dev": true + }, + "node_modules/@types/mocha": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-9.0.0.tgz", + "integrity": "sha512-scN0hAWyLVAvLR9AyW7HoFF5sJZglyBsbPuHO4fv7JRvfmPBMfp1ozWqOf/e4wwPNxezBZXRfWzMb6iFLgEVRA==", + "dev": true + }, + "node_modules/@types/node": { + "version": "14.0.27", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.0.27.tgz", + "integrity": "sha512-kVrqXhbclHNHGu9ztnAwSncIgJv/FaxmzXJvGXNdcCpV1b8u1/Mi6z6m0vwy0LzKeXFTPLH0NzwmoJ3fNCIq0g==", + "dev": true + }, + "node_modules/@types/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-bmUaw0IUPUVldtj4YwU7tbzxllQQgsWdnB45bwTI0f1Lq2Yg8lT2yxV4OGZrMTrP/G9v8eVIhX130xPe/RfPfw==", + "deprecated": "This is a stub types definition. slash provides its own type definitions, so you do not need this installed.", + "dev": true, + "dependencies": { + "slash": "*" + } + }, + "node_modules/@types/superagent": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-4.1.13.tgz", + "integrity": "sha512-YIGelp3ZyMiH0/A09PMAORO0EBGlF5xIKfDpK74wdYvWUs2o96b5CItJcWPdH409b7SAXIIG6p8NdU/4U2Maww==", + "dev": true, + "dependencies": { + "@types/cookiejar": "*", + "@types/node": "*" + } + }, + "node_modules/@types/yazl": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@types/yazl/-/yazl-2.4.2.tgz", + "integrity": "sha512-T+9JH8O2guEjXNxqmybzQ92mJUh2oCwDDMSSimZSe1P+pceZiFROZLYmcbqkzV5EUwz6VwcKXCO2S2yUpra6XQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@ungap/promise-all-settled": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz", + "integrity": "sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==", + "dev": true + }, + "node_modules/agent-base": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.0.tgz", + "integrity": "sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==", + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/agent-base/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/ansi-colors": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", + "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/appcenter-file-upload-client": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/appcenter-file-upload-client/-/appcenter-file-upload-client-0.1.0.tgz", + "integrity": "sha512-W8lueBBvLuItND2vmvfdIDTbIYHOHXr5ohObhqvBNL3XCOGTqQq1rhWUxBX5Mb5geLBuLDC0HQOtq9pcBgi71w==", + "dependencies": { + "detect-node": "^2.0.4", + "superagent": "5.1.0", + "url-parse": "^1.4.7" + } + }, + "node_modules/appcenter-file-upload-client/node_modules/form-data": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz", + "integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/appcenter-file-upload-client/node_modules/superagent": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-5.1.0.tgz", + "integrity": "sha512-7V6JVx5N+eTL1MMqRBX0v0bG04UjrjAvvZJTF/VDH/SH2GjSLqlrcYepFlpTrXpm37aSY6h3GGVWGxXl/98TKA==", + "dependencies": { + "component-emitter": "^1.3.0", + "cookiejar": "^2.1.2", + "debug": "^4.1.1", + "fast-safe-stringify": "^2.0.6", + "form-data": "^2.3.3", + "formidable": "^1.2.1", + "methods": "^1.1.2", + "mime": "^2.4.4", + "qs": "^6.7.0", + "readable-stream": "^3.4.0", + "semver": "^6.1.1" + }, + "engines": { + "node": ">= 6.4.0" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==" + }, + "node_modules/ast-types": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", + "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + }, + "node_modules/balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "node_modules/basic-ftp": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.3.tgz", + "integrity": "sha512-QHX8HLlncOLpy54mh+k/sWIFd0ThmRqwe9ZjELybGZK+tZ8rUb9VO0saKJUROTbE+KhzDUT7xziGpGrW8Kmd+g==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=", + "engines": { + "node": "*" + } + }, + "node_modules/call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dependencies": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/camelcase": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.2.0.tgz", + "integrity": "sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", + "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/component-emitter": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "node_modules/cookiejar": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", + "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==" + }, + "node_modules/data-uri-to-buffer": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-5.0.1.tgz", + "integrity": "sha512-a9l6T1qqDogvvnw0nKlfZzqsyikEBZBClF39V3TFoKhDtGBqHu2HkuomJc02j5zft8zrUaXEuoicLeW54RkzPg==", + "engines": { + "node": ">= 14" + } + }, + "node_modules/debug": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", + "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/degenerator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", + "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", + "dependencies": { + "ast-types": "^0.13.4", + "escodegen": "^2.1.0", + "esprima": "^4.0.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-node": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.0.4.tgz", + "integrity": "sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw==" + }, + "node_modules/dezalgo": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", + "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", + "dependencies": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, + "node_modules/diff": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", + "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==" + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "bin": { + "flat": "cli.js" + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/formidable": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.2.tgz", + "integrity": "sha512-V8gLm+41I/8kguQ4/o1D3RIHRmhYFG4pnNyonvua+40rqcEmT4+V71yaZ3B457xbbgCsCfjSPi65u/W6vK1U5Q==", + "funding": { + "url": "https://ko-fi.com/tunnckoCore/commissions" + } + }, + "node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz", + "integrity": "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==", + "dependencies": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-uri": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.1.tgz", + "integrity": "sha512-7ZqONUVqaabogsYNWlYj0t3YZaL6dhuEueZXGF+/YVmf6dHmaFg8/6psJKqhx9QykIDKzpGcy2cn4oV4YC7V/Q==", + "dependencies": { + "basic-ftp": "^5.0.2", + "data-uri-to-buffer": "^5.0.1", + "debug": "^4.3.4", + "fs-extra": "^8.1.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/get-uri/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + }, + "node_modules/growl": { + "version": "1.10.5", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", + "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", + "dev": true, + "engines": { + "node": ">=4.x" + } + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "bin": { + "he": "bin/he" + } + }, + "node_modules/hexoid": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/hexoid/-/hexoid-1.0.0.tgz", + "integrity": "sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g==", + "engines": { + "node": ">=8" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.0.tgz", + "integrity": "sha512-+ZT+iBxVUQ1asugqnD6oWoRiS25AkjNfG085dKJGtGxkdwLQrMKU5wJr2bOOFAXzKcTuqq+7fZlTMgG3SRfIYQ==", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/http-proxy-agent/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.1.tgz", + "integrity": "sha512-Eun8zV0kcYS1g19r78osiQLEFIRspRUDd9tIfBCTBPBeMieF/EsJNL8VI3xOIdYRDEkjQnqOYPsZ2DsWsVsFwQ==", + "dependencies": { + "agent-base": "^7.0.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/interpret": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz", + "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/ip-address": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", + "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", + "dependencies": { + "jsbn": "1.1.0", + "sprintf-js": "^1.1.3" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.1.tgz", + "integrity": "sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA==", + "dev": true, + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsbn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==" + }, + "node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mime-db": { + "version": "1.43.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.43.0.tgz", + "integrity": "sha512-+5dsGEEovYbT8UY9yD7eE4XTc4UwJ1jBYlgaQQF38ENsKR3wj/8q8RFZrF9WIZpB2V1ArTVFUva8sAul1NzRzQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.26", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.26.tgz", + "integrity": "sha512-01paPWYgLrkqAyrlDorC1uDwl2p3qZT7yl806vW7DvDoxwXi46jsjFbg+WdwotBIk6/MbEhO/dh5aZ5sNj/dWQ==", + "dependencies": { + "mime-db": "1.43.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", + "dev": true + }, + "node_modules/mocha": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-9.2.2.tgz", + "integrity": "sha512-L6XC3EdwT6YrIk0yXpavvLkn8h+EU+Y5UcCHKECyMbdUIxyMuZj4bX4U9e1nvnvUUvQVsV2VHQr5zLdcUkhW/g==", + "dev": true, + "dependencies": { + "@ungap/promise-all-settled": "1.1.2", + "ansi-colors": "4.1.1", + "browser-stdout": "1.3.1", + "chokidar": "3.5.3", + "debug": "4.3.3", + "diff": "5.0.0", + "escape-string-regexp": "4.0.0", + "find-up": "5.0.0", + "glob": "7.2.0", + "growl": "1.10.5", + "he": "1.2.0", + "js-yaml": "4.1.0", + "log-symbols": "4.1.0", + "minimatch": "4.2.1", + "ms": "2.1.3", + "nanoid": "3.3.1", + "serialize-javascript": "6.0.0", + "strip-json-comments": "3.1.1", + "supports-color": "8.1.1", + "which": "2.0.2", + "workerpool": "6.2.0", + "yargs": "16.2.0", + "yargs-parser": "20.2.4", + "yargs-unparser": "2.0.0" + }, + "bin": { + "_mocha": "bin/_mocha", + "mocha": "bin/mocha" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mochajs" + } + }, + "node_modules/mocha/node_modules/minimatch": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-4.2.1.tgz", + "integrity": "sha512-9Uq1ChtSZO+Mxa/CL1eGizn2vRn3MlLgzhT0Iz8zaY8NdvxvB0d5QdPFmCKf7JKA9Lerx5vRrnwO03jsSfGG9g==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mocha/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/nanoid": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.1.tgz", + "integrity": "sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw==", + "dev": true, + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/netmask": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", + "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", + "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pac-proxy-agent": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.0.0.tgz", + "integrity": "sha512-t4tRAMx0uphnZrio0S0Jw9zg3oDbz1zVhQ/Vy18FjLfP1XOLNUEjaVxYCYRI6NS+BsMBXKIzV6cTLOkO9AtywA==", + "dependencies": { + "@tootallnate/quickjs-emscripten": "^0.23.0", + "agent-base": "^7.0.2", + "debug": "^4.3.4", + "get-uri": "^6.0.1", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.0", + "pac-resolver": "^7.0.0", + "socks-proxy-agent": "^8.0.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-proxy-agent/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/pac-resolver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", + "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", + "dependencies": { + "degenerator": "^5.0.0", + "netmask": "^2.0.2" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/proxy-agent": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.3.0.tgz", + "integrity": "sha512-0LdR757eTj/JfuU7TL2YCuAZnxWXu3tkJbg4Oq3geW/qFNT/32T0sp2HnZ9O0lMR4q3vwAt0+xCA8SR0WAD0og==", + "dependencies": { + "agent-base": "^7.0.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.0", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.0.0", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/proxy-agent/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, + "node_modules/qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/querystringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.1.1.tgz", + "integrity": "sha512-w7fLxIRCRT7U8Qu53jQnJyPkYZIaR4n5151KMfcJlO/A9397Wxb1amJvROTK6TOnp7PfoAmg/qXiNHI+08jRfA==" + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/readable-stream": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.4.0.tgz", + "integrity": "sha512-jItXPLmrSR8jmTRmRWJXCnGJsfy85mB3Wd/uINMXA65yrnFo0cPClFIUWzo2najVNSl+mx7/4W8ttlLWJe99pQ==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/rechoir": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", + "integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=", + "dev": true, + "dependencies": { + "resolve": "^1.1.6" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/recursive-fs": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/recursive-fs/-/recursive-fs-2.1.0.tgz", + "integrity": "sha512-oed3YruYsD52Mi16s/07eYblQOLi5dTtxpIJNdfCEJ7S5v8dDgVcycar0pRWf4IBuPMIkoctC8RTqGJzIKMNAQ==", + "bin": { + "recursive-copy": "bin/recursive-copy", + "recursive-delete": "bin/recursive-delete" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=" + }, + "node_modules/resolve": { + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz", + "integrity": "sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==", + "dev": true, + "dependencies": { + "is-core-module": "^2.8.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", + "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", + "dev": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/shelljs": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.5.tgz", + "integrity": "sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==", + "dev": true, + "dependencies": { + "glob": "^7.0.0", + "interpret": "^1.0.0", + "rechoir": "^0.6.2" + }, + "bin": { + "shjs": "bin/shjs" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/shx": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/shx/-/shx-0.3.4.tgz", + "integrity": "sha512-N6A9MLVqjxZYcVn8hLmtneQWIJtp8IKzMP4eMnx+nqkvXoqinUPCbUFLp2UcWTEIUONhlk0ewxr/jaVGlc+J+g==", + "dev": true, + "dependencies": { + "minimist": "^1.2.3", + "shelljs": "^0.8.5" + }, + "bin": { + "shx": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dependencies": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "engines": { + "node": ">=8" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz", + "integrity": "sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==", + "dependencies": { + "ip-address": "^9.0.5", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.1.tgz", + "integrity": "sha512-59EjPbbgg8U3x62hhKOFVAmySQUcfRQ4C7Q/D5sEHnZTQRrQlNKINks44DMR1gwXp0p4LaVIeccX2KHTTcHVqQ==", + "dependencies": { + "agent-base": "^7.0.1", + "debug": "^4.3.4", + "socks": "^2.7.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/socks-proxy-agent/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==" + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/superagent": { + "version": "8.0.6", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-8.0.6.tgz", + "integrity": "sha512-HqSe6DSIh3hEn6cJvCkaM1BLi466f1LHi4yubR0tpewlMpk4RUFFy35bKz8SsPBwYfIIJy5eclp+3tCYAuX0bw==", + "dependencies": { + "component-emitter": "^1.3.0", + "cookiejar": "^2.1.3", + "debug": "^4.3.4", + "fast-safe-stringify": "^2.1.1", + "form-data": "^4.0.0", + "formidable": "^2.1.1", + "methods": "^1.1.2", + "mime": "2.6.0", + "qs": "^6.11.0", + "semver": "^7.3.8" + }, + "engines": { + "node": ">=6.4.0 <13 || >=14" + } + }, + "node_modules/superagent-mock": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/superagent-mock/-/superagent-mock-4.0.0.tgz", + "integrity": "sha512-+xj+q+sL5sJIcFxwmj5Wuq59Kns0ocOd8OrMkbEXEwseWImAVvM7cP8G7raQRZ+vloUN32t/yKosD+YAXa9rcg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "superagent": ">=3.6.0" + } + }, + "node_modules/superagent/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/superagent/node_modules/formidable": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-2.1.1.tgz", + "integrity": "sha512-0EcS9wCFEzLvfiks7omJ+SiYJAiD+TzK4Pcw1UlUoGnhUxDcMKjt0P7x8wEb0u6OHu8Nb98WG3nxtlF5C7bvUQ==", + "dependencies": { + "dezalgo": "^1.0.4", + "hexoid": "^1.0.0", + "once": "^1.4.0", + "qs": "^6.11.0" + }, + "funding": { + "url": "https://ko-fi.com/tunnckoCore/commissions" + } + }, + "node_modules/superagent/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/superagent/node_modules/semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/superagent/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tslib": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.1.tgz", + "integrity": "sha512-t0hLfiEKfMUoqhG+U1oid7Pva4bbDPHYfJNiB7BiIjRkj1pyC++4N3huJfqY6aRH6VTB0rvtzQwjM4K6qpfOig==" + }, + "node_modules/typescript": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz", + "integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/workerpool": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.0.tgz", + "integrity": "sha512-Rsk5qQHJ9eowMH28Jwhe8HEbmdYDX4lwoMWshiCXugjtHqMD9ZbiqSDLxcsfdqsETPzVUtX5s1Z5kStiIM6l4A==", + "dev": true + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", + "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-parser": { + "version": "20.2.4", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", + "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-unparser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", + "dev": true, + "dependencies": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", + "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yazl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/yazl/-/yazl-2.5.1.tgz", + "integrity": "sha512-phENi2PLiHnHb6QBVot+dJnaAZ0xosj7p3fWl+znIjBDlnMI2PsZCJZ306BPTFOaHf5qdDEI8x5qFrSOBN5vrw==", + "dependencies": { + "buffer-crc32": "~0.2.3" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + }, + "dependencies": { + "@tootallnate/quickjs-emscripten": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", + "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==" + }, + "@types/cookiejar": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.1.tgz", + "integrity": "sha512-aRnpPa7ysx3aNW60hTiCtLHlQaIFsXFCgQlpakNgDNVFzbtusSY8PwjAQgRWfSk0ekNoBjO51eQRB6upA9uuyw==", + "dev": true + }, + "@types/mocha": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-9.0.0.tgz", + "integrity": "sha512-scN0hAWyLVAvLR9AyW7HoFF5sJZglyBsbPuHO4fv7JRvfmPBMfp1ozWqOf/e4wwPNxezBZXRfWzMb6iFLgEVRA==", + "dev": true + }, + "@types/node": { + "version": "14.0.27", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.0.27.tgz", + "integrity": "sha512-kVrqXhbclHNHGu9ztnAwSncIgJv/FaxmzXJvGXNdcCpV1b8u1/Mi6z6m0vwy0LzKeXFTPLH0NzwmoJ3fNCIq0g==", + "dev": true + }, + "@types/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-bmUaw0IUPUVldtj4YwU7tbzxllQQgsWdnB45bwTI0f1Lq2Yg8lT2yxV4OGZrMTrP/G9v8eVIhX130xPe/RfPfw==", + "dev": true, + "requires": { + "slash": "*" + } + }, + "@types/superagent": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-4.1.13.tgz", + "integrity": "sha512-YIGelp3ZyMiH0/A09PMAORO0EBGlF5xIKfDpK74wdYvWUs2o96b5CItJcWPdH409b7SAXIIG6p8NdU/4U2Maww==", + "dev": true, + "requires": { + "@types/cookiejar": "*", + "@types/node": "*" + } + }, + "@types/yazl": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@types/yazl/-/yazl-2.4.2.tgz", + "integrity": "sha512-T+9JH8O2guEjXNxqmybzQ92mJUh2oCwDDMSSimZSe1P+pceZiFROZLYmcbqkzV5EUwz6VwcKXCO2S2yUpra6XQ==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@ungap/promise-all-settled": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz", + "integrity": "sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==", + "dev": true + }, + "agent-base": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.0.tgz", + "integrity": "sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==", + "requires": { + "debug": "^4.3.4" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + } + } + }, + "ansi-colors": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "dev": true + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "anymatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", + "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "appcenter-file-upload-client": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/appcenter-file-upload-client/-/appcenter-file-upload-client-0.1.0.tgz", + "integrity": "sha512-W8lueBBvLuItND2vmvfdIDTbIYHOHXr5ohObhqvBNL3XCOGTqQq1rhWUxBX5Mb5geLBuLDC0HQOtq9pcBgi71w==", + "requires": { + "detect-node": "^2.0.4", + "superagent": "5.1.0", + "url-parse": "^1.4.7" + }, + "dependencies": { + "form-data": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz", + "integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + }, + "superagent": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-5.1.0.tgz", + "integrity": "sha512-7V6JVx5N+eTL1MMqRBX0v0bG04UjrjAvvZJTF/VDH/SH2GjSLqlrcYepFlpTrXpm37aSY6h3GGVWGxXl/98TKA==", + "requires": { + "component-emitter": "^1.3.0", + "cookiejar": "^2.1.2", + "debug": "^4.1.1", + "fast-safe-stringify": "^2.0.6", + "form-data": "^2.3.3", + "formidable": "^1.2.1", + "methods": "^1.1.2", + "mime": "^2.4.4", + "qs": "^6.7.0", + "readable-stream": "^3.4.0", + "semver": "^6.1.1" + } + } + } + }, + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==" + }, + "ast-types": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", + "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", + "requires": { + "tslib": "^2.0.1" + } + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "basic-ftp": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.3.tgz", + "integrity": "sha512-QHX8HLlncOLpy54mh+k/sWIFd0ThmRqwe9ZjELybGZK+tZ8rUb9VO0saKJUROTbE+KhzDUT7xziGpGrW8Kmd+g==" + }, + "binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "requires": { + "fill-range": "^7.1.1" + } + }, + "browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true + }, + "buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=" + }, + "call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "requires": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + } + }, + "camelcase": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.2.0.tgz", + "integrity": "sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg==", + "dev": true + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "dependencies": { + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "requires": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "fsevents": "~2.3.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + } + }, + "cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "string-width": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", + "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + } + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.0" + } + } + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "component-emitter": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==" + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "cookiejar": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", + "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==" + }, + "data-uri-to-buffer": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-5.0.1.tgz", + "integrity": "sha512-a9l6T1qqDogvvnw0nKlfZzqsyikEBZBClF39V3TFoKhDtGBqHu2HkuomJc02j5zft8zrUaXEuoicLeW54RkzPg==" + }, + "debug": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", + "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "requires": { + "ms": "2.1.2" + } + }, + "decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "dev": true + }, + "degenerator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", + "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", + "requires": { + "ast-types": "^0.13.4", + "escodegen": "^2.1.0", + "esprima": "^4.0.1" + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" + }, + "detect-node": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.0.4.tgz", + "integrity": "sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw==" + }, + "dezalgo": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", + "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", + "requires": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, + "diff": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", + "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", + "dev": true + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true + }, + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true + }, + "escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "requires": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2", + "source-map": "~0.6.1" + } + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" + }, + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==" + }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==" + }, + "fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==" + }, + "fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "requires": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + } + }, + "flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true + }, + "form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + }, + "formidable": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.2.tgz", + "integrity": "sha512-V8gLm+41I/8kguQ4/o1D3RIHRmhYFG4pnNyonvua+40rqcEmT4+V71yaZ3B457xbbgCsCfjSPi65u/W6vK1U5Q==" + }, + "fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "optional": true + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true + }, + "get-intrinsic": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz", + "integrity": "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==", + "requires": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.3" + } + }, + "get-uri": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.1.tgz", + "integrity": "sha512-7ZqONUVqaabogsYNWlYj0t3YZaL6dhuEueZXGF+/YVmf6dHmaFg8/6psJKqhx9QykIDKzpGcy2cn4oV4YC7V/Q==", + "requires": { + "basic-ftp": "^5.0.2", + "data-uri-to-buffer": "^5.0.1", + "debug": "^4.3.4", + "fs-extra": "^8.1.0" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + } + } + }, + "glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, + "graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + }, + "growl": { + "version": "1.10.5", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", + "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", + "dev": true + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" + }, + "he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true + }, + "hexoid": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/hexoid/-/hexoid-1.0.0.tgz", + "integrity": "sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g==" + }, + "http-proxy-agent": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.0.tgz", + "integrity": "sha512-+ZT+iBxVUQ1asugqnD6oWoRiS25AkjNfG085dKJGtGxkdwLQrMKU5wJr2bOOFAXzKcTuqq+7fZlTMgG3SRfIYQ==", + "requires": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + } + } + }, + "https-proxy-agent": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.1.tgz", + "integrity": "sha512-Eun8zV0kcYS1g19r78osiQLEFIRspRUDd9tIfBCTBPBeMieF/EsJNL8VI3xOIdYRDEkjQnqOYPsZ2DsWsVsFwQ==", + "requires": { + "agent-base": "^7.0.2", + "debug": "4" + } + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "interpret": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz", + "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==", + "dev": true + }, + "ip-address": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", + "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", + "requires": { + "jsbn": "1.1.0", + "sprintf-js": "^1.1.3" + } + }, + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "requires": { + "binary-extensions": "^2.0.0" + } + }, + "is-core-module": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.1.tgz", + "integrity": "sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA==", + "dev": true, + "requires": { + "has": "^1.0.3" + } + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true + }, + "is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "dev": true + }, + "is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "requires": { + "argparse": "^2.0.1" + } + }, + "jsbn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==" + }, + "jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "requires": { + "graceful-fs": "^4.1.6" + } + }, + "locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "requires": { + "p-locate": "^5.0.0" + } + }, + "log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "requires": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + } + }, + "lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==" + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" + }, + "mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==" + }, + "mime-db": { + "version": "1.43.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.43.0.tgz", + "integrity": "sha512-+5dsGEEovYbT8UY9yD7eE4XTc4UwJ1jBYlgaQQF38ENsKR3wj/8q8RFZrF9WIZpB2V1ArTVFUva8sAul1NzRzQ==" + }, + "mime-types": { + "version": "2.1.26", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.26.tgz", + "integrity": "sha512-01paPWYgLrkqAyrlDorC1uDwl2p3qZT7yl806vW7DvDoxwXi46jsjFbg+WdwotBIk6/MbEhO/dh5aZ5sNj/dWQ==", + "requires": { + "mime-db": "1.43.0" + } + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", + "dev": true + }, + "mocha": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-9.2.2.tgz", + "integrity": "sha512-L6XC3EdwT6YrIk0yXpavvLkn8h+EU+Y5UcCHKECyMbdUIxyMuZj4bX4U9e1nvnvUUvQVsV2VHQr5zLdcUkhW/g==", + "dev": true, + "requires": { + "@ungap/promise-all-settled": "1.1.2", + "ansi-colors": "4.1.1", + "browser-stdout": "1.3.1", + "chokidar": "3.5.3", + "debug": "4.3.3", + "diff": "5.0.0", + "escape-string-regexp": "4.0.0", + "find-up": "5.0.0", + "glob": "7.2.0", + "growl": "1.10.5", + "he": "1.2.0", + "js-yaml": "4.1.0", + "log-symbols": "4.1.0", + "minimatch": "4.2.1", + "ms": "2.1.3", + "nanoid": "3.3.1", + "serialize-javascript": "6.0.0", + "strip-json-comments": "3.1.1", + "supports-color": "8.1.1", + "which": "2.0.2", + "workerpool": "6.2.0", + "yargs": "16.2.0", + "yargs-parser": "20.2.4", + "yargs-unparser": "2.0.0" + }, + "dependencies": { + "minimatch": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-4.2.1.tgz", + "integrity": "sha512-9Uq1ChtSZO+Mxa/CL1eGizn2vRn3MlLgzhT0Iz8zaY8NdvxvB0d5QdPFmCKf7JKA9Lerx5vRrnwO03jsSfGG9g==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + } + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "nanoid": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.1.tgz", + "integrity": "sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw==", + "dev": true + }, + "netmask": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", + "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==" + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + }, + "object-inspect": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", + "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==" + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "1" + } + }, + "p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "requires": { + "yocto-queue": "^0.1.0" + } + }, + "p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "requires": { + "p-limit": "^3.0.2" + } + }, + "pac-proxy-agent": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.0.0.tgz", + "integrity": "sha512-t4tRAMx0uphnZrio0S0Jw9zg3oDbz1zVhQ/Vy18FjLfP1XOLNUEjaVxYCYRI6NS+BsMBXKIzV6cTLOkO9AtywA==", + "requires": { + "@tootallnate/quickjs-emscripten": "^0.23.0", + "agent-base": "^7.0.2", + "debug": "^4.3.4", + "get-uri": "^6.0.1", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.0", + "pac-resolver": "^7.0.0", + "socks-proxy-agent": "^8.0.1" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + } + } + }, + "pac-resolver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", + "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", + "requires": { + "degenerator": "^5.0.0", + "netmask": "^2.0.2" + } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true + }, + "proxy-agent": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.3.0.tgz", + "integrity": "sha512-0LdR757eTj/JfuU7TL2YCuAZnxWXu3tkJbg4Oq3geW/qFNT/32T0sp2HnZ9O0lMR4q3vwAt0+xCA8SR0WAD0og==", + "requires": { + "agent-base": "^7.0.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.0", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.0.0", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.1" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + } + } + }, + "proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, + "qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "requires": { + "side-channel": "^1.0.4" + } + }, + "querystringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.1.1.tgz", + "integrity": "sha512-w7fLxIRCRT7U8Qu53jQnJyPkYZIaR4n5151KMfcJlO/A9397Wxb1amJvROTK6TOnp7PfoAmg/qXiNHI+08jRfA==" + }, + "randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "requires": { + "safe-buffer": "^5.1.0" + } + }, + "readable-stream": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.4.0.tgz", + "integrity": "sha512-jItXPLmrSR8jmTRmRWJXCnGJsfy85mB3Wd/uINMXA65yrnFo0cPClFIUWzo2najVNSl+mx7/4W8ttlLWJe99pQ==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "requires": { + "picomatch": "^2.2.1" + } + }, + "rechoir": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", + "integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=", + "dev": true, + "requires": { + "resolve": "^1.1.6" + } + }, + "recursive-fs": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/recursive-fs/-/recursive-fs-2.1.0.tgz", + "integrity": "sha512-oed3YruYsD52Mi16s/07eYblQOLi5dTtxpIJNdfCEJ7S5v8dDgVcycar0pRWf4IBuPMIkoctC8RTqGJzIKMNAQ==" + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", + "dev": true + }, + "requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=" + }, + "resolve": { + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz", + "integrity": "sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==", + "dev": true, + "requires": { + "is-core-module": "^2.8.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==" + }, + "serialize-javascript": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", + "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", + "dev": true, + "requires": { + "randombytes": "^2.1.0" + } + }, + "shelljs": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.5.tgz", + "integrity": "sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==", + "dev": true, + "requires": { + "glob": "^7.0.0", + "interpret": "^1.0.0", + "rechoir": "^0.6.2" + } + }, + "shx": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/shx/-/shx-0.3.4.tgz", + "integrity": "sha512-N6A9MLVqjxZYcVn8hLmtneQWIJtp8IKzMP4eMnx+nqkvXoqinUPCbUFLp2UcWTEIUONhlk0ewxr/jaVGlc+J+g==", + "dev": true, + "requires": { + "minimist": "^1.2.3", + "shelljs": "^0.8.5" + } + }, + "side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "requires": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + } + }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==" + }, + "smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==" + }, + "socks": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz", + "integrity": "sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==", + "requires": { + "ip-address": "^9.0.5", + "smart-buffer": "^4.2.0" + } + }, + "socks-proxy-agent": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.1.tgz", + "integrity": "sha512-59EjPbbgg8U3x62hhKOFVAmySQUcfRQ4C7Q/D5sEHnZTQRrQlNKINks44DMR1gwXp0p4LaVIeccX2KHTTcHVqQ==", + "requires": { + "agent-base": "^7.0.1", + "debug": "^4.3.4", + "socks": "^2.7.1" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + } + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "optional": true + }, + "sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==" + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true + }, + "superagent": { + "version": "8.0.6", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-8.0.6.tgz", + "integrity": "sha512-HqSe6DSIh3hEn6cJvCkaM1BLi466f1LHi4yubR0tpewlMpk4RUFFy35bKz8SsPBwYfIIJy5eclp+3tCYAuX0bw==", + "requires": { + "component-emitter": "^1.3.0", + "cookiejar": "^2.1.3", + "debug": "^4.3.4", + "fast-safe-stringify": "^2.1.1", + "form-data": "^4.0.0", + "formidable": "^2.1.1", + "methods": "^1.1.2", + "mime": "2.6.0", + "qs": "^6.11.0", + "semver": "^7.3.8" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + }, + "formidable": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-2.1.1.tgz", + "integrity": "sha512-0EcS9wCFEzLvfiks7omJ+SiYJAiD+TzK4Pcw1UlUoGnhUxDcMKjt0P7x8wEb0u6OHu8Nb98WG3nxtlF5C7bvUQ==", + "requires": { + "dezalgo": "^1.0.4", + "hexoid": "^1.0.0", + "once": "^1.4.0", + "qs": "^6.11.0" + } + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "requires": { + "yallist": "^4.0.0" + } + }, + "semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "requires": { + "lru-cache": "^6.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + } + } + }, + "superagent-mock": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/superagent-mock/-/superagent-mock-4.0.0.tgz", + "integrity": "sha512-+xj+q+sL5sJIcFxwmj5Wuq59Kns0ocOd8OrMkbEXEwseWImAVvM7cP8G7raQRZ+vloUN32t/yKosD+YAXa9rcg==", + "dev": true, + "requires": {} + }, + "supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, + "tslib": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.1.tgz", + "integrity": "sha512-t0hLfiEKfMUoqhG+U1oid7Pva4bbDPHYfJNiB7BiIjRkj1pyC++4N3huJfqY6aRH6VTB0rvtzQwjM4K6qpfOig==" + }, + "typescript": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz", + "integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==", + "dev": true + }, + "universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==" + }, + "url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "requires": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "workerpool": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.0.tgz", + "integrity": "sha512-Rsk5qQHJ9eowMH28Jwhe8HEbmdYDX4lwoMWshiCXugjtHqMD9ZbiqSDLxcsfdqsETPzVUtX5s1Z5kStiIM6l4A==", + "dev": true + }, + "wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "string-width": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", + "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + } + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.0" + } + } + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true + }, + "yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "requires": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "string-width": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", + "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + } + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.0" + } + } + } + }, + "yargs-parser": { + "version": "20.2.4", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", + "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", + "dev": true + }, + "yargs-unparser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", + "dev": true, + "requires": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + } + }, + "yazl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/yazl/-/yazl-2.5.1.tgz", + "integrity": "sha512-phENi2PLiHnHb6QBVot+dJnaAZ0xosj7p3fWl+znIjBDlnMI2PsZCJZ306BPTFOaHf5qdDEI8x5qFrSOBN5vrw==", + "requires": { + "buffer-crc32": "~0.2.3" + } + }, + "yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true + } + } +} diff --git a/package.json b/package.json index 87f32180..755780f7 100644 --- a/package.json +++ b/package.json @@ -1,35 +1,45 @@ { + "name": "code-push", + "version": "4.2.3", + "description": "Management SDK for the CodePush service", + "main": "script/index.js", + "types": "script/index.d.ts", + "scripts": { + "clean": "shx rm -rf bin", + "setup": "npm install --quiet --no-progress", + "prebuild": "npm run clean", + "build": "tsc && npm run content", + "prebuild:release": "npm run clean", + "build:release": "tsc -p ./tsconfig-release.json && npm run check:release && npm run content", + "check:release": "npx ts-node .github/scripts/check-for-declaration.ts", + "test": "npm run build && mocha --recursive bin/test", + "test:debugger": "mocha --recursive --inspect-brk=0.0.0.0 bin/test", + "content": "shx cp {README.md,package.json,.npmignore} bin" + }, + "repository": { + "type": "git", + "url": "https://github.com/Microsoft/code-push.git" + }, + "author": "Microsoft Corporation", + "license": "MIT", + "homepage": "https://microsoft.github.io/code-push", + "dependencies": { + "appcenter-file-upload-client": "0.1.0", + "proxy-agent": "^6.3.0", + "recursive-fs": "^2.1.0", + "slash": "^3.0.0", + "superagent": "^8.0.0", + "yazl": "^2.5.1" + }, "devDependencies": { - "chmod": "^0.2.1", - "del": "^1.2.0", - "dotenv": "^1.2.0", - "dts-generator": "1.5.0", - "express": "^4.13.1", - "gulp": "^3.9.0", - "gulp-debug": "^2.0.1", - "gulp-if": "^1.2.5", - "gulp-install": "^0.4.0", - "gulp-istanbul": "^0.10.0", - "gulp-load-plugins": "^1.0.0-rc.1", - "gulp-mocha": "^2.1.2", - "gulp-plumber": "^1.0.1", - "gulp-tsd": "0.0.4", - "gulp-typescript": "^2.7.8", - "gulp-util": "^3.0.6", - "merge2": "^0.3.6", - "mkdirp": "^0.5.1", - "node-libs-browser": "^0.5.2", - "node-rsa": "^0.2.24", - "require-all": "^1.1.0", - "run-sequence": "^1.1.2", - "sinon": "1.16.0", - "superagent-mock": "^1.3.0", - "supertest": "^1.0.1", - "through2": "^2.0.0", - "ts-jsx-loader": "^0.2.1", - "typescript": "1.5.0-alpha", - "webpack": "^1.10.1", - "which": "^1.1.1", - "yauzl": "^2.6.0" + "@types/mocha": "^9.0.0", + "@types/node": "^14.0.27", + "@types/slash": "^3.0.0", + "@types/superagent": "^4.1.13", + "@types/yazl": "^2.4.2", + "mocha": "^9.2.0", + "shx": "^0.3.4", + "superagent-mock": "^4.0.0", + "typescript": "^5.1.6" } } diff --git a/sdk/README.md b/sdk/README.md deleted file mode 100644 index dc858a90..00000000 --- a/sdk/README.md +++ /dev/null @@ -1,102 +0,0 @@ -# CodePush Management SDK (Node.js) - -A JavaScript library for programmatically managing your CodePush account (e.g. creating apps, promoting releases), which allows authoring Node.js-based build and/or deployment scripts, without needing to shell out to the [CLI](https://github.com/Microsoft/code-push/blob/master/cli/README.md). - - -## Getting Started - -1. Create an access key to authenticate with the CodePush server using the following CodePush CLI command: - - ```shell - code-push access-key add "DESCRIPTION_OF_THE_KEY" - ``` - - If you already created a key that you want to use here, then you can retrieve it by running `code-push access-key ls` and using the value of the `Key` column for the key you wish to use. - -2. Install the management SDK by running `npm install code-push --save` - -3. Import it using the following statement (using ES6 syntax as applicable): - - ```javascript - var CodePush = require("code-push"); - ``` - -4. Create an instance of the `CodePush` class, passing it the access key you created or retrieved in step #1: - - ```javascript - var codePush = new CodePush("YOUR_ACCESS_KEY"); - ``` - -5. Begin automating the management of your account! For more details on what you can do with this `codePush` object, refer to the API reference section below. - -## API Reference - -The `code-push` module exports a single class (typically referred to as `CodePush`), which represents a proxy to the CodePush account management REST API. This class has a single constructor for authenticating with the CodePush service, and a collection of instance methods that correspond to the commands in the management [CLI](https://github.com/Microsoft/code-push/blob/master/cli/README.md), which allow you to programmatically control every aspect of your CodePush account. - -### Constructors - -- __CodePush(accessKey: string)__ - Creates a new instance of the CodePush management SDK, using the specified access key to authenticated with the server. - -### Methods - -- __addAccessKey(description: string): Promise<AccessKey>__ - Creates a new access key with the specified description (e.g. "VSTS CI"). - -- __addApp(appName: string): Promise<App>__ - Creates a new CodePush app with the specified name. - -- __addCollaborator(appName: string, email: string): Promise<void>__ - Adds the specified CodePush user as a collaborator to the specified CodePush app. - -- __addDeployment(appName: string, deploymentName: string): Promise<Deployment>__ - Creates a new deployment with the specified name, and associated with the specified app. - -- __clearDeploymentHistory(appName: string, deploymentName: string): Promise<void>__ - Clears the release history associated with the specified app deployment. - -- __getAccessKey(accessKey: string): Promise<AccessKey>__ - Retrieves the metadata about the specific access key. - -- __getAccessKeys(): Promise<AccessKey[]>__ - Retrieves the list of access keys associated with your CodePush account. - -- __getApp(appName: string): Promise<App>__ - Retrieves the metadata about the specified app. - -- __getApps(): Promise<App[]>__ - Retrieves the list of apps associated with your CodePush account. - -- __getCollaborators(appName: string): Promise<CollaboratorMap>__ - Retrieves the list of collaborators associated with the specified app. - -- __getDeployment(appName: string, deploymentName: string): Promise<Deployment>__ - Retrieves the metadata for the specified app deployment. - -- __getDeploymentHistory(appName: string, deploymentName: string): Promise<Package[]>__ - Retrieves the list of releases that have been made to the specified app deployment. - -- __getDeploymentMetrics(appName: string, deploymentName): Promise<DeploymentMetrics>__ - Retrieves the installation metrics for the specified app deployment. - -- __getDeployments(appName: string): Promose<Deployment[]>__ - Retrieves the list of deployments associated with the specified app. - -- __patchRelease(appName: string, deploymentName: string, label: string, updateMetadata: PackageInfo): Promise<void>__ - Updates the specified release's metadata with the given information. - -- __promote(appName: string, sourceDeploymentName: string, destinationDeploymentName: string, updateMetadata: PackageInfo): Promise<void>__ - Promotes the latest release from one deployment to another for the specified app and updates the release with the given metadata. - -- __release(appName: string, deploymentName: string, updateContentsPath: string, targetBinaryVersion: string, updateMetadata: PackageInfo): Promise<void>__ - Releases a new update to the specified deployment with the given metadata. - -- __removeAccessKey(accessKey: string): Promise<void>__ - Removes the specified access key from your CodePush account. - -- __removeApp(appName: string): Promise<void>__ - Deletes the specified CodePush app from your account. - -- __removeCollaborator(appName: string, email: string): Promise<void>__ - Removes the specified account as a collaborator from the specified app. - -- __removeDeployment(appName: string, deploymentName: string): Promise<void>__ - Removes the specified deployment from the specified app. - -- __renameApp(oldAppName: string, newAppName: string): Promise<void>__ - Renames an existing app. - -- __renameDeployment(appName: string, oldDeploymentName: string, newDeploymentName: string): Promise<void>__ - Renames an existing deployment within the specified app. - -- __rollback(appName: string, deploymentName: string, targetRelease?: string): Promise<void>__ - Rolls back the latest release within the specified deployment. Optionally allows you to target a specific release in the deployment's history, as opposed to rolling to the previous release. - -- __transferApp(appName: string, email: string): Promise<void>__ - Transfers the ownership of the specified app to the specified account. - -### Error Handling - -When an error occurs in any of the methods, the promise will be rejected with a CodePushError object with the following properties: - -- __message__: A user-friendly message that describes the error. -- __statusCode__: An HTTP response code that identifies the category of error: - - __CodePush.ERROR_GATEWAY_TIMEOUT__: A network error prevented you from connecting to the CodePush server. - - __CodePush.ERROR_INTERNAL_SERVER__: An error occurred internally on the CodePush server. - - __CodePush.ERROR_NOT_FOUND__: The resource you are attempting to retrieve does not exist. - - __CodePush.ERROR_CONFLICT__: The resource you are attempting to create already exists. - - __CodePush.ERROR_UNAUTHORIZED__: The access key you configured is invalid or expired. diff --git a/sdk/definitions/harness.d.ts b/sdk/definitions/harness.d.ts deleted file mode 100644 index 044e410d..00000000 --- a/sdk/definitions/harness.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -/// -/// -/// -/// -/// -/// - diff --git a/sdk/definitions/slash.d.ts b/sdk/definitions/slash.d.ts deleted file mode 100644 index dbce08b1..00000000 --- a/sdk/definitions/slash.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -declare module "slash" { - function slash(str: string): string; - - export = slash; -} \ No newline at end of file diff --git a/sdk/package.json b/sdk/package.json deleted file mode 100644 index bebe004a..00000000 --- a/sdk/package.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "name": "code-push", - "version": "1.11.1-beta", - "description": "Management SDK for the CodePush service", - "main": "script/index.js", - "scripts": { - "test": "gulp" - }, - "repository": { - "type": "git", - "url": "https://github.com/Microsoft/code-push.git" - }, - "author": "Microsoft Corporation", - "license": "MIT", - "homepage": "https://microsoft.github.io/code-push", - "dependencies": { - "base-64": "^0.1.0", - "node-uuid": "^1.4.3", - "q": "^1.4.1", - "recursive-fs": "0.1.4", - "slash": "1.0.0", - "superagent": "^1.7.2", - "superagent-proxy": "^1.0.0", - "yazl": "^2.4.1" - } -} diff --git a/sdk/plugin.xml b/sdk/plugin.xml deleted file mode 100644 index b2eb7648..00000000 --- a/sdk/plugin.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - CodePushAcquisition - CodePush Acquisition Plugin for Apache Cordova - MIT - cordova,code,push,acquisition - https://github.com/Microsoft/code-push.git - - - - diff --git a/sdk/script/management-sdk.ts b/sdk/script/management-sdk.ts deleted file mode 100644 index 97241a4a..00000000 --- a/sdk/script/management-sdk.ts +++ /dev/null @@ -1,517 +0,0 @@ -import * as base64 from "base-64"; -import crypto = require("crypto"); -import * as fs from "fs"; -import * as os from "os"; -import * as path from "path"; -import Q = require("q"); -import slash = require("slash"); -import superagent = require("superagent"); -import * as recursiveFs from "recursive-fs"; -import * as yazl from "yazl"; - -import Promise = Q.Promise; - -import { AccessKey, AccessKeyRequest, Account, App, CodePushError, CollaboratorMap, CollaboratorProperties, Deployment, DeploymentMetrics, Headers, Package, PackageInfo, ServerAccessKey, Session, UpdateMetrics } from "./types"; - -var superproxy = require("superagent-proxy"); -superproxy(superagent); - -var packageJson = require("../package.json"); - -interface JsonResponse { - headers: Headers; - body?: any; -} - -interface PackageFile { - isTemporary: boolean; - path: string; -} - -// A template string tag function that URL encodes the substituted values -function urlEncode(strings: string[], ...values: string[]): string { - var result = ""; - for (var i = 0; i < strings.length; i++) { - result += strings[i]; - if (i < values.length) { - result += encodeURIComponent(values[i]); - } - } - - return result; -} - -class AccountManager { - public static AppPermission = { - OWNER: "Owner", - COLLABORATOR: "Collaborator" - }; - public static SERVER_URL = "https://codepush-management.azurewebsites.net"; - - private static API_VERSION: number = 2; - - public static ERROR_GATEWAY_TIMEOUT = 504; // Used if there is a network error - public static ERROR_INTERNAL_SERVER = 500; - public static ERROR_NOT_FOUND = 404; - public static ERROR_CONFLICT = 409; // Used if the resource already exists - public static ERROR_UNAUTHORIZED = 401; - - private _accessKey: string; - private _serverUrl: string; - private _customHeaders: Headers; - private _proxy: string; - - constructor(accessKey: string, customHeaders?: Headers, serverUrl?: string, proxy?: string) { - if (!accessKey) throw new Error("An access key must be specified."); - - this._accessKey = accessKey; - this._customHeaders = customHeaders; - this._serverUrl = serverUrl || AccountManager.SERVER_URL; - this._proxy = proxy; - } - - public get accessKey(): string { - return this._accessKey; - } - - public isAuthenticated(throwIfUnauthorized?: boolean): Promise { - return Promise((resolve, reject, notify) => { - var request: superagent.Request = superagent.get(this._serverUrl + urlEncode `/authenticated`); - if (this._proxy) (request).proxy(this._proxy); - this.attachCredentials(request); - - request.end((err: any, res: superagent.Response) => { - var status: number = this.getErrorStatus(err, res); - if (err && status !== AccountManager.ERROR_UNAUTHORIZED) { - reject(this.getCodePushError(err, res)); - return; - } - - var authenticated: boolean = status === 200; - - if (!authenticated && throwIfUnauthorized){ - reject(this.getCodePushError(err, res)); - return; - } - - resolve(authenticated); - }); - }); - } - - public addAccessKey(friendlyName: string, ttl?: number): Promise { - if (!friendlyName) { - throw new Error("A name must be specified when adding an access key."); - } - - var accessKeyRequest: AccessKeyRequest = { - createdBy: os.hostname(), - friendlyName, - ttl - }; - - return this.post(urlEncode `/accessKeys/`, JSON.stringify(accessKeyRequest), /*expectResponseBody=*/ true) - .then((response: JsonResponse) => { - return { - createdTime: response.body.accessKey.createdTime, - expires: response.body.accessKey.expires, - key: response.body.accessKey.name, - name: response.body.accessKey.friendlyName - }; - }); - } - - public getAccessKey(accessKeyName: string): Promise { - return this.get(urlEncode `/accessKeys/${accessKeyName}`) - .then((res: JsonResponse) => { - return { - createdTime: res.body.accessKey.createdTime, - expires: res.body.accessKey.expires, - name: res.body.accessKey.friendlyName, - }; - }); - } - - public getAccessKeys(): Promise { - return this.get(urlEncode `/accessKeys`) - .then((res: JsonResponse) => { - var accessKeys: AccessKey[] = []; - - res.body.accessKeys.forEach((serverAccessKey: ServerAccessKey) => { - !serverAccessKey.isSession && accessKeys.push({ - createdTime: serverAccessKey.createdTime, - expires: serverAccessKey.expires, - name: serverAccessKey.friendlyName - }); - }); - - return accessKeys; - }); - } - - public getSessions(): Promise { - return this.get(urlEncode `/accessKeys`) - .then((res: JsonResponse) => { - // A machine name might be associated with multiple session keys, - // but we should only return one per machine name. - var sessionMap: { [machineName: string]: Session } = {}; - var now: number = new Date().getTime(); - res.body.accessKeys.forEach((serverAccessKey: ServerAccessKey) => { - if (serverAccessKey.isSession && serverAccessKey.expires > now) { - sessionMap[serverAccessKey.createdBy] = { - loggedInTime: serverAccessKey.createdTime, - machineName: serverAccessKey.createdBy - }; - } - }); - - var sessions: Session[] = Object.keys(sessionMap) - .map((machineName: string) => sessionMap[machineName]); - - return sessions; - }); - } - - - public patchAccessKey(oldName: string, newName?: string, ttl?: number): Promise { - var accessKeyRequest: AccessKeyRequest = { - friendlyName: newName, - ttl - }; - - return this.patch(urlEncode `/accessKeys/${oldName}`, JSON.stringify(accessKeyRequest)) - .then((res: JsonResponse) => { - return { - createdTime: res.body.accessKey.createdTime, - expires: res.body.accessKey.expires, - name: res.body.accessKey.friendlyName, - }; - }); - } - - public removeAccessKey(name: string): Promise { - return this.del(urlEncode `/accessKeys/${name}`) - .then(() => null); - } - - public removeSession(machineName: string): Promise { - return this.del(urlEncode `/sessions/${machineName}`) - .then(() => null); - } - - // Account - public getAccountInfo(): Promise { - return this.get(urlEncode `/account`) - .then((res: JsonResponse) => res.body.account); - } - - // Apps - public getApps(): Promise { - return this.get(urlEncode `/apps`) - .then((res: JsonResponse) => res.body.apps); - } - - public getApp(appName: string): Promise { - return this.get(urlEncode `/apps/${appName}`) - .then((res: JsonResponse) => res.body.app); - } - - public addApp(appName: string): Promise { - var app: App = { name: appName }; - return this.post(urlEncode `/apps/`, JSON.stringify(app), /*expectResponseBody=*/ false) - .then(() => app); - } - - public removeApp(appName: string): Promise { - return this.del(urlEncode `/apps/${appName}`) - .then(() => null); - } - - public renameApp(oldAppName: string, newAppName: string): Promise { - return this.patch(urlEncode `/apps/${oldAppName}`, JSON.stringify({ name: newAppName })) - .then(() => null); - } - - public transferApp(appName: string, email: string): Promise { - return this.post(urlEncode `/apps/${appName}/transfer/${email}`, /*requestBody=*/ null, /*expectResponseBody=*/ false) - .then(() => null); - } - - // Collaborators - public getCollaborators(appName: string): Promise { - return this.get(urlEncode `/apps/${appName}/collaborators`) - .then((res: JsonResponse) => res.body.collaborators); - } - - public addCollaborator(appName: string, email: string): Promise { - return this.post(urlEncode `/apps/${appName}/collaborators/${email}`, /*requestBody=*/ null, /*expectResponseBody=*/ false) - .then(() => null); - } - - public removeCollaborator(appName: string, email: string): Promise { - return this.del(urlEncode `/apps/${appName}/collaborators/${email}`) - .then(() => null); - } - - // Deployments - public addDeployment(appName: string, deploymentName: string): Promise { - var deployment = { name: deploymentName }; - return this.post(urlEncode `/apps/${appName}/deployments/`, JSON.stringify(deployment), /*expectResponseBody=*/ true) - .then((res: JsonResponse) => res.body.deployment); - } - - public clearDeploymentHistory(appName: string, deploymentName: string): Promise { - return this.del(urlEncode `/apps/${appName}/deployments/${deploymentName}/history`) - .then(() => null); - } - - public getDeployments(appName: string): Promise { - return this.get(urlEncode `/apps/${appName}/deployments/`) - .then((res: JsonResponse) => res.body.deployments); - } - - public getDeployment(appName: string, deploymentName: string): Promise { - return this.get(urlEncode `/apps/${appName}/deployments/${deploymentName}`) - .then((res: JsonResponse) => res.body.deployment); - } - - public renameDeployment(appName: string, oldDeploymentName: string, newDeploymentName: string): Promise { - return this.patch(urlEncode `/apps/${appName}/deployments/${oldDeploymentName}`, JSON.stringify({ name: newDeploymentName })) - .then(() => null); - } - - public removeDeployment(appName: string, deploymentName: string): Promise { - return this.del(urlEncode `/apps/${appName}/deployments/${deploymentName}`) - .then(() => null); - } - - public getDeploymentMetrics(appName: string, deploymentName: string): Promise { - return this.get(urlEncode `/apps/${appName}/deployments/${deploymentName}/metrics`) - .then((res: JsonResponse) => res.body.metrics); - } - - public getDeploymentHistory(appName: string, deploymentName: string): Promise { - return this.get(urlEncode `/apps/${appName}/deployments/${deploymentName}/history`) - .then((res: JsonResponse) => res.body.history); - } - - public release(appName: string, deploymentName: string, filePath: string, targetBinaryVersion: string, updateMetadata: PackageInfo, uploadProgressCallback?: (progress: number) => void): Promise { - - return Promise((resolve, reject, notify) => { - - updateMetadata.appVersion = targetBinaryVersion; - var request: superagent.Request = superagent.post(this._serverUrl + urlEncode `/apps/${appName}/deployments/${deploymentName}/release`); - if (this._proxy) (request).proxy(this._proxy); - this.attachCredentials(request); - - var getPackageFilePromise: Promise = this.packageFileFromPath(filePath); - - getPackageFilePromise.then((packageFile: PackageFile) => { - var file: any = fs.createReadStream(packageFile.path); - request.attach("package", file) - .field("packageInfo", JSON.stringify(updateMetadata)) - .on("progress", (event: any) => { - if (uploadProgressCallback && event && event.total > 0) { - var currentProgress: number = event.loaded / event.total * 100; - uploadProgressCallback(currentProgress); - } - }) - .end((err: any, res: superagent.Response) => { - - if (packageFile.isTemporary) { - fs.unlinkSync(packageFile.path); - } - - if (err) { - reject(this.getCodePushError(err, res)); - return; - } - - if (res.ok) { - resolve(null); - } else { - try { - var body = JSON.parse(res.text); - } catch (err) { - } - - if (body) { - reject({ message: body.message, statusCode: res && res.status }); - } else { - reject({ message: res.text, statusCode: res && res.status }); - } - } - }); - }); - }); - } - - public patchRelease(appName: string, deploymentName: string, label: string, updateMetadata: PackageInfo): Promise { - updateMetadata.label = label; - var requestBody: string = JSON.stringify({ packageInfo: updateMetadata }); - return this.patch(urlEncode `/apps/${appName}/deployments/${deploymentName}/release`, requestBody, /*expectResponseBody=*/ false) - .then(() => null); - } - - public promote(appName: string, sourceDeploymentName: string, destinationDeploymentName: string, updateMetadata: PackageInfo): Promise { - var requestBody: string = JSON.stringify({ packageInfo: updateMetadata }); - return this.post(urlEncode `/apps/${appName}/deployments/${sourceDeploymentName}/promote/${destinationDeploymentName}`, requestBody, /*expectResponseBody=*/ false) - .then(() => null); - } - - public rollback(appName: string, deploymentName: string, targetRelease?: string): Promise { - return this.post(urlEncode `/apps/${appName}/deployments/${deploymentName}/rollback/${targetRelease || ``}`, /*requestBody=*/ null, /*expectResponseBody=*/ false) - .then(() => null); - } - - private packageFileFromPath(filePath: string): Promise { - var getPackageFilePromise: Promise; - if (fs.lstatSync(filePath).isDirectory()) { - getPackageFilePromise = Promise((resolve: (file: PackageFile) => void, reject: (reason: Error) => void): void => { - var directoryPath: string = filePath; - - recursiveFs.readdirr(directoryPath, (error?: any, directories?: string[], files?: string[]): void => { - if (error) { - reject(error); - return; - } - - var baseDirectoryPath = path.dirname(directoryPath); - var fileName: string = this.generateRandomFilename(15) + ".zip"; - var zipFile = new yazl.ZipFile(); - var writeStream: fs.WriteStream = fs.createWriteStream(fileName); - - zipFile.outputStream.pipe(writeStream) - .on("error", (error: Error): void => { - reject(error); - }) - .on("close", (): void => { - filePath = path.join(process.cwd(), fileName); - - resolve({ isTemporary: true, path: filePath }); - }); - - for (var i = 0; i < files.length; ++i) { - var file: string = files[i]; - var relativePath: string = path.relative(baseDirectoryPath, file); - - // yazl does not like backslash (\) in the metadata path. - relativePath = slash(relativePath); - - zipFile.addFile(file, relativePath); - } - - zipFile.end(); - }); - }); - } else { - getPackageFilePromise = Q({ isTemporary: false, path: filePath }); - } - return getPackageFilePromise; - } - - private generateRandomFilename(length: number): string { - var filename: string = ""; - var validChar: string = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; - - for (var i = 0; i < length; i++) { - filename += validChar.charAt(Math.floor(Math.random() * validChar.length)); - } - - return filename; - } - - private get(endpoint: string, expectResponseBody: boolean = true): Promise { - return this.makeApiRequest("get", endpoint, /*requestBody=*/ null, expectResponseBody, /*contentType=*/ null); - } - - private post(endpoint: string, requestBody: string, expectResponseBody: boolean, contentType: string = "application/json;charset=UTF-8"): Promise { - return this.makeApiRequest("post", endpoint, requestBody, expectResponseBody, contentType); - } - - private patch(endpoint: string, requestBody: string, expectResponseBody: boolean = false, contentType: string = "application/json;charset=UTF-8"): Promise { - return this.makeApiRequest("patch", endpoint, requestBody, expectResponseBody, contentType); - } - - private del(endpoint: string, expectResponseBody: boolean = false): Promise { - return this.makeApiRequest("del", endpoint, /*requestBody=*/ null, expectResponseBody, /*contentType=*/ null); - } - - private makeApiRequest(method: string, endpoint: string, requestBody: string, expectResponseBody: boolean, contentType: string): Promise { - return Promise((resolve, reject, notify) => { - var request: superagent.Request = (superagent)[method](this._serverUrl + endpoint); - if (this._proxy) (request).proxy(this._proxy); - this.attachCredentials(request); - - if (requestBody) { - if (contentType) { - request = request.set("Content-Type", contentType); - } - - request = request.send(requestBody); - } - - request.end((err: any, res: superagent.Response) => { - if (err) { - reject(this.getCodePushError(err, res)); - return; - } - - try { - var body = JSON.parse(res.text); - } catch (err) { - } - - if (res.ok) { - if (expectResponseBody && !body) { - reject({ message: `Could not parse response: ${res.text}`, statusCode: AccountManager.ERROR_INTERNAL_SERVER }); - } else { - resolve({ - headers: res.header, - body: body - }); - } - } else { - if (body) { - reject({ message: body.message, statusCode: this.getErrorStatus(err, res) }); - } else { - reject({ message: res.text, statusCode: this.getErrorStatus(err, res) }); - } - } - }); - }); - } - - private getCodePushError(error: any, response: superagent.Response): CodePushError { - if (error.syscall === "getaddrinfo") { - error.message = `Unable to connect to the CodePush server. Are you offline, or behind a firewall or proxy?\n(${error.message})`; - } - - return { - message: this.getErrorMessage(error, response), - statusCode: this.getErrorStatus(error, response) - }; - } - - private getErrorStatus(error: any, response: superagent.Response): number { - return (error && error.status) || (response && response.status) || AccountManager.ERROR_GATEWAY_TIMEOUT; - } - - private getErrorMessage(error: Error, response: superagent.Response): string { - return response && response.text ? response.text : error.message; - } - - private attachCredentials(request: superagent.Request): void { - if (this._customHeaders) { - for (var headerName in this._customHeaders) { - request.set(headerName, this._customHeaders[headerName]); - } - } - - request.set("Accept", `application/vnd.code-push.v${AccountManager.API_VERSION}+json`); - request.set("Authorization", `Bearer ${this._accessKey}`); - request.set("X-CodePush-SDK-Version", packageJson.version); - } -} - -export = AccountManager; diff --git a/sdk/script/types.ts b/sdk/script/types.ts deleted file mode 100644 index 1542a186..00000000 --- a/sdk/script/types.ts +++ /dev/null @@ -1,20 +0,0 @@ -export { AccessKeyRequest, Account, App, CollaboratorMap, CollaboratorProperties, Deployment, DeploymentMetrics, Package, PackageInfo, AccessKey as ServerAccessKey, UpdateMetrics } from "rest-definitions"; - -export interface CodePushError { - message: string; - statusCode: number; -} - -export interface AccessKey { - createdTime: number; - expires: number; - name: string; - key?: string; -} - -export interface Session { - loggedInTime: number; - machineName: string; -} - -export type Headers = { [headerName: string]: string }; diff --git a/sdk/test/acquisition-rest-mock.ts b/sdk/test/acquisition-rest-mock.ts deleted file mode 100644 index f38c621f..00000000 --- a/sdk/test/acquisition-rest-mock.ts +++ /dev/null @@ -1,111 +0,0 @@ -/// - -import * as express from "express"; -import * as querystring from "querystring"; - -import * as acquisitionSdk from "../script/acquisition-sdk"; -import * as rest from "rest-definitions"; - -export var validDeploymentKey = "asdfasdfawerqw"; -export var latestPackage = { - downloadURL: "http://www.windowsazure.com/blobs/awperoiuqpweru", - description: "Angry flappy birds", - appVersion: "1.5.0", - label: "2.4.0", - isMandatory: false, - isAvailable: true, - updateAppVersion: false, - packageHash: "hash240", - packageSize: 1024 -}; - -export var serverUrl = "http://myurl.com"; -var reportStatusDeployUrl = serverUrl + "/reportStatus/deploy"; -var reportStatusDownloadUrl = serverUrl + "/reportStatus/download"; -var updateCheckUrl = serverUrl + "/updateCheck?"; - -export class HttpRequester implements acquisitionSdk.Http.Requester { - public request(verb: acquisitionSdk.Http.Verb, url: string, requestBodyOrCallback: string | acquisitionSdk.Callback, callback?: acquisitionSdk.Callback): void { - if (!callback && typeof requestBodyOrCallback === "function") { - callback = >requestBodyOrCallback; - } - - if (verb === acquisitionSdk.Http.Verb.GET && url.indexOf(updateCheckUrl) === 0) { - var params = querystring.parse(url.substring(updateCheckUrl.length)); - Server.onUpdateCheck(params, callback); - } else if (verb === acquisitionSdk.Http.Verb.POST && url === reportStatusDeployUrl) { - Server.onReportStatus(callback); - } else if (verb === acquisitionSdk.Http.Verb.POST && url === reportStatusDownloadUrl) { - Server.onReportStatus(callback); - } else { - throw new Error("Unexpected call"); - } - } -} - -export class CustomResponseHttpRequester implements acquisitionSdk.Http.Requester { - response: acquisitionSdk.Http.Response; - - constructor(response: acquisitionSdk.Http.Response) { - this.response = response; - } - - public request(verb: acquisitionSdk.Http.Verb, url: string, requestBodyOrCallback: string|acquisitionSdk.Callback, callback?: acquisitionSdk.Callback): void { - if (typeof requestBodyOrCallback !== "function") { - throw new Error("Unexpected request body"); - } - - callback = >requestBodyOrCallback; - callback(null, this.response); - } -} - -class Server { - public static onAcquire(params: any, callback: acquisitionSdk.Callback): void { - if (params.deploymentKey !== validDeploymentKey) { - callback(/*error=*/ null, { - statusCode: 200, - body: JSON.stringify({ updateInfo: { isAvailable: false } }) - }); - } else { - callback(/*error=*/ null, { - statusCode: 200, - body: JSON.stringify({ updateInfo: latestPackage }) - }); - } - } - - public static onUpdateCheck(params: any, callback: acquisitionSdk.Callback): void { - var updateRequest: rest.UpdateCheckRequest = { - deploymentKey: params.deploymentKey, - appVersion: params.appVersion, - packageHash: params.packageHash, - isCompanion: !!(params.isCompanion), - label: params.label - }; - - if (!updateRequest.deploymentKey || !updateRequest.appVersion) { - callback(/*error=*/ null, { statusCode: 400 }); - } else { - var updateInfo = { isAvailable: false }; - if (updateRequest.deploymentKey === validDeploymentKey) { - if (updateRequest.isCompanion || updateRequest.appVersion === latestPackage.appVersion) { - if (updateRequest.packageHash !== latestPackage.packageHash) { - updateInfo = latestPackage; - } - } else if (updateRequest.appVersion < latestPackage.appVersion) { - updateInfo = { updateAppVersion: true, appVersion: latestPackage.appVersion }; - } - } - - callback(/*error=*/ null, { - statusCode: 200, - body: JSON.stringify({ updateInfo: updateInfo }) - }); - } - } - - public static onReportStatus(callback: acquisitionSdk.Callback): void { - callback(/*error*/ null, /*response*/ { statusCode: 200 }); - } -} diff --git a/sdk/test/management-sdk.ts b/sdk/test/management-sdk.ts deleted file mode 100644 index cf2c850e..00000000 --- a/sdk/test/management-sdk.ts +++ /dev/null @@ -1,408 +0,0 @@ -/// - -import * as assert from "assert"; -import * as Q from "q"; - -import AccountManager = require("../script/management-sdk"); - -var request = require("superagent"); - -var manager: AccountManager; -describe("Management SDK", () => { - - beforeEach(() => { - manager = new AccountManager(/*accessKey=*/ "dummyAccessKey", /*customHeaders=*/ null, /*serverUrl=*/ "http://localhost"); - }); - - after(() => { - // Prevent an exception that occurs due to how superagent-mock overwrites methods - request.Request.prototype._callback = function() { }; - }); - - it("methods reject the promise with status code info when an error occurs", (done: MochaDone) => { - mockReturn("Text", 404); - - var methodsWithErrorHandling: any[] = [ - manager.addApp.bind(manager, "appName"), - manager.getApp.bind(manager, "appName"), - manager.renameApp.bind(manager, "appName", {}), - manager.removeApp.bind(manager, "appName"), - manager.transferApp.bind(manager, "appName", "email1"), - - manager.addDeployment.bind(manager, "appName", "deploymentName"), - manager.getDeployment.bind(manager, "appName", "deploymentName"), - manager.getDeployments.bind(manager, "appName"), - manager.renameDeployment.bind(manager, "appName", "deploymentName", { name: "newDeploymentName" }), - manager.removeDeployment.bind(manager, "appName", "deploymentName"), - - manager.addCollaborator.bind(manager, "appName", "email1"), - manager.getCollaborators.bind(manager, "appName"), - manager.removeCollaborator.bind(manager, "appName", "email1"), - - manager.patchRelease.bind(manager, "appName", "deploymentName", "label", { description: "newDescription" }), - manager.promote.bind(manager, "appName", "deploymentName", "newDeploymentName", { description: "newDescription" }), - manager.rollback.bind(manager, "appName", "deploymentName", "targetReleaseLabel") - ]; - - var result = Q(null); - methodsWithErrorHandling.forEach(function(f) { - result = result.then(() => { - return testErrors(f); - }); - }); - - result.done(() => { - done(); - }); - - // Test that the proper error code and text is passed through on a server error - function testErrors(method: any): Q.Promise { - return Q.Promise((resolve: any, reject: any, notify: any) => { - method().done(() => { - assert.fail("Should have thrown an error"); - reject(); - }, (error: any) => { - assert.equal(error.message, "Text"); - assert(error.statusCode); - resolve(); - }); - }); - } - }); - - it("isAuthenticated handles successful auth", (done: MochaDone) => { - mockReturn(JSON.stringify({ authenticated: true }), 200, {}); - manager.isAuthenticated() - .done((authenticated: boolean) => { - assert(authenticated, "Should be authenticated"); - done(); - }); - }); - - it("isAuthenticated handles unsuccessful auth", (done: MochaDone) => { - mockReturn("Unauthorized", 401, {}); - manager.isAuthenticated() - .done((authenticated: boolean) => { - assert(!authenticated, "Should not be authenticated"); - done(); - }); - }); - - it("isAuthenticated handles unsuccessful auth with promise rejection", (done: MochaDone) => { - mockReturn("Unauthorized", 401, {}); - - // use optional parameter to ask for rejection of the promise if not authenticated - manager.isAuthenticated(true) - .done((authenticated: boolean) => { - assert.fail("isAuthenticated should have rejected the promise"); - done(); - }, (err) => { - assert.equal(err.message, "Unauthorized", "Error message should be 'Unauthorized'"); - done(); - }); - }); - - it("isAuthenticated handles unexpected status codes", (done: MochaDone) => { - mockReturn("Not Found", 404, {}); - manager.isAuthenticated() - .done((authenticated: boolean) => { - assert.fail("isAuthenticated should have rejected the promise"); - done(); - }, (err) => { - assert.equal(err.message, "Not Found", "Error message should be 'Not Found'"); - done(); - }); - }); - - it("addApp handles successful response", (done: MochaDone) => { - mockReturn(JSON.stringify({ success: true }), 201, { location: "/appName" }); - manager.addApp("appName") - .done((obj) => { - assert.ok(obj); - done(); - }, rejectHandler); - }); - - it("addApp handles error response", (done: MochaDone) => { - mockReturn(JSON.stringify({ success: false }), 404, {}); - manager.addApp("appName") - .done((obj) => { - throw new Error("Call should not complete successfully"); - }, (error: Error) => done()); - }); - - it("getApp handles JSON response", (done: MochaDone) => { - mockReturn(JSON.stringify({ app: {} }), 200, {}); - - manager.getApp("appName") - .done((obj: any) => { - assert.ok(obj); - done(); - }, rejectHandler); - }); - - it("updateApp handles success response", (done: MochaDone) => { - mockReturn(JSON.stringify({ apps: [] }), 200, {}); - - manager.renameApp("appName", "newAppName") - .done((obj: any) => { - assert.ok(!obj); - done(); - }, rejectHandler); - }); - - it("removeApp handles success response", (done: MochaDone) => { - mockReturn("", 200, {}); - - manager.removeApp("appName") - .done((obj: any) => { - assert.ok(!obj); - done(); - }, rejectHandler); - }); - - it("transferApp handles successful response", (done: MochaDone) => { - mockReturn("", 201); - manager.transferApp("appName", "email1") - .done((obj) => { - assert.ok(!obj); - done(); - }, rejectHandler); - }); - - it("addDeployment handles success response", (done: MochaDone) => { - mockReturn(JSON.stringify({ deployment: { name: "name", key: "key" } }), 201, { location: "/deploymentName" }); - - manager.addDeployment("appName", "deploymentName") - .done((obj: any) => { - assert.ok(obj); - done(); - }, rejectHandler); - }); - - it("getDeployment handles JSON response", (done: MochaDone) => { - mockReturn(JSON.stringify({ deployment: {} }), 200, {}); - - manager.getDeployment("appName", "deploymentName") - .done((obj: any) => { - assert.ok(obj); - done(); - }, rejectHandler); - }); - - it("getDeployments handles JSON response", (done: MochaDone) => { - mockReturn(JSON.stringify({ deployments: [] }), 200, {}); - - manager.getDeployments("appName") - .done((obj: any) => { - assert.ok(obj); - done(); - }, rejectHandler); - }); - - it("renameDeployment handles success response", (done: MochaDone) => { - mockReturn(JSON.stringify({ apps: [] }), 200, {}); - - manager.renameDeployment("appName", "deploymentName", "newDeploymentName") - .done((obj: any) => { - assert.ok(!obj); - done(); - }, rejectHandler); - }); - - it("removeDeployment handles success response", (done: MochaDone) => { - mockReturn("", 200, {}); - - manager.removeDeployment("appName", "deploymentName") - .done((obj: any) => { - assert.ok(!obj); - done(); - }, rejectHandler); - }); - - it("getDeploymentHistory handles success response with no packages", (done: MochaDone) => { - mockReturn(JSON.stringify({ history: [] }), 200); - - manager.getDeploymentHistory("appName", "deploymentName") - .done((obj: any) => { - assert.ok(obj); - assert.equal(obj.length, 0); - done(); - }, rejectHandler); - }); - - it("getDeploymentHistory handles success response with two packages", (done: MochaDone) => { - mockReturn(JSON.stringify({ history: [{ label: "v1" }, { label: "v2" }] }), 200); - - manager.getDeploymentHistory("appName", "deploymentName") - .done((obj: any) => { - assert.ok(obj); - assert.equal(obj.length, 2); - assert.equal(obj[0].label, "v1"); - assert.equal(obj[1].label, "v2"); - done(); - }, rejectHandler); - }); - - it("getDeploymentHistory handles error response", (done: MochaDone) => { - mockReturn("", 404); - - manager.getDeploymentHistory("appName", "deploymentName") - .done((obj: any) => { - throw new Error("Call should not complete successfully"); - }, (error: Error) => done()); - }); - - it("clearDeploymentHistory handles success response", (done: MochaDone) => { - mockReturn("", 204); - - manager.clearDeploymentHistory("appName", "deploymentName") - .done((obj: any) => { - assert.ok(!obj); - done(); - }, rejectHandler); - }); - - it("clearDeploymentHistory handles error response", (done: MochaDone) => { - mockReturn("", 404); - - manager.clearDeploymentHistory("appName", "deploymentName") - .done((obj: any) => { - throw new Error("Call should not complete successfully"); - }, (error: Error) => done()); - }); - - it("addCollaborator handles successful response", (done: MochaDone) => { - mockReturn("", 201, { location: "/collaborators" }); - manager.addCollaborator("appName", "email1") - .done((obj) => { - assert.ok(!obj); - done(); - }, rejectHandler); - }); - - it("addCollaborator handles error response", (done: MochaDone) => { - mockReturn("", 404, {}); - manager.addCollaborator("appName", "email1") - .done((obj) => { - throw new Error("Call should not complete successfully"); - }, (error: Error) => done()); - }); - - it("getCollaborators handles success response with no collaborators", (done: MochaDone) => { - mockReturn(JSON.stringify({ collaborators: {} }), 200); - - manager.getCollaborators("appName") - .done((obj: any) => { - assert.ok(obj); - assert.equal(Object.keys(obj).length, 0); - done(); - }, rejectHandler); - }); - - it("getCollaborators handles success response with multiple collaborators", (done: MochaDone) => { - mockReturn(JSON.stringify({ - collaborators: { - "email1": { permission: "Owner", isCurrentAccount: true }, - "email2": { permission: "Collaborator", isCurrentAccount: false } - } - }), 200); - - manager.getCollaborators("appName") - .done((obj: any) => { - assert.ok(obj); - assert.equal(obj["email1"].permission, "Owner"); - assert.equal(obj["email2"].permission, "Collaborator"); - done(); - }, rejectHandler); - }); - - it("removeCollaborator handles success response", (done: MochaDone) => { - mockReturn("", 200, {}); - - manager.removeCollaborator("appName", "email1") - .done((obj: any) => { - assert.ok(!obj); - done(); - }, rejectHandler); - }); - - it("patchRelease handles success response", (done: MochaDone) => { - mockReturn(JSON.stringify({ package: { description: "newDescription" } }), 200); - - manager.patchRelease("appName", "deploymentName", "label", { description: "newDescription" }) - .done((obj: any) => { - assert.ok(!obj); - done(); - }, rejectHandler); - }); - - it("patchRelease handles error response", (done: MochaDone) => { - mockReturn("", 400); - - manager.patchRelease("appName", "deploymentName", "label", {}) - .done((obj: any) => { - throw new Error("Call should not complete successfully"); - }, (error: Error) => done()); - }); - - it("promote handles success response", (done: MochaDone) => { - mockReturn(JSON.stringify({ package: { description: "newDescription" } }), 200); - - manager.promote("appName", "deploymentName", "newDeploymentName", { description: "newDescription" }) - .done((obj: any) => { - assert.ok(!obj); - done(); - }, rejectHandler); - }); - - it("promote handles error response", (done: MochaDone) => { - mockReturn("", 400); - - manager.promote("appName", "deploymentName", "newDeploymentName", { rollout: 123 }) - .done((obj: any) => { - throw new Error("Call should not complete successfully"); - }, (error: Error) => done()); - }); - - it("rollback handles success response", (done: MochaDone) => { - mockReturn(JSON.stringify({ package: { label: "v1" } }), 200); - - manager.rollback("appName", "deploymentName", "v1") - .done((obj: any) => { - assert.ok(!obj); - done(); - }, rejectHandler); - }); - - it("rollback handles error response", (done: MochaDone) => { - mockReturn("", 400); - - manager.rollback("appName", "deploymentName", "v1") - .done((obj: any) => { - throw new Error("Call should not complete successfully"); - }, (error: Error) => done()); - }); -}); - -// Helper method that is used everywhere that an assert.fail() is needed in a promise handler -function rejectHandler(val: any): void { - assert.fail(); -} - -// Wrapper for superagent-mock that abstracts away information not needed for SDK tests -function mockReturn(bodyText: string, statusCode: number, header = {}): void { - require("superagent-mock")(request, [{ - pattern: "http://localhost/(\\w+)/?", - fixtures: function(match: any, params: any): any { - var isOk = statusCode >= 200 && statusCode < 300; - if (!isOk) { - var err: any = new Error(bodyText); - err.status = statusCode; - throw err; - } - return { text: bodyText, status: statusCode, ok: isOk, header: header, headers: {} }; - }, - callback: function(match: any, data: any): any { return data; } - }]); -} diff --git a/sdk/test/superagent-mock-config.js b/sdk/test/superagent-mock-config.js deleted file mode 100644 index a68c18e9..00000000 --- a/sdk/test/superagent-mock-config.js +++ /dev/null @@ -1,55 +0,0 @@ -// ./superagent-mock-config.js file -module.exports = [ - { - pattern: 'http://localhost/(\\w+)/', - - /** - * returns the data - * - * @param match array Result of the resolution of the regular expression - * @param params object sent by 'send' function - */ - fixtures: function (match, params) { - return {text: "Error", status: 403, ok: false}; - - /** - * example: - * request.get('https://error.example/404').end(function(err, res){ - * console.log(err); // 404 - * }) - */ - if (match[1] === '404') { - throw new Error(404); - } - - /** - * example: - * request.get('https://error.example/200').end(function(err, res){ - * console.log(res.body); // "Data fixtures" - * }) - */ - - /** - * example: - * request.get('https://domain.send.example/').send({superhero: "me"}).end(function(err, res){ - * console.log(res.body); // "Data fixtures - superhero:me" - * }) - */ - if(params["superhero"]) { - return 'Data fixtures - superhero:' + params["superhero"]; - } else { - return 'Data fixtures'; - } - }, - - /** - * returns the result of the request - * - * @param match array Result of the resolution of the regular expression - * @param data mixed Data returns by `fixtures` attribute - */ - callback: function (match, data) { - return data; - } - }, -]; \ No newline at end of file diff --git a/sdk/definitions/recursive-fs.d.ts b/src/@types/recursive-fs.d.ts similarity index 100% rename from sdk/definitions/recursive-fs.d.ts rename to src/@types/recursive-fs.d.ts diff --git a/sdk/script/acquisition-sdk.ts b/src/script/acquisition-sdk.ts similarity index 57% rename from sdk/script/acquisition-sdk.ts rename to src/script/acquisition-sdk.ts index d3812ffa..b916f004 100644 --- a/sdk/script/acquisition-sdk.ts +++ b/src/script/acquisition-sdk.ts @@ -1,6 +1,5 @@ -/// - -import { UpdateCheckResponse, UpdateCheckRequest, DeploymentStatusReport, DownloadReport } from "rest-definitions"; +import { UpdateCheckResponse, UpdateCheckRequest, DeploymentStatusReport, DownloadReport } from "./types"; +import { CodePushHttpError, CodePushDeployStatusError, CodePushPackageError } from "./code-push-error" export module Http { export const enum Verb { @@ -59,13 +58,16 @@ export class AcquisitionStatus { } export class AcquisitionManager { + private readonly BASE_URL_PART = "appcenter.ms"; private _appVersion: string; private _clientUniqueId: string; private _deploymentKey: string; private _httpRequester: Http.Requester; private _ignoreAppVersion: boolean; private _serverUrl: string; - + private _publicPrefixUrl: string = "v0.1/public/codepush/"; + private _statusCode: number; + private static _apiCallsDisabled: boolean = false; constructor(httpRequester: Http.Requester, configuration: Configuration) { this._httpRequester = httpRequester; @@ -80,21 +82,35 @@ export class AcquisitionManager { this._ignoreAppVersion = configuration.ignoreAppVersion; } + private isRecoverable = (statusCode: number): boolean => statusCode >= 500 || statusCode === 408 || statusCode === 429; + + private handleRequestFailure() { + if (this._serverUrl.includes(this.BASE_URL_PART) && !this.isRecoverable(this._statusCode)) { + AcquisitionManager._apiCallsDisabled = true; + } + } + public queryUpdateWithCurrentPackage(currentPackage: Package, callback?: Callback): void { + if (AcquisitionManager._apiCallsDisabled) { + console.log(`[CodePush] Api calls are disabled, skipping API call`); + callback(/*error=*/ null, /*remotePackage=*/ null); + return; + } + if (!currentPackage || !currentPackage.appVersion) { - throw new Error("Calling common acquisition SDK with incorrect package"); // Unexpected; indicates error in our implementation + throw new CodePushPackageError("Calling common acquisition SDK with incorrect package"); // Unexpected; indicates error in our implementation } var updateRequest: UpdateCheckRequest = { - deploymentKey: this._deploymentKey, - appVersion: currentPackage.appVersion, - packageHash: currentPackage.packageHash, - isCompanion: this._ignoreAppVersion, + deployment_key: this._deploymentKey, + app_version: currentPackage.appVersion, + package_hash: currentPackage.packageHash, + is_companion: this._ignoreAppVersion, label: currentPackage.label, - clientUniqueId: this._clientUniqueId + client_unique_id: this._clientUniqueId }; - var requestUrl: string = this._serverUrl + "updateCheck?" + queryStringify(updateRequest); + var requestUrl: string = this._serverUrl + this._publicPrefixUrl + "update_check?" + queryStringify(updateRequest); this._httpRequester.request(Http.Verb.GET, requestUrl, (error: Error, response: Http.Response) => { if (error) { @@ -102,14 +118,21 @@ export class AcquisitionManager { return; } - if (response.statusCode !== 200) { - callback(new Error(response.statusCode + ": " + response.body), /*remotePackage=*/ null); + if (response.statusCode < 200 || response.statusCode >= 300) { + let errorMessage: any; + this._statusCode = response.statusCode; + this.handleRequestFailure(); + if (response.statusCode === 0) { + errorMessage = `Couldn't send request to ${requestUrl}, xhr.statusCode = 0 was returned. One of the possible reasons for that might be connection problems. Please, check your internet connection.`; + } else { + errorMessage = `${response.statusCode}: ${response.body}`; + } + callback(new CodePushHttpError(errorMessage), /*remotePackage=*/ null); return; } - try { var responseObject = JSON.parse(response.body); - var updateInfo: UpdateCheckResponse = responseObject.updateInfo; + var updateInfo: UpdateCheckResponse = responseObject.update_info; } catch (error) { callback(error, /*remotePackage=*/ null); return; @@ -118,10 +141,10 @@ export class AcquisitionManager { if (!updateInfo) { callback(error, /*remotePackage=*/ null); return; - } else if (updateInfo.updateAppVersion) { - callback(/*error=*/ null, { updateAppVersion: true, appVersion: updateInfo.appVersion }); + } else if (updateInfo.update_app_version) { + callback(/*error=*/ null, { updateAppVersion: true, appVersion: updateInfo.target_binary_range }); return; - } else if (!updateInfo.isAvailable) { + } else if (!updateInfo.is_available) { callback(/*error=*/ null, /*remotePackage=*/ null); return; } @@ -130,11 +153,11 @@ export class AcquisitionManager { deploymentKey: this._deploymentKey, description: updateInfo.description, label: updateInfo.label, - appVersion: updateInfo.appVersion, - isMandatory: updateInfo.isMandatory, - packageHash: updateInfo.packageHash, - packageSize: updateInfo.packageSize, - downloadUrl: updateInfo.downloadURL + appVersion: updateInfo.target_binary_range, + isMandatory: updateInfo.is_mandatory, + packageHash: updateInfo.package_hash, + packageSize: updateInfo.package_size, + downloadUrl: updateInfo.download_url }; callback(/*error=*/ null, remotePackage); @@ -142,19 +165,25 @@ export class AcquisitionManager { } public reportStatusDeploy(deployedPackage?: Package, status?: string, previousLabelOrAppVersion?: string, previousDeploymentKey?: string, callback?: Callback): void { - var url: string = this._serverUrl + "reportStatus/deploy"; + if (AcquisitionManager._apiCallsDisabled) { + console.log(`[CodePush] Api calls are disabled, skipping API call`); + callback(/*error*/ null, /*not used*/ null); + return; + } + + var url: string = this._serverUrl + this._publicPrefixUrl + "report_status/deploy"; var body: DeploymentStatusReport = { - appVersion: this._appVersion, - deploymentKey: this._deploymentKey + app_version: this._appVersion, + deployment_key: this._deploymentKey }; if (this._clientUniqueId) { - body.clientUniqueId = this._clientUniqueId; + body.client_unique_id = this._clientUniqueId; } if (deployedPackage) { body.label = deployedPackage.label; - body.appVersion = deployedPackage.appVersion; + body.app_version = deployedPackage.appVersion; switch (status) { case AcquisitionStatus.DeploymentSucceeded: @@ -165,9 +194,9 @@ export class AcquisitionManager { default: if (callback) { if (!status) { - callback(new Error("Missing status argument."), /*not used*/ null); + callback(new CodePushDeployStatusError("Missing status argument."), /*not used*/ null); } else { - callback(new Error("Unrecognized status \"" + status + "\"."), /*not used*/ null); + callback(new CodePushDeployStatusError("Unrecognized status \"" + status + "\"."), /*not used*/ null); } } return; @@ -175,11 +204,11 @@ export class AcquisitionManager { } if (previousLabelOrAppVersion) { - body.previousLabelOrAppVersion = previousLabelOrAppVersion; + body.previous_label_or_app_version = previousLabelOrAppVersion; } if (previousDeploymentKey) { - body.previousDeploymentKey = previousDeploymentKey; + body.previous_deployment_key = previousDeploymentKey; } callback = typeof arguments[arguments.length - 1] === "function" && arguments[arguments.length - 1]; @@ -191,8 +220,10 @@ export class AcquisitionManager { return; } - if (response.statusCode !== 200) { - callback(new Error(response.statusCode + ": " + response.body), /*not used*/ null); + if (response.statusCode < 200 || response.statusCode >= 300) { + this._statusCode = response.statusCode; + this.handleRequestFailure(); + callback(new CodePushHttpError(response.statusCode + ": " + response.body), /*not used*/ null); return; } @@ -202,10 +233,16 @@ export class AcquisitionManager { } public reportStatusDownload(downloadedPackage: Package, callback?: Callback): void { - var url: string = this._serverUrl + "reportStatus/download"; + if (AcquisitionManager._apiCallsDisabled) { + console.log(`[CodePush] Api calls are disabled, skipping API call`); + callback(/*error*/ null, /*not used*/ null); + return; + } + + var url: string = this._serverUrl + this._publicPrefixUrl + "report_status/download"; var body: DownloadReport = { - clientUniqueId: this._clientUniqueId, - deploymentKey: this._deploymentKey, + client_unique_id: this._clientUniqueId, + deployment_key: this._deploymentKey, label: downloadedPackage.label }; @@ -216,8 +253,10 @@ export class AcquisitionManager { return; } - if (response.statusCode !== 200) { - callback(new Error(response.statusCode + ": " + response.body), /*not used*/ null); + if (response.statusCode < 200 || response.statusCode >= 300) { + this._statusCode = response.statusCode; + this.handleRequestFailure(); + callback(new CodePushHttpError(response.statusCode + ": " + response.body), /*not used*/ null); return; } @@ -234,12 +273,12 @@ function queryStringify(object: Object): string { for (var property in object) { if (object.hasOwnProperty(property)) { var value: string = (object)[property]; - if (!isFirst) { - queryString += "&"; - } - - queryString += encodeURIComponent(property) + "="; if (value !== null && typeof value !== "undefined") { + if (!isFirst) { + queryString += "&"; + } + + queryString += encodeURIComponent(property) + "="; queryString += encodeURIComponent(value); } @@ -248,4 +287,4 @@ function queryStringify(object: Object): string { } return queryString; -} +} \ No newline at end of file diff --git a/src/script/code-push-error.ts b/src/script/code-push-error.ts new file mode 100644 index 00000000..21ca7fd9 --- /dev/null +++ b/src/script/code-push-error.ts @@ -0,0 +1,34 @@ +export class CodePushError extends Error { + constructor(message: string) { + super(message); + Object.setPrototypeOf(this, CodePushError.prototype); + } +} + +export class CodePushHttpError extends CodePushError { + constructor(message: string) { + super(message); + Object.setPrototypeOf(this, CodePushHttpError.prototype); + } +} + +export class CodePushDeployStatusError extends CodePushError { + constructor(message: string) { + super(message); + Object.setPrototypeOf(this, CodePushDeployStatusError.prototype); + } +} + +export class CodePushPackageError extends CodePushError { + constructor(message: string) { + super(message); + Object.setPrototypeOf(this, CodePushPackageError.prototype); + } +} + +export class CodePushUnauthorizedError extends CodePushError { + constructor(message: string) { + super(message); + Object.setPrototypeOf(this, CodePushUnauthorizedError.prototype); + } +} diff --git a/sdk/script/index.ts b/src/script/index.ts similarity index 100% rename from sdk/script/index.ts rename to src/script/index.ts diff --git a/src/script/management-sdk.ts b/src/script/management-sdk.ts new file mode 100644 index 00000000..d361b2e7 --- /dev/null +++ b/src/script/management-sdk.ts @@ -0,0 +1,396 @@ +import * as fs from "fs"; +import * as path from "path"; +import slash = require("slash"); +import * as recursiveFs from "recursive-fs"; +import * as yazl from "yazl"; +import Adapter from "../utils/adapter/adapter" +import RequestManager from "../utils/request-manager" +import { CodePushUnauthorizedError } from "./code-push-error" +import FileUploadClient, { IProgress } from "appcenter-file-upload-client"; + +import { AccessKey, AccessKeyRequest, Account, App, AppCreationRequest, CollaboratorMap, Deployment, DeploymentMetrics, Headers, Package, PackageInfo, ReleaseUploadAssets, UploadReleaseProperties, CodePushError } from "./types"; + +interface JsonResponse { + headers: Headers; + body?: any; +} + +interface PackageFile { + isTemporary: boolean; + path: string; +} + +// A template string tag function that URL encodes the substituted values +function urlEncode(strings: TemplateStringsArray, ...values: string[]): string { + var result = ""; + for (var i = 0; i < strings.length; i++) { + result += strings[i]; + if (i < values.length) { + result += encodeURIComponent(values[i]); + } + } + + return result; +} + +class AccountManager { + public static AppPermission = { + OWNER: "Owner", + COLLABORATOR: "Collaborator" + }; + + private _accessKey: string; + private _requestManager: RequestManager; + private _adapter: Adapter; + private _fileUploadClient: FileUploadClient; + + constructor(accessKey: string, customHeaders?: Headers, serverUrl?: string, proxy?: string) { + if (!accessKey) throw new CodePushUnauthorizedError("A token must be specified."); + + this._accessKey = accessKey; + this._requestManager = new RequestManager(accessKey, customHeaders, serverUrl, proxy); + this._adapter = new Adapter(this._requestManager); + this._fileUploadClient = new FileUploadClient(); + } + + public get accessKey(): string { + return this._accessKey; + } + + public async isAuthenticated(throwIfUnauthorized?: boolean): Promise { + let res: JsonResponse; + let codePushError: CodePushError; + + try { + res = await this._requestManager.get(urlEncode`/user`, false); + } catch (error) { + codePushError = error as CodePushError; + if (codePushError && (codePushError.statusCode !== RequestManager.ERROR_UNAUTHORIZED || throwIfUnauthorized)) { + throw codePushError; + } + } + + const authenticated: boolean = !!res && !!res.body; + + return authenticated; + } + + // Access keys + public async addAccessKey(friendlyName: string, ttl?: number): Promise { + if (!friendlyName) { + throw new CodePushUnauthorizedError("A name must be specified when adding an access key."); + } + + const accessKeyRequest: AccessKeyRequest = { + description: friendlyName + }; + + const res: JsonResponse = await this._requestManager.post(urlEncode`/api_tokens`, JSON.stringify(accessKeyRequest), /*expectResponseBody=*/ true); + const accessKey = this._adapter.toLegacyAccessKey(res.body); + return accessKey; + } + + public async getAccessKeys(): Promise { + const res: JsonResponse = await this._requestManager.get(urlEncode`/api_tokens`); + const accessKeys = this._adapter.toLegacyAccessKeyList(res.body); + return accessKeys; + } + + public async removeAccessKey(name: string): Promise { + const accessKey = await this._adapter.resolveAccessKey(name); + + await this._requestManager.del(urlEncode`/api_tokens/${accessKey.id}`); + return null; + } + + // Account + public async getAccountInfo(): Promise { + const res: JsonResponse = await this._requestManager.get(urlEncode`/user`); + const accountInfo = this._adapter.toLegacyAccount(res.body); + return accountInfo; + } + + // Apps + public async getApps(): Promise { + const res: JsonResponse = await this._requestManager.get(urlEncode`/apps`); + const apps = await this._adapter.toLegacyApps(res.body); + return apps; + } + + public async getApp(appName: string): Promise { + const appParams = await this._adapter.parseApiAppName(appName); + const res: JsonResponse = await this._requestManager.get(urlEncode`/apps/${appParams.appOwner}/${appParams.appName}`); + const app = await this._adapter.toLegacyApp(res.body); + return app; + } + + public async addApp(appName: string, appOs: string, appPlatform: string, manuallyProvisionDeployments: boolean = false): Promise { + var app: AppCreationRequest = { + name: appName, + os: appOs, + platform: appPlatform, + manuallyProvisionDeployments: manuallyProvisionDeployments + }; + + const apigatewayAppCreationRequest = this._adapter.toApigatewayAppCreationRequest(app); + + const path = apigatewayAppCreationRequest.org ? `/orgs/${apigatewayAppCreationRequest.org}/apps` : `/apps`; + await this._requestManager.post(path, JSON.stringify(apigatewayAppCreationRequest.appcenterClientApp), /*expectResponseBody=*/ false); + + if (!manuallyProvisionDeployments) { + await this._adapter.addStandardDeployments(appName); + } + return app; + } + + public async removeApp(appName: string): Promise { + const appParams = await this._adapter.parseApiAppName(appName); + await this._requestManager.del(urlEncode`/apps/${appParams.appOwner}/${appParams.appName}`); + return null; + } + + public async renameApp(oldAppName: string, newAppName: string): Promise { + const { appOwner, appName } = await this._adapter.parseApiAppName(oldAppName); + const updatedApp = await this._adapter.getRenamedApp(newAppName, appOwner, appName); + + await this._requestManager.patch(urlEncode`/apps/${appOwner}/${appName}`, JSON.stringify(updatedApp)); + return null; + } + + public async transferApp(appName: string, orgName: string): Promise { + const appParams = await this._adapter.parseApiAppName(appName); + + await this._requestManager.post(urlEncode`/apps/${appParams.appOwner}/${appParams.appName}/transfer/${orgName}`, /*requestBody=*/ null, /*expectResponseBody=*/ false); + return null; + } + + // Collaborators + public async getCollaborators(appName: string): Promise { + const appParams = await this._adapter.parseApiAppName(appName); + + const res: JsonResponse = await this._requestManager.get(urlEncode`/apps/${appParams.appOwner}/${appParams.appName}/users`); + const collaborators = await this._adapter.toLegacyCollaborators(res.body, appParams.appOwner); + return collaborators; + } + + public async addCollaborator(appName: string, email: string): Promise { + const appParams = await this._adapter.parseApiAppName(appName); + const userEmailRequest = { + user_email: email + }; + await this._requestManager.post(urlEncode`/apps/${appParams.appOwner}/${appParams.appName}/invitations`, JSON.stringify(userEmailRequest), /*expectResponseBody=*/ false); + return null; + } + + public async removeCollaborator(appName: string, email: string): Promise { + const appParams = await this._adapter.parseApiAppName(appName); + + await this._requestManager.del(urlEncode`/apps/${appParams.appOwner}/${appParams.appName}/invitations/${email}`); + return null; + } + + // Deployments + public async addDeployment(appName: string, deploymentName: string): Promise { + const deployment = { name: deploymentName }; + const appParams = await this._adapter.parseApiAppName(appName); + const res = await this._requestManager.post(urlEncode`/apps/${appParams.appOwner}/${appParams.appName}/deployments/`, JSON.stringify(deployment), /*expectResponseBody=*/ true); + + return this._adapter.toLegacyDeployment(res.body); + } + + public async clearDeploymentHistory(appName: string, deploymentName: string): Promise { + const appParams = await this._adapter.parseApiAppName(appName); + + await this._requestManager.del(urlEncode`/apps/${appParams.appOwner}/${appParams.appName}/deployments/${deploymentName}/releases`); + return null; + } + + public async getDeployments(appName: string): Promise { + const appParams = await this._adapter.parseApiAppName(appName); + const res: JsonResponse = await this._requestManager.get(urlEncode`/apps/${appParams.appOwner}/${appParams.appName}/deployments/`); + + return this._adapter.toLegacyDeployments(res.body); + } + + public async getDeployment(appName: string, deploymentName: string): Promise { + const appParams = await this._adapter.parseApiAppName(appName); + const res: JsonResponse = await this._requestManager.get(urlEncode`/apps/${appParams.appOwner}/${appParams.appName}/deployments/${deploymentName}`); + + return this._adapter.toLegacyDeployment(res.body); + } + + public async renameDeployment(appName: string, oldDeploymentName: string, newDeploymentName: string): Promise { + const appParams = await this._adapter.parseApiAppName(appName); + await this._requestManager.patch(urlEncode`/apps/${appParams.appOwner}/${appParams.appName}/deployments/${oldDeploymentName}`, JSON.stringify({ name: newDeploymentName })); + + return null; + } + + public async removeDeployment(appName: string, deploymentName: string): Promise { + const appParams = await this._adapter.parseApiAppName(appName); + await this._requestManager.del(urlEncode`/apps/${appParams.appOwner}/${appParams.appName}/deployments/${deploymentName}`); + + return null; + } + + public async getDeploymentMetrics(appName: string, deploymentName: string): Promise { + const appParams = await this._adapter.parseApiAppName(appName); + + const res = await this._requestManager.get(urlEncode`/apps/${appParams.appOwner}/${appParams.appName}/deployments/${deploymentName}/metrics`); + const deploymentMetrics = this._adapter.toLegacyDeploymentMetrics(res.body); + return deploymentMetrics; + } + + public async getDeploymentHistory(appName: string, deploymentName: string): Promise { + const appParams = await this._adapter.parseApiAppName(appName); + const res = await this._requestManager.get(urlEncode`/apps/${appParams.appOwner}/${appParams.appName}/deployments/${deploymentName}/releases`); + + return this._adapter.toLegacyDeploymentHistory(res.body); + } + + // Releases + public async release(appName: string, deploymentName: string, filePath: string, targetBinaryVersion: string, updateMetadata: PackageInfo, uploadProgressCallback?: (progress: number) => void): Promise { + updateMetadata.appVersion = targetBinaryVersion; + const packageFile: PackageFile = await this.packageFileFromPath(filePath); + const appParams = await this._adapter.parseApiAppName(appName); + + const assetJsonResponse: JsonResponse = await this._requestManager.post(urlEncode`/apps/${appParams.appOwner}/${appParams.appName}/deployments/${deploymentName}/uploads`, null, true) + const assets = assetJsonResponse.body as ReleaseUploadAssets; + + await this._fileUploadClient.upload({ + assetId: assets.id, + assetDomain: assets.upload_domain, + assetToken: assets.token, + file: packageFile.path, + onProgressChanged: (progressData: IProgress) => { + if (uploadProgressCallback) { + uploadProgressCallback(progressData.percentCompleted); + } + }, + }); + + const releaseUploadProperties: UploadReleaseProperties = this._adapter.toReleaseUploadProperties(updateMetadata, assets, deploymentName); + const releaseJsonResponse: JsonResponse = await this._requestManager.post(urlEncode`/apps/${appParams.appOwner}/${appParams.appName}/deployments/${deploymentName}/releases`, JSON.stringify(releaseUploadProperties), true); + const releasePackage: Package = this._adapter.releaseToPackage(releaseJsonResponse.body); + + return releasePackage; + } + + public async patchRelease(appName: string, deploymentName: string, label: string, updateMetadata: PackageInfo): Promise { + const appParams = await this._adapter.parseApiAppName(appName); + const requestBody = this._adapter.toRestReleaseModification(updateMetadata); + + await this._requestManager.patch(urlEncode`/apps/${appParams.appOwner}/${appParams.appName}/deployments/${deploymentName}/releases/${label}`, JSON.stringify(requestBody), /*expectResponseBody=*/ false) + return null; + } + + public async promote(appName: string, sourceDeploymentName: string, destinationDeploymentName: string, updateMetadata: PackageInfo): Promise { + const appParams = await this._adapter.parseApiAppName(appName); + const requestBody = this._adapter.toRestReleaseModification(updateMetadata); + const res = await this._requestManager.post(urlEncode`/apps/${appParams.appOwner}/${appParams.appName}/deployments/${sourceDeploymentName}/promote_release/${destinationDeploymentName}`, JSON.stringify(requestBody), /*expectResponseBody=*/ true); + const releasePackage: Package = this._adapter.releaseToPackage(res.body); + + return releasePackage; + } + + public async rollback(appName: string, deploymentName: string, targetRelease?: string): Promise { + const appParams = await this._adapter.parseApiAppName(appName); + const requestBody = targetRelease ? { + label: targetRelease + } : {}; + + await this._requestManager.post(urlEncode`/apps/${appParams.appOwner}/${appParams.appName}/deployments/${deploymentName}/rollback_release`, JSON.stringify(requestBody), /*expectResponseBody=*/ false); + return null; + } + + // Deprecated + public getAccessKey(accessKeyName: string): CodePushError { + throw { + message: 'Method is deprecated', + statusCode: 404 + } + } + + // Deprecated + public getSessions(): CodePushError { + throw this.getDeprecatedMethodError(); + } + + // Deprecated + public patchAccessKey(oldName: string, newName?: string, ttl?: number): CodePushError { + throw this.getDeprecatedMethodError(); + } + + // Deprecated + public removeSession(machineName: string): CodePushError { + throw this.getDeprecatedMethodError(); + } + + private packageFileFromPath(filePath: string): Promise { + var getPackageFilePromise: Promise; + if (fs.lstatSync(filePath).isDirectory()) { + getPackageFilePromise = new Promise((resolve: (file: PackageFile) => void, reject: (reason: Error) => void): void => { + var directoryPath: string = filePath; + + recursiveFs.readdirr(directoryPath, (error?: any, directories?: string[], files?: string[]): void => { + if (error) { + reject(error); + return; + } + + var baseDirectoryPath = path.dirname(directoryPath); + var fileName: string = this.generateRandomFilename(15) + ".zip"; + var zipFile = new yazl.ZipFile(); + var writeStream: fs.WriteStream = fs.createWriteStream(fileName); + + zipFile.outputStream.pipe(writeStream) + .on("error", (error: Error): void => { + reject(error); + }) + .on("close", (): void => { + filePath = path.join(process.cwd(), fileName); + + resolve({ isTemporary: true, path: filePath }); + }); + + for (var i = 0; i < files.length; ++i) { + var file: string = files[i]; + var relativePath: string = path.relative(baseDirectoryPath, file); + + // yazl does not like backslash (\) in the metadata path. + relativePath = slash(relativePath); + + zipFile.addFile(file, relativePath); + } + + zipFile.end(); + }); + }); + } else { + getPackageFilePromise = new Promise((resolve: (file: PackageFile) => void, reject: (reason: Error) => void): void => { + resolve({ isTemporary: false, path: filePath }); + }); + } + return getPackageFilePromise; + } + + private generateRandomFilename(length: number): string { + var filename: string = ""; + var validChar: string = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + + for (var i = 0; i < length; i++) { + filename += validChar.charAt(Math.floor(Math.random() * validChar.length)); + } + + return filename; + } + + private getDeprecatedMethodError() { + return { + message: 'Method is deprecated', + statusCode: 404 + }; + } +} + +export = AccountManager; diff --git a/src/script/types.ts b/src/script/types.ts new file mode 100644 index 00000000..3145d2ae --- /dev/null +++ b/src/script/types.ts @@ -0,0 +1,197 @@ +/** + * Annotations for properties on 'inout' interfaces: + * - generated: This property cannot be specified on any input requests (PUT/PATCH/POST). + * As a result, generated properties are always marked as optional. + * - key: This property is the identifier for an object, with certain uniqueness constraints. + */ + +interface AccessKeyBase { + createdBy?: string; + /*legacy*/ description?: string; + /*key*/ friendlyName?: string; + /*generated key*/ name?: string; +} + +/*out*/ +export interface ServerAccessKey extends AccessKeyBase { + /*generated*/ createdTime?: number; + expires: number; + /*generated*/ isSession?: boolean; +} + +/*in*/ +export interface AccessKeyRequest extends AccessKeyBase { + ttl?: number; +} + +/*out*/ +export interface DeploymentMetrics { + [packageLabelOrAppVersion: string]: UpdateMetrics; +} + +/*in*/ +export interface DeploymentStatusReport { + app_version: string; + client_unique_id?: string; + deployment_key: string; + previous_deployment_key?: string; + previous_label_or_app_version?: string; + label?: string; + status?: string; +} + +/*in*/ +export interface DownloadReport { + client_unique_id: string; + deployment_key: string; + label: string; +} + +/*inout*/ +export interface PackageInfo { + appVersion?: string; + description?: string; + isDisabled?: boolean; + isMandatory?: boolean; + /*generated*/ label?: string; + /*generated*/ packageHash?: string; + rollout?: number; +} + +/*out*/ +export interface UpdateCheckResponse { + download_url?: string; + description?: string; + is_available: boolean; + is_disabled?: boolean; + target_binary_range: string; + /*generated*/ label?: string; + /*generated*/ package_hash?: string; + package_size?: number; + should_run_binary_version?: boolean; + update_app_version?: boolean; + is_mandatory?: boolean; +} + +/*in*/ +export interface UpdateCheckRequest { + app_version: string; + client_unique_id?: string; + deployment_key: string; + is_companion?: boolean; + label?: string; + package_hash?: string; +} + +/*out*/ +export interface UpdateMetrics { + active: number; + downloaded?: number; + failed?: number; + installed?: number; +} + +/*out*/ +export interface Account { + /*key*/ email: string; + name: string; + linkedProviders: string[]; +} + +/*out*/ +export interface CollaboratorProperties { + isCurrentAccount?: boolean; + permission: string; +} + +/*out*/ +export interface CollaboratorMap { + [email: string]: CollaboratorProperties; +} + +/*inout*/ +export interface App { + /*generated*/ collaborators?: CollaboratorMap; + /*key*/ name: string; + /* generated */ deployments?: string[]; + os?: string; + platform?: string; +} + +/*in*/ +export interface AppCreationRequest extends App { + manuallyProvisionDeployments?: boolean; +} + +/*inout*/ +export interface Deployment { + /*generated key*/ key?: string; + /*key*/ name: string; + /*generated*/ package?: Package; +} + +/*out*/ +export interface BlobInfo { + size: number; + url: string; +} + +/*out*/ +export interface PackageHashToBlobInfoMap { + [packageHash: string]: BlobInfo; +} + +/*inout*/ +export interface Package extends PackageInfo { + /*generated*/ blobUrl: string; + /*generated*/ diffPackageMap?: PackageHashToBlobInfoMap; + /*generated*/ originalLabel?: string; // Set on "Promote" and "Rollback" + /*generated*/ originalDeployment?: string; // Set on "Promote" + /*generated*/ releasedBy?: string; // Set by commitPackage + /*generated*/ releaseMethod?: string; // "Upload", "Promote" or "Rollback". Unknown if unspecified + /*generated*/ size: number; + /*generated*/ uploadTime: number; + /*legacy*/ releasedByUserId?: string; + /*legacy*/ manifestBlobUrl?: string; +} + +/*out*/ +export interface CodePushError { + message: string; + statusCode: number; +} + +/*in*/ +export interface AccessKey { + createdTime: number; + expires: number; + name: string; + key?: string; +} + +/*inout*/ +export interface Session { + loggedInTime: number; + machineName: string; +} + +/*in*/ +export interface ReleaseUploadAssets { + id: string; + upload_domain: string; + token: string; +} + +/*out*/ +export interface UploadReleaseProperties { + release_upload: ReleaseUploadAssets, + target_binary_version: string, + deployment_name?: string, + description?: string, + disabled?: boolean, + mandatory?: boolean, + no_duplicate_release_error?: boolean, + rollout?: number +} + +export type Headers = { [headerName: string]: string }; diff --git a/src/test/acquisition-rest-mock.ts b/src/test/acquisition-rest-mock.ts new file mode 100644 index 00000000..b3620d83 --- /dev/null +++ b/src/test/acquisition-rest-mock.ts @@ -0,0 +1,121 @@ +import * as querystring from "querystring"; + +import * as acquisitionSdk from "../script/acquisition-sdk"; +import * as types from "../script/types"; + +export var validDeploymentKey = "Valid Deployment Key"; +export var latestPackage = { + download_url: "http://www.windowsazure.com/blobs/awperoiuqpweru", + description: "Angry flappy birds", + target_binary_range: "1.5.0", + label: "2.4.0", + is_mandatory: false, + is_available: true, + update_app_version: false, + package_hash: "hash240", + package_size: 1024 +}; + +export var serverUrl = "http://myurl.com"; +var publicPrefixUrl = "/v0.1/public/codepush"; +var reportStatusDeployUrl = serverUrl + publicPrefixUrl + "/report_status/deploy"; +var reportStatusDownloadUrl = serverUrl + publicPrefixUrl + "/report_status/download"; +var updateCheckUrl = serverUrl + publicPrefixUrl + "/update_check?"; + +export function updateMockUrl() { + reportStatusDeployUrl = serverUrl + publicPrefixUrl + "/report_status/deploy"; + reportStatusDownloadUrl = serverUrl + publicPrefixUrl + "/report_status/download"; + updateCheckUrl = serverUrl + publicPrefixUrl + "/update_check?"; +} + +export class HttpRequester implements acquisitionSdk.Http.Requester { + private expectedStatusCode: number; + + constructor(expectedStatusCode?: number) { + this.expectedStatusCode = expectedStatusCode; + } + + public request(verb: acquisitionSdk.Http.Verb, url: string, requestBodyOrCallback: string | acquisitionSdk.Callback, callback?: acquisitionSdk.Callback): void { + if (!callback && typeof requestBodyOrCallback === "function") { + callback = >requestBodyOrCallback; + } + + if (verb === acquisitionSdk.Http.Verb.GET && url.indexOf(updateCheckUrl) === 0) { + var params = querystring.parse(url.substring(updateCheckUrl.length)); + Server.onUpdateCheck(params, callback, this.expectedStatusCode); + } else if (verb === acquisitionSdk.Http.Verb.POST && url === reportStatusDeployUrl) { + Server.onReportStatus(callback, this.expectedStatusCode); + } else if (verb === acquisitionSdk.Http.Verb.POST && url === reportStatusDownloadUrl) { + Server.onReportStatus(callback, this.expectedStatusCode); + } else { + throw new Error("Unexpected call"); + } + } +} + +export class CustomResponseHttpRequester implements acquisitionSdk.Http.Requester { + response: acquisitionSdk.Http.Response; + + constructor(response: acquisitionSdk.Http.Response) { + this.response = response; + } + + public request(verb: acquisitionSdk.Http.Verb, url: string, requestBodyOrCallback: string | acquisitionSdk.Callback, callback?: acquisitionSdk.Callback): void { + if (typeof requestBodyOrCallback !== "function") { + throw new Error("Unexpected request body"); + } + + callback = >requestBodyOrCallback; + callback(null, this.response); + } +} + +class Server { + public static onAcquire(params: any, callback: acquisitionSdk.Callback): void { + if (params.deploymentKey !== validDeploymentKey) { + callback(/*error=*/ null, { + statusCode: 200, + body: JSON.stringify({ update_info: { isAvailable: false } }) + }); + } else { + callback(/*error=*/ null, { + statusCode: 200, + body: JSON.stringify({ update_info: latestPackage }) + }); + } + } + + public static onUpdateCheck(params: any, callback: acquisitionSdk.Callback, expectedStatusCode?: number): void { + var updateRequest: types.UpdateCheckRequest = { + deployment_key: params.deployment_key, + app_version: params.app_version, + package_hash: params.package_hash, + is_companion: !!(params.is_companion), + label: params.label + }; + + if (!updateRequest.deployment_key || !updateRequest.app_version) { + callback(/*error=*/ null, { statusCode: 400 }); + } else { + var updateInfo = { is_available: false }; + if (updateRequest.deployment_key === validDeploymentKey) { + if (updateRequest.is_companion || updateRequest.app_version === latestPackage.target_binary_range) { + if (updateRequest.package_hash !== latestPackage.package_hash) { + updateInfo = latestPackage; + } + } else if (updateRequest.app_version < latestPackage.target_binary_range) { + updateInfo = { update_app_version: true, target_binary_range: latestPackage.target_binary_range }; + } + } + + callback(/*error=*/ null, { + statusCode: expectedStatusCode ? expectedStatusCode : 200, + body: JSON.stringify({ update_info: updateInfo }) + }); + } + } + + public static onReportStatus(callback: acquisitionSdk.Callback, expectedStatusCode: number): void { + callback(/*error*/ null, /*response*/ { statusCode: expectedStatusCode ? expectedStatusCode : 200 }); + } +} diff --git a/sdk/test/acquisition-sdk.ts b/src/test/acquisition-sdk.ts similarity index 60% rename from sdk/test/acquisition-sdk.ts rename to src/test/acquisition-sdk.ts index 2dd3f1af..18aa86f8 100644 --- a/sdk/test/acquisition-sdk.ts +++ b/src/test/acquisition-sdk.ts @@ -1,14 +1,13 @@ -/// - import * as assert from "assert"; -import * as express from "express"; -import * as http from "http"; import * as acquisitionSdk from "../script/acquisition-sdk"; -import * as mockApi from "./acquisition-rest-mock"; -import * as rest from "rest-definitions"; +import * as acquisitionRestMock from "./acquisition-rest-mock"; +import * as types from "../script/types"; +import { CodePushPackageError } from "../script/code-push-error" +import { updateMockUrl } from "./acquisition-rest-mock"; -var latestPackage: rest.UpdateCheckResponse = clone(mockApi.latestPackage); +const mockApi = acquisitionRestMock; +var latestPackage: types.UpdateCheckResponse = clone(mockApi.latestPackage); var configuration: acquisitionSdk.Configuration = { appVersion: "1.5.0", @@ -19,9 +18,9 @@ var configuration: acquisitionSdk.Configuration = { var templateCurrentPackage: acquisitionSdk.Package = { deploymentKey: mockApi.validDeploymentKey, - description: "sdfsdf", + description: "Standard description", label: "v1", - appVersion: latestPackage.appVersion, + appVersion: latestPackage.target_binary_range, packageHash: "hash001", isMandatory: false, packageSize: 100 @@ -30,25 +29,27 @@ var templateCurrentPackage: acquisitionSdk.Package = { var scriptUpdateResult: acquisitionSdk.RemotePackage = { deploymentKey: mockApi.validDeploymentKey, description: latestPackage.description, - downloadUrl: latestPackage.downloadURL, + downloadUrl: latestPackage.download_url, label: latestPackage.label, - appVersion: latestPackage.appVersion, - isMandatory: latestPackage.isMandatory, - packageHash: latestPackage.packageHash, - packageSize: latestPackage.packageSize + appVersion: latestPackage.target_binary_range, + isMandatory: latestPackage.is_mandatory, + packageHash: latestPackage.package_hash, + packageSize: latestPackage.package_size }; var nativeUpdateResult: acquisitionSdk.NativeUpdateNotification = { updateAppVersion: true, - appVersion: latestPackage.appVersion + appVersion: latestPackage.target_binary_range }; describe("Acquisition SDK", () => { beforeEach(() => { mockApi.latestPackage = clone(latestPackage); + mockApi.serverUrl = "http://myurl.com"; + updateMockUrl(); }); - it("Package with lower label and different package hash gives update", (done: MochaDone) => { + it("Package with lower label and different package hash gives update", (done: Mocha.Done) => { var acquisition = new acquisitionSdk.AcquisitionManager(new mockApi.HttpRequester(), configuration); acquisition.queryUpdateWithCurrentPackage(templateCurrentPackage, (error: Error, returnPackage: acquisitionSdk.RemotePackage | acquisitionSdk.NativeUpdateNotification) => { assert.equal(null, error); @@ -57,9 +58,9 @@ describe("Acquisition SDK", () => { }); }); - it("Package with equal package hash gives no update", (done: MochaDone) => { + it("Package with equal package hash gives no update", (done: Mocha.Done) => { var equalVersionPackage: acquisitionSdk.Package = clone(templateCurrentPackage); - equalVersionPackage.packageHash = latestPackage.packageHash; + equalVersionPackage.packageHash = latestPackage.package_hash; var acquisition = new acquisitionSdk.AcquisitionManager(new mockApi.HttpRequester(), configuration); acquisition.queryUpdateWithCurrentPackage(equalVersionPackage, (error: Error, returnPackage: acquisitionSdk.RemotePackage | acquisitionSdk.NativeUpdateNotification) => { @@ -69,7 +70,7 @@ describe("Acquisition SDK", () => { }); }); - it("Package with higher different hash and higher label version gives update", (done: MochaDone) => { + it("Package with higher different hash and higher label version gives update", (done: Mocha.Done) => { var higherVersionPackage: acquisitionSdk.Package = clone(templateCurrentPackage); higherVersionPackage.packageHash = "hash990"; @@ -81,7 +82,7 @@ describe("Acquisition SDK", () => { }); }); - it("Package with lower native version gives update notification", (done: MochaDone) => { + it("Package with lower native version gives update notification", (done: Mocha.Done) => { var lowerAppVersionPackage: acquisitionSdk.Package = clone(templateCurrentPackage); lowerAppVersionPackage.appVersion = "0.0.1"; @@ -93,7 +94,7 @@ describe("Acquisition SDK", () => { }); }); - it("Package with higher native version gives no update", (done: MochaDone) => { + it("Package with higher native version gives no update", (done: Mocha.Done) => { var higherAppVersionPackage: acquisitionSdk.Package = clone(templateCurrentPackage); higherAppVersionPackage.appVersion = "9.9.0"; @@ -105,23 +106,23 @@ describe("Acquisition SDK", () => { }); }); - it("An empty response gives no update", (done: MochaDone) => { + it("An empty response gives no update", (done: Mocha.Done) => { var lowerAppVersionPackage: acquisitionSdk.Package = clone(templateCurrentPackage); lowerAppVersionPackage.appVersion = "0.0.1"; - var emptyReponse: acquisitionSdk.Http.Response = { + var emptyResponse: acquisitionSdk.Http.Response = { statusCode: 200, body: JSON.stringify({}) }; - var acquisition = new acquisitionSdk.AcquisitionManager(new mockApi.CustomResponseHttpRequester(emptyReponse), configuration); - acquisition.queryUpdateWithCurrentPackage(lowerAppVersionPackage, (error: Error, returnPackage: acquisitionSdk.RemotePackage|acquisitionSdk.NativeUpdateNotification) => { + var acquisition = new acquisitionSdk.AcquisitionManager(new mockApi.CustomResponseHttpRequester(emptyResponse), configuration); + acquisition.queryUpdateWithCurrentPackage(lowerAppVersionPackage, (error: Error, returnPackage: acquisitionSdk.RemotePackage | acquisitionSdk.NativeUpdateNotification) => { assert.equal(null, error); done(); }); }); - it("An unexpected (but valid) JSON response gives no update", (done: MochaDone) => { + it("An unexpected (but valid) JSON response gives no update", (done: Mocha.Done) => { var lowerAppVersionPackage: acquisitionSdk.Package = clone(templateCurrentPackage); lowerAppVersionPackage.appVersion = "0.0.1"; @@ -131,13 +132,13 @@ describe("Acquisition SDK", () => { }; var acquisition = new acquisitionSdk.AcquisitionManager(new mockApi.CustomResponseHttpRequester(unexpectedResponse), configuration); - acquisition.queryUpdateWithCurrentPackage(lowerAppVersionPackage, (error: Error, returnPackage: acquisitionSdk.RemotePackage|acquisitionSdk.NativeUpdateNotification) => { + acquisition.queryUpdateWithCurrentPackage(lowerAppVersionPackage, (error: Error, returnPackage: acquisitionSdk.RemotePackage | acquisitionSdk.NativeUpdateNotification) => { assert.equal(null, error); done(); }); }); - it("Package for companion app ignores high native version and gives update", (done: MochaDone) => { + it("Package for companion app ignores high native version and gives update", (done: Mocha.Done) => { var higherAppVersionCompanionPackage: acquisitionSdk.Package = clone(templateCurrentPackage); higherAppVersionCompanionPackage.appVersion = "9.9.0"; @@ -152,9 +153,8 @@ describe("Acquisition SDK", () => { }); }); - it("If latest package is mandatory, returned package is mandatory", (done: MochaDone) => { - mockApi.latestPackage.isMandatory = true; - + it("If latest package is mandatory, returned package is mandatory", (done: Mocha.Done) => { + mockApi.latestPackage.is_mandatory = true; var acquisition = new acquisitionSdk.AcquisitionManager(new mockApi.HttpRequester(), configuration); acquisition.queryUpdateWithCurrentPackage(templateCurrentPackage, (error: Error, returnPackage: acquisitionSdk.RemotePackage) => { assert.equal(null, error); @@ -163,9 +163,10 @@ describe("Acquisition SDK", () => { }); }); - it("If invalid arguments are provided, an error is raised", (done: MochaDone) => { + it("If invalid arguments are provided, an error is raised", (done: Mocha.Done) => { var invalidPackage: acquisitionSdk.Package = clone(templateCurrentPackage); invalidPackage.appVersion = null; + var expectedError = new CodePushPackageError("Calling common acquisition SDK with incorrect package") var acquisition = new acquisitionSdk.AcquisitionManager(new mockApi.HttpRequester(), configuration); try { @@ -174,32 +175,34 @@ describe("Acquisition SDK", () => { done(); }); } catch (error) { + assert.deepEqual(error, expectedError); + assert.equal(error instanceof CodePushPackageError, true) done(); } }); - it("If an invalid JSON response is returned by the server, an error is raised", (done: MochaDone) => { + it("If an invalid JSON response is returned by the server, an error is raised", (done: Mocha.Done) => { var lowerAppVersionPackage: acquisitionSdk.Package = clone(templateCurrentPackage); lowerAppVersionPackage.appVersion = "0.0.1"; - var invalidJsonReponse: acquisitionSdk.Http.Response = { + var invalidJsonResponse: acquisitionSdk.Http.Response = { statusCode: 200, body: "invalid {{ json" }; - var acquisition = new acquisitionSdk.AcquisitionManager(new mockApi.CustomResponseHttpRequester(invalidJsonReponse), configuration); - acquisition.queryUpdateWithCurrentPackage(lowerAppVersionPackage, (error: Error, returnPackage: acquisitionSdk.RemotePackage|acquisitionSdk.NativeUpdateNotification) => { + var acquisition = new acquisitionSdk.AcquisitionManager(new mockApi.CustomResponseHttpRequester(invalidJsonResponse), configuration); + acquisition.queryUpdateWithCurrentPackage(lowerAppVersionPackage, (error: Error, returnPackage: acquisitionSdk.RemotePackage | acquisitionSdk.NativeUpdateNotification) => { assert.notEqual(null, error); done(); }); }); - it("If deploymentKey is not valid...", (done: MochaDone) => { - // TODO: behaviour is not defined + it("If deploymentKey is not valid...", (done: Mocha.Done) => { + // TODO: behavior is not defined done(); }); - it("reportStatusDeploy(...) signals completion", (done: MochaDone): void => { + it("reportStatusDeploy(...) signals completion", (done: Mocha.Done): void => { var acquisition = new acquisitionSdk.AcquisitionManager(new mockApi.HttpRequester(), configuration); acquisition.reportStatusDeploy(templateCurrentPackage, acquisitionSdk.AcquisitionStatus.DeploymentFailed, "1.5.0", mockApi.validDeploymentKey, ((error: Error, parameter: void): void => { @@ -213,7 +216,7 @@ describe("Acquisition SDK", () => { })); }); - it("reportStatusDownload(...) signals completion", (done: MochaDone): void => { + it("reportStatusDownload(...) signals completion", (done: Mocha.Done): void => { var acquisition = new acquisitionSdk.AcquisitionManager(new mockApi.HttpRequester(), configuration); acquisition.reportStatusDownload(templateCurrentPackage, ((error: Error, parameter: void): void => { @@ -226,6 +229,68 @@ describe("Acquisition SDK", () => { done(); })); }); + + it("disables api calls on unsuccessful response", (done: Mocha.Done): void => { + var invalidJsonResponse: acquisitionSdk.Http.Response = { + statusCode: 404, + body: "Not found" + }; + + mockApi.serverUrl = "https://codepush.appcenter.ms"; + updateMockUrl(); + configuration = { ...configuration, serverUrl: "https://codepush.appcenter.ms" }; + + var acquisition = new acquisitionSdk.AcquisitionManager(new mockApi.CustomResponseHttpRequester(invalidJsonResponse), configuration); + acquisition.queryUpdateWithCurrentPackage(templateCurrentPackage, (error: Error, returnPackage: acquisitionSdk.RemotePackage | acquisitionSdk.NativeUpdateNotification) => { + assert.strictEqual((acquisitionSdk.AcquisitionManager as any)._apiCallsDisabled, true); + (acquisitionSdk.AcquisitionManager as any)._apiCallsDisabled = false; + }); + + acquisition.queryUpdateWithCurrentPackage(templateCurrentPackage, (error: Error, returnPackage: acquisitionSdk.RemotePackage | acquisitionSdk.NativeUpdateNotification) => { + assert.strictEqual(returnPackage, null); + acquisition = new acquisitionSdk.AcquisitionManager(new mockApi.HttpRequester(404), configuration); + (acquisitionSdk.AcquisitionManager as any)._apiCallsDisabled = false; + }); + + acquisition.reportStatusDeploy(templateCurrentPackage, acquisitionSdk.AcquisitionStatus.DeploymentSucceeded, "1.5.0", mockApi.validDeploymentKey, ((error: Error, parameter: void): void => { + assert.strictEqual((acquisitionSdk.AcquisitionManager as any)._apiCallsDisabled, true); + acquisition = new acquisitionSdk.AcquisitionManager(new mockApi.HttpRequester(404), configuration); + (acquisitionSdk.AcquisitionManager as any)._apiCallsDisabled = false; + })); + + acquisition.reportStatusDownload(templateCurrentPackage, ((error: Error, parameter: void): void => { + assert.strictEqual((acquisitionSdk.AcquisitionManager as any)._apiCallsDisabled, true); + acquisition = acquisition = new acquisitionSdk.AcquisitionManager(new mockApi.CustomResponseHttpRequester(invalidJsonResponse), configuration); + (acquisitionSdk.AcquisitionManager as any)._apiCallsDisabled = false; + })); + + done(); + }) + + it("doesn't disable api calls on successful response", (done: Mocha.Done): void => { + var acquisition = new acquisitionSdk.AcquisitionManager(new mockApi.HttpRequester(), configuration); + mockApi.serverUrl = "https://codepush.appcenter.ms"; + updateMockUrl(); + + acquisition.queryUpdateWithCurrentPackage(templateCurrentPackage, (error: Error, returnPackage: acquisitionSdk.RemotePackage | acquisitionSdk.NativeUpdateNotification) => { + assert.strictEqual((acquisitionSdk.AcquisitionManager as any)._apiCallsDisabled, false); + }); + + acquisition.queryUpdateWithCurrentPackage(templateCurrentPackage, (error: Error, returnPackage: acquisitionSdk.RemotePackage | acquisitionSdk.NativeUpdateNotification) => { + assert.notStrictEqual(returnPackage, null); + }); + + acquisition.reportStatusDeploy(templateCurrentPackage, acquisitionSdk.AcquisitionStatus.DeploymentSucceeded, "1.5.0", mockApi.validDeploymentKey, ((error: Error, parameter: void): void => { + assert.strictEqual((acquisitionSdk.AcquisitionManager as any)._apiCallsDisabled, false); + })); + + acquisition.reportStatusDownload(templateCurrentPackage, ((error: Error, parameter: void): void => { + assert.strictEqual((acquisitionSdk.AcquisitionManager as any)._apiCallsDisabled, false); + })); + + done(); + }) + }); function clone(initialObject: T): T { diff --git a/src/test/management-sdk.ts b/src/test/management-sdk.ts new file mode 100644 index 00000000..5f13d7c6 --- /dev/null +++ b/src/test/management-sdk.ts @@ -0,0 +1,508 @@ +import assert from "assert"; + +import AccountManager = require("../script/management-sdk"); +import adapterTypes = require("../utils/adapter/adapter-types"); + +var request = require("superagent"); + +var manager: AccountManager; + +const testUser: adapterTypes.UserProfile = { + id: "testId", + avatar_url: "testAvatarUrl", + can_change_password: false, + display_name: "testDisplayName", + email: "testEmail", + name: "testUserName", + permissions: ["manager"], +} + +const testDeployment: adapterTypes.Deployment = { + createdTime: 123, + name: "testDeployment1", + key: "testKey1" +}; + +const testDeployment2: adapterTypes.Deployment = { + createdTime: 123, + name: "testDeployment2", + key: "testKey2" +}; +const testApp: adapterTypes.App = { + name: "testAppName", + owner: { ...testUser, type: "user" } +} + +const codePushRelease: adapterTypes.CodePushRelease = { + description: "testDescription", + target_binary_range: "testTargetBinaryRange", + upload_time: 123456789, + blob_url: "testBlobUrl", + size: 123456789 +} + +describe("Management SDK", () => { + + beforeEach(() => { + manager = new AccountManager(/*accessKey=*/ "dummyAccessKey", /*customHeaders=*/ null, /*serverUrl=*/ "http://localhost"); + }); + + after(() => { + // Prevent an exception that occurs due to how superagent-mock overwrites methods + request.Request.prototype._callback = function () { }; + }); + + it("methods reject the promise with status code info when an error occurs", (done: Mocha.Done) => { + mockReturn("Text", 404); + + var methodsWithErrorHandling: any[] = [ + manager.addApp.bind(manager, "appName", "iOS", "React-Native"), + manager.getApp.bind(manager, "appName"), + manager.renameApp.bind(manager, "appName", {}), + manager.removeApp.bind(manager, "appName"), + manager.transferApp.bind(manager, "appName", "email1"), + + manager.addDeployment.bind(manager, "appName", "deploymentName"), + manager.getDeployment.bind(manager, "appName", "deploymentName"), + manager.getDeployments.bind(manager, "appName"), + manager.renameDeployment.bind(manager, "appName", "deploymentName", { name: "newDeploymentName" }), + manager.removeDeployment.bind(manager, "appName", "deploymentName"), + + manager.addCollaborator.bind(manager, "appName", "email1"), + manager.getCollaborators.bind(manager, "appName"), + manager.removeCollaborator.bind(manager, "appName", "email1"), + + manager.patchRelease.bind(manager, "appName", "deploymentName", "label", { description: "newDescription" }), + manager.promote.bind(manager, "appName", "deploymentName", "newDeploymentName", { description: "newDescription" }), + manager.rollback.bind(manager, "appName", "deploymentName", "targetReleaseLabel") + ]; + + var result = Promise.resolve(null); + methodsWithErrorHandling.forEach(function (f) { + result = result.then(() => { + return testErrors(f); + }); + }); + + result.then(() => { + done(); + }); + + // Test that the proper error code and text is passed through on a server error + function testErrors(method: any): Promise { + return new Promise((resolve: any, reject: any) => { + method().then(() => { + assert.fail("Should have thrown an error"); + reject(); + }, (error: any) => { + assert.equal(error.message, "Text"); + assert(error.statusCode); + resolve(); + }); + }); + } + }); + + describe("isAuthenticated", () => { + it("isAuthenticated handles successful auth", (done: Mocha.Done) => { + mockReturn(JSON.stringify(testUser), 200); + manager.isAuthenticated() + .then((authenticated: boolean) => { + assert(authenticated, "Should be authenticated"); + done(); + }); + }); + + it("isAuthenticated handles unsuccessful auth", (done: Mocha.Done) => { + mockReturn("Unauthorized", 401); + manager.isAuthenticated() + .then((authenticated: boolean) => { + assert(!authenticated, "Should not be authenticated"); + done(); + }); + }); + + it("isAuthenticated handles unsuccessful auth with promise rejection", (done: Mocha.Done) => { + mockReturn("Unauthorized", 401); + + // use optional parameter to ask for rejection of the promise if not authenticated + manager.isAuthenticated(true) + .then((authenticated: boolean) => { + assert.fail("isAuthenticated should have rejected the promise"); + done(); + }, (err) => { + assert.equal(err.message, "Unauthorized", "Error message should be 'Unauthorized'"); + done(); + }); + }); + + it("isAuthenticated handles unexpected status codes", (done: Mocha.Done) => { + mockReturn("Not Found", 404); + manager.isAuthenticated() + .then((authenticated: boolean) => { + assert.fail("isAuthenticated should have rejected the promise"); + done(); + }, (err) => { + assert.equal(err.message, "Not Found", "Error message should be 'Not Found'"); + done(); + }); + }); + }); + + describe("addApp", () => { + it("addApp handles successful response", (done: Mocha.Done) => { + mockReturn(JSON.stringify(testApp), 201, null, { location: "/appName" }); + manager.addApp("appName", "iOS", "React-Native") + .then((obj) => { + assert.ok(obj); + done(); + }, rejectHandler); + }); + + it("addApp handles error response", (done: Mocha.Done) => { + mockReturn(JSON.stringify({ success: false }), 404); + manager.addApp("appName", "iOS", "React-Native") + .then((obj) => { + throw new Error("Call should not complete successfully"); + }, (error: Error) => done()); + }); + }); + + describe("getApp", () => { + it("getApp handles JSON response", (done: Mocha.Done) => { + mockReturn(JSON.stringify(testApp), 200); + mockUser(); + mockReturn(JSON.stringify([testDeployment, testDeployment2]), 200, `/apps/${testUser.name}/${testApp.name}/deployments/`) + + manager.getApp("appName") + .then((obj: any) => { + assert.ok(obj); + done(); + }, rejectHandler); + }); + }); + + describe("updateApp", () => { + it("updateApp handles success response", (done: Mocha.Done) => { + mockReturn(JSON.stringify(testApp), 200); + + manager.renameApp("appName", "newAppName") + .then((obj: any) => { + assert.ok(!obj); + done(); + }, rejectHandler); + }); + }); + + describe("removeApp", () => { + it("removeApp handles success response", (done: Mocha.Done) => { + mockReturn("", 200); + mockUser(); + + manager.removeApp("appName") + .then((obj: any) => { + assert.ok(!obj); + done(); + }, rejectHandler); + }); + }); + + describe("transferApp", () => { + it("transferApp handles successful response", (done: Mocha.Done) => { + mockReturn("", 201); + mockUser(); + manager.transferApp("appName", "email1") + .then( + () => done(), + rejectHandler + ); + }); + }); + + describe("addDeployment", () => { + it("addDeployment handles success response", (done: Mocha.Done) => { + const testDeploymentWithoutPackage: adapterTypes.Deployment = { ...testDeployment, latest_release: null } + + mockReturn(JSON.stringify(testDeploymentWithoutPackage), 201, null, { location: "/deploymentName" }); + + manager.addDeployment("appName", "deploymentName") + .then((obj: any) => { + assert.ok(obj); + done(); + }, rejectHandler); + }); + }); + + describe("getDeployment", () => { + it("getDeployment handles JSON response", (done: Mocha.Done) => { + mockReturn(JSON.stringify(testDeployment), 200); + mockUser(); + + manager.getDeployment("appName", "deploymentName") + .then((obj: any) => { + assert.ok(obj); + done(); + }, rejectHandler); + }); + }); + + describe("getDeployments", () => { + it("getDeployments handles JSON response", (done: Mocha.Done) => { + mockReturn(JSON.stringify([testDeployment, testDeployment2]), 200); + mockUser(); + + manager.getDeployments("appName") + .then((obj: any) => { + assert.ok(obj); + done(); + }, rejectHandler); + }); + }); + + describe("renameDeployment", () => { + it("renameDeployment handles success response", (done: Mocha.Done) => { + mockReturn(JSON.stringify(testApp), 200); + + manager.renameDeployment("appName", "deploymentName", "newDeploymentName") + .then((obj: any) => { + assert.ok(!obj); + done(); + }, rejectHandler); + }); + }); + + describe("removeDeployment", () => { + it("removeDeployment handles success response", (done: Mocha.Done) => { + mockReturn("", 200); + mockUser(); + + manager.removeDeployment("appName", "deploymentName") + .then((obj: any) => { + assert.ok(!obj); + done(); + }, rejectHandler); + }); + }); + + describe("getDeploymentHistory", () => { + it("getDeploymentHistory handles success response with no packages", (done: Mocha.Done) => { + mockReturn(JSON.stringify([]), 200); + mockUser(); + + manager.getDeploymentHistory("appName", "deploymentName") + .then((obj: any) => { + assert.ok(obj); + assert.equal(obj.length, 0); + done(); + }, rejectHandler); + }); + + it("getDeploymentHistory handles success response with two packages", (done: Mocha.Done) => { + const release: adapterTypes.CodePushRelease = { ...codePushRelease, label: "v1" }; + const release2: adapterTypes.CodePushRelease = { ...codePushRelease, label: "v2" }; + + mockReturn(JSON.stringify([release, release2]), 200); + mockUser(); + + manager.getDeploymentHistory("appName", "deploymentName") + .then((obj: any) => { + assert.ok(obj); + assert.equal(obj.length, 2); + assert.equal(obj[0].label, "v1"); + assert.equal(obj[1].label, "v2"); + done(); + }, rejectHandler); + }); + + it("getDeploymentHistory handles error response", (done: Mocha.Done) => { + mockReturn("", 404); + + manager.getDeploymentHistory("appName", "deploymentName") + .then((obj: any) => { + throw new Error("Call should not complete successfully"); + }, (error: Error) => done()); + }); + }); + + describe("clearDeploymentHistory", () => { + it("clearDeploymentHistory handles success response", (done: Mocha.Done) => { + mockReturn("", 204); + mockUser(); + + manager.clearDeploymentHistory("appName", "deploymentName") + .then((obj: any) => { + assert.ok(!obj); + done(); + }, rejectHandler); + }); + + it("clearDeploymentHistory handles error response", (done: Mocha.Done) => { + mockReturn("", 404); + + manager.clearDeploymentHistory("appName", "deploymentName") + .then((obj: any) => { + throw new Error("Call should not complete successfully"); + }, (error: Error) => done()); + }); + }); + + describe("addCollaborator", () => { + it("addCollaborator handles successful response", (done: Mocha.Done) => { + mockReturn("", 201, null, { location: "/collaborators" }); + mockUser(); + manager.addCollaborator("appName", "email1") + .then( + () => done(), + rejectHandler + ); + }); + + it("addCollaborator handles error response", (done: Mocha.Done) => { + mockReturn("", 404); + manager.addCollaborator("appName", "email1") + .then( + () => { throw new Error("Call should not complete successfully") }, + () => done() + ); + }); + }); + + describe("getCollaborators", () => { + it("getCollaborators handles success response with no collaborators", (done: Mocha.Done) => { + mockReturn(JSON.stringify([]), 200); + mockUser(); + + manager.getCollaborators("appName") + .then((obj: any) => { + assert.ok(obj); + assert.equal(Object.keys(obj).length, 0); + done(); + }, rejectHandler); + }); + + it("getCollaborators handles success response with multiple collaborators", (done: Mocha.Done) => { + const testUser2: adapterTypes.UserProfile = { + ...testUser, + email: "testEmail2", + permissions: ["developer"] + } + + mockReturn(JSON.stringify([testUser, testUser2]), 200); + mockUser(); + + manager.getCollaborators("appName") + .then((obj: any) => { + assert.ok(obj); + assert.equal(obj[testUser.email].permission, "Owner"); + assert.equal(obj[testUser2.email].permission, "Collaborator"); + done(); + }, rejectHandler); + }); + }); + + describe("removeCollaborator", () => { + it("removeCollaborator handles success response", (done: Mocha.Done) => { + mockReturn("", 200); + mockUser(); + + manager.removeCollaborator("appName", "email1") + .then((obj: any) => { + assert.ok(!obj); + done(); + }, rejectHandler); + }); + }); + + describe("patchRelease", () => { + it("patchRelease handles success response", (done: Mocha.Done) => { + const newReleasePackage: adapterTypes.CodePushRelease = { ...codePushRelease, description: "newDescription" }; + mockReturn(JSON.stringify(newReleasePackage), 200); + + manager.patchRelease("appName", "deploymentName", "label", { description: "newDescription" }) + .then((obj: any) => { + assert.ok(!obj); + done(); + }, rejectHandler); + }); + + it("patchRelease handles error response", (done: Mocha.Done) => { + mockReturn("", 400); + + manager.patchRelease("appName", "deploymentName", "label", {}) + .then((obj: any) => { + throw new Error("Call should not complete successfully"); + }, (error: Error) => done()); + }); + }); + + describe("promote", () => { + it("promote handles success response", (done: Mocha.Done) => { + const newReleasePackage: adapterTypes.CodePushRelease = { ...codePushRelease, description: "newDescription" }; + + mockReturn(JSON.stringify(newReleasePackage), 200); + + manager.promote("appName", "deploymentName", "newDeploymentName", { description: "newDescription" }) + .then((obj: any) => { + assert.ok(obj); + assert.equal(obj.description, "newDescription") + done(); + }, rejectHandler); + }); + + it("promote handles error response", (done: Mocha.Done) => { + mockReturn("", 400); + + manager.promote("appName", "deploymentName", "newDeploymentName", { rollout: 123 }) + .then((obj: any) => { + throw new Error("Call should not complete successfully"); + }, (error: Error) => done()); + }); + }); + + describe("rollback", () => { + it("rollback handles success response", (done: Mocha.Done) => { + mockReturn(JSON.stringify(codePushRelease), 200); + + manager.rollback("appName", "deploymentName", "v1") + .then((obj: any) => { + assert.ok(!obj); + done(); + }, rejectHandler); + }); + + it("rollback handles error response", (done: Mocha.Done) => { + mockReturn("", 400); + + manager.rollback("appName", "deploymentName", "v1") + .then((obj: any) => { + throw new Error("Call should not complete successfully"); + }, (error: Error) => done()); + }); + }); +}); + +// Helper method that is used everywhere that an assert.fail() is needed in a promise handler +function rejectHandler(val: any): void { + assert.fail(); +} + +function mockUser(): void { + mockReturn(JSON.stringify(testUser), 200, "/user"); +} + +// Wrapper for superagent-mock that abstracts away information not needed for SDK tests +function mockReturn(bodyText: string, statusCode: number, pattern?: string, header = {}): void { + require("superagent-mock")(request, [{ + pattern: "http://localhost" + (pattern ? pattern : "/(\\w+)/?"), + fixtures: function (match: any, params: any): any { + var isOk = statusCode >= 200 && statusCode < 300; + if (!isOk) { + var err: any = new Error(bodyText); + err.status = statusCode; + throw err; + } + return { text: bodyText, status: statusCode, ok: isOk, header: header, headers: {} }; + }, + callback: function (match: any, data: any): any { return data; } + }]); +} diff --git a/src/test/superagent-mock-config.js b/src/test/superagent-mock-config.js new file mode 100644 index 00000000..9a387ea2 --- /dev/null +++ b/src/test/superagent-mock-config.js @@ -0,0 +1,55 @@ +// ./superagent-mock-config.js file +module.exports = [ + { + pattern: 'http://localhost/(\\w+)/', + + /** + * returns the data + * + * @param match array Result of the resolution of the regular expression + * @param params object sent by 'send' function + */ + fixtures: function (match, params) { + return { text: "Error", status: 403, ok: false }; + + /** + * example: + * request.get('https://error.example/404').end(function(err, res){ + * console.log(err); // 404 + * }) + */ + if (match[1] === '404') { + throw new Error(404); + } + + /** + * example: + * request.get('https://error.example/200').end(function(err, res){ + * console.log(res.body); // "Data fixtures" + * }) + */ + + /** + * example: + * request.get('https://domain.send.example/').send({superhero: "me"}).end(function(err, res){ + * console.log(res.body); // "Data fixtures - superhero:me" + * }) + */ + if (params["superhero"]) { + return 'Data fixtures - superhero:' + params["superhero"]; + } else { + return 'Data fixtures'; + } + }, + + /** + * returns the result of the request + * + * @param match array Result of the resolution of the regular expression + * @param data mixed Data returns by `fixtures` attribute + */ + callback: function (match, data) { + return data; + } + }, +]; diff --git a/src/utils/adapter/adapter-types.ts b/src/utils/adapter/adapter-types.ts new file mode 100644 index 00000000..fc8aa37f --- /dev/null +++ b/src/utils/adapter/adapter-types.ts @@ -0,0 +1,121 @@ +import { PackageHashToBlobInfoMap } from "../../script/types" + +export type AppOs = 'iOS' | 'Android' | 'Tizen' | 'Windows' | 'Linux' | 'Custom'; // "Custom" is used for apps migrated from CodePush (where OS is unknown) +export type AppPlatform = + | 'Cordova' + | 'Java' + | 'Objective-C-Swift' + | 'React-Native' + | 'Unity' + | 'UWP' + | 'Xamarin' + | 'Electron' + | 'Unknown'; // "Unknown" is used for apps migrated from CodePush (where platform is unknown) +export type AppMemberPermissions = 'manager' | 'developer' | 'viewer' | 'tester'; +export type AppOrigin = 'app-center' | 'codepush'; + +export interface UserProfile { + id: string; + avatar_url: string; + can_change_password: boolean; + display_name: string; + email: string; + name: string; + permissions?: AppMemberPermissions[]; +} + +export interface ApiToken { + id: string; + api_token: string; + description: string; + created_at: string; +} + +export interface ApiTokensGetResponse { + id: string; + description: string; + created_at: string; +} + +export interface App { + id?: string; + app_secret?: string; + azure_subscription_id?: string; + description?: string; + display_name?: string; + icon_url?: string; + name?: string; + os?: AppOs; + owner?: Owner; + platform?: AppPlatform; + origin?: AppOrigin; +} + +interface Owner { + id: string; + avatar_url: string; + display_name: string; + email: string; + name: string; + type: OwnerType; +} + +type OwnerType = 'org' | 'user'; + +export interface Deployment { + createdTime: number; + id?: string; + name: string; + key: string; + latest_release?: any; + removedEmail?: string; +} + +export interface CodePushRelease { + target_binary_range?: string; + is_disabled?: boolean; + package_hash?: string; + released_by?: string; + description?: string; + release_method?: string; + upload_time?: number; + is_mandatory?: boolean; + blob_url?: string; + label?: string + rollout?: number; + size?: number; + original_label?: string; + original_deployment?: string; + diff_package_map?: PackageHashToBlobInfoMap; +} + +export interface ReleaseModification { + target_binary_range: string; + description: string; + is_disabled: boolean; + is_mandatory: boolean; + rollout: number; + label?: string; +} + +export interface DeploymentMetrics { + label: string, + active: number, + downloaded: number, + failed: number, + installed: number +} + +export interface UpdatedApp { + name: string; + display_name?: string; +} + +export interface ApigatewayAppCreationRequest { + org: string, + appcenterClientApp: App +} +export interface appParams { + appOwner: string; + appName: string; +} diff --git a/src/utils/adapter/adapter.ts b/src/utils/adapter/adapter.ts new file mode 100644 index 00000000..5bfd35da --- /dev/null +++ b/src/utils/adapter/adapter.ts @@ -0,0 +1,434 @@ +import * as adapterTypes from "./adapter-types"; +import * as sdkTypes from "../../script/types"; +import RequestManager from "../request-manager"; + +class Adapter { + constructor(private readonly _requestManager: RequestManager) { } + + public toLegacyAccount(profile: adapterTypes.UserProfile): sdkTypes.Account { + return { + name: profile.name, + email: profile.email, + linkedProviders: [] + }; + } + + public toLegacyAccessKey(apiToken: adapterTypes.ApiToken): sdkTypes.AccessKey { + const accessKey: sdkTypes.AccessKey = { + createdTime: Date.parse(apiToken.created_at), + expires: Date.parse('9999-12-31T23:59:59'), // never, + key: apiToken.api_token, + name: apiToken.description + }; + + return accessKey; + } + + public toLegacyAccessKeyList(apiTokens: adapterTypes.ApiTokensGetResponse[]): sdkTypes.AccessKey[] { + console.log(apiTokens); + const accessKeyList: sdkTypes.AccessKey[] = apiTokens.map((apiToken) => { + const accessKey: sdkTypes.AccessKey = { + createdTime: Date.parse(apiToken.created_at), + expires: Date.parse('9999-12-31T23:59:59'), // never, + name: apiToken.description, + }; + + return accessKey; + }); + + accessKeyList.sort( + (first: sdkTypes.AccessKey, second: sdkTypes.AccessKey) => { + const firstTime = first.createdTime || 0; + const secondTime = second.createdTime || 0; + return firstTime - secondTime; + } + ); + + return accessKeyList; + } + + public async toLegacyApp(app: adapterTypes.App): Promise { + const [user, deployments] = await Promise.all([this.getUser(), this.getDeployments(app.owner.name, app.name)]); + const deploymentsNames = deployments.map((deployment: adapterTypes.Deployment) => deployment.name); + return this.toLegacyRestApp(app, user, deploymentsNames); + }; + + public async toLegacyApps(apps: adapterTypes.App[]): Promise { + const user = await this.getUser(); + const sortedApps = await Promise.all( + apps.sort((first: adapterTypes.App, second: adapterTypes.App) => { + const firstOwner = first.owner.name || ''; + const secondOwner = second.owner.name || ''; + + // First sort by owner, then by app name + if (firstOwner !== secondOwner) { + return firstOwner.localeCompare(secondOwner); + } else { + return first.name.localeCompare(second.name); + } + }) + ); + + const legacyApps = await Promise.all( + sortedApps.map(async (app) => { + const deployments: adapterTypes.Deployment[] = await this.getDeployments(app.owner.name, app.name); + const deploymentsNames = deployments.map((deployment: adapterTypes.Deployment) => deployment.name); + + return this.toLegacyRestApp(app, user, deploymentsNames); + }) + ); + + return legacyApps; + }; + + public toApigatewayAppCreationRequest(appToCreate: sdkTypes.AppCreationRequest): adapterTypes.ApigatewayAppCreationRequest { + if ( + appToCreate.os !== 'iOS' && + appToCreate.os !== 'Android' && + appToCreate.os !== 'Windows' && + appToCreate.os !== 'Linux' + ) { + throw this.getCodePushError(`The app OS "${appToCreate.os}" isn't valid. It should be "iOS", "Android", "Windows" or "Linux".`, RequestManager.ERROR_CONFLICT); + } + + if ( + appToCreate.platform !== 'React-Native' && + appToCreate.platform !== 'Cordova' && + appToCreate.platform !== 'Electron' + ) { + throw this.getCodePushError(`The app platform "${appToCreate.platform}" isn't valid. It should be "React-Native", "Cordova" or "Electron".`, RequestManager.ERROR_CONFLICT); + } + + const org: string = this.getOrgFromLegacyAppRequest(appToCreate); + const appcenterClientApp: adapterTypes.App = this.toAppcenterClientApp(appToCreate); + + if (!this.isValidAppCenterAppName(appcenterClientApp.display_name)) { + throw this.getCodePushError(`The app name "${appcenterClientApp.display_name}" isn't valid. It can only contain alphanumeric characters, dashes, periods, or underscores.`, RequestManager.ERROR_CONFLICT); + } + + return { org, appcenterClientApp }; + } + + public async addStandardDeployments(apiAppName: string): Promise { + const { appOwner, appName } = await this.parseApiAppName(apiAppName); + const deploymentsToCreate = ['Staging', 'Production']; + await Promise.all( + deploymentsToCreate.map(async (deploymentName) => { + const deployment = { name: deploymentName }; + return await this._requestManager.post(`/apps/${appOwner}/${appName}/deployments/`, JSON.stringify(deployment), /*expectResponseBody=*/ true); + }) + ); + + return; + }; + + public async getRenamedApp(newName: string, appOwner: string, oldName: string): Promise { + const app = await this.getApp(appOwner, oldName); + + if (newName.indexOf('/') !== -1) { + throw this.getCodePushError(`The new app name "${newName}" must be unqualified, not having a '/' character.`, RequestManager.ERROR_CONFLICT); + } + + if (!this.isValidAppCenterAppName(newName)) { + throw this.getCodePushError(`The app name "${newName}" isn't valid. It can only contain alphanumeric characters, dashes, periods, or underscores.`, RequestManager.ERROR_CONFLICT); + } + + // If the display name was set on the existing app, then it was different than the app name. In that case, leave the display name unchanged; + // the user can change the display name through the Mobile Center web portal if they want to rename it. + // But if the display name and app name were the same, then rename them both. + const updatedApp = + app.name === app.display_name + ? { + name: newName, + display_name: newName + } + : { name: newName }; + + return updatedApp; + } + + public async resolveAccessKey(accessKeyName: string): Promise { + const accessKeys = await this.getApiTokens(); + const foundAccessKey = accessKeys.find((key) => { + return key.description === accessKeyName; + }); + + if (!foundAccessKey) { + throw this.getCodePushError(`Access key "${accessKeyName}" does not exist.`, RequestManager.ERROR_NOT_FOUND); + } + + return foundAccessKey; + } + + public toLegacyDeployments(deployments: adapterTypes.Deployment[]): sdkTypes.Deployment[] { + deployments.sort((first: adapterTypes.Deployment, second: adapterTypes.Deployment) => { + return first.name.localeCompare(second.name); + }); + + return this.toLegacyRestDeployments(deployments); + }; + + public toLegacyDeployment(deployment: adapterTypes.Deployment): sdkTypes.Deployment { + return this.toLegacyRestDeployment(deployment); + }; + + public async toLegacyCollaborators( + userList: adapterTypes.UserProfile[], + appOwner: string, + ): Promise { + const callingUser = await this.getUser(); + const legacyCollaborators: sdkTypes.CollaboratorMap = {}; + userList.forEach((user) => { + legacyCollaborators[user.email] = { + isCurrentAccount: callingUser.email === user.email, + permission: this.toLegacyUserPermission(user.permissions[0], user.name && user.name === appOwner) + }; + }); + return legacyCollaborators; + } + + public async toLegacyDeploymentMetrics( + deploymentMetrics: adapterTypes.DeploymentMetrics[], + ): Promise { + const legacyDeploymentMetrics: sdkTypes.DeploymentMetrics = {}; + deploymentMetrics.forEach((deployment) => { + legacyDeploymentMetrics[deployment.label] = { + active: deployment.active, + downloaded: deployment.downloaded, + failed: deployment.failed, + installed: deployment.installed + }; + }); + return legacyDeploymentMetrics; + } + + public async parseApiAppName(apiAppName: string): Promise { + const callingUser = await this.getUser(); + // If the separating / is not included, assume the owner is the calling user and only the app name is provided + if (!apiAppName.includes("/")) { + return { + appOwner: callingUser.name, + appName: apiAppName, + }; + } + const [appOwner, appName] = apiAppName.split("/"); + return { + appOwner: appOwner, + appName: appName, + }; + } + + public toLegacyDeploymentHistory(releases: adapterTypes.CodePushRelease[]): sdkTypes.Package[] { + return releases.map((release) => this.releaseToPackage(release)); + } + + private toLegacyRestApp(app: adapterTypes.App, user: adapterTypes.UserProfile, deployments: string[]): sdkTypes.App { + const isCurrentAccount: boolean = user.id === app.owner.id; + const isNameAndDisplayNameSame: boolean = app.name === app.display_name; + + let appName: string = app.name; + if (!isCurrentAccount) { + appName = app.owner.name + '/' + app.name; + } + + if (!isNameAndDisplayNameSame) { + appName += ` (${app.display_name})`; + } + + return { + name: appName, + collaborators: { + [app.owner.name]: { + isCurrentAccount: user.id === app.owner.id, + permission: 'Owner' + } + }, + deployments, + os: app.os, + platform: app.platform + }; + } + + public toReleaseUploadProperties(updateMetadata: sdkTypes.PackageInfo, releaseUploadAssets: sdkTypes.ReleaseUploadAssets, deploymentName: string): sdkTypes.UploadReleaseProperties { + const releaseUpload: sdkTypes.UploadReleaseProperties = { + release_upload: releaseUploadAssets, + target_binary_version: updateMetadata.appVersion, + deployment_name: deploymentName, + no_duplicate_release_error: false, // This property is not implemented in CodePush SDK Management + } + + if (updateMetadata.description) releaseUpload.description = updateMetadata.description; + + if (updateMetadata.isDisabled) releaseUpload.disabled = updateMetadata.isDisabled; + + if (updateMetadata.isMandatory) releaseUpload.mandatory = updateMetadata.isMandatory; + + if (updateMetadata.rollout) releaseUpload.rollout = updateMetadata.rollout; + + return releaseUpload; + } + + public toRestReleaseModification( + legacyCodePushReleaseInfo: sdkTypes.PackageInfo + ): adapterTypes.ReleaseModification { + let releaseModification: adapterTypes.ReleaseModification = {} as adapterTypes.ReleaseModification ; + + if (legacyCodePushReleaseInfo.appVersion) releaseModification.target_binary_range = legacyCodePushReleaseInfo.appVersion; + + if (legacyCodePushReleaseInfo.isDisabled) releaseModification.is_disabled = legacyCodePushReleaseInfo.isDisabled; + + if (legacyCodePushReleaseInfo.isMandatory !== undefined) releaseModification.is_mandatory = legacyCodePushReleaseInfo.isMandatory === true; + + if (legacyCodePushReleaseInfo.description) releaseModification.description = legacyCodePushReleaseInfo.description; + + if (legacyCodePushReleaseInfo.rollout) releaseModification.rollout = legacyCodePushReleaseInfo.rollout; + + if (legacyCodePushReleaseInfo.label) releaseModification.label = legacyCodePushReleaseInfo.label; + + return releaseModification; + } + + public releaseToPackage(releasePackage: adapterTypes.CodePushRelease): sdkTypes.Package { + const sdkPackage: sdkTypes.Package = { + blobUrl: releasePackage.blob_url, + size: releasePackage.size, + uploadTime: releasePackage.upload_time, + isDisabled: !!releasePackage.is_disabled, + isMandatory: !!releasePackage.is_mandatory, + } + + if (releasePackage.target_binary_range) sdkPackage.appVersion = releasePackage.target_binary_range; + + if (releasePackage.description) sdkPackage.description = releasePackage.description; + + if (releasePackage.label) sdkPackage.label = releasePackage.label; + + if (releasePackage.package_hash) sdkPackage.packageHash = releasePackage.package_hash; + + if (releasePackage.rollout) sdkPackage.rollout = releasePackage.rollout; + + if (releasePackage.diff_package_map) sdkPackage.diffPackageMap = releasePackage.diff_package_map; + + if (releasePackage.original_label) sdkPackage.originalLabel = releasePackage.original_label; + + if (releasePackage.original_deployment) sdkPackage.originalDeployment = releasePackage.original_deployment; + + if (releasePackage.released_by) sdkPackage.releasedBy = releasePackage.released_by; + + if (releasePackage.release_method) sdkPackage.releaseMethod = releasePackage.release_method; + + return sdkPackage; + } + + private toLegacyRestDeployments(apiGatewayDeployments: adapterTypes.Deployment[]): sdkTypes.Deployment[] { + const deployments: sdkTypes.Deployment[] = apiGatewayDeployments.map((deployment) => { + return this.toLegacyRestDeployment(deployment); + }); + + return deployments; + } + + private toLegacyRestDeployment(deployment: adapterTypes.Deployment): sdkTypes.Deployment { + const apiGatewayPackage = deployment.latest_release ? this.releaseToPackage(deployment.latest_release) : null; + + const restDeployment: sdkTypes.Deployment = { + name: deployment.name, + key: deployment.key, + package: apiGatewayPackage + }; + + return restDeployment; + } + + private async getUser(): Promise { + try { + const res = await this._requestManager.get(`/user`); + return res.body; + } catch (error) { + throw error; + } + } + + private async getApiTokens(): Promise { + try { + const res = await this._requestManager.get(`/api_tokens`); + return res.body; + } catch (error) { + throw error; + } + } + + private async getApp(appOwner: string, appName: string): Promise { + try { + const res = await this._requestManager.get(`/apps/${appOwner}/${appName}`); + return res.body; + } catch (error) { + throw error; + } + } + + private async getDeployments(appOwner: string, appName: string): Promise { + try { + const res = await this._requestManager.get(`/apps/${appOwner}/${appName}/deployments/`); + return res.body; + } catch (error) { + throw error; + } + } + + private toLegacyUserPermission(expectedPermission: adapterTypes.AppMemberPermissions, isOwner: boolean): string { + if (expectedPermission === 'manager') { + return isOwner ? 'Owner' : 'Manager'; + } else if (expectedPermission === 'developer') { + return 'Collaborator'; + } + return 'Reader'; + } + + private getOrgFromLegacyAppRequest(legacyCreateAppRequest: sdkTypes.AppCreationRequest) { + const slashIndex = legacyCreateAppRequest.name.indexOf('/'); + const org = slashIndex !== -1 ? legacyCreateAppRequest.name.substring(0, slashIndex) : null; + + return org; + } + + private toAppcenterClientApp(legacyCreateAppRequest: sdkTypes.AppCreationRequest): adapterTypes.App { + // If the app name contains a slash, then assume that the app is intended to be owned by an org, with the org name + // before the slash. Update the app info accordingly. + const slashIndex = legacyCreateAppRequest.name.indexOf('/'); + + return { + os: legacyCreateAppRequest.os as adapterTypes.AppOs, + platform: legacyCreateAppRequest.platform as adapterTypes.AppPlatform, + display_name: + slashIndex !== -1 ? legacyCreateAppRequest.name.substring(slashIndex + 1) : legacyCreateAppRequest.name + }; + } + + private isValidAppCenterAppName(name: any): boolean { + return this.getStringValidator(/*maxLength=*/ 1000, /*minLength=*/ 1)(name) && /^[a-zA-Z0-9-._]+$/.test(name); // Only allow alphanumeric characters, dashes, periods, or underscores + } + + private getStringValidator(maxLength: number = 1000, minLength: number = 0): (value: any) => boolean { + return function isValidString(value: string): boolean { + if (typeof value !== 'string') { + return false; + } + + if (maxLength > 0 && value.length > maxLength) { + return false; + } + + return value.length >= minLength; + }; + } + + private getCodePushError(message: string, errorCode: number): sdkTypes.CodePushError { + return { + message: message, + statusCode: errorCode + }; + } +} + +export = Adapter; diff --git a/src/utils/request-manager.ts b/src/utils/request-manager.ts new file mode 100644 index 00000000..e9b18ca5 --- /dev/null +++ b/src/utils/request-manager.ts @@ -0,0 +1,129 @@ +import superagent = require("superagent"); +import { ProxyAgent } from "proxy-agent"; +import { CodePushUnauthorizedError } from "../script/code-push-error" +import { CodePushError, Headers } from "../script/types"; + +interface JsonResponse { + headers: Headers; + body?: any; +} + +class RequestManager { + public static SERVER_URL = "https://api.appcenter.ms/v0.1"; + + public static ERROR_GATEWAY_TIMEOUT = 504; // Used if there is a network error + public static ERROR_INTERNAL_SERVER = 500; + public static ERROR_NOT_FOUND = 404; + public static ERROR_CONFLICT = 409; // Used if the resource already exists + public static ERROR_UNAUTHORIZED = 401; + + private _accessKey: string; + private _serverUrl: string; + private _customHeaders: Headers; + private _proxy: string; + + constructor(accessKey: string, customHeaders?: Headers, serverUrl?: string, proxy?: string) { + if (!accessKey) throw new CodePushUnauthorizedError("A token must be specified."); + + this._accessKey = accessKey; + this._customHeaders = customHeaders; + this._serverUrl = serverUrl || RequestManager.SERVER_URL; + this._proxy = proxy; + } + + public get(endpoint: string, expectResponseBody: boolean = true): Promise { + return this.makeApiRequest("get", endpoint, /*requestBody=*/ null, expectResponseBody, /*contentType=*/ null); + } + + public post(endpoint: string, requestBody: string, expectResponseBody: boolean, contentType: string = "application/json;charset=UTF-8"): Promise { + return this.makeApiRequest("post", endpoint, requestBody, expectResponseBody, contentType); + } + + public patch(endpoint: string, requestBody: string, expectResponseBody: boolean = false, contentType: string = "application/json;charset=UTF-8"): Promise { + return this.makeApiRequest("patch", endpoint, requestBody, expectResponseBody, contentType); + } + + public del(endpoint: string, expectResponseBody: boolean = false): Promise { + return this.makeApiRequest("del", endpoint, /*requestBody=*/ null, expectResponseBody, /*contentType=*/ null) + } + + private makeApiRequest(method: string, endpoint: string, requestBody: string, expectResponseBody: boolean, contentType: string): Promise { + return new Promise((resolve, reject) => { + var request: superagent.Request = (superagent)[method](this._serverUrl + endpoint); + + if (this._proxy) { + (request).agent(new ProxyAgent({ getProxyForUrl: () => this._proxy })) + } + + this.attachCredentials(request); + + if (requestBody) { + if (contentType) { + request = request.set("Content-Type", contentType); + } + + request = request.send(requestBody); + } + + request.end((err: any, res: superagent.Response) => { + if (err) { + reject(this.getCodePushError(err, res)); + return; + } + + try { + var body = JSON.parse(res.text); + } catch (err) { + } + + if (res.ok) { + if (expectResponseBody && !body) { + reject({ message: `Could not parse response: ${res.text}`, statusCode: RequestManager.ERROR_INTERNAL_SERVER }); + } else { + resolve({ + headers: res.header, + body: body + }); + } + } else { + if (body) { + reject({ message: body.message, statusCode: this.getErrorStatus(err, res) }); + } else { + reject({ message: res.text, statusCode: this.getErrorStatus(err, res) }); + } + } + }); + }) + } + + private getCodePushError(error: any, response?: superagent.Response): CodePushError { + if (error.syscall === "getaddrinfo") { + error.message = `Unable to connect to the CodePush server. Are you offline, or behind a firewall or proxy?\n(${error.message})`; + } + + return { + message: this.getErrorMessage(error, response), + statusCode: this.getErrorStatus(error, response) + }; + } + + private getErrorStatus(error: any, response?: superagent.Response): number { + return (error && error.status) || (response && response.status) || RequestManager.ERROR_GATEWAY_TIMEOUT; + } + + private getErrorMessage(error: Error, response?: superagent.Response): string { + return response && response.body.message ? response.body.message : error.message; + } + + private attachCredentials(request: superagent.Request): void { + if (this._customHeaders) { + for (var headerName in this._customHeaders) { + request.set(headerName, this._customHeaders[headerName]); + } + } + + request.set("x-api-token", `${this._accessKey}`); + } +} + +export = RequestManager; diff --git a/tsconfig-release.json b/tsconfig-release.json new file mode 100644 index 00000000..ee51e4be --- /dev/null +++ b/tsconfig-release.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "sourceMap": false, + "declaration": true + }, + "exclude": [ + "src/test" + ] +} diff --git a/tsconfig.json b/tsconfig.json index 7e254f65..ad75c04c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,8 +1,15 @@ { "compilerOptions": { + "target": "ES5", "module": "commonjs", "noImplicitAny": true, - "noResolve": true, - "target": "ES5" + "noEmitOnError": true, + "moduleResolution": "node", + "sourceMap": true, + "rootDir": "src", + "outDir": "bin", + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "skipLibCheck": true } } diff --git a/tsd.json b/tsd.json deleted file mode 100644 index 8849aa57..00000000 --- a/tsd.json +++ /dev/null @@ -1,86 +0,0 @@ -{ - "version": "v4", - "repo": "borisyankov/DefinitelyTyped", - "ref": "master", - "path": "definitions/external", - "installed": { - "body-parser/body-parser.d.ts": { - "commit": "f5c3f32801622770aa14328269a624f53aec538a" - }, - "express/express.d.ts": { - "commit": "f5c3f32801622770aa14328269a624f53aec538a" - }, - "jsonwebtoken/jsonwebtoken.d.ts": { - "commit": "f5c3f32801622770aa14328269a624f53aec538a" - }, - "mocha/mocha.d.ts": { - "commit": "563b64359d06907fb83c1b2367f9fd5b970d5ad7" - }, - "node/node.d.ts": { - "commit": "563b64359d06907fb83c1b2367f9fd5b970d5ad7" - }, - "q/Q.d.ts": { - "commit": "f5c3f32801622770aa14328269a624f53aec538a" - }, - "supertest/supertest.d.ts": { - "commit": "f5c3f32801622770aa14328269a624f53aec538a" - }, - "multer/multer.d.ts": { - "commit": "43b6bf88758852b9ab713a9b011487f047f94f4e" - }, - "node-uuid/node-uuid.d.ts": { - "commit": "84dfeeac378552c22297a5d555b1999a396d7e7c" - }, - "passport/passport.d.ts": { - "commit": "3e117d8c9ddf6443d7ed71a74f6bc8b6bcd36077" - }, - "passport-local/passport-local.d.ts": { - "commit": "3e117d8c9ddf6443d7ed71a74f6bc8b6bcd36077" - }, - "express-session/express-session.d.ts": { - "commit": "3e117d8c9ddf6443d7ed71a74f6bc8b6bcd36077" - }, - "cookie-parser/cookie-parser.d.ts": { - "commit": "3e117d8c9ddf6443d7ed71a74f6bc8b6bcd36077" - }, - "react/react.d.ts": { - "commit": "3e117d8c9ddf6443d7ed71a74f6bc8b6bcd36077" - }, - "request/request.d.ts": { - "commit": "977da240f263e88215a27cb44f2e31524ca07135" - }, - "form-data/form-data.d.ts": { - "commit": "977da240f263e88215a27cb44f2e31524ca07135" - }, - "shortid/shortid.d.ts": { - "commit": "8f51d25ad3523cd01f2d92e343247852f6b70dff" - }, - "sinon/sinon.d.ts": { - "commit": "639337b32bed76f70e1e7918c4ad076c4492e1ed" - }, - "chalk/chalk.d.ts": { - "commit": "4df20c9706ce6ca27137617770b57f3a0d3f9689" - }, - "open/open.d.ts": { - "commit": "e95958ac847d9a343bdc8d7cbc796a5f6da29f71" - }, - "redis/redis.d.ts": { - "commit": "fa8d9683f37d07ed79617a6e037916c6718aeb11" - }, - "semver/semver.d.ts": { - "commit": "71a7d5306ae4f9893aafd2d85d38aac8789ebf33" - }, - "applicationinsights/applicationinsights.d.ts": { - "commit": "832c51db815c766fa42a2e995a7b3136ec6d15c2" - }, - "moment/moment.d.ts": { - "commit": "708609e0764daeb5eb64104af7aca50c520c4e6e" - }, - "moment/moment-node.d.ts": { - "commit": "708609e0764daeb5eb64104af7aca50c520c4e6e" - }, - "update-notifier/update-notifier.d.ts": { - "commit": "715df764419937a75a25dcd76bfc954157bc7b96" - } - } -}