See More

/// var AccountManager = require("code-push"); var chalk = require("chalk"); var childProcess = require("child_process"); var debug_1 = require("./commands/debug"); var fs = require("fs"); var g2js = require("gradle-to-js/lib/parser"); var moment = require("moment"); var opener = require("opener"); var os = require("os"); var path = require("path"); var plist = require("plist"); var progress = require("progress"); var prompt = require("prompt"); var Q = require("q"); var recursiveFs = require("recursive-fs"); var rimraf = require("rimraf"); var semver = require("semver"); var simctl = require("simctl"); var slash = require("slash"); var Table = require("cli-table"); var yazl = require("yazl"); var which = require("which"); var wordwrap = require("wordwrap"); var cli = require("../definitions/cli"); var configFilePath = 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"); var Promise = Q.Promise; var properties = require("properties"); var ACTIVE_METRICS_KEY = "Active"; var CLI_HEADERS = { "X-CodePush-CLI-Version": packageJson.version }; var DOWNLOADED_METRICS_KEY = "Downloaded"; exports.log = function (message) { return console.log(message); }; exports.sdk; exports.spawn = childProcess.spawn; exports.execSync = childProcess.execSync; var connectionInfo; exports.confirm = function () { return Promise(function (resolve, reject, notify) { prompt.message = ""; prompt.delimiter = ""; prompt.start(); prompt.get({ properties: { response: { description: chalk.cyan("Are you sure? (Y/n):") } } }, function (err, result) { 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) { return exports.sdk.addAccessKey(command.name, command.ttl) .then(function (accessKey) { exports.log("Successfully created the \"" + command.name + "\" access key: " + accessKey.key); exports.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) { var willUpdateName = isCommandOptionSpecified(command.newName) && command.oldName !== command.newName; var willUpdateTtl = isCommandOptionSpecified(command.ttl); if (!willUpdateName && !willUpdateTtl) { throw new Error("A new name and/or TTL must be provided."); } return exports.sdk.patchAccessKey(command.oldName, command.newName, command.ttl) .then(function (accessKey) { var logMessage = "Successfully "; if (willUpdateName) { logMessage += "renamed the access key \"" + command.oldName + "\" to \"" + command.newName + "\""; } if (willUpdateTtl) { var 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; } } exports.log(logMessage + "."); }); } function accessKeyList(command) { throwForInvalidOutputFormat(command.format); return exports.sdk.getAccessKeys() .then(function (accessKeys) { printAccessKeys(command.format, accessKeys); }); } function accessKeyRemove(command) { return exports.confirm() .then(function (wasConfirmed) { if (wasConfirmed) { return exports.sdk.removeAccessKey(command.accessKey) .then(function () { exports.log("Successfully removed the \"" + command.accessKey + "\" access key."); }); } exports.log("Access key removal cancelled."); }); } function appAdd(command) { return exports.sdk.addApp(command.appName) .then(function (app) { exports.log("Successfully added the \"" + command.appName + "\" app, along with the following default deployments:"); var deploymentListCommand = { type: cli.CommandType.deploymentList, appName: app.name, format: "table", displayKeys: true }; return exports.deploymentList(deploymentListCommand, false); }); } function appList(command) { throwForInvalidOutputFormat(command.format); var apps; return exports.sdk.getApps() .then(function (retrievedApps) { printAppList(command.format, retrievedApps); }); } function appRemove(command) { return exports.confirm() .then(function (wasConfirmed) { if (wasConfirmed) { return exports.sdk.removeApp(command.appName) .then(function () { exports.log("Successfully removed the \"" + command.appName + "\" app."); }); } exports.log("App removal cancelled."); }); } function appRename(command) { return exports.sdk.renameApp(command.currentAppName, command.newAppName) .then(function () { exports.log("Successfully renamed the \"" + command.currentAppName + "\" app to \"" + command.newAppName + "\"."); }); } exports.createEmptyTempReleaseFolder = function (folderPath) { return deleteFolder(folderPath) .then(function () { fs.mkdirSync(folderPath); }); }; function appTransfer(command) { throwForInvalidEmail(command.email); return exports.confirm() .then(function (wasConfirmed) { if (wasConfirmed) { return exports.sdk.transferApp(command.appName, command.email) .then(function () { exports.log("Successfully transferred the ownership of app \"" + command.appName + "\" to the account with email \"" + command.email + "\"."); }); } exports.log("App transfer cancelled."); }); } function addCollaborator(command) { throwForInvalidEmail(command.email); return exports.sdk.addCollaborator(command.appName, command.email) .then(function () { exports.log("Successfully added \"" + command.email + "\" as a collaborator to the app \"" + command.appName + "\"."); }); } function listCollaborators(command) { throwForInvalidOutputFormat(command.format); return exports.sdk.getCollaborators(command.appName) .then(function (retrievedCollaborators) { printCollaboratorsList(command.format, retrievedCollaborators); }); } function removeCollaborator(command) { throwForInvalidEmail(command.email); return exports.confirm() .then(function (wasConfirmed) { if (wasConfirmed) { return exports.sdk.removeCollaborator(command.appName, command.email) .then(function () { exports.log("Successfully removed \"" + command.email + "\" as a collaborator from the app \"" + command.appName + "\"."); }); } exports.log("App collaborator removal cancelled."); }); } function deleteConnectionInfoCache(printMessage) { if (printMessage === void 0) { printMessage = true; } try { fs.unlinkSync(configFilePath); if (printMessage) { exports.log("Successfully logged-out. The session file located at " + chalk.cyan(configFilePath) + " has been deleted.\r\n"); } } catch (ex) { } } function deleteFolder(folderPath) { return Promise(function (resolve, reject, notify) { rimraf(folderPath, function (err) { if (err) { reject(err); } else { resolve(null); } }); }); } function deploymentAdd(command) { return exports.sdk.addDeployment(command.appName, command.deploymentName) .then(function (deployment) { exports.log("Successfully added the \"" + command.deploymentName + "\" deployment with key \"" + deployment.key + "\" to the \"" + command.appName + "\" app."); }); } function deploymentHistoryClear(command) { return exports.confirm() .then(function (wasConfirmed) { if (wasConfirmed) { return exports.sdk.clearDeploymentHistory(command.appName, command.deploymentName) .then(function () { exports.log("Successfully cleared the release history associated with the \"" + command.deploymentName + "\" deployment from the \"" + command.appName + "\" app."); }); } exports.log("Clear deployment cancelled."); }); } exports.deploymentList = function (command, showPackage) { if (showPackage === void 0) { showPackage = true; } throwForInvalidOutputFormat(command.format); var deployments; return exports.sdk.getDeployments(command.appName) .then(function (retrievedDeployments) { deployments = retrievedDeployments; if (showPackage) { var metricsPromises = deployments.map(function (deployment) { if (deployment.package) { return exports.sdk.getDeploymentMetrics(command.appName, deployment.name) .then(function (metrics) { if (metrics[deployment.package.label]) { var totalActive = 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(function () { printDeploymentList(command, deployments, showPackage); }); }; function deploymentRemove(command) { return exports.confirm() .then(function (wasConfirmed) { if (wasConfirmed) { return exports.sdk.removeDeployment(command.appName, command.deploymentName) .then(function () { exports.log("Successfully removed the \"" + command.deploymentName + "\" deployment from the \"" + command.appName + "\" app."); }); } exports.log("Deployment removal cancelled."); }); } function deploymentRename(command) { return exports.sdk.renameDeployment(command.appName, command.currentDeploymentName, command.newDeploymentName) .then(function () { exports.log("Successfully renamed the \"" + command.currentDeploymentName + "\" deployment to \"" + command.newDeploymentName + "\" for the \"" + command.appName + "\" app."); }); } function deploymentHistory(command) { throwForInvalidOutputFormat(command.format); return Q.all([ exports.sdk.getAccountInfo(), exports.sdk.getDeploymentHistory(command.appName, command.deploymentName), exports.sdk.getDeploymentMetrics(command.appName, command.deploymentName) ]) .spread(function (account, deploymentHistory, metrics) { var totalActive = getTotalActiveFromDeploymentMetrics(metrics); deploymentHistory.forEach(function (packageObject) { 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() { try { var savedConnection = fs.readFileSync(configFilePath, { encoding: "utf8" }); var connectionInfo = 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; } } function execute(command) { connectionInfo = deserializeConnectionInfo(); return Q(null) .then(function () { 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 (!!exports.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."); } exports.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 debug_1.default(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 exports.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 exports.release(command); case cli.CommandType.releaseCordova: return exports.releaseCordova(command); case cli.CommandType.releaseReact: return exports.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)); } }); } exports.execute = execute; function fileDoesNotExistOrIsDirectory(filePath) { try { return fs.lstatSync(filePath).isDirectory(); } catch (error) { return true; } } function generateRandomFilename(length) { var filename = ""; var validChar = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; for (var i = 0; i < length; i++) { filename += validChar.charAt(Math.floor(Math.random() * validChar.length)); } return filename; } function getTotalActiveFromDeploymentMetrics(metrics) { var totalActive = 0; Object.keys(metrics).forEach(function (label) { totalActive += metrics[label].active; }); return totalActive; } function initiateExternalAuthenticationAsync(action, serverUrl) { var message = "A browser is being launched to authenticate your account. Follow the instructions " + ("it displays to complete your " + (action === "register" ? "registration" : action) + "."); exports.log(message); var hostname = os.hostname(); var url = (serverUrl || AccountManager.SERVER_URL) + "/auth/" + action + "?hostname=" + hostname; opener(url); } function link(command) { initiateExternalAuthenticationAsync("link", command.serverUrl); return Q(null); } function login(command) { // Check if one of the flags were provided. if (command.accessKey) { var proxy = getProxy(command.proxy, command.noProxy); exports.sdk = getSdk(command.accessKey, CLI_HEADERS, command.serverUrl, proxy); return exports.sdk.isAuthenticated() .then(function (isAuthenticated) { if (isAuthenticated) { serializeConnectionInfo(command.accessKey, 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, serverUrl, proxy, noProxy) { initiateExternalAuthenticationAsync(action, serverUrl); exports.log(""); // Insert newline return requestAccessKey() .then(function (accessKey) { if (accessKey === null) { // The user has aborted the synchronous prompt (e.g.: via [CTRL]+[C]). return; } exports.sdk = getSdk(accessKey, CLI_HEADERS, serverUrl, getProxy(proxy, noProxy)); return exports.sdk.isAuthenticated() .then(function (isAuthenticated) { if (isAuthenticated) { serializeConnectionInfo(accessKey, false, serverUrl, proxy, noProxy); } else { throw new Error("Invalid access key."); } }); }); } function logout(command) { return Q(null) .then(function () { if (!connectionInfo.preserveAccessKeyOnLogout) { var machineName = os.hostname(); return exports.sdk.removeSession(machineName); } }) .finally(function () { exports.sdk = null; deleteConnectionInfoCache(); }); } function formatDate(unixOffset) { var date = moment(unixOffset); var now = 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, apps) { if (format === "json") { printJson(apps); } else if (format === "table") { var headers = ["Name", "Deployments"]; printTable(headers, function (dataSource) { apps.forEach(function (app, index) { var row = [app.name, wordwrap(50)(app.deployments.join(", "))]; dataSource.push(row); }); }); } } function getCollaboratorDisplayName(email, collaboratorProperties) { return (collaboratorProperties.permission === AccountManager.AppPermission.OWNER) ? email + chalk.magenta(" (Owner)") : email; } function printCollaboratorsList(format, collaborators) { if (format === "json") { var dataSource = { "collaborators": collaborators }; printJson(dataSource); } else if (format === "table") { var headers = ["E-mail Address"]; printTable(headers, function (dataSource) { Object.keys(collaborators).forEach(function (email) { var row = [getCollaboratorDisplayName(email, collaborators[email])]; dataSource.push(row); }); }); } } function printDeploymentList(command, deployments, showPackage) { if (showPackage === void 0) { showPackage = true; } 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, function (dataSource) { deployments.forEach(function (deployment) { 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, deploymentHistory, currentUserEmail) { 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, function (dataSource) { deploymentHistory.forEach(function (packageObject) { var releaseTime = formatDate(packageObject.uploadTime); var releaseSource; if (packageObject.releaseMethod === "Promote") { releaseSource = "Promoted " + packageObject.originalLabel + " from \"" + packageObject.originalDeployment + "\""; } else if (packageObject.releaseMethod === "Rollback") { var labelNumber = parseInt(packageObject.label.substring(1)); var lastLabel = "v" + (labelNumber - 1); releaseSource = "Rolled back " + lastLabel + " to " + packageObject.originalLabel; } if (releaseSource) { releaseTime += "\n" + chalk.magenta("(" + releaseSource + ")").toString(); } var row = [packageObject.label, releaseTime, packageObject.appVersion, packageObject.isMandatory ? "Yes" : "No"]; if (command.displayAuthor) { var releasedBy = 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(function (cellContents) { return applyChalkSkippingLineBreaks(cellContents, chalk.dim); }); } dataSource.push(row); }); }); } } function applyChalkSkippingLineBreaks(applyString, chalkMethod) { // 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(function (token) { return chalkMethod(token); }) .join("\n"); } function getPackageString(packageObject) { if (!packageObject) { return chalk.magenta("No updates released").toString(); } var packageString = 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) { var packageObject = obj; var rolloutString = (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 = packageObject.metrics.totalActive ? packageObject.metrics.active / packageObject.metrics.totalActive * 100 : 0.0; var percentString; if (activePercent === 100.0) { percentString = "100%"; } else if (activePercent === 0.0) { percentString = "0%"; } else { percentString = activePercent.toPrecision(2) + "%"; } var numPending = packageObject.metrics.downloaded - packageObject.metrics.installed - packageObject.metrics.failed; var returnString = 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, projectName) { var fileExists = function (file) { try { return fs.statSync(file).isFile(); } catch (e) { return false; } }; var isValidVersion = function (version) { return !!semver.valid(version) || /^\d+\.\d+$/.test(version); }; exports.log(chalk.cyan("Detecting " + command.platform + " app version:\n")); if (command.platform === "ios") { var resolvedPlistFile = 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 += "-"; } var iOSDirectory = "ios"; var plistFileName = (command.plistFilePrefix || "") + "Info.plist"; var 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."); } } var 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)) { exports.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") { var buildGradlePath = path.join("android", "app", "build.gradle"); if (fileDoesNotExistOrIsDirectory(buildGradlePath)) { throw new Error("Unable to find the \"build.gradle\" file in your \"android/app\" directory."); } return g2js.parseFile(buildGradlePath) .catch(function () { throw new Error("Unable to parse the \"" + buildGradlePath + "\" file. Please ensure it is a well-formed Gradle file."); }) .then(function (buildGradle) { 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."); } var appVersion = 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. exports.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. var propertyName = appVersion.replace("project.", ""); var propertiesFileName = "gradle.properties"; var knownLocations = [ path.join("android", "app", propertiesFileName), path.join("android", propertiesFileName) ]; var propertiesFile = knownLocations.find(fileExists); var propertiesContent = fs.readFileSync(propertiesFile).toString(); try { var parsedProperties = 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)."); } exports.log("Using the target binary version value \"" + appVersion + "\" from the \"" + propertyName + "\" key in the \"" + propertiesFile + "\" file.\n"); return appVersion.toString(); }); } else { var appxManifestFileName = "Package.appxmanifest"; try { var appxManifestContainingFolder = path.join("windows", projectName); var appxManifestContents = 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(function (err) { throw new Error("Unable to parse the \"" + path.join(appxManifestContainingFolder, appxManifestFileName) + "\" file, it could be malformed."); }) .then(function (parsedAppxManifest) { 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) { exports.log(JSON.stringify(object, null, 2)); } function printAccessKeys(format, keys) { if (format === "json") { printJson(keys); } else if (format === "table") { printTable(["Name", "Created", "Expires"], function (dataSource) { var now = new Date().getTime(); function isExpired(key) { return now >= key.expires; } function keyToTableRow(key, dim) { var row = [ key.name, key.createdTime ? formatDate(key.createdTime) : "", formatDate(key.expires) ]; if (dim) { row.forEach(function (col, index) { row[index] = chalk.dim(col); }); } return row; } keys.forEach(function (key) { return !isExpired(key) && dataSource.push(keyToTableRow(key, false)); }); keys.forEach(function (key) { return isExpired(key) && dataSource.push(keyToTableRow(key, true)); }); }); } } function printSessions(format, sessions) { if (format === "json") { printJson(sessions); } else if (format === "table") { printTable(["Machine", "Logged in"], function (dataSource) { sessions.forEach(function (session) { return dataSource.push([session.machineName, formatDate(session.loggedInTime)]); }); }); } } function printTable(columnNames, readData) { var table = new Table({ head: columnNames, style: { head: ["cyan"] } }); readData(table); exports.log(table.toString()); } function register(command) { return loginWithExternalAuthentication("register", command.serverUrl, command.proxy, command.noProxy); } function promote(command) { var packageInfo = { appVersion: command.appStoreVersion, description: command.description, isDisabled: getYargsBooleanOrNull(command.disabled), isMandatory: getYargsBooleanOrNull(command.mandatory), rollout: command.rollout }; return exports.sdk.promote(command.appName, command.sourceDeploymentName, command.destDeploymentName, packageInfo) .then(function () { exports.log("Successfully promoted the \"" + command.sourceDeploymentName + "\" deployment of the \"" + command.appName + "\" app to the \"" + command.destDeploymentName + "\" deployment."); }); } function patch(command) { var 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 exports.sdk.patchRelease(command.appName, command.deploymentName, command.label, packageInfo) .then(function () { exports.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."); } exports.release = function (command) { 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 = command.package; var getPackageFilePromise; var isSingleFilePackage = true; if (fs.lstatSync(filePath).isDirectory()) { isSingleFilePackage = false; getPackageFilePromise = Promise(function (resolve, reject) { var directoryPath = filePath; recursiveFs.readdirr(directoryPath, function (error, directories, files) { if (error) { reject(error); return; } var baseDirectoryPath = path.dirname(directoryPath); var fileName = generateRandomFilename(15) + ".zip"; var zipFile = new yazl.ZipFile(); var writeStream = fs.createWriteStream(fileName); zipFile.outputStream.pipe(writeStream) .on("error", function (error) { reject(error); }) .on("close", function () { filePath = path.join(process.cwd(), fileName); resolve({ isTemporary: true, path: filePath }); }); for (var i = 0; i < files.length; ++i) { var file = files[i]; var relativePath = 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 }); } var lastTotalProgress = 0; var progressBar = new progress("Upload progress:[:bar] :percent :etas", { complete: "=", incomplete: " ", width: 50, total: 100 }); var uploadProgress = function (currentProgress) { progressBar.tick(currentProgress - lastTotalProgress); lastTotalProgress = currentProgress; }; var updateMetadata = { description: command.description, isDisabled: command.disabled, isMandatory: command.mandatory, rollout: command.rollout }; return getPackageFilePromise .then(function (file) { return exports.sdk.release(command.appName, command.deploymentName, file.path, command.appStoreVersion, updateMetadata, uploadProgress) .then(function () { exports.log("Successfully released an update containing the \"" + command.package + "\" " + (isSingleFilePackage ? "file" : "directory") + " to the \"" + command.deploymentName + "\" deployment of the \"" + command.appName + "\" app."); }) .finally(function () { if (file.isTemporary) { fs.unlinkSync(filePath); } }); }); }; exports.releaseCordova = function (command) { var platform = command.platform.toLowerCase(); var projectRoot = process.cwd(); var platformFolder = path.join(projectRoot, "platforms", platform); var platformCordova = path.join(platformFolder, "cordova"); var outputFolder; 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 = command.build ? "build" : "prepare"; var cordovaCLI = "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."); } } exports.log(chalk.cyan("Running \"" + cordovaCLI + " " + cordovaCommand + "\" command:\n")); try { exports.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 = 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 = parseXml(configString); var releaseCommand = command; releaseCommand.package = outputFolder; releaseCommand.type = cli.CommandType.release; return configPromise .catch(function (err) { throw new Error("Unable to parse \"config.xml\" in the CWD. Ensure that the contents of \"config.xml\" is valid XML."); }) .then(function (parsedConfig) { var config = parsedConfig.widget; var releaseTargetVersion; if (command.appStoreVersion) { releaseTargetVersion = command.appStoreVersion; } else { releaseTargetVersion = config["$"].version; } throwForInvalidSemverRange(releaseTargetVersion); releaseCommand.appStoreVersion = releaseTargetVersion; exports.log(chalk.cyan("\nReleasing update contents to CodePush:\n")); return exports.release(releaseCommand); }); }; exports.releaseReact = function (command) { var bundleName = command.bundleName; var entryFile = command.entryFile; var outputFolder = path.join(os.tmpdir(), "CodePush"); var platform = command.platform = command.platform.toLowerCase(); var releaseCommand = 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 = require(path.join(process.cwd(), "package.json")); var projectName = 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 = command.appStoreVersion ? Q(command.appStoreVersion) : getReactNativeProjectAppVersion(command, projectName); return appVersionPromise .then(function (appVersion) { releaseCommand.appStoreVersion = appVersion; return exports.createEmptyTempReleaseFolder(outputFolder); }) .then(function () { return deleteFolder(os.tmpdir() + "/react-*"); }) .then(function () { return exports.runReactNativeBundleCommand(bundleName, command.development || false, entryFile, outputFolder, platform, command.sourcemapOutput); }) .then(function () { exports.log(chalk.cyan("\nReleasing update contents to CodePush:\n")); return exports.release(releaseCommand); }) .then(function () { return deleteFolder(outputFolder); }) .catch(function (err) { deleteFolder(outputFolder); throw err; }); }; function rollback(command) { return exports.confirm() .then(function (wasConfirmed) { if (!wasConfirmed) { exports.log("Rollback cancelled."); return; } return exports.sdk.rollback(command.appName, command.deploymentName, command.targetRelease || undefined) .then(function () { exports.log("Successfully performed a rollback on the \"" + command.deploymentName + "\" deployment of the \"" + command.appName + "\" app."); }); }); } function requestAccessKey() { return Promise(function (resolve, reject, notify) { prompt.message = ""; prompt.delimiter = ""; prompt.start(); prompt.get({ properties: { response: { description: chalk.cyan("Enter your access key: ") } } }, function (err, result) { if (err) { resolve(null); } else { resolve(result.response.trim()); } }); }); } exports.runReactNativeBundleCommand = function (bundleName, development, entryFile, outputFolder, platform, sourcemapOutput) { 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); } exports.log(chalk.cyan("Running \"react-native bundle\" command:\n")); var reactNativeBundleProcess = exports.spawn("node", reactNativeBundleArgs); exports.log("node " + reactNativeBundleArgs.join(" ")); return Promise(function (resolve, reject, notify) { reactNativeBundleProcess.stdout.on("data", function (data) { exports.log(data.toString().trim()); }); reactNativeBundleProcess.stderr.on("data", function (data) { console.error(data.toString().trim()); }); reactNativeBundleProcess.on("close", function (exitCode) { if (exitCode) { reject(new Error("\"react-native bundle\" command exited with code " + exitCode + ".")); } resolve(null); }); }); }; function serializeConnectionInfo(accessKey, preserveAccessKeyOnLogout, customServerUrl, proxy, noProxy) { var connectionInfo = { accessKey: accessKey, preserveAccessKeyOnLogout: preserveAccessKeyOnLogout, proxy: proxy, noProxy: noProxy }; if (customServerUrl) { connectionInfo.customServerUrl = customServerUrl; } var json = JSON.stringify(connectionInfo); fs.writeFileSync(configFilePath, json, { encoding: "utf8" }); exports.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) { throwForInvalidOutputFormat(command.format); return exports.sdk.getSessions() .then(function (sessions) { printSessions(command.format, sessions); }); } function sessionRemove(command) { 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 exports.confirm() .then(function (wasConfirmed) { if (wasConfirmed) { return exports.sdk.removeSession(command.machineName) .then(function () { exports.log("Successfully removed the login session for \"" + command.machineName + "\"."); }); } exports.log("Session removal cancelled."); }); } } function isBinaryOrZip(path) { return path.search(/\.zip$/i) !== -1 || path.search(/\.apk$/i) !== -1 || path.search(/\.ipa$/i) !== -1; } function getYargsBooleanOrNull(value) { // 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) { if (!emailValidator.validate(email)) { throw new Error("\"" + email + "\" is an invalid e-mail address."); } } function throwForInvalidSemverRange(semverRange) { 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) { switch (format) { case "json": case "table": break; default: throw new Error("Invalid format: " + format + "."); } } function whoami(command) { return exports.sdk.getAccountInfo() .then(function (account) { var accountInfo = account.email + " (" + account.linkedProviders.join(", ") + ")"; var connectionInfo = deserializeConnectionInfo(); if (connectionInfo.noProxy || connectionInfo.proxy) { exports.log(chalk.green('Account: ') + accountInfo); var proxyInfo = chalk.green('Proxy: ') + (connectionInfo.noProxy ? 'Ignored' : connectionInfo.proxy); exports.log(proxyInfo); } else { exports.log(accountInfo); } }); } function getProxy(proxy, noProxy) { 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) { return option !== undefined && option !== null; } function getSdk(accessKey, headers, customServerUrl, proxy) { var sdk = new AccountManager(accessKey, CLI_HEADERS, customServerUrl, proxy); /* * If the server returns 401 (Unauthorized), it must be due to an invalid * (probably expired) access key. For convenience, we patch every SDK call * to delete the cached connection if we receive a 401 so the user can simply * login again instead of having to log out first. */ Object.getOwnPropertyNames(AccountManager.prototype).forEach(function (functionName) { if (typeof sdk[functionName] === "function") { var originalFunction = sdk[functionName]; sdk[functionName] = function () { var maybePromise = originalFunction.apply(sdk, arguments); if (maybePromise && maybePromise.then !== undefined) { maybePromise = maybePromise .catch(function (error) { if (error.statusCode && error.statusCode === 401) { deleteConnectionInfoCache(false); error.message = "Invalid credentials. Run the 'code-push login' command to authenticate with the CodePush server."; } throw error; }); } return maybePromise; }; } }); return sdk; }