diff --git a/_build/app.js b/_build/app.js new file mode 100644 index 00000000..ae7f9874 --- /dev/null +++ b/_build/app.js @@ -0,0 +1,27 @@ +"use strict"; +var command = require("./lib/command"); +var common = require("./lib/common"); +var errHandler = require("./lib/errorhandler"); +var loader = require("./lib/loader"); +// Set app root +common.APP_ROOT = __dirname; +var Bootstrap; +(function (Bootstrap) { + function begin() { + return command.getCommand().then(function (cmd) { + common.EXEC_PATH = cmd.execPath; + return loader.load(cmd.execPath, cmd.args).then(function (tfCommand) { + return tfCommand.showBanner().then(function () { + return tfCommand.ensureInitialized().then(function (executor) { + return executor(cmd); + }); + }); + }); + }); + } + Bootstrap.begin = begin; +})(Bootstrap || (Bootstrap = {})); +Bootstrap.begin().then(function () { +}).catch(function (reason) { + errHandler.errLog(reason); +}); diff --git a/_build/exec/build/default.js b/_build/exec/build/default.js new file mode 100644 index 00000000..339dc3ce --- /dev/null +++ b/_build/exec/build/default.js @@ -0,0 +1,32 @@ +"use strict"; +var __extends = (this && this.__extends) || function (d, b) { + for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); +}; +var tfcommand_1 = require("../../lib/tfcommand"); +var args = require("../../lib/arguments"); +function getCommand(args) { + return new BuildBase(args); +} +exports.getCommand = getCommand; +var BuildBase = (function (_super) { + __extends(BuildBase, _super); + function BuildBase() { + _super.apply(this, arguments); + this.description = "Commands for managing Builds."; + } + BuildBase.prototype.setCommandArgs = function () { + _super.prototype.setCommandArgs.call(this); + this.registerCommandArgument("definitionId", "Build Definition ID", "Identifies a build definition.", args.IntArgument, null); + this.registerCommandArgument("definitionName", "Build Definition Name", "Name of a Build Definition.", args.StringArgument, null); + this.registerCommandArgument("status", "Build Status", "Build status filter.", args.StringArgument, null); + this.registerCommandArgument("top", "Number of builds", "Maximum number of builds to return.", args.IntArgument, null); + this.registerCommandArgument("buildId", "Build ID", "Identifies a particular Build.", args.IntArgument); + }; + BuildBase.prototype.exec = function (cmd) { + return this.getHelp(cmd); + }; + return BuildBase; +}(tfcommand_1.TfCommand)); +exports.BuildBase = BuildBase; diff --git a/_build/exec/build/list.js b/_build/exec/build/list.js new file mode 100644 index 00000000..b6e11da1 --- /dev/null +++ b/_build/exec/build/list.js @@ -0,0 +1,77 @@ +"use strict"; +var __extends = (this && this.__extends) || function (d, b) { + for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); +}; +var buildBase = require("./default"); +var buildContracts = require("vso-node-api/interfaces/BuildInterfaces"); +var trace = require("../../lib/trace"); +function getCommand(args) { + return new BuildGetList(args); +} +exports.getCommand = getCommand; +var BuildGetList = (function (_super) { + __extends(BuildGetList, _super); + function BuildGetList() { + _super.apply(this, arguments); + this.description = "Get a list of builds."; + } + BuildGetList.prototype.getHelpArgs = function () { + return ["definitionId", "definitionName", "status", "top", "project"]; + }; + BuildGetList.prototype.exec = function () { + var _this = this; + trace.debug("build-list.exec"); + var buildapi = this.webApi.getBuildApi(); + return Promise.all([ + this.commandArgs.project.val(), + this.commandArgs.definitionId.val(), + this.commandArgs.definitionName.val(), + this.commandArgs.status.val(), + this.commandArgs.top.val() + ]).then(function (values) { + var project = values[0], definitionId = values[1], definitionName = values[2], status = values[3], top = values[4]; + var definitions = null; + if (definitionId) { + definitions = [definitionId]; + } + else if (definitionName) { + trace.debug("No definition Id provided, checking for definitions with name " + definitionName); + return buildapi.getDefinitions(project, definitionName).then(function (defs) { + if (defs.length > 0) { + definitions = [defs[0].id]; + return _this._getBuilds(buildapi, project, definitions, buildContracts.BuildStatus[status], top); + } + else { + trace.debug("No definition found with name " + definitionName); + throw new Error("No definition found with name " + definitionName); + } + }); + } + return _this._getBuilds(buildapi, project, definitions, buildContracts.BuildStatus[status], top); + }); + }; + BuildGetList.prototype.friendlyOutput = function (data) { + if (!data) { + throw new Error("no build supplied"); + } + if (!(data instanceof Array)) { + throw new Error("expected an array of builds"); + } + data.forEach(function (build) { + trace.println(); + trace.info("id : %s", build.id); + trace.info("definition name : %s", build.definition ? build.definition.name : "unknown"); + trace.info("requested by : %s", build.requestedBy ? build.requestedBy.displayName : "unknown"); + trace.info("status : %s", buildContracts.BuildStatus[build.status]); + trace.info("queue time : %s", build.queueTime ? build.queueTime.toJSON() : "unknown"); + }); + }; + BuildGetList.prototype._getBuilds = function (buildapi, project, definitions, status, top) { + // I promise that this was as painful to write as it is to read + return buildapi.getBuilds(project, definitions, null, null, null, null, null, null, buildContracts.BuildStatus[status], null, null, null, top, null, null, null, null, null, null, null, null); + }; + return BuildGetList; +}(buildBase.BuildBase)); +exports.BuildGetList = BuildGetList; diff --git a/_build/exec/build/queue.js b/_build/exec/build/queue.js new file mode 100644 index 00000000..7eed5896 --- /dev/null +++ b/_build/exec/build/queue.js @@ -0,0 +1,77 @@ +"use strict"; +var __extends = (this && this.__extends) || function (d, b) { + for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); +}; +var buildBase = require("./default"); +var buildContracts = require("vso-node-api/interfaces/BuildInterfaces"); +var trace = require("../../lib/trace"); +function describe() { + return "queue a build"; +} +exports.describe = describe; +function getCommand(args) { + return new BuildQueue(args); +} +exports.getCommand = getCommand; +var BuildQueue = (function (_super) { + __extends(BuildQueue, _super); + function BuildQueue() { + _super.apply(this, arguments); + this.description = "Queue a build."; + } + BuildQueue.prototype.getHelpArgs = function () { + return ["project", "definitionId", "definitionName"]; + }; + BuildQueue.prototype.exec = function () { + var _this = this; + var buildapi = this.webApi.getBuildApi(); + return this.commandArgs.project.val().then(function (project) { + return _this.commandArgs.definitionId.val(true).then(function (definitionId) { + var definitionPromise; + if (definitionId) { + definitionPromise = buildapi.getDefinition(definitionId, project); + } + else { + definitionPromise = _this.commandArgs.definitionName.val().then(function (definitionName) { + trace.debug("No definition id provided, Searching for definitions with name: " + definitionName); + return buildapi.getDefinitions(project, definitionName).then(function (definitions) { + if (definitions.length > 0) { + var definition = definitions[0]; + return definition; + } + else { + trace.debug("No definition found with name " + definitionName); + throw new Error("No definition found with name " + definitionName); + } + }); + }); + } + return definitionPromise.then(function (definition) { + return _this._queueBuild(buildapi, definition, project); + }); + }); + }); + }; + BuildQueue.prototype.friendlyOutput = function (build) { + if (!build) { + throw new Error("no build supplied"); + } + trace.println(); + trace.info("id : %s", build.id); + trace.info("definition name : %s", build.definition ? build.definition.name : "unknown"); + trace.info("requested by : %s", build.requestedBy ? build.requestedBy.displayName : "unknown"); + trace.info("status : %s", buildContracts.BuildStatus[build.status]); + trace.info("queue time : %s", build.queueTime ? build.queueTime.toJSON() : "unknown"); + }; + BuildQueue.prototype._queueBuild = function (buildapi, definition, project) { + trace.debug("Queueing build..."); + var build = { + definition: definition + }; + return buildapi.queueBuild(build, project); + }; + return BuildQueue; +}(buildBase.BuildBase)); +exports.BuildQueue = BuildQueue; diff --git a/_build/exec/build/show.js b/_build/exec/build/show.js new file mode 100644 index 00000000..c2898911 --- /dev/null +++ b/_build/exec/build/show.js @@ -0,0 +1,46 @@ +"use strict"; +var __extends = (this && this.__extends) || function (d, b) { + for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); +}; +var buildBase = require("./default"); +var buildContracts = require("vso-node-api/interfaces/BuildInterfaces"); +var trace = require("../../lib/trace"); +function getCommand(args) { + return new BuildShow(args); +} +exports.getCommand = getCommand; +var BuildShow = (function (_super) { + __extends(BuildShow, _super); + function BuildShow() { + _super.apply(this, arguments); + this.description = "Show build details."; + } + BuildShow.prototype.getHelpArgs = function () { + return ["project", "buildId"]; + }; + BuildShow.prototype.exec = function () { + var _this = this; + trace.debug("build-show.exec"); + var buildapi = this.webApi.getBuildApi(); + return this.commandArgs.project.val().then(function (project) { + return _this.commandArgs.buildId.val().then(function (buildId) { + return buildapi.getBuild(buildId, project); + }); + }); + }; + BuildShow.prototype.friendlyOutput = function (build) { + if (!build) { + throw new Error("no build supplied"); + } + trace.println(); + trace.info("id : %s", build.id); + trace.info("definition name : %s", build.definition ? build.definition.name : "unknown"); + trace.info("requested by : %s", build.requestedBy ? build.requestedBy.displayName : "unknown"); + trace.info("status : %s", buildContracts.BuildStatus[build.status]); + trace.info("queue time : %s", build.queueTime ? build.queueTime.toJSON() : "unknown"); + }; + return BuildShow; +}(buildBase.BuildBase)); +exports.BuildShow = BuildShow; diff --git a/_build/exec/build/tasks/create.js b/_build/exec/build/tasks/create.js new file mode 100644 index 00000000..faec4ca1 --- /dev/null +++ b/_build/exec/build/tasks/create.js @@ -0,0 +1,143 @@ +"use strict"; +var __extends = (this && this.__extends) || function (d, b) { + for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); +}; +var check = require("validator"); +var fs = require("fs"); +var path = require("path"); +var shell = require("shelljs"); +var tasksBase = require("./default"); +var trace = require("../../../lib/trace"); +var uuid = require("node-uuid"); +function getCommand(args) { + return new TaskCreate(args); +} +exports.getCommand = getCommand; +var TaskCreate = (function (_super) { + __extends(TaskCreate, _super); + function TaskCreate(args) { + _super.call(this, args, false); + this.description = "Create files for new Build Task"; + } + TaskCreate.prototype.getHelpArgs = function () { + return ["taskName", "friendlyName", "description", "author"]; + }; + TaskCreate.prototype.exec = function () { + trace.debug("build-create.exec"); + return Promise.all([ + this.commandArgs.taskName.val(), + this.commandArgs.friendlyName.val(), + this.commandArgs.description.val(), + this.commandArgs.author.val(), + ]).then(function (values) { + var taskName = values[0], friendlyName = values[1], description = values[2], author = values[3]; + if (!taskName || !check.isAlphanumeric(taskName)) { + throw new Error("name is a required alphanumeric string with no spaces"); + } + if (!friendlyName || !check.isLength(friendlyName, 1, 40)) { + throw new Error("friendlyName is a required string <= 40 chars"); + } + if (!description || !check.isLength(description, 1, 80)) { + throw new Error("description is a required string <= 80 chars"); + } + if (!author || !check.isLength(author, 1, 40)) { + throw new Error("author is a required string <= 40 chars"); + } + var ret = {}; + // create definition + trace.debug("creating folder for task"); + var tp = path.join(process.cwd(), taskName); + trace.debug(tp); + shell.mkdir("-p", tp); + trace.debug("created folder"); + ret.taskPath = tp; + trace.debug("creating definition"); + var def = {}; + def.id = uuid.v1(); + trace.debug("id: " + def.id); + def.name = taskName; + trace.debug("name: " + def.name); + def.friendlyName = friendlyName; + trace.debug("friendlyName: " + def.friendlyName); + def.description = description; + trace.debug("description: " + def.description); + def.author = author; + trace.debug("author: " + def.author); + def.helpMarkDown = "Replace with markdown to show in help"; + def.category = "Utility"; + def.visibility = ["Build", "Release"]; + def.demands = []; + def.version = { Major: "0", Minor: "1", Patch: "0" }; + def.minimumAgentVersion = "1.95.0"; + def.instanceNameFormat = taskName + " $(message)"; + var cwdInput = { + name: "cwd", + type: "filePath", + label: "Working Directory", + defaultValue: "", + required: false, + helpMarkDown: "Current working directory when " + taskName + " is run." + }; + var msgInput = { + name: "msg", + type: "string", + label: "Message", + defaultValue: "Hello World", + required: true, + helpMarkDown: "Message to echo out" + }; + def.inputs = [cwdInput, msgInput]; + def.execution = { + Node: { + target: "sample.js", + argumentFormat: "" + }, + PowerShell3: { + target: "sample.ps1" + } + }; + ret.definition = def; + trace.debug("writing definition file"); + var defPath = path.join(tp, "task.json"); + trace.debug(defPath); + try { + var defStr = JSON.stringify(def, null, 2); + trace.debug(defStr); + fs.writeFileSync(defPath, defStr); + } + catch (err) { + throw new Error("Failed creating task: " + err.message); + } + trace.debug("created definition file."); + var copyResource = function (fileName) { + var src = path.join(__dirname, "_resources", fileName); + trace.debug("src: " + src); + var dest = path.join(tp, fileName); + trace.debug("dest: " + dest); + shell.cp(src, dest); + trace.debug(fileName + " copied"); + }; + trace.debug("creating temporary icon"); + copyResource("icon.png"); + copyResource("sample.js"); + copyResource("sample.ps1"); + return ret; + }); + }; + TaskCreate.prototype.friendlyOutput = function (data) { + if (!data) { + throw new Error("no results"); + } + trace.println(); + trace.success("created task @ %s", data.taskPath); + var def = data.definition; + trace.info("id : %s", def.id); + trace.info("name: %s", def.name); + trace.println(); + trace.info("A temporary task icon was created. Replace with a 32x32 png with transparencies"); + }; + return TaskCreate; +}(tasksBase.BuildTaskBase)); +exports.TaskCreate = TaskCreate; diff --git a/_build/exec/build/tasks/default.js b/_build/exec/build/tasks/default.js new file mode 100644 index 00000000..015b8aa7 --- /dev/null +++ b/_build/exec/build/tasks/default.js @@ -0,0 +1,35 @@ +"use strict"; +var __extends = (this && this.__extends) || function (d, b) { + for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); +}; +var args = require("../../../lib/arguments"); +var buildBase = require("../default"); +function getCommand(args) { + return new BuildTaskBase(args); +} +exports.getCommand = getCommand; +var BuildTaskBase = (function (_super) { + __extends(BuildTaskBase, _super); + function BuildTaskBase() { + _super.apply(this, arguments); + this.description = "Commands for managing Build Tasks."; + } + BuildTaskBase.prototype.setCommandArgs = function () { + _super.prototype.setCommandArgs.call(this); + this.registerCommandArgument("all", "All Tasks?", "Get all build tasks.", args.BooleanArgument, "false"); + this.registerCommandArgument("taskId", "Task ID", "Identifies a particular Build Task.", args.StringArgument); + this.registerCommandArgument("taskPath", "Task path", "Local path to a Build Task.", args.ExistingDirectoriesArgument); + this.registerCommandArgument("overwrite", "Overwrite?", "Overwrite existing Build Task.", args.BooleanArgument, "false"); + this.registerCommandArgument("taskName", "Task Name", "Name of the Build Task.", args.StringArgument); + this.registerCommandArgument("friendlyName", "Friendly Task Name", null, args.StringArgument); + this.registerCommandArgument("description", "Task Description", null, args.StringArgument); + this.registerCommandArgument("author", "Task Author", null, args.StringArgument); + }; + BuildTaskBase.prototype.exec = function (cmd) { + return this.getHelp(cmd); + }; + return BuildTaskBase; +}(buildBase.BuildBase)); +exports.BuildTaskBase = BuildTaskBase; diff --git a/_build/exec/build/tasks/delete.js b/_build/exec/build/tasks/delete.js new file mode 100644 index 00000000..0e509ce0 --- /dev/null +++ b/_build/exec/build/tasks/delete.js @@ -0,0 +1,47 @@ +"use strict"; +var __extends = (this && this.__extends) || function (d, b) { + for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); +}; +var tasksBase = require("./default"); +var trace = require('../../../lib/trace'); +function getCommand(args) { + return new BuildTaskDelete(args); +} +exports.getCommand = getCommand; +var BuildTaskDelete = (function (_super) { + __extends(BuildTaskDelete, _super); + function BuildTaskDelete() { + _super.apply(this, arguments); + this.description = "Delete a Build Task."; + } + BuildTaskDelete.prototype.getHelpArgs = function () { + return ["taskId"]; + }; + BuildTaskDelete.prototype.exec = function () { + var agentApi = this.webApi.getTaskAgentApi(this.connection.getCollectionUrl()); + return this.commandArgs.taskId.val().then(function (taskId) { + return agentApi.getTaskDefinitions(taskId).then(function (tasks) { + if (tasks && tasks.length > 0) { + trace.debug("Deleting task(s)..."); + return agentApi.deleteTaskDefinition(taskId).then(function () { + return { + id: taskId + }; + }); + } + else { + trace.debug("No such task."); + throw new Error("No task found with provided ID: " + taskId); + } + }); + }); + }; + BuildTaskDelete.prototype.friendlyOutput = function (data) { + trace.println(); + trace.success('Task %s deleted successfully!', data.id); + }; + return BuildTaskDelete; +}(tasksBase.BuildTaskBase)); +exports.BuildTaskDelete = BuildTaskDelete; diff --git a/_build/exec/build/tasks/list.js b/_build/exec/build/tasks/list.js new file mode 100644 index 00000000..1a59f641 --- /dev/null +++ b/_build/exec/build/tasks/list.js @@ -0,0 +1,114 @@ +"use strict"; +var __extends = (this && this.__extends) || function (d, b) { + for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); +}; +var tasksBase = require("./default"); +var trace = require('../../../lib/trace'); +function getCommand(args) { + return new BuildTaskList(args); +} +exports.getCommand = getCommand; +var BuildTaskList = (function (_super) { + __extends(BuildTaskList, _super); + function BuildTaskList() { + _super.apply(this, arguments); + this.description = "Get a list of build tasks"; + } + BuildTaskList.prototype.getHelpArgs = function () { + return ["all"]; + }; + BuildTaskList.prototype.exec = function () { + var _this = this; + var agentapi = this.webApi.getTaskAgentApi(this.connection.getCollectionUrl()); + trace.debug("Searching for build tasks..."); + return agentapi.getTaskDefinitions(null, ['build'], null).then(function (tasks) { + trace.debug("Retrieved " + tasks.length + " build tasks from server."); + return _this.commandArgs.all.val().then(function (all) { + if (all) { + trace.debug("Listing all build tasks."); + return tasks; + } + else { + trace.debug("Filtering build tasks to give only the latest versions."); + return _this._getNewestTasks(tasks); + } + }); + }); + }; + /* + * takes a list of non-unique task definitions and returns only the newest unique definitions + * TODO: move this code to the server, add a parameter to the controllers + */ + BuildTaskList.prototype._getNewestTasks = function (allTasks) { + var taskDictionary = {}; + for (var i = 0; i < allTasks.length; i++) { + var currTask = allTasks[i]; + if (taskDictionary[currTask.id]) { + var newVersion = new TaskVersion(currTask.version); + var knownVersion = new TaskVersion(taskDictionary[currTask.id].version); + trace.debug("Found additional version of " + currTask.name + " and comparing to the previously encountered version."); + if (this._compareTaskVersion(newVersion, knownVersion) > 0) { + trace.debug("Found newer version of " + currTask.name + ". Previous: " + knownVersion.toString() + "; New: " + newVersion.toString()); + taskDictionary[currTask.id] = currTask; + } + } + else { + trace.debug("Found task " + currTask.name); + taskDictionary[currTask.id] = currTask; + } + } + var newestTasks = []; + for (var id in taskDictionary) { + newestTasks.push(taskDictionary[id]); + } + return newestTasks; + }; + /* + * compares two versions of tasks, which are stored in version objects with fields 'major', 'minor', and 'patch' + * @return positive value if version1 > version2, negative value if version2 > version1, 0 otherwise + */ + BuildTaskList.prototype._compareTaskVersion = function (version1, version2) { + if (version1.major != version2.major) { + return version1.major - version2.major; + } + if (version1.minor != version2.minor) { + return version1.minor - version2.minor; + } + if (version1.patch != version2.patch) { + return version1.patch - version2.patch; + } + return 0; + }; + BuildTaskList.prototype.friendlyOutput = function (data) { + if (!data) { + throw new Error('no tasks supplied'); + } + if (!(data instanceof Array)) { + throw new Error('expected an array of tasks'); + } + data.forEach(function (task) { + trace.println(); + trace.info('id : %s', task.id); + trace.info('name : %s', task.name); + trace.info('friendly name : %s', task.friendlyName); + trace.info('visibility : %s', task.visibility ? task.visibility.join(",") : ""); + trace.info('description : %s', task.description); + trace.info('version : %s', new TaskVersion(task.version).toString()); + }); + }; + return BuildTaskList; +}(tasksBase.BuildTaskBase)); +exports.BuildTaskList = BuildTaskList; +var TaskVersion = (function () { + function TaskVersion(versionData) { + this.major = versionData.major || 0; + this.minor = versionData.minor || 0; + this.patch = versionData.patch || 0; + } + TaskVersion.prototype.toString = function () { + return this.major + "." + this.minor + "." + this.patch; + }; + return TaskVersion; +}()); diff --git a/_build/exec/build/tasks/upload.js b/_build/exec/build/tasks/upload.js new file mode 100644 index 00000000..7bda3b88 --- /dev/null +++ b/_build/exec/build/tasks/upload.js @@ -0,0 +1,60 @@ +"use strict"; +var __extends = (this && this.__extends) || function (d, b) { + for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); +}; +var archiver = require('archiver'); +var path = require('path'); +var tasksBase = require("./default"); +var trace = require('../../../lib/trace'); +var vm = require('../../../lib/jsonvalidate'); +function getCommand(args) { + return new BuildTaskUpload(args); +} +exports.getCommand = getCommand; +var c_taskJsonFile = 'task.json'; +var BuildTaskUpload = (function (_super) { + __extends(BuildTaskUpload, _super); + function BuildTaskUpload() { + _super.apply(this, arguments); + this.description = "Upload a Build Task."; + } + BuildTaskUpload.prototype.getHelpArgs = function () { + return ["taskPath", "overwrite"]; + }; + BuildTaskUpload.prototype.exec = function () { + var _this = this; + return this.commandArgs.taskPath.val().then(function (taskPaths) { + var taskPath = taskPaths[0]; + return _this.commandArgs.overwrite.val().then(function (overwrite) { + vm.exists(taskPath, 'specified directory ' + taskPath + ' does not exist.'); + //directory is good, check json + var tp = path.join(taskPath, c_taskJsonFile); + return vm.validate(tp, 'no ' + c_taskJsonFile + ' in specified directory').then(function (taskJson) { + var archive = archiver('zip'); + archive.on('error', function (error) { + trace.debug('Archiving error: ' + error.message); + error.message = 'Archiving error: ' + error.message; + throw error; + }); + archive.directory(path.resolve(taskPath), false); + var agentApi = _this.webApi.getTaskAgentApi(_this.connection.getCollectionUrl()); + archive.finalize(); + return agentApi.uploadTaskDefinition(null, archive, taskJson.id, overwrite).then(function (task) { + trace.debug('Success'); + return { + sourceLocation: taskPath + }; + }); + }); + }); + }); + }; + BuildTaskUpload.prototype.friendlyOutput = function (data) { + trace.println(); + trace.success('Task at %s uploaded successfully!', data.sourceLocation); + }; + return BuildTaskUpload; +}(tasksBase.BuildTaskBase)); +exports.BuildTaskUpload = BuildTaskUpload; diff --git a/_build/exec/default.js b/_build/exec/default.js new file mode 100644 index 00000000..7df8634c --- /dev/null +++ b/_build/exec/default.js @@ -0,0 +1,22 @@ +"use strict"; +var __extends = (this && this.__extends) || function (d, b) { + for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); +}; +var tfcommand_1 = require("../lib/tfcommand"); +function getCommand(args) { + return new DefaultCommand(args); +} +exports.getCommand = getCommand; +var DefaultCommand = (function (_super) { + __extends(DefaultCommand, _super); + function DefaultCommand(passedArgs) { + _super.call(this, passedArgs, false); + } + DefaultCommand.prototype.exec = function (cmd) { + return this.getHelp(cmd); + }; + return DefaultCommand; +}(tfcommand_1.TfCommand)); +exports.DefaultCommand = DefaultCommand; diff --git a/_build/exec/extension/_lib/extension-composer-factory.js b/_build/exec/extension/_lib/extension-composer-factory.js new file mode 100644 index 00000000..f79b6dbd --- /dev/null +++ b/_build/exec/extension/_lib/extension-composer-factory.js @@ -0,0 +1,63 @@ +"use strict"; +var composer_1 = require("./targets/Microsoft.VisualStudio.Services/composer"); +var composer_2 = require("./targets/Microsoft.VisualStudio.Services.Integration/composer"); +var composer_3 = require("./targets/Microsoft.VisualStudio.Offer/composer"); +var trace = require("../../../lib/trace"); +var ComposerFactory = (function () { + function ComposerFactory() { + } + ComposerFactory.GetComposer = function (settings, targets) { + var composers = []; + // @TODO: Targets should be declared by the composer + targets.forEach(function (target) { + switch (target.id) { + case "Microsoft.VisualStudio.Services": + case "Microsoft.VisualStudio.Services.Cloud": + composers.push(new composer_1.VSSExtensionComposer(settings)); + break; + case "Microsoft.VisualStudio.Services.Integration": + composers.push(new composer_2.VSSIntegrationComposer(settings)); + break; + case "Microsoft.VisualStudio.Offer": + composers.push(new composer_3.VSOfferComposer(settings)); + break; + default: + trace.warn("'" + target.id + "' is not a recognized target. Defualting to Microsoft.VisualStudio.Services."); + break; + } + }); + if (composers.length === 0) { + if (targets.length === 0) { + throw "No recognized targets found. Ensure that your manifest includes a target property. E.g. \"targets\":[{\"id\":\"Microsoft.VisualStudio.Services\"}],..."; + } + else { + composers.push(new composer_1.VSSExtensionComposer(settings)); + } + } + // Build a new type of composer on the fly that is the + // concatenation of all of the composers necessary for + // this extension. + var PolyComposer = (function () { + function PolyComposer(settings) { + this.settings = settings; + } + PolyComposer.prototype.getBuilders = function () { + return composers.reduce(function (p, c) { + return p.concat(c.getBuilders()); + }, []); + }; + PolyComposer.prototype.validate = function (components) { + return Promise.all(composers.reduce(function (p, c) { + return p.concat(c.validate(components)); + }, [])).then(function (multi) { + // flatten + return multi.reduce(function (p, c) { return p.concat(c); }, []); + }); + }; + return PolyComposer; + })(); + return new PolyComposer(settings); + }; + return ComposerFactory; +}()); +exports.ComposerFactory = ComposerFactory; diff --git a/_build/exec/extension/_lib/extension-composer.js b/_build/exec/extension/_lib/extension-composer.js new file mode 100644 index 00000000..c68a6ed3 --- /dev/null +++ b/_build/exec/extension/_lib/extension-composer.js @@ -0,0 +1,115 @@ +"use strict"; +var vsix_manifest_builder_1 = require("./vsix-manifest-builder"); +var _ = require("lodash"); +var Q = require("q"); +var ExtensionComposer = (function () { + function ExtensionComposer(settings) { + this.settings = settings; + } + ExtensionComposer.prototype.getBuilders = function () { + return [new vsix_manifest_builder_1.VsixManifestBuilder(this.settings.root)]; + }; + /** + * Return a string[] of validation errors + */ + ExtensionComposer.prototype.validate = function (components) { + // Take the validators and run each's method against the vsix manifest's data + var errorMessages = Object.keys(ExtensionComposer.vsixValidators).map(function (path) { return ExtensionComposer.vsixValidators[path](_.get(components.builders.filter(function (b) { return b.getType() === vsix_manifest_builder_1.VsixManifestBuilder.manifestType; })[0].getData(), path)); }).filter(function (r) { return !!r; }); + return Q.resolve(errorMessages); + }; + // Basic/global extension validations. + ExtensionComposer.vsixValidators = { + "PackageManifest.Metadata[0].Identity[0].$.Id": function (value) { + if (/^[A-z0-9_-]+$/.test(value)) { + return null; + } + else { + return "'extensionId' may only include letters, numbers, underscores, and dashes."; + } + }, + "PackageManifest.Metadata[0].Identity[0].$.Version": function (value) { + if (typeof value === "string" && value.length > 0) { + return null; + } + else { + return "'version' must be provided."; + } + }, + "PackageManifest.Metadata[0].Description[0]._": function (value) { + if (!value || value.length <= 200) { + return null; + } + else { + return "'description' must be less than 200 characters."; + } + }, + "PackageManifest.Metadata[0].DisplayName[0]": function (value) { + if (typeof value === "string" && value.length > 0) { + return null; + } + else { + return "'name' must be provided."; + } + }, + "PackageManifest.Assets[0].Asset": function (value) { + var usedAssetTypes = {}; + if (_.isArray(value)) { + for (var i = 0; i < value.length; ++i) { + var asset = value[i].$; + if (asset) { + if (!asset.Path) { + return "All 'files' must include a 'path'."; + } + if (asset.Type && asset.Addressable) { + if (usedAssetTypes[asset.Type]) { + return "Cannot have multiple 'addressable' files with the same 'assetType'.\nFile1: " + usedAssetTypes[asset.Type] + ", File 2: " + asset.Path + " (asset type: " + asset.Type + ")"; + } + else { + usedAssetTypes[asset.Type] = asset.Path; + } + } + } + } + } + return null; + }, + "PackageManifest.Metadata[0].Identity[0].$.Publisher": function (value) { + if (typeof value === "string" && value.length > 0) { + return null; + } + else { + return "'publisher' must be provided."; + } + }, + "PackageManifest.Metadata[0].Categories[0]": function (value) { + if (!value) { + return null; + } + var categories = value.split(","); + if (categories.length > 1) { + return "For now, extensions are limited to a single category."; + } + var validCategories = [ + "Build and release", + "Collaborate", + "Code", + "Test", + "Plan and track", + "Insights", + "Integrate", + "Developer samples" + ]; + _.remove(categories, function (c) { return !c; }); + var badCategories = categories.filter(function (c) { return validCategories.indexOf(c) < 0; }); + return badCategories.length ? "The following categories are not valid: " + badCategories.join(", ") + ". Valid categories are: " + validCategories.join(", ") + "." : null; + }, + "PackageManifest.Installation[0].InstallationTarget": function (value) { + if (_.isArray(value) && value.length > 0) { + return null; + } + return "Your manifest must include at least one 'target'."; + } + }; + return ExtensionComposer; +}()); +exports.ExtensionComposer = ExtensionComposer; diff --git a/_build/exec/extension/_lib/extensioninfo.js b/_build/exec/extension/_lib/extensioninfo.js new file mode 100644 index 00000000..3ac602ce --- /dev/null +++ b/_build/exec/extension/_lib/extensioninfo.js @@ -0,0 +1,57 @@ +"use strict"; +var _ = require('lodash'); +var Q = require('q'); +var trace = require('../../../lib/trace'); +var fs = require('fs'); +var xml2js = require("xml2js"); +var zip = require("jszip"); +function getExtInfo(vsixPath, extensionId, publisherName, cachedInfo) { + trace.debug('extensioninfo.getExtInfo'); + var vsixInfoPromise; + if (cachedInfo) { + return Q.resolve(cachedInfo); + } + else if (extensionId && publisherName) { + vsixInfoPromise = Q.resolve({ id: extensionId, publisher: publisherName, version: null }); + } + else if (vsixPath) { + vsixInfoPromise = Q.Promise(function (resolve, reject, notify) { + fs.readFile(vsixPath, function (err, data) { + if (err) + reject(err); + trace.debug(vsixPath); + trace.debug("Read vsix as zip... Size (bytes): %s", data.length.toString()); + try { + resolve(new zip(data)); + } + catch (err) { + reject(err); + } + }); + }).then(function (zip) { + trace.debug("Files in the zip: %s", Object.keys(zip.files).join(", ")); + var vsixManifestFileNames = Object.keys(zip.files).filter(function (key) { return _.endsWith(key, "vsixmanifest"); }); + if (vsixManifestFileNames.length > 0) { + return Q.nfcall(xml2js.parseString, zip.files[vsixManifestFileNames[0]].asText()); + } + else { + throw new Error("Could not locate vsix manifest!"); + } + }).then(function (vsixManifestAsJson) { + var foundExtId = extensionId || _.get(vsixManifestAsJson, "PackageManifest.Metadata[0].Identity[0].$.Id"); + var foundPublisher = publisherName || _.get(vsixManifestAsJson, "PackageManifest.Metadata[0].Identity[0].$.Publisher"); + var extensionVersion = _.get(vsixManifestAsJson, "PackageManifest.Metadata[0].Identity[0].$.Version"); + if (foundExtId && foundPublisher) { + return { id: foundExtId, publisher: foundPublisher, version: extensionVersion }; + } + else { + throw new Error("Could not locate both the extension id and publisher in vsix manfiest! Ensure your manifest includes both a namespace and a publisher property, or specify the necessary --publisher and/or --extension options."); + } + }); + } + else { + throw new Error("Either --vsix or BOTH of --extensionid and --name is required"); + } + return vsixInfoPromise; +} +exports.getExtInfo = getExtInfo; diff --git a/_build/exec/extension/_lib/interfaces.js b/_build/exec/extension/_lib/interfaces.js new file mode 100644 index 00000000..3918c74e --- /dev/null +++ b/_build/exec/extension/_lib/interfaces.js @@ -0,0 +1 @@ +"use strict"; diff --git a/_build/exec/extension/_lib/loc.js b/_build/exec/extension/_lib/loc.js new file mode 100644 index 00000000..8bb5595c --- /dev/null +++ b/_build/exec/extension/_lib/loc.js @@ -0,0 +1,187 @@ +"use strict"; +var vsix_manifest_builder_1 = require("./vsix-manifest-builder"); +var _ = require("lodash"); +var fs = require("fs"); +var trace = require("../../../lib/trace"); +var mkdirp = require('mkdirp'); +var path = require("path"); +var Q = require("q"); +var LocPrep; +(function (LocPrep) { + /** + * Creates a deep copy of document, replacing resource keys with the values from + * the resources object. + * If a resource cannot be found, the same string from the defaults document will be substituted. + * The defaults object must have the same structure/schema as document. + */ + function makeReplacements(document, resources, defaults) { + var locDocument = _.isArray(document) ? [] : {}; + for (var key in document) { + if (propertyIsComment(key)) { + continue; + } + else if (_.isObject(document[key])) { + locDocument[key] = makeReplacements(document[key], resources, defaults); + } + else if (_.isString(document[key]) && _.startsWith(document[key], "resource:")) { + var resourceKey = document[key].substr("resource:".length).trim(); + var replacement = resources[resourceKey]; + if (!_.isString(replacement)) { + replacement = defaults[resourceKey]; + trace.warn("Could not find a replacement for resource key %s. Falling back to '%s'.", resourceKey, replacement); + } + locDocument[key] = replacement; + } + else { + locDocument[key] = document[key]; + } + } + return locDocument; + } + LocPrep.makeReplacements = makeReplacements; + /** + * If the resjsonPath setting is set... + * Check if the path exists. If it does, check if it's a directory. + * If it's a directory, write to path + extension.resjson + * All other cases just write to path. + */ + function writeResourceFile(fullResjsonPath, resources) { + return Q.Promise(function (resolve, reject, notify) { + fs.exists(fullResjsonPath, function (exists) { + resolve(exists); + }); + }).then(function (exists) { + if (exists) { + return Q.nfcall(fs.lstat, fullResjsonPath).then(function (obj) { + return obj.isDirectory(); + }).then(function (isDir) { + if (isDir) { + return path.join(fullResjsonPath, "extension.resjson"); + } + else { + return fullResjsonPath; + } + }); + } + else { + return Q.resolve(fullResjsonPath); + } + }).then(function (determinedPath) { + return Q.nfcall(mkdirp, path.dirname(determinedPath)).then(function () { + return Q.nfcall(fs.writeFile, determinedPath, JSON.stringify(resources, null, 4), "utf8"); + }); + }); + } + LocPrep.writeResourceFile = writeResourceFile; + function propertyIsComment(property) { + return _.startsWith(property, "_") && _.endsWith(property, ".comment"); + } + LocPrep.propertyIsComment = propertyIsComment; + var LocKeyGenerator = (function () { + function LocKeyGenerator(manifestBuilders) { + this.manifestBuilders = manifestBuilders; + this.initStringObjs(); + // find the vsixmanifest and pull it out because we treat it a bit differently + var vsixManifest = manifestBuilders.filter(function (b) { return b.getType() === vsix_manifest_builder_1.VsixManifestBuilder.manifestType; }); + if (vsixManifest.length === 1) { + this.vsixManifestBuilder = vsixManifest[0]; + } + else { + throw "Found " + vsixManifest.length + " vsix manifest builders (expected 1). Something is not right!"; + } + } + LocKeyGenerator.prototype.initStringObjs = function () { + var _this = this; + this.resourceFileMap = {}; + this.manifestBuilders.forEach(function (b) { + _this.resourceFileMap[b.getType()] = {}; + }); + this.combined = {}; + }; + /** + * Destructive method modifies the manifests by replacing i18nable strings with resource: + * keys. Adds all the original resources to the resources object. + */ + LocKeyGenerator.prototype.generateLocalizationKeys = function () { + var _this = this; + this.initStringObjs(); + this.manifestBuilders.forEach(function (builder) { + if (builder.getType() !== vsix_manifest_builder_1.VsixManifestBuilder.manifestType) { + _this.jsonReplaceWithKeysAndGenerateDefaultStrings(builder); + } + }); + this.vsixGenerateDefaultStrings(); + return { + manifestResources: this.resourceFileMap, + combined: this.generateCombinedResourceFile() + }; + }; + LocKeyGenerator.prototype.generateCombinedResourceFile = function () { + var _this = this; + var combined = {}; + var resValues = Object.keys(this.resourceFileMap).map(function (k) { return _this.resourceFileMap[k]; }); + // the .d.ts file falls short in this case + var anyAssign = _.assign; + anyAssign.apply(void 0, [combined].concat(resValues)); + return combined; + }; + LocKeyGenerator.prototype.addResource = function (builderType, sourceKey, resourceKey, obj) { + var resourceVal = this.removeI18nPrefix(obj[sourceKey]); + this.resourceFileMap[builderType][resourceKey] = resourceVal; + var comment = obj["_" + sourceKey + ".comment"]; + if (comment) { + this.resourceFileMap[builderType]["_" + resourceKey + ".comment"] = comment; + } + obj[sourceKey] = "resource:" + resourceKey; + }; + LocKeyGenerator.prototype.removeI18nPrefix = function (str) { + if (_.startsWith(str, LocKeyGenerator.I18N_PREFIX)) { + return str.substr(LocKeyGenerator.I18N_PREFIX.length); + } + return str; + }; + LocKeyGenerator.prototype.vsixGenerateDefaultStrings = function () { + var vsixManifest = this.vsixManifestBuilder.getData(); + var displayName = this.removeI18nPrefix(_.get(vsixManifest, "PackageManifest.Metadata[0].DisplayName[0]")); + var description = this.removeI18nPrefix(_.get(vsixManifest, "PackageManifest.Metadata[0].Description[0]._")); + var releaseNotes = this.removeI18nPrefix(_.get(vsixManifest, "PackageManifest.Metadata[0].ReleaseNotes[0]")); + var vsixRes = {}; + if (displayName) { + vsixRes["displayName"] = displayName; + _.set(vsixManifest, "PackageManifest.Metadata[0].DisplayName[0]", displayName); + } + if (displayName) { + vsixRes["description"] = description; + _.set(vsixManifest, "PackageManifest.Metadata[0].Description[0]._", description); + } + if (releaseNotes) { + vsixRes["releaseNotes"] = releaseNotes; + _.set(vsixManifest, "PackageManifest.Metadata[0].ReleaseNotes[0]", releaseNotes); + } + this.resourceFileMap[this.vsixManifestBuilder.getType()] = vsixRes; + }; + LocKeyGenerator.prototype.jsonReplaceWithKeysAndGenerateDefaultStrings = function (builder, json, path) { + if (json === void 0) { json = null; } + if (path === void 0) { path = ""; } + if (!json) { + json = builder.getData(); + } + for (var key in json) { + var val = json[key]; + if (_.isObject(val)) { + var nextPath = builder.getLocKeyPath(path + key + "."); + while (_.endsWith(nextPath, ".")) { + nextPath = nextPath.substr(0, nextPath.length - 1); + } + this.jsonReplaceWithKeysAndGenerateDefaultStrings(builder, val, nextPath); + } + else if (_.isString(val) && _.startsWith(val, LocKeyGenerator.I18N_PREFIX)) { + this.addResource(builder.getType(), key, path + key, json); + } + } + }; + LocKeyGenerator.I18N_PREFIX = "i18n:"; + return LocKeyGenerator; + }()); + LocPrep.LocKeyGenerator = LocKeyGenerator; +})(LocPrep = exports.LocPrep || (exports.LocPrep = {})); diff --git a/_build/exec/extension/_lib/manifest.js b/_build/exec/extension/_lib/manifest.js new file mode 100644 index 00000000..05f336b1 --- /dev/null +++ b/_build/exec/extension/_lib/manifest.js @@ -0,0 +1,191 @@ +"use strict"; +var utils_1 = require("./utils"); +var _ = require("lodash"); +var common = require("../../../lib/common"); +var os = require("os"); +var path = require("path"); +var trace = require('../../../lib/trace'); +var ManifestBuilder = (function () { + function ManifestBuilder(extRoot) { + this.extRoot = extRoot; + this.packageFiles = {}; + this.lcPartNames = {}; + this.data = {}; + } + /** + * Gets the path to the localized resource associated with this manifest + */ + ManifestBuilder.prototype.getLocPath = function () { + return this.getPath(); + }; + /** + * Called just before the package is written to make any final adjustments. + */ + ManifestBuilder.prototype.finalize = function (files, builders) { + return Promise.resolve(null); + }; + /** + * Gives the manifest the chance to transform the key that is used when generating the localization + * strings file. Path will be a dot-separated set of keys to address the string (or another + * object/array) in question. See vso-manifest-builder for an example. + */ + ManifestBuilder.prototype.getLocKeyPath = function (path) { + return path; + }; + /** + * Write this manifest to a stream. + */ + ManifestBuilder.prototype.getResult = function () { + return JSON.stringify(utils_1.removeMetaKeys(this.data), null, 4).replace(/\n/g, os.EOL); + }; + /** + * Gets the contents of the file that will serve as localization for this asset. + * Default implementation returns JSON with all strings replaced given by the translations/defaults objects. + */ + ManifestBuilder.prototype.getLocResult = function (translations, defaults) { + return JSON.stringify(this._getLocResult(this.expandResourceFile(translations), this.expandResourceFile(defaults)), null, 4); + }; + ManifestBuilder.prototype._getLocResult = function (translations, defaults, locData, currentPath) { + var _this = this; + if (locData === void 0) { locData = {}; } + if (currentPath === void 0) { currentPath = ""; } + var currentData = currentPath ? _.get(this.data, currentPath) : this.data; + // CurrentData should be guaranteed to be + // This magically works for arrays too, just go with it. + Object.keys(currentData).forEach(function (key) { + var nextPath = currentPath + "." + key; + if (_.isString(currentData[key])) { + var translation = _.get(translations, nextPath); + if (translation !== undefined) { + _.set(locData, nextPath, translation); + } + else { + var defaultString = _.get(defaults, nextPath); + if (defaultString !== undefined) { + _.set(locData, nextPath, defaultString); + } + else { + throw "Couldn't find a default string - this is definitely a bug."; + } + } + } + else if (_.isObject(currentData[key])) { + _this._getLocResult(translations, defaults, locData, nextPath); + } + else { + // must be a number of boolean + _.set(locData, nextPath, currentData[key]); + } + }); + return locData; + }; + /** + * Resource files are flat key-value pairs where the key is the json "path" to the original element. + * This routine expands the resource files back into their original schema + */ + ManifestBuilder.prototype.expandResourceFile = function (resources) { + var expanded = {}; + Object.keys(resources).forEach(function (path) { + _.set(expanded, path, resources[path]); + }); + return expanded; + }; + /** + * Get the raw JSON data. Please do not modify it. + */ + ManifestBuilder.prototype.getData = function () { + return this.data; + }; + Object.defineProperty(ManifestBuilder.prototype, "files", { + /** + * Get a list of files to be included in the package + */ + get: function () { + return this.packageFiles; + }, + enumerable: true, + configurable: true + }); + /** + * Set 'value' to data[path] in this manifest if it has not been set, or if override is true. + * If it has been set, issue a warning. + */ + ManifestBuilder.prototype.singleValueProperty = function (path, value, manifestKey, override) { + if (override === void 0) { override = false; } + var existingValue = _.get(this.data, path); + if (!override && existingValue !== undefined) { + trace.warn("Multiple values found for '%s'. Ignoring future occurrences and using the value '%s'.", manifestKey, JSON.stringify(existingValue, null, 4)); + return false; + } + else { + _.set(this.data, path, value); + return true; + } + }; + /** + * Read a value as a delimited string or array and concat it to the existing list at data[path] + */ + ManifestBuilder.prototype.handleDelimitedList = function (value, path, delimiter, uniq) { + if (delimiter === void 0) { delimiter = ","; } + if (uniq === void 0) { uniq = true; } + if (_.isString(value)) { + value = value.split(delimiter); + _.remove(value, function (v) { return v === ""; }); + } + var items = _.get(this.data, path, "").split(delimiter); + _.remove(items, function (v) { return v === ""; }); + var val = items.concat(value); + if (uniq) { + val = _.uniq(val); + } + _.set(this.data, path, val.join(delimiter)); + }; + /** + * Add a file to the vsix package + */ + ManifestBuilder.prototype.addFile = function (file) { + if (typeof file.assetType === "string") { + file.assetType = [file.assetType]; + } + file.path = utils_1.cleanAssetPath(file.path, this.extRoot); + if (!file.partName) { + file.partName = "/" + path.relative(this.extRoot, file.path); + } + if (!file.partName) { + throw "Every file must have a path specified name."; + } + file.partName = utils_1.forwardSlashesPath(file.partName); + // Default the assetType to the partName. + if (file.addressable && !file.assetType) { + file.assetType = [utils_1.toZipItemName(file.partName)]; + } + if (this.packageFiles[file.path]) { + if (_.isArray(this.packageFiles[file.path].assetType) && file.assetType) { + file.assetType = (this.packageFiles[file.path].assetType).concat(file.assetType); + this.packageFiles[file.path].assetType = file.assetType; + } + } + // Files added recursively, i.e. from a directory, get lower + // priority than those specified explicitly. Therefore, if + // the file has already been added to the package list, don't + // re-add (overwrite) with this file if it is an auto (from a dir) + if (file.auto && this.packageFiles[file.path]) { + } + else { + var existPartName = this.lcPartNames[file.partName.toLowerCase()]; + if (!existPartName || file.partName === existPartName) { + // key off a guid if there is no file path. + this.packageFiles[file.path || common.newGuid()] = file; + this.lcPartNames[file.partName.toLowerCase()] = file.partName; + } + else { + throw "All files in the package must have a case-insensitive unique filename. Trying to add " + file.partName + ", but " + existPartName + " was already added to the package."; + } + } + if (file.contentType && this.packageFiles[file.path]) { + this.packageFiles[file.path].contentType = file.contentType; + } + }; + return ManifestBuilder; +}()); +exports.ManifestBuilder = ManifestBuilder; diff --git a/_build/exec/extension/_lib/merger.js b/_build/exec/extension/_lib/merger.js new file mode 100644 index 00000000..26276228 --- /dev/null +++ b/_build/exec/extension/_lib/merger.js @@ -0,0 +1,213 @@ +"use strict"; +var extension_composer_factory_1 = require("./extension-composer-factory"); +var _ = require("lodash"); +var fs = require("fs"); +var glob = require("glob"); +var jsonInPlace = require("json-in-place"); +var loc = require("./loc"); +var path = require("path"); +var Q = require("q"); +var qfs = require("../../../lib/qfs"); +var trace = require("../../../lib/trace"); +var version = require("../../../lib/version"); +/** + * Facilitates the gathering/reading of partial manifests and creating the merged + * manifests (one for each manifest builder) + */ +var Merger = (function () { + /** + * constructor. Instantiates one of each manifest builder. + */ + function Merger(settings) { + this.settings = settings; + this.manifestBuilders = []; + } + Merger.prototype.gatherManifests = function () { + var _this = this; + trace.debug('merger.gatherManifests'); + if (this.settings.manifestGlobs && this.settings.manifestGlobs.length > 0) { + var globs = this.settings.manifestGlobs.map(function (p) { return path.isAbsolute(p) ? p : path.join(_this.settings.root, p); }); + trace.debug('merger.gatherManifestsFromGlob'); + var promises = globs.map(function (pattern) { return Q.nfcall(glob, pattern); }); + return Promise.all(promises) + .then(function (results) { return _.uniq(_.flatten(results)); }) + .then(function (results) { + if (results.length > 0) { + trace.debug("Merging %s manifests from the following paths: ", results.length.toString()); + results.forEach(function (path) { return trace.debug(path); }); + } + else { + throw new Error("No manifests found from the following glob patterns: \n" + _this.settings.manifestGlobs.join("\n")); + } + return results; + }); + } + else { + var manifests = this.settings.manifests; + if (!manifests || manifests.length === 0) { + return Q.reject("No manifests specified."); + } + this.settings.manifests = _.uniq(manifests).map(function (m) { return path.resolve(m); }); + trace.debug("Merging %s manifest%s from the following paths: ", manifests.length.toString(), manifests.length === 1 ? "" : "s"); + manifests.forEach(function (path) { return trace.debug(path); }); + return Q.resolve(this.settings.manifests); + } + }; + /** + * Finds all manifests and merges them into two JS Objects: vsoManifest and vsixManifest + * @return Q.Promise An object containing the two manifests + */ + Merger.prototype.merge = function () { + var _this = this; + trace.debug('merger.merge'); + return this.gatherManifests().then(function (files) { + var overridesProvided = false; + var manifestPromises = []; + files.forEach(function (file) { + manifestPromises.push(Q.nfcall(fs.readFile, file, "utf8").then(function (data) { + var jsonData = data.replace(/^\uFEFF/, ''); + try { + var result = JSON.parse(jsonData); + result.__origin = file; // save the origin in order to resolve relative paths later. + return result; + } + catch (err) { + trace.error("Error parsing the JSON in %s: ", file); + trace.debug(jsonData, null); + throw err; + } + })); + }); + // Add the overrides if necessary + if (_this.settings.overrides) { + overridesProvided = true; + manifestPromises.push(Q.resolve(_this.settings.overrides)); + } + return Promise.all(manifestPromises).then(function (partials) { + // Determine the targets so we can construct the builders + var targets = []; + partials.forEach(function (partial) { + if (_.isArray(partial["targets"])) { + targets = targets.concat(partial["targets"]); + } + }); + _this.extensionComposer = extension_composer_factory_1.ComposerFactory.GetComposer(_this.settings, targets); + _this.manifestBuilders = _this.extensionComposer.getBuilders(); + var updateVersionPromise = Promise.resolve(null); + partials.forEach(function (partial, partialIndex) { + // Rev the version if necessary + if (_this.settings.revVersion) { + if (partial["version"] && partial.__origin) { + try { + var semver = version.SemanticVersion.parse(partial["version"]); + var newVersion = new version.SemanticVersion(semver.major, semver.minor, semver.patch + 1); + var newVersionString_1 = newVersion.toString(); + partial["version"] = newVersionString_1; + updateVersionPromise = qfs.readFile(partial.__origin, "utf8").then(function (versionPartial) { + try { + var newPartial = jsonInPlace(versionPartial).set("version", newVersionString_1); + return qfs.writeFile(partial.__origin, newPartial); + } + catch (e) { + trace.warn("Failed to lex partial as JSON to update the version. Skipping version rev..."); + } + }); + } + catch (e) { + trace.warn("Could not parse %s as a semantic version (major.minor.patch). Skipping version rev...", partial["version"]); + } + } + } + // Transform asset paths to be relative to the root of all manifests, verify assets + if (_.isArray(partial["files"])) { + partial["files"].forEach(function (asset) { + var keys = Object.keys(asset); + if (keys.indexOf("path") < 0) { + throw new Error("Files must have an absolute or relative (to the manifest) path."); + } + var absolutePath; + if (path.isAbsolute(asset.path)) { + absolutePath = asset.path; + } + else { + absolutePath = path.join(path.dirname(partial.__origin), asset.path); + } + asset.path = path.relative(_this.settings.root, absolutePath); + }); + } + // Transform icon paths as above + if (_.isObject(partial["icons"])) { + var icons_1 = partial["icons"]; + Object.keys(icons_1).forEach(function (iconKind) { + var absolutePath = path.join(path.dirname(partial.__origin), icons_1[iconKind]); + icons_1[iconKind] = path.relative(_this.settings.root, absolutePath); + }); + } + // Expand any directories listed in the files array + if (_.isArray(partial["files"])) { + for (var i = partial["files"].length - 1; i >= 0; --i) { + var fileDecl = partial["files"][i]; + var fsPath = path.join(_this.settings.root, fileDecl.path); + if (fs.lstatSync(fsPath).isDirectory()) { + Array.prototype.splice.apply(partial["files"], [i, 1].concat(_this.pathToFileDeclarations(fsPath, _this.settings.root, fileDecl.addressable))); + } + } + } + // Process each key by each manifest builder. + Object.keys(partial).forEach(function (key) { + var isOverridePartial = partials.length - 1 === partialIndex && overridesProvided; + if (partial[key] !== undefined && (partial[key] !== null || isOverridePartial)) { + // Notify each manifest builder of the key/value pair + _this.manifestBuilders.forEach(function (builder) { + builder.processKey(key, partial[key], isOverridePartial); + }); + } + }); + }); + // Generate localization resources + var locPrepper = new loc.LocPrep.LocKeyGenerator(_this.manifestBuilders); + var resources = locPrepper.generateLocalizationKeys(); + // Build up a master file list + var packageFiles = {}; + _this.manifestBuilders.forEach(function (builder) { + _.assign(packageFiles, builder.files); + }); + var components = { builders: _this.manifestBuilders, resources: resources }; + // Finalize each builder + return Promise.all([updateVersionPromise].concat(_this.manifestBuilders.map(function (b) { return b.finalize(packageFiles, _this.manifestBuilders); }))).then(function () { + // Let the composer do validation + return _this.extensionComposer.validate(components).then(function (validationResult) { + if (validationResult.length === 0 || _this.settings.bypassValidation) { + return components; + } + else { + throw new Error("There were errors with your extension. Address the following and re-run the tool.\n" + validationResult); + } + }); + }); + }); + }); + }; + /** + * Recursively converts a given path to a flat list of FileDeclaration + * @TODO: Async. + */ + Merger.prototype.pathToFileDeclarations = function (fsPath, root, addressable) { + var _this = this; + var files = []; + if (fs.lstatSync(fsPath).isDirectory()) { + trace.debug("Path '%s` is a directory. Adding all contained files (recursive).", fsPath); + fs.readdirSync(fsPath).forEach(function (dirChildPath) { + trace.debug("-- %s", dirChildPath); + files = files.concat(_this.pathToFileDeclarations(path.join(fsPath, dirChildPath), root, addressable)); + }); + } + else { + var relativePath = path.relative(root, fsPath); + files.push({ path: relativePath, partName: "/" + relativePath, auto: true, addressable: addressable }); + } + return files; + }; + return Merger; +}()); +exports.Merger = Merger; diff --git a/_build/exec/extension/_lib/publish.js b/_build/exec/extension/_lib/publish.js new file mode 100644 index 00000000..470ece21 --- /dev/null +++ b/_build/exec/extension/_lib/publish.js @@ -0,0 +1,302 @@ +"use strict"; +var __extends = (this && this.__extends) || function (d, b) { + for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); +}; +var _ = require("lodash"); +var colors = require("colors"); +var errHandler = require("../../../lib/errorhandler"); +var fs = require("fs"); +var GalleryInterfaces = require("vso-node-api/interfaces/GalleryInterfaces"); +var Q = require("q"); +var trace = require("../../../lib/trace"); +var xml2js = require("xml2js"); +var zip = require("jszip"); +var GalleryBase = (function () { + /** + * Constructor + * @param PublishSettings + */ + function GalleryBase(settings, galleryClient, extInfo) { + this.settings = settings; + this.galleryClient = galleryClient; + if (extInfo) { + this.vsixInfoPromise = Q.resolve(extInfo); + } + // if (!settings.galleryUrl || !/^https?:\/\//.test(settings.galleryUrl)) { + // throw "Invalid or missing gallery URL."; + // } + // if (!settings.token || !/^[A-z0-9]{52}$/.test(settings.token)) { + // throw "Invalid or missing personal access token."; + // } + } + GalleryBase.prototype.getExtInfo = function () { + if (!this.vsixInfoPromise) { + this.vsixInfoPromise = GalleryBase.getExtInfo({ + extensionId: this.settings.extensionId, + publisher: this.settings.publisher, + vsixPath: this.settings.vsixPath }); + } + return this.vsixInfoPromise; + }; + GalleryBase.getExtInfo = function (info) { + var promise; + if (info.extensionId && info.publisher) { + promise = Q.resolve({ id: info.extensionId, publisher: info.publisher, version: null }); + } + else { + promise = Q.Promise(function (resolve, reject, notify) { + fs.readFile(info.vsixPath, function (err, data) { + if (err) + reject(err); + trace.debug("Read vsix as zip... Size (bytes): %s", data.length.toString()); + try { + resolve(new zip(data)); + } + catch (err) { + reject(err); + } + }); + }).then(function (zip) { + trace.debug("Files in the zip: %s", Object.keys(zip.files).join(", ")); + var vsixManifestFileNames = Object.keys(zip.files).filter(function (key) { return _.endsWith(key, "vsixmanifest"); }); + if (vsixManifestFileNames.length > 0) { + return Q.nfcall(xml2js.parseString, zip.files[vsixManifestFileNames[0]].asText()); + } + else { + throw "Could not locate vsix manifest!"; + } + }).then(function (vsixManifestAsJson) { + var extensionId = info.extensionId || _.get(vsixManifestAsJson, "PackageManifest.Metadata[0].Identity[0].$.Id"); + var extensionPublisher = info.publisher || _.get(vsixManifestAsJson, "PackageManifest.Metadata[0].Identity[0].$.Publisher"); + var extensionVersion = _.get(vsixManifestAsJson, "PackageManifest.Metadata[0].Identity[0].$.Version"); + if (extensionId && extensionPublisher) { + return { id: extensionId, publisher: extensionPublisher, version: extensionVersion }; + } + else { + throw "Could not locate both the extension id and publisher in vsix manfiest! Ensure your manifest includes both a namespace and a publisher property, or specify the necessary --publisher and/or --extension options."; + } + }); + } + return promise; + }; + return GalleryBase; +}()); +exports.GalleryBase = GalleryBase; +/** + * Class that handles creating and deleting publishers + */ +var PublisherManager = (function (_super) { + __extends(PublisherManager, _super); + /** + * Constructor + * @param PublishSettings + */ + function PublisherManager(settings, galleryClient) { + _super.call(this, settings, galleryClient); + this.settings = settings; + this.galleryClient = galleryClient; + } + /** + * Create a a publisher with the given name, displayName, and description + * @param string Publisher's unique name + * @param string Publisher's display name + * @param string Publisher description + * @return Q.Promise that is resolved when publisher is created + */ + PublisherManager.prototype.createPublisher = function (name, displayName, description) { + return this.galleryClient.createPublisher({ + publisherName: name, + displayName: displayName, + longDescription: description, + shortDescription: description + }).catch(errHandler.httpErr); + }; + /** + * Delete the publisher with the given name + * @param string Publisher's unique name + * @return Q.promise that is resolved when publisher is deleted + */ + PublisherManager.prototype.deletePublisher = function (name) { + return this.galleryClient.deletePublisher(name).catch(errHandler.httpErr); + }; + return PublisherManager; +}(GalleryBase)); +exports.PublisherManager = PublisherManager; +var SharingManager = (function (_super) { + __extends(SharingManager, _super); + function SharingManager() { + _super.apply(this, arguments); + } + SharingManager.prototype.shareWith = function (accounts) { + var _this = this; + return this.getExtInfo().then(function (extInfo) { + return Promise.all(accounts.map(function (account) { + trace.info("Sharing extension with %s.", account); + return _this.galleryClient.shareExtension(extInfo.publisher, extInfo.id, account).catch(errHandler.httpErr); + })); + }); + }; + SharingManager.prototype.unshareWith = function (accounts) { + var _this = this; + return this.getExtInfo().then(function (extInfo) { + return Promise.all(accounts.map(function (account) { + return _this.galleryClient.unshareExtension(extInfo.publisher, extInfo.id, account).catch(errHandler.httpErr); + })); + }); + }; + SharingManager.prototype.unshareWithAll = function () { + var _this = this; + return this.getSharedWithAccounts().then(function (accounts) { + return _this.unshareWith(accounts); + }); + }; + SharingManager.prototype.getSharedWithAccounts = function () { + return this.getExtensionInfo().then(function (ext) { + return ext.sharedWith.map(function (acct) { return acct.name; }); + }); + }; + SharingManager.prototype.getExtensionInfo = function () { + var _this = this; + return this.getExtInfo().then(function (extInfo) { + return _this.galleryClient.getExtension(extInfo.publisher, extInfo.id, null, GalleryInterfaces.ExtensionQueryFlags.IncludeVersions | + GalleryInterfaces.ExtensionQueryFlags.IncludeFiles | + GalleryInterfaces.ExtensionQueryFlags.IncludeCategoryAndTags | + GalleryInterfaces.ExtensionQueryFlags.IncludeSharedAccounts).then(function (extension) { + return extension; + }).catch(errHandler.httpErr); + }); + }; + return SharingManager; +}(GalleryBase)); +exports.SharingManager = SharingManager; +var PackagePublisher = (function (_super) { + __extends(PackagePublisher, _super); + function PackagePublisher() { + _super.apply(this, arguments); + } + PackagePublisher.prototype.checkVsixPublished = function () { + var _this = this; + return this.getExtInfo().then(function (extInfo) { + return _this.galleryClient.getExtension(extInfo.publisher, extInfo.id).then(function (ext) { + if (ext) { + extInfo.published = true; + return extInfo; + } + return extInfo; + }).catch(function () { return extInfo; }); + }); + }; + /** + * Publish the VSIX extension given by vsixPath + * @param string path to a VSIX extension to publish + * @return Q.Promise that is resolved when publish is complete + */ + PackagePublisher.prototype.publish = function () { + var _this = this; + var extPackage = { + extensionManifest: fs.readFileSync(this.settings.vsixPath, "base64") + }; + trace.debug("Publishing %s", this.settings.vsixPath); + // Check if the app is already published. If so, call the update endpoint. Otherwise, create. + trace.info("Checking if this extension is already published"); + return this.createOrUpdateExtension(extPackage).then(function (ext) { + trace.info("Waiting for server to validate extension package..."); + var versions = ext.versions; + versions.sort(function (a, b) { + var aTime = a.lastUpdated.getTime(); + var bTime = b.lastUpdated.getTime(); + return aTime < bTime ? 1 : (aTime === bTime ? 0 : -1); + }); + return _this.waitForValidation(versions[0].version).then(function (result) { + if (result === PackagePublisher.validated) { + return ext; + } + else { + throw "Extension validation failed. Please address the following issues and retry publishing.\n" + result; + } + }); + }); + }; + PackagePublisher.prototype.createOrUpdateExtension = function (extPackage) { + var _this = this; + return this.checkVsixPublished().then(function (extInfo) { + var publishPromise; + if (extInfo && extInfo.published) { + trace.info("It is, %s the extension", colors.cyan("update").toString()); + publishPromise = _this.galleryClient.updateExtension(extPackage, extInfo.publisher, extInfo.id).catch(errHandler.httpErr); + } + else { + trace.info("It isn't, %s a new extension.", colors.cyan("create").toString()); + publishPromise = _this.galleryClient.createExtension(extPackage).catch(errHandler.httpErr); + } + return publishPromise.then(function () { + return _this.galleryClient.getExtension(extInfo.publisher, extInfo.id, null, GalleryInterfaces.ExtensionQueryFlags.IncludeVersions); + }); + }); + }; + PackagePublisher.prototype.waitForValidation = function (version, interval, retries) { + var _this = this; + if (interval === void 0) { interval = PackagePublisher.validationInterval; } + if (retries === void 0) { retries = PackagePublisher.validationRetries; } + if (retries === 0) { + throw "Validation timed out. There may be a problem validating your extension. Please try again later."; + } + else if (retries === 25) { + trace.info("This is taking longer than usual. Hold tight..."); + } + trace.debug("Polling for validation (%s retries remaining).", retries.toString()); + // Compiler nonsense below. Sorry. + return (Q.delay(this.getValidationStatus(version), interval)).then(function (status) { + trace.debug("--Retrieved validation status: %s", status); + if (status === PackagePublisher.validationPending) { + return _this.waitForValidation(version, interval, retries - 1); + } + else { + return Q.resolve(status); // otherwise TypeScript gets upset... I don't really know why. + } + }); + }; + PackagePublisher.prototype.getValidationStatus = function (version) { + var _this = this; + return this.getExtInfo().then(function (extInfo) { + return _this.galleryClient.getExtension(extInfo.publisher, extInfo.id, extInfo.version, GalleryInterfaces.ExtensionQueryFlags.IncludeVersions).then(function (ext) { + if (!ext || ext.versions.length === 0) { + throw "Extension not published."; + } + var extVersion = ext.versions[0]; + if (version) { + extVersion = _this.getVersionedExtension(ext, version); + } + // If there is a validationResultMessage, validation failed and this is the error + // If the validated flag is missing and there is no validationResultMessage, validation is pending + // If the validated flag is present and there is no validationResultMessage, the extension is validated. + if (extVersion.validationResultMessage) { + return extVersion.validationResultMessage; + } + else if ((extVersion.flags & GalleryInterfaces.ExtensionVersionFlags.Validated) === 0) { + return PackagePublisher.validationPending; + } + else { + return PackagePublisher.validated; + } + }); + }); + }; + PackagePublisher.prototype.getVersionedExtension = function (extension, version) { + var matches = extension.versions.filter(function (ev) { return ev.version === version; }); + if (matches.length > 0) { + return matches[0]; + } + else { + return null; + } + }; + PackagePublisher.validationPending = "__validation_pending"; + PackagePublisher.validated = "__validated"; + PackagePublisher.validationInterval = 1000; + PackagePublisher.validationRetries = 50; + return PackagePublisher; +}(GalleryBase)); +exports.PackagePublisher = PackagePublisher; diff --git a/_build/exec/extension/_lib/targets/Microsoft.VisualStudio.Offer/composer.js b/_build/exec/extension/_lib/targets/Microsoft.VisualStudio.Offer/composer.js new file mode 100644 index 00000000..d7e1d3c8 --- /dev/null +++ b/_build/exec/extension/_lib/targets/Microsoft.VisualStudio.Offer/composer.js @@ -0,0 +1,15 @@ +"use strict"; +var __extends = (this && this.__extends) || function (d, b) { + for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); +}; +var extension_composer_1 = require("../../extension-composer"); +var VSOfferComposer = (function (_super) { + __extends(VSOfferComposer, _super); + function VSOfferComposer() { + _super.apply(this, arguments); + } + return VSOfferComposer; +}(extension_composer_1.ExtensionComposer)); +exports.VSOfferComposer = VSOfferComposer; diff --git a/_build/exec/extension/_lib/targets/Microsoft.VisualStudio.Services.Integration/composer.js b/_build/exec/extension/_lib/targets/Microsoft.VisualStudio.Services.Integration/composer.js new file mode 100644 index 00000000..0bd29418 --- /dev/null +++ b/_build/exec/extension/_lib/targets/Microsoft.VisualStudio.Services.Integration/composer.js @@ -0,0 +1,29 @@ +"use strict"; +var __extends = (this && this.__extends) || function (d, b) { + for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); +}; +var extension_composer_1 = require("../../extension-composer"); +var vsix_manifest_builder_1 = require("../../vsix-manifest-builder"); +var _ = require("lodash"); +var VSSIntegrationComposer = (function (_super) { + __extends(VSSIntegrationComposer, _super); + function VSSIntegrationComposer() { + _super.apply(this, arguments); + } + VSSIntegrationComposer.prototype.validate = function (components) { + return _super.prototype.validate.call(this, components).then(function (result) { + var vsixData = components.builders.filter(function (b) { return b.getType() === vsix_manifest_builder_1.VsixManifestBuilder.manifestType; })[0].getData(); + // Ensure that an Action link or a Getstarted link exists. + var properties = _.get(vsixData, "PackageManifest.Metadata[0].Properties[0].Property", []); + var pIds = properties.map(function (p) { return _.get(p, "$.Id"); }); + if (_.intersection(["Microsoft.VisualStudio.Services.Links.Action", "Microsoft.VisualStudio.Services.Links.Getstarted"], pIds).length === 0) { + result.push("An 'integration' extension must provide a 'getstarted' link."); + } + return result; + }); + }; + return VSSIntegrationComposer; +}(extension_composer_1.ExtensionComposer)); +exports.VSSIntegrationComposer = VSSIntegrationComposer; diff --git a/_build/exec/extension/_lib/targets/Microsoft.VisualStudio.Services/composer.js b/_build/exec/extension/_lib/targets/Microsoft.VisualStudio.Services/composer.js new file mode 100644 index 00000000..8a935966 --- /dev/null +++ b/_build/exec/extension/_lib/targets/Microsoft.VisualStudio.Services/composer.js @@ -0,0 +1,29 @@ +"use strict"; +var __extends = (this && this.__extends) || function (d, b) { + for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); +}; +var extension_composer_1 = require("../../extension-composer"); +var vso_manifest_builder_1 = require("./vso-manifest-builder"); +var Q = require("q"); +var VSSExtensionComposer = (function (_super) { + __extends(VSSExtensionComposer, _super); + function VSSExtensionComposer() { + _super.apply(this, arguments); + } + VSSExtensionComposer.prototype.getBuilders = function () { + return _super.prototype.getBuilders.call(this).concat([new vso_manifest_builder_1.VsoManifestBuilder(this.settings.root)]); + }; + VSSExtensionComposer.prototype.validate = function (components) { + return _super.prototype.validate.call(this, components).then(function (result) { + var data = components.builders.filter(function (b) { return b.getType() === vso_manifest_builder_1.VsoManifestBuilder.manifestType; })[0].getData(); + if (data.contributions.length === 0 && data.contributionTypes.length === 0) { + result.push("Your extension must define at least one contribution or contribution type."); + } + return Q.resolve(result); + }); + }; + return VSSExtensionComposer; +}(extension_composer_1.ExtensionComposer)); +exports.VSSExtensionComposer = VSSExtensionComposer; diff --git a/_build/exec/extension/_lib/targets/Microsoft.VisualStudio.Services/vso-manifest-builder.js b/_build/exec/extension/_lib/targets/Microsoft.VisualStudio.Services/vso-manifest-builder.js new file mode 100644 index 00000000..8c5afa94 --- /dev/null +++ b/_build/exec/extension/_lib/targets/Microsoft.VisualStudio.Services/vso-manifest-builder.js @@ -0,0 +1,140 @@ +"use strict"; +var __extends = (this && this.__extends) || function (d, b) { + for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); +}; +var manifest_1 = require("../../manifest"); +var _ = require("lodash"); +var VsoManifestBuilder = (function (_super) { + __extends(VsoManifestBuilder, _super); + function VsoManifestBuilder() { + _super.apply(this, arguments); + } + /** + * Gets the package path to this manifest. + */ + VsoManifestBuilder.prototype.getPath = function () { + return "extension.vsomanifest"; + }; + /** + * Explains the type of manifest builder + */ + VsoManifestBuilder.prototype.getType = function () { + return VsoManifestBuilder.manifestType; + }; + VsoManifestBuilder.prototype.getContentType = function () { + return "application/json"; + }; + VsoManifestBuilder.prototype.finalize = function (files) { + // Ensure some default values are set + if (!this.data.contributions) { + this.data.contributions = []; + } + if (!this.data.scopes) { + this.data.scopes = []; + } + if (!this.data.contributionTypes) { + this.data.contributionTypes = []; + } + if (!this.data.manifestVersion) { + this.data.manifestVersion = 1; + } + return Promise.resolve(null); + }; + /** + * Some elements of this file are arrays, which would typically produce a localization + * key like "contributions.3.name". We want to turn the 3 into the contribution id to + * make it more friendly to translators. + */ + VsoManifestBuilder.prototype.getLocKeyPath = function (path) { + var pathParts = path.split(".").filter(function (p) { return !!p; }); + if (pathParts && pathParts.length >= 2) { + var cIndex = parseInt(pathParts[1]); + if (pathParts[0] === "contributions" && !isNaN(cIndex) && this.data.contributions[cIndex] && this.data.contributions[cIndex].id) { + return "contributions" + this.data.contributions[cIndex].id; + } + else { + return path; + } + } + }; + VsoManifestBuilder.prototype.processKey = function (key, value, override) { + switch (key.toLowerCase()) { + case "eventcallbacks": + if (_.isObject(value)) { + this.singleValueProperty("eventCallbacks", value, key, override); + } + break; + case "manifestversion": + var version = value; + if (_.isString(version)) { + version = parseFloat(version); + } + this.singleValueProperty("manifestVersion", version, key, override); + break; + case "scopes": + if (_.isArray(value)) { + if (!this.data.scopes) { + this.data.scopes = []; + } + this.data.scopes = _.uniq(this.data.scopes.concat(value)); + } + break; + case "baseuri": + this.singleValueProperty("baseUri", value, key, override); + break; + case "contributions": + if (_.isArray(value)) { + if (!this.data.contributions) { + this.data.contributions = []; + } + this.data.contributions = this.data.contributions.concat(value); + } + else { + throw "\"contributions\" must be an array of Contribution objects."; + } + break; + case "contributiontypes": + if (_.isArray(value)) { + if (!this.data.contributionTypes) { + this.data.contributionTypes = []; + } + this.data.contributionTypes = this.data.contributionTypes.concat(value); + } + break; + // Ignore all the vsixmanifest keys so we can take a default case below. + case "namespace": + case "extensionid": + case "id": + case "version": + case "name": + case "description": + case "icons": + case "screenshots": + case "details": + case "targets": + case "links": + case "branding": + case "public": + case "publisher": + case "releasenotes": + case "tags": + case "flags": + case "vsoflags": + case "galleryflags": + case "categories": + case "files": + case "githubflavoredmarkdown": + break; + default: + if (key.substr(0, 2) !== "__") { + this.singleValueProperty(key, value, key, override); + } + break; + } + }; + VsoManifestBuilder.manifestType = "Microsoft.VisualStudio.Services.Manifest"; + return VsoManifestBuilder; +}(manifest_1.ManifestBuilder)); +exports.VsoManifestBuilder = VsoManifestBuilder; diff --git a/_build/exec/extension/_lib/utils.js b/_build/exec/extension/_lib/utils.js new file mode 100644 index 00000000..b283a4af --- /dev/null +++ b/_build/exec/extension/_lib/utils.js @@ -0,0 +1,63 @@ +"use strict"; +var _ = require("lodash"); +var os = require("os"); +var path = require("path"); +var xml = require("xml2js"); +function removeMetaKeys(obj) { + return _.omit(obj, function (v, k) { return _.startsWith(k, "__meta_"); }); +} +exports.removeMetaKeys = removeMetaKeys; +function cleanAssetPath(assetPath, root) { + if (root === void 0) { root = "."; } + if (!assetPath) { + return null; + } + return forwardSlashesPath(path.resolve(root, assetPath)); +} +exports.cleanAssetPath = cleanAssetPath; +function forwardSlashesPath(filePath) { + if (!filePath) { + return null; + } + var cleanPath = filePath.replace(/\\/g, "/"); + return cleanPath; +} +exports.forwardSlashesPath = forwardSlashesPath; +/** + * OPC Convention implementation. See + * http://www.ecma-international.org/news/TC45_current_work/tc45-2006-335.pdf §10.1.3.2 & §10.2.3 + */ +function toZipItemName(partName) { + if (_.startsWith(partName, "/")) { + return partName.substr(1); + } + else { + return partName; + } +} +exports.toZipItemName = toZipItemName; +function jsonToXml(json) { + var builder = new xml.Builder(exports.DEFAULT_XML_BUILDER_SETTINGS); + return builder.buildObject(json); +} +exports.jsonToXml = jsonToXml; +function maxKey(obj, func) { + var maxProp; + for (var prop in obj) { + if (!maxProp || func(obj[prop]) > func(obj[maxProp])) { + maxProp = prop; + } + } + return maxProp; +} +exports.maxKey = maxKey; +exports.DEFAULT_XML_BUILDER_SETTINGS = { + indent: " ", + newline: os.EOL, + pretty: true, + xmldec: { + encoding: "utf-8", + standalone: null, + version: "1.0" + } +}; diff --git a/_build/exec/extension/_lib/vsix-manifest-builder.js b/_build/exec/extension/_lib/vsix-manifest-builder.js new file mode 100644 index 00000000..898eff3e --- /dev/null +++ b/_build/exec/extension/_lib/vsix-manifest-builder.js @@ -0,0 +1,630 @@ +"use strict"; +var __extends = (this && this.__extends) || function (d, b) { + for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); +}; +var manifest_1 = require("./manifest"); +var utils_1 = require("./utils"); +var _ = require("lodash"); +var childProcess = require("child_process"); +var onecolor = require("onecolor"); +var os = require("os"); +var path = require("path"); +var Q = require("q"); +var trace = require("../../../lib/trace"); +var winreg = require("winreg"); +var VsixManifestBuilder = (function (_super) { + __extends(VsixManifestBuilder, _super); + function VsixManifestBuilder(extRoot) { + _super.call(this, extRoot); + _.set(this.data, "PackageManifest.$", { + "Version": "2.0.0", + "xmlns": "http://schemas.microsoft.com/developer/vsx-schema/2011", + "xmlns:d": "http://schemas.microsoft.com/developer/vsx-schema-design/2011" + }); + _.set(this.data, "PackageManifest.Metadata[0].Identity[0].$", { "Language": "en-US" }); + _.set(this.data, "PackageManifest.Dependencies", [""]); + } + /** + * Explains the type of manifest builder + */ + VsixManifestBuilder.prototype.getType = function () { + return VsixManifestBuilder.manifestType; + }; + VsixManifestBuilder.prototype.getContentType = function () { + return "text/xml"; + }; + /** + * Gets the package path to this manifest + */ + VsixManifestBuilder.prototype.getPath = function () { + return "extension.vsixmanifest"; + }; + /** + * VSIX Manifest loc assets are vsixlangpack files. + */ + VsixManifestBuilder.prototype.getLocPath = function () { + return "Extension.vsixlangpack"; + }; + /** + * Gets the contents of the vsixLangPack file for this manifest + */ + VsixManifestBuilder.prototype.getLocResult = function (translations, defaults) { + var langPack = this.generateVsixLangPack(translations, defaults); + return utils_1.jsonToXml(langPack); + }; + VsixManifestBuilder.prototype.generateVsixLangPack = function (translations, defaults) { + return { + VsixLanguagePack: { + $: { + Version: "1.0.0", + xmlns: "http://schemas.microsoft.com/developer/vsx-schema-lp/2010" + }, + LocalizedName: [translations["displayName"] || defaults["displayName"]], + LocalizedDescription: [translations["description"] || defaults["description"]], + LocalizedReleaseNotes: [translations["releaseNotes"] || defaults["releaseNotes"]], + License: [null], + MoreInfoUrl: [null] + } + }; + }; + /** + * Add an asset: add a file to the vsix package and if there is an assetType on the + * file, add an entry in the vsixmanifest. + */ + VsixManifestBuilder.prototype.addAsset = function (file) { + this.addFile(file); + }; + /** + * Add an entry to the vsixmanifest. + */ + VsixManifestBuilder.prototype.addAssetToManifest = function (assetPath, type, addressable, lang) { + var _this = this; + if (addressable === void 0) { addressable = false; } + if (lang === void 0) { lang = null; } + var cleanAssetPath = utils_1.toZipItemName(assetPath); + var types; + if (typeof type === "string") { + types = [type]; + } + else { + types = type; + } + types.forEach(function (type) { + var asset = { + "Type": type, + "d:Source": "File", + "Path": cleanAssetPath + }; + if (addressable) { + asset["Addressable"] = "true"; + } + if (lang) { + asset["Lang"] = lang; + } + var assetElem = _.get(_this.data, "PackageManifest.Assets[0].Asset", []); + assetElem.push({ + "$": asset + }); + _.set(_this.data, "PackageManifest.Assets[0].Asset", assetElem); + if (type === "Microsoft.VisualStudio.Services.Icons.Default") { + _.set(_this.data, "PackageManifest.Metadata[0].Icon[0]", cleanAssetPath); + } + if (type === "Microsoft.VisualStudio.Services.Content.License") { + _.set(_this.data, "PackageManifest.Metadata[0].License[0]", cleanAssetPath); + } + }); + }; + /** + * Add a property to the vsixmanifest. + */ + VsixManifestBuilder.prototype.addProperty = function (id, value) { + var defaultProperties = []; + var existingProperties = _.get(this.data, "PackageManifest.Metadata[0].Properties[0].Property", defaultProperties); + if (defaultProperties === existingProperties) { + _.set(this.data, "PackageManifest.Metadata[0].Properties[0].Property", defaultProperties); + } + existingProperties.push({ + $: { + Id: id, + Value: value + } + }); + }; + /** + * Given a key/value pair, decide how this effects the manifest + */ + VsixManifestBuilder.prototype.processKey = function (key, value, override) { + var _this = this; + switch (key.toLowerCase()) { + case "namespace": + case "extensionid": + case "id": + if (_.isString(value)) { + this.singleValueProperty("PackageManifest.Metadata[0].Identity[0].$.Id", value, "namespace/extensionId/id", override); + } + break; + case "version": + this.singleValueProperty("PackageManifest.Metadata[0].Identity[0].$.Version", value, key, override); + break; + case "name": + this.singleValueProperty("PackageManifest.Metadata[0].DisplayName[0]", value, key, override); + break; + case "description": + _.set(this.data, "PackageManifest.Metadata[0].Description[0].$", { "xml:space": "preserve" }); + this.singleValueProperty("PackageManifest.Metadata[0].Description[0]._", value, key, override); + break; + case "icons": + Object.keys(value).forEach(function (key) { + var iconType = _.startCase(key.toLowerCase()); + var fileDecl = { + path: value[key], + addressable: true, + assetType: "Microsoft.VisualStudio.Services.Icons." + iconType + }; + _this.addAsset(fileDecl); + }); + break; + case "screenshots": + if (_.isArray(value)) { + var screenshotIndex_1 = 0; + value.forEach(function (screenshot) { + var fileDecl = { + path: screenshot.path, + addressable: true, + assetType: "Microsoft.VisualStudio.Services.Screenshots." + (++screenshotIndex_1), + contentType: screenshot.contentType + }; + _this.addAsset(fileDecl); + }); + } + break; + case "content": + Object.keys(value).forEach(function (key) { + var contentKey = _.startCase(key.toLowerCase()); + if (value[key].path) { + var fileDecl = { + path: value[key].path, + addressable: true, + assetType: "Microsoft.VisualStudio.Services.Content." + contentKey + }; + if (value[key].contentType) { + fileDecl.contentType = value[key].contentType; + } + _this.addAsset(fileDecl); + } + else { + trace.warn("Did not find 'path' property for content item '%s'. Ignoring.", key); + } + }); + break; + case "details": + if (_.isObject(value) && value.path) { + var fileDecl = { + path: value.path, + addressable: true, + assetType: "Microsoft.VisualStudio.Services.Content.Details", + contentType: value.contentType + }; + this.addAsset(fileDecl); + } + break; + case "targets": + if (_.isArray(value)) { + var existingTargets_1 = _.get(this.data, "PackageManifest.Installation[0].InstallationTarget", []); + value.forEach(function (target) { + if (!target.id) { + return; + } + var newTargetAttrs = { + Id: target.id + }; + if (target.version) { + newTargetAttrs["Version"] = target.version; + } + existingTargets_1.push({ + $: newTargetAttrs + }); + }); + _.set(this.data, "PackageManifest.Installation[0].InstallationTarget", existingTargets_1); + } + break; + case "links": + if (_.isObject(value)) { + Object.keys(value).forEach(function (linkType) { + var url = _.get(value, linkType + ".uri") || _.get(value, linkType + ".url"); + if (url) { + var linkTypeCased = _.capitalize(_.camelCase(linkType)); + _this.addProperty("Microsoft.VisualStudio.Services.Links." + linkTypeCased, url); + } + else { + trace.warn("'uri' property not found for link: '%s'... ignoring.", linkType); + } + }); + } + break; + case "repository": + if (_.isObject(value)) { + var type = value.type, url = value.url; + if (!type) { + throw new Error("Repository must have a 'type' property."); + } + if (type !== "git") { + throw new Error("Currently 'git' is the only supported repository type."); + } + if (!url) { + throw new Error("Repository must contain a 'url' property."); + } + this.addProperty("Microsoft.VisualStudio.Services.Links.GitHub", url); + } + break; + case "badges": + if (_.isArray(value)) { + var existingBadges_1 = _.get(this.data, "PackageManifest.Metadata[0].Badges[0].Badge", []); + value.forEach(function (badge) { + existingBadges_1.push({ + $: { + Link: badge.link, + ImgUri: badge.imgUri, + Description: badge.description + } + }); + }); + _.set(this.data, "PackageManifest.Metadata[0].Badges[0].Badge", existingBadges_1); + } + break; + case "branding": + if (_.isObject(value)) { + Object.keys(value).forEach(function (brandingType) { + var brandingTypeCased = _.capitalize(_.camelCase(brandingType)); + var brandingValue = value[brandingType]; + if (brandingTypeCased === "Color") { + try { + brandingValue = onecolor(brandingValue).hex(); + } + catch (e) { + throw "Could not parse branding color as a valid color. Please use a hex or rgb format, e.g. #00ff00 or rgb(0, 255, 0)"; + } + } + _this.addProperty("Microsoft.VisualStudio.Services.Branding." + brandingTypeCased, brandingValue); + }); + } + break; + case "githubflavoredmarkdown": + if (typeof value !== "boolean") { + throw "Value for gitHubFlavoredMarkdown is invalid. Only boolean values are allowed."; + } + this.addProperty("Microsoft.VisualStudio.Services.GitHubFlavoredMarkdown", value.toString()); + case "public": + if (typeof value === "boolean") { + var flags = _.get(this.data, "PackageManifest.Metadata[0].GalleryFlags[0]", "").split(" "); + _.remove(flags, function (v) { return v === ""; }); + if (value === true) { + flags.push("Public"); + } + if (value === false) { + _.remove(flags, function (v) { return v === "Public"; }); + } + _.set(this.data, "PackageManifest.Metadata[0].GalleryFlags[0]", _.uniq(flags).join(" ")); + } + break; + case "publisher": + this.singleValueProperty("PackageManifest.Metadata[0].Identity[0].$.Publisher", value, key, override); + break; + case "releasenotes": + this.singleValueProperty("PackageManifest.Metadata[0].ReleaseNotes[0]", value, key, override); + break; + case "tags": + this.handleDelimitedList(value, "PackageManifest.Metadata[0].Tags[0]"); + break; + case "galleryflags": + // Gallery Flags are space-separated since it's a Flags enum. + this.handleDelimitedList(value, "PackageManifest.Metadata[0].GalleryFlags[0]", " ", true); + break; + case "categories": + this.handleDelimitedList(value, "PackageManifest.Metadata[0].Categories[0]"); + break; + case "files": + if (_.isArray(value)) { + value.forEach(function (asset) { + _this.addAsset(asset); + }); + } + break; + } + }; + /** + * Get the id of the extension this vsixmanifest goes to + */ + VsixManifestBuilder.prototype.getExtensionId = function () { + return _.get(this.data, "PackageManifest.Metadata[0].Identity[0].$.Id"); + }; + /** + * Get the publisher this vsixmanifest goes to + */ + VsixManifestBuilder.prototype.getExtensionPublisher = function () { + return _.get(this.data, "PackageManifest.Metadata[0].Identity[0].$.Publisher"); + }; + /** + * --Ensures an entry is added for each file as appropriate + * --Builds the [Content_Types].xml file + */ + VsixManifestBuilder.prototype.finalize = function (files, builders) { + var _this = this; + // Default installation target to VSS if not provided (and log warning) + var installationTarget = _.get(this.data, "PackageManifest.Installation[0].InstallationTarget"); + if (!(_.isArray(installationTarget) && installationTarget.length > 0)) { + trace.warn("No 'target' provided. Defaulting to Microsoft.VisualStudio.Services."); + _.set(this.data, "PackageManifest.Installation[0].InstallationTarget", [ + { + $: { + Id: "Microsoft.VisualStudio.Services" + } + } + ]); + } + Object.keys(files).forEach(function (fileName) { + var file = files[fileName]; + // Add all assets to manifest except the vsixmanifest (duh) + if (file.assetType && file.path !== _this.getPath()) { + _this.addAssetToManifest(file.partName, file.assetType, file.addressable, file.lang); + } + }); + // Add the manifests as assets. + builders.forEach(function (builder) { + var builderType = builder.getType(); + if (builderType != VsixManifestBuilder.manifestType) { + _this.addAssetToManifest(builder.getPath(), builder.getType(), true); + } + }); + // The vsixmanifest will be responsible for generating the [Content_Types].xml file + // Obviously this is kind of strange, but hey ho. + return this.genContentTypesXml(builders).then(function (result) { + _this.addFile({ + path: null, + content: result, + partName: "/[Content_Types].xml" + }); + }); + }; + /** + * Gets the string representation (XML) of this manifest + */ + VsixManifestBuilder.prototype.getResult = function () { + return utils_1.jsonToXml(utils_1.removeMetaKeys(this.data)).replace(/\n/g, os.EOL); + }; + /** + * Generates the required [Content_Types].xml file for the vsix package. + * This xml contains a entry for each different file extension + * found in the package, mapping it to the appropriate MIME type. + */ + VsixManifestBuilder.prototype.genContentTypesXml = function (builders) { + var _this = this; + var typeMap = VsixManifestBuilder.CONTENT_TYPE_MAP; + trace.debug("Generating [Content_Types].xml"); + var contentTypes = { + Types: { + $: { + xmlns: "http://schemas.openxmlformats.org/package/2006/content-types" + }, + Default: [], + Override: [] + } + }; + var windows = /^win/.test(process.platform); + var contentTypePromise; + var showWarningForExtensionMap = {}; + if (windows) { + // On windows, check HKCR to get the content type of the file based on the extension + var contentTypePromises_1 = []; + var extensionlessFiles = []; + var uniqueExtensions = _.uniq(Object.keys(this.files).map(function (f) { + var extName = path.extname(f); + var filename = path.basename(f); + // Look in the best guess table. Or, default to text/plain if the file starts with a "." + var bestGuess = VsixManifestBuilder.BEST_GUESS_CONTENT_TYPES[filename.toUpperCase()] || (filename[0] === "." ? "text/plain" : null); + if (!extName && !_this.files[f].contentType && _this.files[f].addressable && !bestGuess) { + trace.warn("File %s does not have an extension, and its content-type is not declared. Defaulting to application/octet-stream.", path.resolve(f)); + _this.files[f].contentType = "application/octet-stream"; + } + else if (bestGuess) { + _this.files[f].contentType = bestGuess; + } + if (_this.files[f].contentType) { + // If there is an override for this file, ignore its extension + return ""; + } + // Later, we will show warnings for extensions with unknown content types if there + // was at least one file with this extension that was addressable. + if (!showWarningForExtensionMap[extName] && _this.files[f].addressable) { + showWarningForExtensionMap[extName] = true; + } + return extName; + })); + uniqueExtensions.forEach(function (ext) { + if (!ext.trim()) { + return; + } + if (typeMap[ext.toLowerCase()]) { + contentTypes.Types.Default.push({ + $: { + Extension: ext, + ContentType: typeMap[ext.toLowerCase()] + } + }); + return; + } + var hkcrKey = new winreg({ + hive: winreg.HKCR, + key: "\\" + ext.toLowerCase() + }); + var regPromise = Q.ninvoke(hkcrKey, "get", "Content Type").then(function (type) { + trace.debug("Found content type for %s: %s.", ext, type.value); + var contentType = "application/octet-stream"; + if (type) { + contentType = type.value; + } + return contentType; + }).catch(function (err) { + if (showWarningForExtensionMap[ext]) { + trace.warn("Could not determine content type for extension %s. Defaulting to application/octet-stream. To override this, add a contentType property to this file entry in the manifest.", ext); + } + return "application/octet-stream"; + }).then(function (contentType) { + contentTypes.Types.Default.push({ + $: { + Extension: ext, + ContentType: contentType + } + }); + }); + contentTypePromises_1.push(regPromise); + }); + contentTypePromise = Promise.all(contentTypePromises_1); + } + else { + // If not on windows, run the file --mime-type command to use magic to get the content type. + // If the file has an extension, rev a hit counter for that extension and the extension + // If there is no extension, create an element for the element + // For each file with an extension that doesn't match the most common type for that extension + // (tracked by the hit counter), create an element. + // Finally, add a element for each extension mapped to the most common type. + var contentTypePromises_2 = []; + var extTypeCounter_1 = {}; + Object.keys(this.files).filter(function (fileName) { + return !_this.files[fileName].contentType; + }).forEach(function (fileName) { + var extension = path.extname(fileName); + var mimePromise; + if (typeMap[extension]) { + if (!extTypeCounter_1[extension]) { + extTypeCounter_1[extension] = {}; + } + if (!extTypeCounter_1[extension][typeMap[extension]]) { + extTypeCounter_1[extension][typeMap[extension]] = []; + } + extTypeCounter_1[extension][typeMap[extension]].push(fileName); + mimePromise = Q.resolve(null); + return; + } + mimePromise = Q.Promise(function (resolve, reject, notify) { + var child = childProcess.exec("file --mime-type \"" + fileName + "\"", function (err, stdout, stderr) { + try { + if (err) { + reject(err); + } + var magicMime = _.trimEnd(stdout.substr(stdout.lastIndexOf(" ") + 1), "\n"); + trace.debug("Magic mime type for %s is %s.", fileName, magicMime); + if (magicMime) { + if (extension) { + if (!extTypeCounter_1[extension]) { + extTypeCounter_1[extension] = {}; + } + var hitCounters = extTypeCounter_1[extension]; + if (!hitCounters[magicMime]) { + hitCounters[magicMime] = []; + } + hitCounters[magicMime].push(fileName); + } + else { + if (!_this.files[fileName].contentType) { + _this.files[fileName].contentType = magicMime; + } + } + } + else { + if (stderr) { + reject(stderr); + } + else { + if (_this.files[fileName].addressable) { + trace.warn("Could not determine content type for %s. Defaulting to application/octet-stream. To override this, add a contentType property to this file entry in the manifest.", fileName); + } + _this.files[fileName].contentType = "application/octet-stream"; + } + } + resolve(null); + } + catch (e) { + reject(e); + } + }); + }); + contentTypePromises_2.push(mimePromise); + }); + contentTypePromise = Promise.all(contentTypePromises_2).then(function () { + Object.keys(extTypeCounter_1).forEach(function (ext) { + var hitCounts = extTypeCounter_1[ext]; + var bestMatch = utils_1.maxKey(hitCounts, (function (i) { return i.length; })); + Object.keys(hitCounts).forEach(function (type) { + if (type === bestMatch) { + return; + } + hitCounts[type].forEach(function (fileName) { + _this.files[fileName].contentType = type; + }); + }); + contentTypes.Types.Default.push({ + $: { + Extension: ext, + ContentType: bestMatch + } + }); + }); + }); + } + return contentTypePromise.then(function () { + Object.keys(_this.files).forEach(function (filePath) { + if (_this.files[filePath].contentType) { + contentTypes.Types.Override.push({ + $: { + ContentType: _this.files[filePath].contentType, + PartName: "/" + utils_1.toZipItemName(_this.files[filePath].partName) + } + }); + } + }); + // Add the Default entries for manifests. + builders.forEach(function (builder) { + var manifestExt = path.extname(builder.getPath()); + if (contentTypes.Types.Default.filter(function (t) { return t.$.Extension === manifestExt; }).length === 0) { + contentTypes.Types.Default.push({ + $: { + Extension: manifestExt, + ContentType: builder.getContentType() + } + }); + } + }); + return utils_1.jsonToXml(contentTypes).replace(/\n/g, os.EOL); + }); + }; + /** + * List of known file types to use in the [Content_Types].xml file in the VSIX package. + */ + VsixManifestBuilder.CONTENT_TYPE_MAP = { + ".md": "text/markdown", + ".pdf": "application/pdf", + ".png": "image/png", + ".jpeg": "image/jpeg", + ".jpg": "image/jpeg", + ".gif": "image/gif", + ".bat": "application/bat", + ".json": "application/json", + ".vsixlangpack": "text/xml", + ".vsixmanifest": "text/xml", + ".vsomanifest": "application/json", + ".ps1": "text/ps1", + ".js": "application/javascript", + ".css": "text/css" + }; + VsixManifestBuilder.BEST_GUESS_CONTENT_TYPES = { + "README": "text/plain", + "LICENSE": "text/plain", + "AUTHORS": "text/plain" + }; + VsixManifestBuilder.manifestType = "vsix"; + return VsixManifestBuilder; +}(manifest_1.ManifestBuilder)); +exports.VsixManifestBuilder = VsixManifestBuilder; diff --git a/_build/exec/extension/_lib/vsix-writer.js b/_build/exec/extension/_lib/vsix-writer.js new file mode 100644 index 00000000..0293f167 --- /dev/null +++ b/_build/exec/extension/_lib/vsix-writer.js @@ -0,0 +1,209 @@ +"use strict"; +var vsix_manifest_builder_1 = require("./vsix-manifest-builder"); +var utils_1 = require("./utils"); +var _ = require("lodash"); +var fs = require("fs"); +var mkdirp = require("mkdirp"); +var os = require("os"); +var path = require("path"); +var Q = require("q"); +var trace = require('../../../lib/trace'); +var zip = require("jszip"); +/** + * Facilitates packaging the vsix and writing it to a file + */ +var VsixWriter = (function () { + /** + * constructor + * @param any vsoManifest JS Object representing a vso manifest + * @param any vsixManifest JS Object representing the XML for a vsix manifest + */ + function VsixWriter(settings, components) { + this.settings = settings; + this.manifestBuilders = components.builders; + this.resources = components.resources; + } + /** + * If outPath is {auto}, generate an automatic file name. + * Otherwise, try to determine if outPath is a directory (checking for a . in the filename) + * If it is, generate an automatic filename in the given outpath + * Otherwise, outPath doesn't change. + */ + VsixWriter.prototype.getOutputPath = function (outPath) { + // Find the vsix manifest, if it exists + var vsixBuilders = this.manifestBuilders.filter(function (b) { return b.getType() === vsix_manifest_builder_1.VsixManifestBuilder.manifestType; }); + var autoName = "extension.vsix"; + if (vsixBuilders.length === 1) { + var vsixManifest = vsixBuilders[0].getData(); + var pub = _.get(vsixManifest, "PackageManifest.Metadata[0].Identity[0].$.Publisher"); + var ns = _.get(vsixManifest, "PackageManifest.Metadata[0].Identity[0].$.Id"); + var version = _.get(vsixManifest, "PackageManifest.Metadata[0].Identity[0].$.Version"); + autoName = pub + "." + ns + "-" + version + ".vsix"; + } + if (outPath === "{auto}") { + return path.resolve(autoName); + } + else { + var basename = path.basename(outPath); + if (basename.indexOf(".") > 0) { + return path.resolve(outPath); + } + else { + return path.resolve(path.join(outPath, autoName)); + } + } + }; + VsixWriter.validatePartName = function (partName) { + var segments = partName.split("/"); + if (segments.length === 1 && segments[0] === "[Content_Types].xml") { + return true; + } + // matches most invalid segments. + var re = /(%2f)|(%5c)|(^$)|(%[^0-9a-f])|(%.[^0-9a-f])|(\.$)|([^a-z0-9._~%!$&'()*+,;=:@-])/i; + return segments.filter(function (segment) { return re.test(segment); }).length === 0; + }; + /** + * Write a vsix package to the given file name + */ + VsixWriter.prototype.writeVsix = function () { + var _this = this; + var outputPath = this.getOutputPath(this.settings.outputPath); + var vsix = new zip(); + var builderPromises = []; + this.manifestBuilders.forEach(function (builder) { + // Avoid the error EMFILE: too many open files + var addPackageFilesBatch = function (paths, numBatch, batchSize, deferred) { + deferred = deferred || Q.defer(); + var readFilePromises = []; + var start = numBatch * batchSize; + var end = Math.min(paths.length, start + batchSize); + var _loop_1 = function(i) { + var path_1 = paths[i]; + var itemName = utils_1.toZipItemName(builder.files[path_1].partName); + if (!VsixWriter.validatePartName(itemName)) { + var eol = require("os").EOL; + throw "Part Name '" + itemName + "' is invalid. Please check the following: " + eol + "1. No whitespace or any of these characters: #^[]<>?" + eol + "2. Cannot end with a period." + eol + "3. No percent-encoded / or \\ characters. Additionally, % must be followed by two hex characters."; + } + if (itemName.indexOf(" ")) + if (!builder.files[path_1].content) { + var readFilePromise = Q.nfcall(fs.readFile, path_1).then(function (result) { + vsix.file(itemName, result); + }); + readFilePromises.push(readFilePromise); + } + else { + vsix.file(itemName, builder.files[path_1].content); + readFilePromises.push(Promise.resolve(null)); + } + }; + for (var i = start; i < end; i++) { + _loop_1(i); + } + Promise.all(readFilePromises).then(function () { + if (end < paths.length) { + // Next batch + addPackageFilesBatch(paths, numBatch + 1, batchSize, deferred); + } + else { + deferred.resolve(null); + } + }).catch(function (err) { + deferred.reject(err); + }); + return deferred.promise; + }; + // Add the package files in batches + var builderPromise = addPackageFilesBatch(Object.keys(builder.files), 0, VsixWriter.VSIX_ADD_FILES_BATCH_SIZE).then(function () { + // Add the manifest itself + vsix.file(utils_1.toZipItemName(builder.getPath()), builder.getResult()); + }); + builderPromises.push(builderPromise); + }); + return Promise.all(builderPromises).then(function () { + return _this.addResourceStrings(vsix); + }).then(function () { + trace.debug("Writing vsix to: %s", outputPath); + return Q.nfcall(mkdirp, path.dirname(outputPath)).then(function () { + var buffer = vsix.generate({ + type: "nodebuffer", + compression: "DEFLATE" + }); + return Q.nfcall(fs.writeFile, outputPath, buffer).then(function () { return outputPath; }); + }); + }); + }; + /** + * For each folder F under the localization folder (--loc-root), + * look for a resources.resjson file within F. If it exists, split the + * resources.resjson into one file per manifest. Add + * each to the vsix archive as F/ and F/Extension.vsixlangpack + */ + VsixWriter.prototype.addResourceStrings = function (vsix) { + // Make sure locRoot is set, that it refers to a directory, and + // iterate each subdirectory of that. + if (!this.settings.locRoot) { + return Promise.resolve(null); + } + var stringsPath = path.resolve(this.settings.locRoot); + return Q.Promise(function (resolve, reject, notify) { + fs.exists(stringsPath, function (exists) { + resolve(exists); + }); + }).then(function (exists) { + if (exists) { + return Q.nfcall(fs.lstat, stringsPath).then(function (stats) { + if (stats.isDirectory()) { + return true; + } + }); + } + else { + return Q.resolve(false); + } + }).then(function (stringsFolderExists) { + if (!stringsFolderExists) { + return Promise.resolve(null); + } + return Q.nfcall(fs.readdir, stringsPath).then(function (files) { + var promises = []; + files.forEach(function (languageTag) { + var filePath = path.join(stringsPath, languageTag); + var promise = Q.nfcall(fs.lstat, filePath).then(function (fileStats) { + if (fileStats.isDirectory()) { + var resourcePath_1 = path.join(filePath, "resources.resjson"); + return Q.Promise(function (resolve, reject, notify) { + fs.exists(resourcePath_1, function (exists) { + resolve(exists); + }); + }).then(function (exists) { + if (exists) { + } + else { + return Promise.resolve(null); + } + }); + } + }); + promises.push(promise); + }); + return Promise.all(promises); + }); + }); + }; + VsixWriter.VSIX_ADD_FILES_BATCH_SIZE = 20; + VsixWriter.VSO_MANIFEST_FILENAME = "extension.vsomanifest"; + VsixWriter.VSIX_MANIFEST_FILENAME = "extension.vsixmanifest"; + VsixWriter.CONTENT_TYPES_FILENAME = "[Content_Types].xml"; + VsixWriter.DEFAULT_XML_BUILDER_SETTINGS = { + indent: " ", + newline: os.EOL, + pretty: true, + xmldec: { + encoding: "utf-8", + standalone: null, + version: "1.0" + } + }; + return VsixWriter; +}()); +exports.VsixWriter = VsixWriter; diff --git a/_build/exec/extension/create.js b/_build/exec/extension/create.js new file mode 100644 index 00000000..9b73cbfc --- /dev/null +++ b/_build/exec/extension/create.js @@ -0,0 +1,59 @@ +"use strict"; +var __extends = (this && this.__extends) || function (d, b) { + for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); +}; +var merger_1 = require("./_lib/merger"); +var vsix_manifest_builder_1 = require("./_lib/vsix-manifest-builder"); +var vsix_writer_1 = require("./_lib/vsix-writer"); +var colors = require("colors"); +var extBase = require("./default"); +var trace = require('../../lib/trace'); +function getCommand(args) { + return new ExtensionCreate(args); +} +exports.getCommand = getCommand; +function createExtension(mergeSettings, packageSettings) { + return new merger_1.Merger(mergeSettings).merge().then(function (components) { + return new vsix_writer_1.VsixWriter(packageSettings, components).writeVsix().then(function (outPath) { + var vsixBuilders = components.builders.filter(function (b) { return b.getType() === vsix_manifest_builder_1.VsixManifestBuilder.manifestType; }); + var vsixBuilder; + if (vsixBuilders.length > 0) { + vsixBuilder = vsixBuilders[0]; + } + return { + path: outPath, + extensionId: vsixBuilder ? vsixBuilder.getExtensionId() : null, + publisher: vsixBuilder ? vsixBuilder.getExtensionPublisher() : null + }; + }); + }); +} +exports.createExtension = createExtension; +var ExtensionCreate = (function (_super) { + __extends(ExtensionCreate, _super); + function ExtensionCreate(passedArgs) { + _super.call(this, passedArgs, false); + this.description = "Create a vsix package for an extension."; + } + ExtensionCreate.prototype.getHelpArgs = function () { + return ["root", "manifests", "manifestGlobs", "override", "overridesFile", "revVersion", "bypassValidation", "publisher", "extensionId", "outputPath", "locRoot"]; + }; + ExtensionCreate.prototype.exec = function () { + var _this = this; + return this.getMergeSettings().then(function (mergeSettings) { + return _this.getPackageSettings().then(function (packageSettings) { + return createExtension(mergeSettings, packageSettings); + }); + }); + }; + ExtensionCreate.prototype.friendlyOutput = function (data) { + trace.info(colors.green("\n=== Completed operation: create extension ===")); + trace.info(" - VSIX: %s", data.path); + trace.info(" - Extension ID: %s", data.extensionId); + trace.info(" - Publisher: %s", data.publisher); + }; + return ExtensionCreate; +}(extBase.ExtensionBase)); +exports.ExtensionCreate = ExtensionCreate; diff --git a/_build/exec/extension/default.js b/_build/exec/extension/default.js new file mode 100644 index 00000000..50907668 --- /dev/null +++ b/_build/exec/extension/default.js @@ -0,0 +1,165 @@ +"use strict"; +var __extends = (this && this.__extends) || function (d, b) { + for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); +}; +var tfcommand_1 = require("../../lib/tfcommand"); +var publish_1 = require("./_lib/publish"); +var qfs_1 = require("../../lib/qfs"); +var _ = require("lodash"); +var args = require("../../lib/arguments"); +var Q = require("q"); +var trace = require("../../lib/trace"); +function getCommand(args) { + return new ExtensionBase(args); +} +exports.getCommand = getCommand; +var ManifestJsonArgument = (function (_super) { + __extends(ManifestJsonArgument, _super); + function ManifestJsonArgument() { + _super.apply(this, arguments); + } + return ManifestJsonArgument; +}(args.JsonArgument)); +exports.ManifestJsonArgument = ManifestJsonArgument; +var ExtensionBase = (function (_super) { + __extends(ExtensionBase, _super); + function ExtensionBase(passedArgs, serverCommand) { + if (serverCommand === void 0) { serverCommand = true; } + _super.call(this, passedArgs, serverCommand); + this.description = "Commands to package, publish, and manage Extensions for Visual Studio Team Services."; + } + ExtensionBase.prototype.getHelpArgs = function () { + return []; + }; + ExtensionBase.prototype.setCommandArgs = function () { + _super.prototype.setCommandArgs.call(this); + this.registerCommandArgument("extensionId", "Extension ID", "Use this as the extension ID instead of what is specified in the manifest.", args.StringArgument); + this.registerCommandArgument("publisher", "Publisher name", "Use this as the publisher ID instead of what is specified in the manifest.", args.StringArgument); + this.registerCommandArgument("serviceUrl", "Market URL", "URL to the VSS Marketplace.", args.StringArgument, "https://marketplace.visualstudio.com"); + this.registerCommandArgument("manifests", "Manifests", "List of individual manifest files (space separated).", args.ArrayArgument, "vss-extension.json"); + this.registerCommandArgument("manifestGlobs", "Manifest globs", "List of globs to find manifests (space separated).", args.ArrayArgument, null); + this.registerCommandArgument("outputPath", "Output path", "Path to write the VSIX.", args.StringArgument, "{auto}"); + this.registerCommandArgument("override", "Overrides JSON", "JSON string which is merged into the manifests, overriding any values.", ManifestJsonArgument, "{}"); + this.registerCommandArgument("overridesFile", "Overrides JSON file", "Path to a JSON file with overrides. This partial manifest will always take precedence over any values in the manifests.", args.ReadableFilePathsArgument, null); + this.registerCommandArgument("shareWith", "Share with", "List of VSTS Accounts with which to share the extension (space separated).", args.ArrayArgument, null); + this.registerCommandArgument("unshareWith", "Un-share with", "List of VSTS Accounts with which to un-share the extension (space separated).", args.ArrayArgument, null); + this.registerCommandArgument("vsix", "VSIX path", "Path to an existing VSIX (to publish or query for).", args.ReadableFilePathsArgument); + this.registerCommandArgument("bypassValidation", "Bypass local validation", null, args.BooleanArgument, "false"); + this.registerCommandArgument("locRoot", "Localization root", "Root of localization hierarchy (see README for more info).", args.ExistingDirectoriesArgument, null); + this.registerCommandArgument("displayName", "Display name", null, args.StringArgument); + this.registerCommandArgument("description", "Description", "Description of the Publisher.", args.StringArgument); + this.registerCommandArgument("revVersion", "Rev version", "Rev the patch-version of the extension and save the result.", args.BooleanArgument, "false"); + }; + ExtensionBase.prototype.getMergeSettings = function () { + return Promise.all([ + this.commandArgs.root.val(), + this.commandArgs.manifests.val(), + this.commandArgs.manifestGlobs.val(), + this.commandArgs.override.val(), + this.commandArgs.overridesFile.val(), + this.commandArgs.revVersion.val(), + this.commandArgs.bypassValidation.val(), + this.commandArgs.publisher.val(true), + this.commandArgs.extensionId.val(true) + ]).then(function (values) { + var root = values[0], manifests = values[1], manifestGlob = values[2], override = values[3], overridesFile = values[4], revVersion = values[5], bypassValidation = values[6], publisher = values[7], extensionId = values[8]; + if (publisher) { + _.set(override, "publisher", publisher); + } + if (extensionId) { + _.set(override, "extensionid", extensionId); + } + var overrideFileContent = Q.resolve(""); + if (overridesFile && overridesFile.length > 0) { + overrideFileContent = qfs_1.readFile(overridesFile[0], "utf-8"); + } + return overrideFileContent.then(function (contentStr) { + var content = contentStr; + if (content === "") { + content = "{}"; + if (overridesFile && overridesFile.length > 0) { + trace.warn("Overrides file was empty. No overrides will be imported from " + overridesFile[0]); + } + } + var mergedOverrides = {}; + var contentJSON = ""; + try { + contentJSON = JSON.parse(content); + } + catch (e) { + throw "Could not parse contents of " + overridesFile[0] + " as JSON. \n" + e; + } + _.merge(mergedOverrides, contentJSON, override); + return { + root: root[0], + manifests: manifests, + manifestGlobs: manifestGlob, + overrides: mergedOverrides, + bypassValidation: bypassValidation, + revVersion: revVersion + }; + }); + }); + }; + ExtensionBase.prototype.getPackageSettings = function () { + return Promise.all([ + this.commandArgs.outputPath.val(), + this.commandArgs.locRoot.val() + ]).then(function (values) { + var outputPath = values[0], locRoot = values[1]; + return { + outputPath: outputPath, + locRoot: locRoot && locRoot[0] + }; + }); + }; + ExtensionBase.prototype.identifyExtension = function () { + var _this = this; + return this.commandArgs.vsix.val(true).then(function (result) { + var vsixPath = _.isArray(result) ? result[0] : null; + var infoPromise; + if (!vsixPath) { + infoPromise = Promise.all([_this.commandArgs.publisher.val(), _this.commandArgs.extensionId.val()]).then(function (values) { + var publisher = values[0], extensionId = values[1]; + return publish_1.GalleryBase.getExtInfo({ extensionId: extensionId, publisher: publisher }); + }); + } + else { + infoPromise = Promise.all([ + _this.commandArgs.publisher.val(true), + _this.commandArgs.extensionId.val(true) + ]).then(function (values) { + var publisher = values[0], extensionId = values[1]; + return publish_1.GalleryBase.getExtInfo({ vsixPath: vsixPath, publisher: publisher, extensionId: extensionId }); + }); + } + return infoPromise; + }); + }; + ExtensionBase.prototype.getPublishSettings = function () { + return Promise.all([ + this.commandArgs.serviceUrl.val(), + this.commandArgs.vsix.val(true), + this.commandArgs.publisher.val(true), + this.commandArgs.extensionId.val(true), + this.commandArgs.shareWith.val() + ]).then(function (values) { + var marketUrl = values[0], vsix = values[1], publisher = values[2], extensionId = values[3], shareWith = values[4]; + var vsixPath = _.isArray(vsix) ? vsix[0] : null; + return { + galleryUrl: marketUrl, + vsixPath: vsixPath, + publisher: publisher, + extensionId: extensionId, + shareWith: shareWith + }; + }); + }; + ExtensionBase.prototype.exec = function (cmd) { + return this.getHelp(cmd); + }; + return ExtensionBase; +}(tfcommand_1.TfCommand)); +exports.ExtensionBase = ExtensionBase; diff --git a/_build/exec/extension/install.js b/_build/exec/extension/install.js new file mode 100644 index 00000000..9ff6b694 --- /dev/null +++ b/_build/exec/extension/install.js @@ -0,0 +1,119 @@ +"use strict"; +var __extends = (this && this.__extends) || function (d, b) { + for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); +}; +var args = require("../../lib/arguments"); +var colors = require("colors"); +var extBase = require("./default"); +var extInfo = require("./_lib/extensioninfo"); +var trace = require("../../lib/trace"); +var SPS_INSTANCE_TYPE = "951917AC-A960-4999-8464-E3F0AA25B381"; +function getCommand(args) { + return new ExtensionInstall(args); +} +exports.getCommand = getCommand; +var AccountInstallReport = (function () { + function AccountInstallReport(itemId, accountName, accountId, installed, reason) { + if (installed === void 0) { installed = false; } + this.itemId = itemId; + this.accountName = accountName; + this.accountId = accountId; + this.installed = installed; + this.reason = reason; + } + AccountInstallReport.prototype.setError = function (reason) { + this.installed = false; + this.reason = reason; + }; + AccountInstallReport.prototype.setInstalled = function (reason) { + this.installed = true; + this.reason = reason; + }; + return AccountInstallReport; +}()); +exports.AccountInstallReport = AccountInstallReport; +var ExtensionInstall = (function (_super) { + __extends(ExtensionInstall, _super); + function ExtensionInstall(passedArgs) { + _super.call(this, passedArgs); + this.description = "Install a Visual Studio Services Extension to a list of VSTS Accounts."; + } + ExtensionInstall.prototype.setCommandArgs = function () { + _super.prototype.setCommandArgs.call(this); + this.registerCommandArgument("accounts", "Installation target accounts", "List of accounts where to install the extension.", args.ArrayArgument); + }; + ExtensionInstall.prototype.getHelpArgs = function () { + return ["publisher", "extensionId", "vsix", "accounts"]; + }; + ExtensionInstall.prototype.exec = function () { + var _this = this; + // Read extension info from arguments + var result = { accounts: {}, extension: null }; + return this._getExtensionInfo() + .then(function (extInfo) { + var itemId = extInfo.publisher + "." + extInfo.id; + var galleryApi = _this.webApi.getGalleryApi(_this.webApi.serverUrl); + result.extension = itemId; + // Read accounts from arguments and resolve them to get its accountIds + return _this.commandArgs.accounts.val().then(function (accounts) { + // Install extension in each account + var installations = accounts.slice().map(function (account) { + var emsApi = _this.webApi.getExtensionManagementApi(_this.getEmsAccountUrl(_this.webApi.serverUrl, account)); + return emsApi.installExtensionByName(extInfo.publisher, extInfo.id).then(function (installation) { return [account, installation]; }); + }); + return Promise.all(installations); + }).then(function (installations) { + installations.forEach(function (installation) { + var account = installation[0]; + var installedExtension = installation[1]; + var installationResult = { installed: true, issues: null }; + if (installedExtension.installState.installationIssues && installedExtension.installState.installationIssues.length > 0) { + installationResult.installed = false; + installationResult.issues = "The following issues were encountered installing to " + account + ": \n" + installedExtension.installState.installationIssues.map(function (i) { return " - " + i; }).join("\n"); + } + result.accounts[account] = installationResult; + }); + return result; + }); + }); + }; + ExtensionInstall.prototype.getEmsAccountUrl = function (marketplaceUrl, accountName) { + if (marketplaceUrl.toLocaleLowerCase().indexOf("marketplace.visualstudio.com") >= 0) { + return "https://" + accountName + ".extmgmt.visualstudio.com"; + } + if (marketplaceUrl.toLocaleLowerCase().indexOf("me.tfsallin.net") >= 0) { + return marketplaceUrl.toLocaleLowerCase().indexOf("https://") === 0 ? + "https://" + accountName + ".me.tfsallin.net:8781" : + "http://" + accountName + ".me.tfsallin.net:8780"; + } + return marketplaceUrl; + }; + ExtensionInstall.prototype.friendlyOutput = function (data) { + trace.success("\n=== Completed operation: install extension ==="); + Object.keys(data.accounts).forEach(function (a) { + trace.info("- " + a + ": " + (data.accounts[a].installed ? colors.green("success") : colors.red(data.accounts[a].issues))); + }); + }; + ExtensionInstall.prototype._getExtensionInfo = function () { + var _this = this; + return this.commandArgs.vsix.val(true).then(function (vsixPath) { + var extInfoPromise; + if (vsixPath !== null) { + extInfoPromise = extInfo.getExtInfo(vsixPath[0], null, null); + } + else { + extInfoPromise = Promise.all([ + _this.commandArgs.publisher.val(), + _this.commandArgs.extensionId.val()]).then(function (values) { + var publisher = values[0], extension = values[1]; + return extInfo.getExtInfo(null, extension, publisher); + }); + } + return extInfoPromise; + }); + }; + return ExtensionInstall; +}(extBase.ExtensionBase)); +exports.ExtensionInstall = ExtensionInstall; diff --git a/_build/exec/extension/publish.js b/_build/exec/extension/publish.js new file mode 100644 index 00000000..219cb377 --- /dev/null +++ b/_build/exec/extension/publish.js @@ -0,0 +1,78 @@ +"use strict"; +var __extends = (this && this.__extends) || function (d, b) { + for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); +}; +var create_1 = require("./create"); +var colors = require("colors"); +var extBase = require("./default"); +var Q = require("q"); +var publishUtils = require("./_lib/publish"); +var trace = require('../../lib/trace'); +function getCommand(args) { + return new ExtensionPublish(args); +} +exports.getCommand = getCommand; +var ExtensionPublish = (function (_super) { + __extends(ExtensionPublish, _super); + function ExtensionPublish() { + _super.apply(this, arguments); + this.description = "Publish a Visual Studio Marketplace Extension."; + } + ExtensionPublish.prototype.getHelpArgs = function () { + return ["root", "manifests", "manifestGlobs", "override", "overridesFile", "bypassValidation", "publisher", "extensionId", "outputPath", "locRoot", + "vsix", "shareWith"]; + }; + ExtensionPublish.prototype.exec = function () { + var _this = this; + var galleryApi = this.webApi.getGalleryApi(this.webApi.serverUrl); + var result = {}; + return this.getPublishSettings().then(function (publishSettings) { + var extensionCreatePromise; + if (publishSettings.vsixPath) { + result.packaged = null; + extensionCreatePromise = Q.resolve(publishSettings.vsixPath); + } + else { + extensionCreatePromise = _this.getMergeSettings().then(function (mergeSettings) { + return _this.getPackageSettings().then(function (packageSettings) { + return create_1.createExtension(mergeSettings, packageSettings); + }); + }).then(function (createResult) { + result.packaged = createResult.path; + return createResult.path; + }); + } + return extensionCreatePromise.then(function (vsixPath) { + publishSettings.vsixPath = vsixPath; + var packagePublisher = new publishUtils.PackagePublisher(publishSettings, galleryApi); + return packagePublisher.publish().then(function (ext) { + result.published = true; + if (publishSettings.shareWith && publishSettings.shareWith.length >= 0) { + var sharingMgr = new publishUtils.SharingManager(publishSettings, galleryApi); + return sharingMgr.shareWith(publishSettings.shareWith).then(function () { + result.shared = publishSettings.shareWith; + return result; + }); + } + else { + result.shared = null; + return result; + } + }); + }); + }); + }; + ExtensionPublish.prototype.friendlyOutput = function (data) { + trace.info(colors.green("\n=== Completed operation: publish extension ===")); + var packagingStr = data.packaged ? colors.green(data.packaged) : colors.yellow("not packaged (existing package used)"); + var publishingStr = data.published ? colors.green("success") : colors.yellow("???"); + var sharingStr = data.shared ? "shared with " + data.shared.map(function (s) { return colors.green(s); }).join(", ") : colors.yellow("not shared (use --share-with to share)"); + trace.info(" - Packaging: %s", packagingStr); + trace.info(" - Publishing: %s", publishingStr); + trace.info(" - Sharing: %s", sharingStr); + }; + return ExtensionPublish; +}(extBase.ExtensionBase)); +exports.ExtensionPublish = ExtensionPublish; diff --git a/_build/exec/extension/publisher/create.js b/_build/exec/extension/publisher/create.js new file mode 100644 index 00000000..b1345776 --- /dev/null +++ b/_build/exec/extension/publisher/create.js @@ -0,0 +1,47 @@ +"use strict"; +var __extends = (this && this.__extends) || function (d, b) { + for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); +}; +var extPubBase = require("./default"); +var trace = require('../../../lib/trace'); +function getCommand(args) { + // this just offers description for help and to offer sub commands + return new ExtensionPublisherCreate(args); +} +exports.getCommand = getCommand; +var ExtensionPublisherCreate = (function (_super) { + __extends(ExtensionPublisherCreate, _super); + function ExtensionPublisherCreate() { + _super.apply(this, arguments); + this.description = "Create a Visual Studio Services Market publisher"; + } + ExtensionPublisherCreate.prototype.getHelpArgs = function () { + return ["publisher", "displayName", "description"]; + }; + ExtensionPublisherCreate.prototype.exec = function () { + var galleryApi = this.webApi.getGalleryApi(this.webApi.serverUrl); + return Promise.all([ + this.commandArgs.publisher.val(), + this.commandArgs.displayName.val(), + this.commandArgs.description.val() + ]).then(function (values) { + var publisherName = values[0], displayName = values[1], description = values[2]; + return galleryApi.createPublisher({ + publisherName: publisherName, + displayName: displayName, + shortDescription: description, + longDescription: description + }); + }); + }; + ExtensionPublisherCreate.prototype.friendlyOutput = function (data) { + trace.success("\n=== Completed operation: create publisher ==="); + trace.info(" - Name: %s", data.publisherName); + trace.info(" - Display Name: %s", data.displayName); + trace.info(" - Description: %s", data.longDescription); + }; + return ExtensionPublisherCreate; +}(extPubBase.ExtensionPublisherBase)); +exports.ExtensionPublisherCreate = ExtensionPublisherCreate; diff --git a/_build/exec/extension/publisher/default.js b/_build/exec/extension/publisher/default.js new file mode 100644 index 00000000..0b377c10 --- /dev/null +++ b/_build/exec/extension/publisher/default.js @@ -0,0 +1,23 @@ +"use strict"; +var __extends = (this && this.__extends) || function (d, b) { + for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); +}; +var extPub = require("../default"); +function getCommand(args) { + return new ExtensionPublisherBase(args); +} +exports.getCommand = getCommand; +var ExtensionPublisherBase = (function (_super) { + __extends(ExtensionPublisherBase, _super); + function ExtensionPublisherBase() { + _super.apply(this, arguments); + this.description = "Commands for managing Visual Studio Services Marketplace Publishers."; + } + ExtensionPublisherBase.prototype.exec = function (cmd) { + return this.getHelp(cmd); + }; + return ExtensionPublisherBase; +}(extPub.ExtensionBase)); +exports.ExtensionPublisherBase = ExtensionPublisherBase; diff --git a/_build/exec/extension/publisher/delete.js b/_build/exec/extension/publisher/delete.js new file mode 100644 index 00000000..97fced00 --- /dev/null +++ b/_build/exec/extension/publisher/delete.js @@ -0,0 +1,40 @@ +"use strict"; +var __extends = (this && this.__extends) || function (d, b) { + for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); +}; +var extPubBase = require("./default"); +var trace = require('../../../lib/trace'); +function getCommand(args) { + return new ExtensionPublisherDelete(args); +} +exports.getCommand = getCommand; +var ExtensionPublisherDelete = (function (_super) { + __extends(ExtensionPublisherDelete, _super); + function ExtensionPublisherDelete() { + _super.apply(this, arguments); + this.description = "Delete a Visual Studio Services Market publisher."; + } + ExtensionPublisherDelete.prototype.getArgs = function () { + return ["publisher"]; + }; + ExtensionPublisherDelete.prototype.exec = function () { + var galleryApi = this.webApi.getGalleryApi(this.webApi.serverUrl); + return this.commandArgs.publisher.val().then(function (publisherName) { + return galleryApi.deletePublisher(publisherName).then(function () { + return { + publisher: { + publisherName: publisherName + } + }; + }); + }); + }; + ExtensionPublisherDelete.prototype.friendlyOutput = function (data) { + trace.success("\n=== Completed operation: delete publisher ==="); + trace.info(" - Name: %s", data.publisher.publisherName); + }; + return ExtensionPublisherDelete; +}(extPubBase.ExtensionPublisherBase)); +exports.ExtensionPublisherDelete = ExtensionPublisherDelete; diff --git a/_build/exec/extension/share.js b/_build/exec/extension/share.js new file mode 100644 index 00000000..9da6a412 --- /dev/null +++ b/_build/exec/extension/share.js @@ -0,0 +1,59 @@ +"use strict"; +var __extends = (this && this.__extends) || function (d, b) { + for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); +}; +var args = require("../../lib/arguments"); +var extBase = require("./default"); +var extInfo = require("./_lib/extensioninfo"); +var trace = require('../../lib/trace'); +function getCommand(args) { + return new ExtensionShare(args); +} +exports.getCommand = getCommand; +var ExtensionShare = (function (_super) { + __extends(ExtensionShare, _super); + function ExtensionShare(passedArgs) { + _super.call(this, passedArgs); + this.description = "Share a Visual Studio Services Extension with VSTS Accounts."; + this.registerCommandArgument("shareWith", "Share with", "List of accounts with which to share the extension.", args.ArrayArgument); + } + ExtensionShare.prototype.getHelpArgs = function () { + return ["publisher", "extensionId", "vsix", "shareWith"]; + }; + ExtensionShare.prototype.exec = function () { + var _this = this; + var galleryApi = this.webApi.getGalleryApi(this.webApi.serverUrl); + return this.commandArgs.vsix.val(true).then(function (vsixPath) { + var extInfoPromise; + if (vsixPath !== null) { + extInfoPromise = extInfo.getExtInfo(vsixPath[0], null, null); + } + else { + extInfoPromise = Promise.all([_this.commandArgs.publisher.val(), _this.commandArgs.extensionId.val()]).then(function (values) { + var publisher = values[0], extension = values[1]; + return extInfo.getExtInfo(null, extension, publisher); + }); + } + return extInfoPromise.then(function (extInfo) { + return _this.commandArgs.shareWith.val().then(function (shareWith) { + var sharePromises = []; + shareWith.forEach(function (account) { + sharePromises.push(galleryApi.shareExtension(extInfo.publisher, extInfo.id, account)); + }); + return Promise.all(sharePromises).then(function () { return shareWith; }); + }); + }); + }); + }; + ExtensionShare.prototype.friendlyOutput = function (data) { + trace.success("\n=== Completed operation: share extension ==="); + trace.info(" - Shared with:"); + data.forEach(function (acct) { + trace.info(" - " + acct); + }); + }; + return ExtensionShare; +}(extBase.ExtensionBase)); +exports.ExtensionShare = ExtensionShare; diff --git a/_build/exec/extension/show.js b/_build/exec/extension/show.js new file mode 100644 index 00000000..93070ae3 --- /dev/null +++ b/_build/exec/extension/show.js @@ -0,0 +1,31 @@ +"use strict"; +var __extends = (this && this.__extends) || function (d, b) { + for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); +}; +var extBase = require("./default"); +var publishUtils = require("./_lib/publish"); +function getCommand(args) { + return new ExtensionShow(args); +} +exports.getCommand = getCommand; +var ExtensionShow = (function (_super) { + __extends(ExtensionShow, _super); + function ExtensionShow() { + _super.apply(this, arguments); + this.description = "Show info about a published Visual Studio Services Extension."; + } + ExtensionShow.prototype.getHelpArgs = function () { + return ["publisher", "extensionId", "vsix"]; + }; + ExtensionShow.prototype.exec = function () { + var galleryApi = this.webApi.getGalleryApi(this.webApi.serverUrl); + return this.identifyExtension().then(function (extInfo) { + var sharingMgr = new publishUtils.SharingManager({}, galleryApi, extInfo); + return sharingMgr.getExtensionInfo(); + }); + }; + return ExtensionShow; +}(extBase.ExtensionBase)); +exports.ExtensionShow = ExtensionShow; diff --git a/_build/exec/extension/unshare.js b/_build/exec/extension/unshare.js new file mode 100644 index 00000000..7acda175 --- /dev/null +++ b/_build/exec/extension/unshare.js @@ -0,0 +1,61 @@ +"use strict"; +var __extends = (this && this.__extends) || function (d, b) { + for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); +}; +var args = require("../../lib/arguments"); +var extBase = require("./default"); +var extInfo = require("./_lib/extensioninfo"); +var trace = require('../../lib/trace'); +function getCommand(args) { + // this just offers description for help and to offer sub commands + return new ExtensionShare(args); +} +exports.getCommand = getCommand; +var ExtensionShare = (function (_super) { + __extends(ExtensionShare, _super); + function ExtensionShare(passedArgs) { + _super.call(this, passedArgs); + this.description = "Unshare a Visual Studio Services Extension with VSTS Accounts."; + // Override this argument so we are prompted (e.g. no default provided) + this.registerCommandArgument("unshareWith", "Un-share with", "List of accounts with which to un-share the extension", args.ArrayArgument); + } + ExtensionShare.prototype.getHelpArgs = function () { + return ["publisher", "extensionId", "vsix", "unshareWith"]; + }; + ExtensionShare.prototype.exec = function () { + var _this = this; + var galleryApi = this.webApi.getGalleryApi(this.webApi.serverUrl); + return this.commandArgs.vsix.val(true).then(function (vsixPath) { + var extInfoPromise; + if (vsixPath !== null) { + extInfoPromise = extInfo.getExtInfo(vsixPath[0], null, null); + } + else { + extInfoPromise = Promise.all([_this.commandArgs.publisher.val(), _this.commandArgs.extensionId.val()]).then(function (values) { + var publisher = values[0], extension = values[1]; + return extInfo.getExtInfo(null, extension, publisher); + }); + } + return extInfoPromise.then(function (extInfo) { + return _this.commandArgs.unshareWith.val().then(function (unshareWith) { + var sharePromises = []; + unshareWith.forEach(function (account) { + sharePromises.push(galleryApi.unshareExtension(extInfo.publisher, extInfo.id, account)); + }); + return Promise.all(sharePromises).then(function () { return unshareWith; }); + }); + }); + }); + }; + ExtensionShare.prototype.friendlyOutput = function (data) { + trace.success("\n=== Completed operation: un-share extension ==="); + trace.info(" - Removed sharing from:"); + data.forEach(function (acct) { + trace.info(" - " + acct); + }); + }; + return ExtensionShare; +}(extBase.ExtensionBase)); +exports.ExtensionShare = ExtensionShare; diff --git a/_build/exec/login.js b/_build/exec/login.js new file mode 100644 index 00000000..5a29928e --- /dev/null +++ b/_build/exec/login.js @@ -0,0 +1,76 @@ +"use strict"; +var __extends = (this && this.__extends) || function (d, b) { + for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); +}; +var tfcommand_1 = require("../lib/tfcommand"); +var diskcache_1 = require("../lib/diskcache"); +var credstore_1 = require("../lib/credstore"); +var colors = require("colors"); +var Q = require('q'); +var os = require('os'); +var trace = require('../lib/trace'); +function getCommand(args) { + // this just offers description for help and to offer sub commands + return new Login(args); +} +exports.getCommand = getCommand; +/** + * Facilitates a "log in" to a service by caching credentials. + */ +var Login = (function (_super) { + __extends(Login, _super); + function Login() { + _super.apply(this, arguments); + this.description = "Login and cache credentials using a PAT or basic auth."; + } + Login.prototype.exec = function () { + var _this = this; + trace.debug('Login.exec'); + var authHandler; + return this.commandArgs.serviceUrl.val().then(function (collectionUrl) { + return _this.getCredentials(collectionUrl, false).then(function (handler) { + authHandler = handler; + return _this.getWebApi(); + }).then(function (webApi) { + var agentApi = webApi.getTaskAgentApi(); + return Q.Promise(function (resolve, reject) { + return agentApi.connect().then(function (obj) { + var tfxCredStore = credstore_1.getCredentialStore("tfx"); + var tfxCache = new diskcache_1.DiskCache("tfx"); + var credString; + if (authHandler.username === "OAuth") { + credString = "pat:" + authHandler.password; + } + else { + credString = "basic:" + authHandler.username + ":" + authHandler.password; + } + return tfxCredStore.storeCredential(collectionUrl, "allusers", credString).then(function () { + return tfxCache.setItem("cache", "connection", collectionUrl); + }); + }).catch(function (err) { + if (err && err.statusCode && err.statusCode === 401) { + trace.debug("Connection failed: invalid credentials."); + throw "Invalid credentials."; + } + else if (err) { + trace.debug("Connection failed."); + throw "Connection failed. Check your internet connection & collection URL." + os.EOL + "Message: " + err.message; + } + }); + }); + }); + }); + }; + Login.prototype.friendlyOutput = function (data) { + if (data.success) { + trace.info(colors.green("Logged in successfully")); + } + else { + trace.error("login unsuccessful."); + } + }; + return Login; +}(tfcommand_1.TfCommand)); +exports.Login = Login; diff --git a/_build/exec/logout.js b/_build/exec/logout.js new file mode 100644 index 00000000..c33b27b7 --- /dev/null +++ b/_build/exec/logout.js @@ -0,0 +1,53 @@ +"use strict"; +var __extends = (this && this.__extends) || function (d, b) { + for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); +}; +var tfcommand_1 = require("../lib/tfcommand"); +var diskcache_1 = require("../lib/diskcache"); +var credStore = require("../lib/credstore"); +var trace = require("../lib/trace"); +function getCommand(args) { + return new Reset(args); +} +exports.getCommand = getCommand; +var Reset = (function (_super) { + __extends(Reset, _super); + function Reset(args) { + _super.call(this, args, false); + this.description = "Log out and clear cached credential."; + } + Reset.prototype.getHelpArgs = function () { return []; }; + Reset.prototype.exec = function () { + return Promise.resolve(null); + }; + Reset.prototype.dispose = function () { + var diskCache = new diskcache_1.DiskCache("tfx"); + return diskCache.itemExists("cache", "connection").then(function (isCachedConnection) { + if (isCachedConnection) { + return diskCache.getItem("cache", "connection").then(function (cachedConnection) { + var store = credStore.getCredentialStore("tfx"); + return store.credentialExists(cachedConnection, "allusers").then(function (isCredential) { + if (isCredential) { + return store.clearCredential(cachedConnection, "allusers"); + } + else { + return Promise.resolve(null); + } + }); + }).then(function () { + return diskCache.deleteItem("cache", "connection"); + }); + } + else { + return Promise.resolve(null); + } + }); + }; + Reset.prototype.friendlyOutput = function () { + trace.success("Successfully logged out."); + }; + return Reset; +}(tfcommand_1.TfCommand)); +exports.Reset = Reset; diff --git a/_build/exec/reset.js b/_build/exec/reset.js new file mode 100644 index 00000000..fab6f91e --- /dev/null +++ b/_build/exec/reset.js @@ -0,0 +1,50 @@ +"use strict"; +var __extends = (this && this.__extends) || function (d, b) { + for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); +}; +var tfcommand_1 = require("../lib/tfcommand"); +var diskcache_1 = require("../lib/diskcache"); +var os_1 = require("os"); +var args = require("../lib/arguments"); +var path = require("path"); +var trace = require("../lib/trace"); +function getCommand(args) { + return new Reset(args); +} +exports.getCommand = getCommand; +var Reset = (function (_super) { + __extends(Reset, _super); + function Reset(args) { + _super.call(this, args, false); + this.description = "Reset any saved options to their defaults."; + } + Reset.prototype.getHelpArgs = function () { return ["all"]; }; + Reset.prototype.setCommandArgs = function () { + _super.prototype.setCommandArgs.call(this); + this.registerCommandArgument("all", "All directories", "Pass this option to reset saved options for all directories.", args.BooleanArgument, "false"); + }; + Reset.prototype.exec = function () { + return Promise.resolve(null); + }; + Reset.prototype.dispose = function () { + var currentPath = path.resolve(); + return this.commandArgs.all.val().then(function (allSettings) { + return args.getOptionsCache().then(function (existingCache) { + if (existingCache[currentPath]) { + existingCache[currentPath] = {}; + return new diskcache_1.DiskCache("tfx").setItem("cache", "command-options", allSettings ? "" : JSON.stringify(existingCache, null, 4).replace(/\n/g, os_1.EOL)); + } + else { + return Promise.resolve(null); + } + }); + }); + }; + Reset.prototype.friendlyOutput = function () { + trace.success("Settings reset."); + }; + return Reset; +}(tfcommand_1.TfCommand)); +exports.Reset = Reset; diff --git a/_build/exec/version.js b/_build/exec/version.js new file mode 100644 index 00000000..3c80fe14 --- /dev/null +++ b/_build/exec/version.js @@ -0,0 +1,30 @@ +"use strict"; +var __extends = (this && this.__extends) || function (d, b) { + for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); +}; +var tfcommand_1 = require("../lib/tfcommand"); +var version = require("../lib/version"); +var trace = require("../lib/trace"); +function getCommand(args) { + return new Version(args); +} +exports.getCommand = getCommand; +var Version = (function (_super) { + __extends(Version, _super); + function Version(args) { + _super.call(this, args, false); + this.description = "Output the version of this tool."; + } + Version.prototype.getHelpArgs = function () { return []; }; + Version.prototype.exec = function () { + trace.debug("version.exec"); + return version.getTfxVersion(); + }; + Version.prototype.friendlyOutput = function (data) { + trace.info("Version %s", data.toString()); + }; + return Version; +}(tfcommand_1.TfCommand)); +exports.Version = Version; diff --git a/_build/exec/workitem/create.js b/_build/exec/workitem/create.js new file mode 100644 index 00000000..f3496255 --- /dev/null +++ b/_build/exec/workitem/create.js @@ -0,0 +1,45 @@ +"use strict"; +var __extends = (this && this.__extends) || function (d, b) { + for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); +}; +var Q = require("q"); +var witBase = require("./default"); +function getCommand(args) { + return new WorkItemCreate(args); +} +exports.getCommand = getCommand; +var WorkItemCreate = (function (_super) { + __extends(WorkItemCreate, _super); + function WorkItemCreate() { + _super.apply(this, arguments); + this.description = "Create a Work Item."; + } + WorkItemCreate.prototype.getHelpArgs = function () { + return ["workItemType", "title", "assignedTo", "description", "project", "values"]; + }; + WorkItemCreate.prototype.exec = function () { + var witapi = this.webApi.getWorkItemTrackingApi(); + return Promise.all([ + this.commandArgs.workItemType.val(), + this.commandArgs.project.val(), + this.commandArgs.title.val(true), + this.commandArgs.assignedTo.val(true), + this.commandArgs.description.val(true), + this.commandArgs.values.val(true) + ]).then(function (promiseValues) { + var wiType = promiseValues[0], project = promiseValues[1], title = promiseValues[2], assignedTo = promiseValues[3], description = promiseValues[4], values = promiseValues[5]; + if (!title && !assignedTo && !description && (!values || Object.keys(values).length <= 0)) { + return Q.reject("At least one field value must be specified."); + } + var patchDoc = witBase.buildWorkItemPatchDoc(title, assignedTo, description, values); + return witapi.createWorkItem(null, patchDoc, project, wiType); + }); + }; + WorkItemCreate.prototype.friendlyOutput = function (workItem) { + return witBase.friendlyOutput([workItem]); + }; + return WorkItemCreate; +}(witBase.WorkItemBase)); +exports.WorkItemCreate = WorkItemCreate; diff --git a/_build/exec/workitem/default.js b/_build/exec/workitem/default.js new file mode 100644 index 00000000..8cb912e9 --- /dev/null +++ b/_build/exec/workitem/default.js @@ -0,0 +1,102 @@ +"use strict"; +var __extends = (this && this.__extends) || function (d, b) { + for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); +}; +var tfcommand_1 = require("../../lib/tfcommand"); +var args = require("../../lib/arguments"); +var vssCoreContracts = require("vso-node-api/interfaces/common/VSSInterfaces"); +var trace = require("../../lib/trace"); +var os_1 = require("os"); +var _ = require("lodash"); +var WorkItemValuesJsonArgument = (function (_super) { + __extends(WorkItemValuesJsonArgument, _super); + function WorkItemValuesJsonArgument() { + _super.apply(this, arguments); + } + return WorkItemValuesJsonArgument; +}(args.JsonArgument)); +exports.WorkItemValuesJsonArgument = WorkItemValuesJsonArgument; +function getCommand(args) { + return new WorkItemBase(args); +} +exports.getCommand = getCommand; +var WorkItemBase = (function (_super) { + __extends(WorkItemBase, _super); + function WorkItemBase() { + _super.apply(this, arguments); + this.description = "Commands for managing Work Items."; + } + WorkItemBase.prototype.setCommandArgs = function () { + _super.prototype.setCommandArgs.call(this); + this.registerCommandArgument("workItemId", "Work Item ID", "Identifies a particular Work Item.", args.IntArgument); + this.registerCommandArgument("query", "Work Item Query (WIQL)", null, args.StringArgument); + this.registerCommandArgument("workItemType", "Work Item Type", "Type of Work Item to create.", args.StringArgument); + this.registerCommandArgument("assignedTo", "Assigned To", "Who to assign the Work Item to.", args.StringArgument); + this.registerCommandArgument("title", "Work Item Title", "Title of the Work Item.", args.StringArgument); + this.registerCommandArgument("description", "Work Item Description", "Description of the Work Item.", args.StringArgument); + this.registerCommandArgument("values", "Work Item Values", "Mapping from field reference name to value to set on the workitem. (E.g. {\"system.assignedto\": \"Some Name\"})", WorkItemValuesJsonArgument, "{}"); + }; + WorkItemBase.prototype.exec = function (cmd) { + return this.getHelp(cmd); + }; + return WorkItemBase; +}(tfcommand_1.TfCommand)); +exports.WorkItemBase = WorkItemBase; +function friendlyOutput(data) { + if (!data) { + throw new Error("no results"); + } + var fieldsToIgnore = ["System.AreaLevel1", "System.IterationId", "System.IterationLevel1", "System.ExternalLinkCount", "System.AreaLevel1"]; + data.forEach(function (workItem) { + trace.info(os_1.EOL); + trace.info("System.Id: " + workItem.id); + trace.info("System.Rev: " + workItem.rev); + Object.keys(workItem.fields).forEach(function (arg) { + if (!_.includes(fieldsToIgnore, arg)) { + trace.info(arg + ": " + workItem.fields[arg]); + } + }); + }); +} +exports.friendlyOutput = friendlyOutput; +function buildWorkItemPatchDoc(title, assignedTo, description, values) { + var patchDoc = []; + // Check the convienience helpers for wit values + if (title) { + patchDoc.push({ + op: vssCoreContracts.Operation.Add, + path: "/fields/System.Title", + value: title, + from: null + }); + } + if (assignedTo) { + patchDoc.push({ + op: vssCoreContracts.Operation.Add, + path: "/fields/System.AssignedTo", + value: assignedTo, + from: null + }); + } + if (description) { + patchDoc.push({ + op: vssCoreContracts.Operation.Add, + path: "/fields/System.Description", + value: description, + from: null + }); + } + // Set the field reference values + Object.keys(values).forEach(function (fieldReference) { + patchDoc.push({ + op: vssCoreContracts.Operation.Add, + path: "/fields/" + fieldReference, + value: values[fieldReference], + from: null + }); + }); + return patchDoc; +} +exports.buildWorkItemPatchDoc = buildWorkItemPatchDoc; diff --git a/_build/exec/workitem/query.js b/_build/exec/workitem/query.js new file mode 100644 index 00000000..c8397f9b --- /dev/null +++ b/_build/exec/workitem/query.js @@ -0,0 +1,41 @@ +"use strict"; +var __extends = (this && this.__extends) || function (d, b) { + for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); +}; +var witBase = require("./default"); +function getCommand(args) { + return new WorkItemQuery(args); +} +exports.getCommand = getCommand; +var WorkItemQuery = (function (_super) { + __extends(WorkItemQuery, _super); + function WorkItemQuery() { + _super.apply(this, arguments); + this.description = "Get a list of Work Items given a query"; + } + WorkItemQuery.prototype.getHelpArgs = function () { + return ["project", "query"]; + }; + WorkItemQuery.prototype.exec = function () { + var _this = this; + var witApi = this.webApi.getWorkItemTrackingApi(); + return this.commandArgs.project.val(true).then(function (projectName) { + return _this.commandArgs.query.val().then(function (query) { + var wiql = { query: query }; + return witApi.queryByWiql(wiql, { project: projectName }).then(function (result) { + var workItemIds = result.workItems.map(function (val) { return val.id; }).slice(0, Math.min(200, result.workItems.length)); + var fieldRefs = result.columns.map(function (val) { return val.referenceName; }); + fieldRefs = fieldRefs.slice(0, Math.min(20, result.columns.length)); + return witApi.getWorkItems(workItemIds, fieldRefs); + }); + }); + }); + }; + WorkItemQuery.prototype.friendlyOutput = function (data) { + return witBase.friendlyOutput(data); + }; + return WorkItemQuery; +}(witBase.WorkItemBase)); +exports.WorkItemQuery = WorkItemQuery; diff --git a/_build/exec/workitem/show.js b/_build/exec/workitem/show.js new file mode 100644 index 00000000..e5413379 --- /dev/null +++ b/_build/exec/workitem/show.js @@ -0,0 +1,32 @@ +"use strict"; +var __extends = (this && this.__extends) || function (d, b) { + for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); +}; +var witBase = require("./default"); +function getCommand(args) { + return new WorkItemShow(args); +} +exports.getCommand = getCommand; +var WorkItemShow = (function (_super) { + __extends(WorkItemShow, _super); + function WorkItemShow() { + _super.apply(this, arguments); + this.description = "Show Work Item details."; + } + WorkItemShow.prototype.getHelpArgs = function () { + return ["workItemId"]; + }; + WorkItemShow.prototype.exec = function () { + var witapi = this.webApi.getWorkItemTrackingApi(); + return this.commandArgs.workItemId.val().then(function (workItemId) { + return witapi.getWorkItem(workItemId); + }); + }; + WorkItemShow.prototype.friendlyOutput = function (workItem) { + return witBase.friendlyOutput([workItem]); + }; + return WorkItemShow; +}(witBase.WorkItemBase)); +exports.WorkItemShow = WorkItemShow; diff --git a/_build/exec/workitem/update.js b/_build/exec/workitem/update.js new file mode 100644 index 00000000..4c844eac --- /dev/null +++ b/_build/exec/workitem/update.js @@ -0,0 +1,44 @@ +"use strict"; +var __extends = (this && this.__extends) || function (d, b) { + for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); +}; +var Q = require("q"); +var witBase = require("./default"); +function getCommand(args) { + return new WorkItemUpdate(args); +} +exports.getCommand = getCommand; +var WorkItemUpdate = (function (_super) { + __extends(WorkItemUpdate, _super); + function WorkItemUpdate() { + _super.apply(this, arguments); + this.description = "Update a Work Item."; + } + WorkItemUpdate.prototype.getHelpArgs = function () { + return ["workItemId", "title", "assignedTo", "description", "values"]; + }; + WorkItemUpdate.prototype.exec = function () { + var witapi = this.webApi.getWorkItemTrackingApi(); + return Promise.all([ + this.commandArgs.workItemId.val(), + this.commandArgs.title.val(true), + this.commandArgs.assignedTo.val(true), + this.commandArgs.description.val(true), + this.commandArgs.values.val(true) + ]).then(function (promiseValues) { + var workItemId = promiseValues[0], title = promiseValues[1], assignedTo = promiseValues[2], description = promiseValues[3], values = promiseValues[4]; + if (!title && !assignedTo && !description && (!values || Object.keys(values).length <= 0)) { + return Q.reject("At least one field value must be specified."); + } + var patchDoc = witBase.buildWorkItemPatchDoc(title, assignedTo, description, values); + return witapi.updateWorkItem(null, patchDoc, workItemId); + }); + }; + WorkItemUpdate.prototype.friendlyOutput = function (workItem) { + return witBase.friendlyOutput([workItem]); + }; + return WorkItemUpdate; +}(witBase.WorkItemBase)); +exports.WorkItemUpdate = WorkItemUpdate; diff --git a/_build/lib/arguments.js b/_build/lib/arguments.js new file mode 100644 index 00000000..f4da185e --- /dev/null +++ b/_build/lib/arguments.js @@ -0,0 +1,467 @@ +"use strict"; +var __extends = (this && this.__extends) || function (d, b) { + for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); +}; +var _ = require("lodash"); +var common = require("../lib/common"); +var diskcache_1 = require("../lib/diskcache"); +var qfs = require("./qfs"); +var path = require("path"); +var qread = require("./qread"); +/** + * Class that represents an argument with a value. Calling .val() will retrieve + * the typed value, parsed from the givenValue (a string). If no givenValue + * was provided, we will prompt the user. + */ +var Argument = (function () { + function Argument(name, friendlyName, description, givenValue, hasDefaultValue) { + if (friendlyName === void 0) { friendlyName = name; } + this.name = name; + this.friendlyName = friendlyName; + this.description = description; + this.hasDefaultValue = hasDefaultValue; + this.silent = false; + if (typeof givenValue === "string") { + this.givenValue = [givenValue]; + } + else { + this.givenValue = givenValue; + } + this.initialize(); + } + /** + * If this argument was given a default value: + * check the cache + * if it's there, set assignedValue to the getValue(cachedValue) + * else set assigned value to given default + * If this argument was given a default value of null + * set null as the assignedValue + * If this argument was not given any value + * check the cache + * if it's there, set assignedValue to cachedValue + * + * Promise is resolved after any values that need parsing are parsed, + * and there are no more calls to the cache. + */ + Argument.prototype.initialize = function () { + var _this = this; + var initPromise = Promise.resolve(null); + if (this.assignedValue === undefined && (this.hasDefaultValue || this.givenValue === undefined)) { + initPromise = getOptionsCache().then(function (cache) { + var cacheKey = path.resolve().replace("/\.\[\]/g", "-") + "." + + common.EXEC_PATH.slice(0, common.EXEC_PATH.length - 1).join("/"); + var cachedValue = _.get(cache, cacheKey + "." + _this.name); + var cachedValueStringArray; + if (typeof cachedValue === "string") { + cachedValueStringArray = [cachedValue]; + } + else if (_.isArray(cachedValue)) { + cachedValueStringArray = cachedValue; + } + if (cachedValue !== undefined) { + return _this.getValue(cachedValueStringArray).then(function (result) { + _this.initializeAssignedValue(result); + }); + } + else if (_this.givenValue !== null && _this.givenValue !== undefined) { + return _this.getValue(_this.givenValue).then(function (result) { + _this.initializeAssignedValue(result); + }); + } + else if (_this.givenValue === null) { + _this.initializeAssignedValue(null); + } + }); + } + else if (this.assignedValue === undefined) { + if (this.givenValue === null) { + this.initializeAssignedValue(null); + } + else if (this.givenValue !== undefined) { + initPromise = this.getValue(this.givenValue).then(function (result) { + _this.initializeAssignedValue(result); + }); + } + } + this.initializePromise = initPromise; + return initPromise; + }; + Argument.prototype.initializeAssignedValue = function (val) { + if (this.assignedValue === undefined) { + this.assignedValue = val; + } + }; + /** + * Override whatever exists and give this argument a value. + */ + Argument.prototype.setValue = function (value) { + this.assignedValue = value; + this.initializePromise = Promise.resolve(null); + }; + /** + * Get the value of this argument by what was passed in. If nothing has + * been passed in, prompt the user. The resulting promise is resolved + * when a value is available. + */ + Argument.prototype.val = function (noPrompt) { + var _this = this; + if (noPrompt === void 0) { noPrompt = false; } + return this.initializePromise.then(function () { + if (_this.assignedValue !== undefined) { + return Promise.resolve(_this.assignedValue); + } + else { + if (!noPrompt) { + if (common.NO_PROMPT) { + throw "Missing required value for argument '" + _this.name + "'."; + } + return qread.read(_this.name, _this.friendlyName, _this.silent).then(function (answer) { + // Split answer into args, just as if they were passed through command line + var splitAnswer = answer.match(/".+?"|[^ ]+/g) || [""]; + var answerArgs = splitAnswer.map(function (a) { + // trim quotes if needed + if (a.substr(0, 1) === '"' && a.substr(a.length - 1, 1) === '"') { + a = a.substr(1, a.length - 1); + } + return a; + }); + return _this.getValue(answerArgs).then(function (result) { + _this.assignedValue = result; + _this.hasDefaultValue = false; + return result; + }); + }); + } + else { + return Promise.resolve(null); + } + } + }); + }; + return Argument; +}()); +exports.Argument = Argument; +/** + * Argument that represents an array of comma-separated strings. + */ +var ArrayArgument = (function (_super) { + __extends(ArrayArgument, _super); + function ArrayArgument() { + _super.apply(this, arguments); + } + ArrayArgument.prototype.getValue = function (argParams) { + if (argParams.length === 1) { + var stripped = argParams[0].replace(/(^\[)|(\]$)/g, ""); + return Promise.resolve(stripped.split(",").map(function (s) { return s.trim(); })); + } + else { + return Promise.resolve(argParams); + } + }; + return ArrayArgument; +}(Argument)); +exports.ArrayArgument = ArrayArgument; +/** + * Argument that represents a set of file paths. + * @TODO: Better validation of valid/invalid file paths (FS call?) + */ +var FilePathsArgument = (function (_super) { + __extends(FilePathsArgument, _super); + function FilePathsArgument() { + _super.apply(this, arguments); + } + FilePathsArgument.prototype.getValue = function (argParams) { + return Promise.resolve(argParams.map(function (p) { return path.resolve(p); })); + }; + return FilePathsArgument; +}(Argument)); +exports.FilePathsArgument = FilePathsArgument; +/** + * Argument that represents a set of existing file paths + */ +var ExistingFilePathsArgument = (function (_super) { + __extends(ExistingFilePathsArgument, _super); + function ExistingFilePathsArgument() { + _super.apply(this, arguments); + } + ExistingFilePathsArgument.prototype.getValue = function (argParams) { + return _super.prototype.getValue.call(this, argParams).then(function (paths) { + var existencePromises = []; + paths.forEach(function (p) { + var promise = qfs.exists(p).then(function (exists) { + if (!exists) { + throw new Error("The file at path " + p + " does not exist."); + } + else { + return p; + } + }); + existencePromises.push(promise); + }); + return Promise.all(existencePromises); + }); + }; + return ExistingFilePathsArgument; +}(FilePathsArgument)); +exports.ExistingFilePathsArgument = ExistingFilePathsArgument; +/** + * Argument that represents a set of writable file paths. + * Paths that refer to existing files are checked for writability + * Paths that refer to non-existent files are assumed writable. + */ +var WritableFilePathsArgument = (function (_super) { + __extends(WritableFilePathsArgument, _super); + function WritableFilePathsArgument() { + _super.apply(this, arguments); + } + WritableFilePathsArgument.prototype.getValue = function (argParams) { + return _super.prototype.getValue.call(this, argParams).then(function (paths) { + var canWritePromises = []; + paths.forEach(function (p) { + var promise = qfs.canWriteTo(p).then(function (canWrite) { + if (canWrite) { + return p; + } + else { + throw new Error("The file at path " + p + " is not writable."); + } + }); + canWritePromises.push(promise); + }); + return Promise.all(canWritePromises); + }); + }; + return WritableFilePathsArgument; +}(FilePathsArgument)); +exports.WritableFilePathsArgument = WritableFilePathsArgument; +/** + * Argument that represents a set of readable file paths + */ +var ReadableFilePathsArgument = (function (_super) { + __extends(ReadableFilePathsArgument, _super); + function ReadableFilePathsArgument() { + _super.apply(this, arguments); + } + ReadableFilePathsArgument.prototype.getValue = function (argParams) { + return _super.prototype.getValue.call(this, argParams).then(function (paths) { + var canReadPromises = []; + paths.forEach(function (p) { + var promise = qfs.fileAccess(p, qfs.R_OK).then(function (canRead) { + if (canRead) { + return p; + } + else { + throw new Error("The file at path " + p + " is not readable."); + } + }); + canReadPromises.push(promise); + }); + return Promise.all(canReadPromises); + }); + }; + return ReadableFilePathsArgument; +}(ExistingFilePathsArgument)); +exports.ReadableFilePathsArgument = ReadableFilePathsArgument; +/** + * Argument that represents a set of existing directory file paths + */ +var ExistingDirectoriesArgument = (function (_super) { + __extends(ExistingDirectoriesArgument, _super); + function ExistingDirectoriesArgument() { + _super.apply(this, arguments); + } + ExistingDirectoriesArgument.prototype.getValue = function (argParams) { + return _super.prototype.getValue.call(this, argParams).then(function (paths) { + var isDirectoryPromises = []; + paths.forEach(function (p) { + var promise = qfs.lstat(p).then(function (stats) { + if (stats.isDirectory()) { + return p; + } + else { + throw new Error("The path " + p + " is not a directory."); + } + }); + isDirectoryPromises.push(promise); + }); + return Promise.all(isDirectoryPromises); + }); + }; + return ExistingDirectoriesArgument; +}(ExistingFilePathsArgument)); +exports.ExistingDirectoriesArgument = ExistingDirectoriesArgument; +/** + * Argument that represents a boolean value. + */ +var BooleanArgument = (function (_super) { + __extends(BooleanArgument, _super); + function BooleanArgument() { + _super.apply(this, arguments); + } + /** + * If a value is given, parse it and cache the value. + */ + BooleanArgument.prototype.initialize = function () { + var _this = this; + this.initializePromise = Promise.resolve(null); + if (this.givenValue !== undefined) { + if (this.givenValue === null) { + this.assignedValue = false; + this.initializePromise = Promise.resolve(null); + } + else { + this.initializePromise = this.getValue(this.givenValue).then(function (result) { + _this.assignedValue = result; + }); + } + } + return this.initializePromise; + }; + /** + * If there is no argument to this option, assume true. + */ + BooleanArgument.prototype.getValue = function (argParams) { + if (argParams.length === 1) { + var yes = ["true", "1", "yes", "y"].indexOf(argParams[0].toLowerCase()) >= 0; + if (yes) { + return Promise.resolve(true); + } + var no = ["false", "0", "no", "n"].indexOf(argParams[0].toLowerCase()) >= 0; + if (no) { + return Promise.resolve(false); + } + throw new Error("'" + argParams[0] + "' is not a recognized Boolean value."); + } + else if (argParams.length === 0) { + return Promise.resolve(true); + } + else { + throw new Error("Multiple values provided for Boolean Argument " + this.name + "."); + } + }; + return BooleanArgument; +}(Argument)); +exports.BooleanArgument = BooleanArgument; +/** + * Argument that reprents an int value. + */ +var IntArgument = (function (_super) { + __extends(IntArgument, _super); + function IntArgument() { + _super.apply(this, arguments); + } + IntArgument.prototype.getValue = function (argParams) { + if (argParams.length === 1) { + var parseResult = parseInt(argParams[0], 10); + if (isNaN(parseResult)) { + throw new Error("Could not parse int argument " + this.name + "."); + } + return Promise.resolve(parseResult); + } + else if (argParams.length === 0) { + throw new Error("No number provided for Int Argument " + this.name + "."); + } + else { + throw new Error("Multiple values provided for Int Argument " + this.name + "."); + } + }; + return IntArgument; +}(Argument)); +exports.IntArgument = IntArgument; +/** + * Argument that reprents a float value. + */ +var FloatArgument = (function (_super) { + __extends(FloatArgument, _super); + function FloatArgument() { + _super.apply(this, arguments); + } + FloatArgument.prototype.getValue = function (argParams) { + if (argParams.length === 1) { + var parseResult = parseFloat(argParams[0]); + if (isNaN(parseResult)) { + throw new Error("Could not parse float argument " + this.name + "."); + } + return Promise.resolve(parseResult); + } + else if (argParams.length === 0) { + throw new Error("No number provided for Float Argument " + this.name + "."); + } + else { + throw new Error("Multiple values provided for Float Argument " + this.name + "."); + } + }; + return FloatArgument; +}(Argument)); +exports.FloatArgument = FloatArgument; +/** + * Argument that represents a block of JSON. + * Note: This class must be extended with a concrete type before its constructor + * function can be referenced. See exec/extensions/default.ts for an example. + */ +var JsonArgument = (function (_super) { + __extends(JsonArgument, _super); + function JsonArgument() { + _super.apply(this, arguments); + } + JsonArgument.prototype.getValue = function (argParams) { + try { + return Promise.resolve(JSON.parse(argParams.join(" "))); + } + catch (parseError) { + var info = parseError.stack || parseError.message; + throw new Error("Failed to parse JSON argument " + this.name + ". Info: " + info); + } + }; + return JsonArgument; +}(Argument)); +exports.JsonArgument = JsonArgument; +/** + * Argument that represents a string. Multiple values are joined together + * by a single space. + */ +var StringArgument = (function (_super) { + __extends(StringArgument, _super); + function StringArgument() { + _super.apply(this, arguments); + } + StringArgument.prototype.getValue = function (argParams) { + return Promise.resolve(argParams.join(" ")); + }; + return StringArgument; +}(Argument)); +exports.StringArgument = StringArgument; +/** + * Argument that represents a string, however, if we ever have to + * prompt the user for the value of this argument, we do not echo + * out the value as it is typed. Good for passwords, tokens, etc. + */ +var SilentStringArgument = (function (_super) { + __extends(SilentStringArgument, _super); + function SilentStringArgument() { + _super.apply(this, arguments); + this.silent = true; + } + return SilentStringArgument; +}(StringArgument)); +exports.SilentStringArgument = SilentStringArgument; +function getOptionsCache() { + var cache = new diskcache_1.DiskCache("tfx"); + return cache.itemExists("cache", "command-options").then(function (cacheExists) { + var existingCache = Promise.resolve("{}"); + if (cacheExists) { + existingCache = cache.getItem("cache", "command-options"); + } + return existingCache.then(function (cacheStr) { + try { + return JSON.parse(cacheStr); + } + catch (ex) { + return {}; + } + }); + }); +} +exports.getOptionsCache = getOptionsCache; diff --git a/_build/lib/command.js b/_build/lib/command.js new file mode 100644 index 00000000..9fcbd9bc --- /dev/null +++ b/_build/lib/command.js @@ -0,0 +1,60 @@ +"use strict"; +var common = require("./common"); +var fs = require("./qfs"); +var path = require("path"); +function getCommand() { + var args = process.argv.slice(2); + return getCommandHierarchy(path.resolve(common.APP_ROOT, "exec")).then(function (hierarchy) { + var execPath = []; + var commandArgs = []; + var currentHierarchy = hierarchy; + var inArgs = false; + args.forEach(function (arg) { + if (currentHierarchy && currentHierarchy[arg] !== undefined) { + currentHierarchy = currentHierarchy[arg]; + execPath.push(arg); + } + else if (arg.substr(0, 2) === "--" || inArgs) { + commandArgs.push(arg); + inArgs = true; + } + else { + throw "Command '" + arg + "' not found. For help, type tfx " + execPath.join(" ") + " --help"; + } + }); + return { + execPath: execPath, + args: commandArgs, + commandHierarchy: hierarchy + }; + }); +} +exports.getCommand = getCommand; +function getCommandHierarchy(root) { + var hierarchy = {}; + return fs.readdir(root).then(function (files) { + var filePromises = []; + files.forEach(function (file) { + if (file.substr(0, 1) === "_") { + return; + } + var fullPath = path.resolve(root, file); + var parsedPath = path.parse(fullPath); + var promise = fs.lstat(fullPath).then(function (stats) { + if (stats.isDirectory()) { + return getCommandHierarchy(fullPath).then(function (subHierarchy) { + hierarchy[parsedPath.name] = subHierarchy; + }); + } + else { + hierarchy[parsedPath.name] = null; + return null; + } + }); + filePromises.push(promise); + }); + return Promise.all(filePromises).then(function () { + return hierarchy; + }); + }); +} diff --git a/_build/lib/common.js b/_build/lib/common.js new file mode 100644 index 00000000..46dd947a --- /dev/null +++ b/_build/lib/common.js @@ -0,0 +1,26 @@ +"use strict"; +function endsWith(str, end) { + return str.slice(-end.length) == end; +} +exports.endsWith = endsWith; +/** + * Generate a new rfc4122 version 4 compliant GUID. + */ +function newGuid() { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { + var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8); + return v.toString(16); + }); +} +exports.newGuid = newGuid; +/** + * Repeat a string times. + */ +function repeatStr(str, count) { + var result = []; + for (var i = 0; i < count; ++i) { + result.push(str); + } + return result.join(""); +} +exports.repeatStr = repeatStr; diff --git a/_build/lib/connection.js b/_build/lib/connection.js new file mode 100644 index 00000000..54527bc6 --- /dev/null +++ b/_build/lib/connection.js @@ -0,0 +1,47 @@ +"use strict"; +var url = require("url"); +var TfsConnection = (function () { + function TfsConnection(serviceUrl) { + this.serviceUrl = serviceUrl; + this.parsedUrl = url.parse(this.serviceUrl); + var splitPath = this.parsedUrl.path.split("/").slice(1); + this.accountUrl = this.parsedUrl.protocol + "//" + this.parsedUrl.host; + if (splitPath.length === 2 && splitPath[0] === "tfs") { + // on prem + this.accountUrl += "/" + "tfs"; + } + else if (!this.parsedUrl.protocol || !this.parsedUrl.host) { + throw new Error("Invalid service url - protocol and host are required"); + } + else if (splitPath.length > 1) { + throw new Error("Invalid service url - path is too long. A service URL should include the account/application URL and the collection, e.g. https://fabrikam.visualstudio.com/DefaultCollection or http://tfs-server:8080/tfs/DefaultCollection"); + } + else if (splitPath.length === 0) { + throw new Error("Expected URL path."); + } + if (splitPath[0].trim() !== "" || (splitPath[0] === "tfs" && splitPath[1].trim() !== "")) { + this.collectionUrl = this.serviceUrl; + } + } + /** + * Returns the account URL from the given service URL + */ + TfsConnection.prototype.getAccountUrl = function () { + return this.accountUrl; + }; + /** + * Returns the collection URL from the given service URL + * if a collection is available. Otherwise, throws. + * @TODO maybe make this prompt for a new url? + */ + TfsConnection.prototype.getCollectionUrl = function () { + if (this.collectionUrl) { + return this.collectionUrl; + } + else { + throw new Error("Provided URL '" + this.serviceUrl + "' is not a collection-level URL. Ensure the URL contains a collection, e.g. https://fabrikam.visualstudio.com/DefaultCollection."); + } + }; + return TfsConnection; +}()); +exports.TfsConnection = TfsConnection; diff --git a/_build/lib/credstore.js b/_build/lib/credstore.js new file mode 100644 index 00000000..a9f35baa --- /dev/null +++ b/_build/lib/credstore.js @@ -0,0 +1,34 @@ +"use strict"; +var osHomedir = require('os-homedir'); +var path = require('path'); +var cm = require('./diskcache'); +var cache = new cm.DiskCache('tfx'); +function getCredentialStore(appName) { + // TODO: switch on OS specific cred stores. + var store = new FileStore(); + store.appName = appName; + return store; +} +exports.getCredentialStore = getCredentialStore; +var FileStore = (function () { + function FileStore() { + } + FileStore.prototype.escapeService = function (service) { + service = service.replace(/:/g, ''); + service = service.replace(/\//g, '_'); + return service; + }; + FileStore.prototype.credentialExists = function (service, user) { + return cache.itemExists(this.escapeService(service), user); + }; + FileStore.prototype.getCredential = function (service, user) { + return cache.getItem(this.escapeService(service), user); + }; + FileStore.prototype.storeCredential = function (service, user, password) { + return cache.setItem(this.escapeService(service), user, password); + }; + FileStore.prototype.clearCredential = function (service, user) { + return cache.deleteItem(this.escapeService(service), user); + }; + return FileStore; +}()); diff --git a/_build/lib/diskcache.js b/_build/lib/diskcache.js new file mode 100644 index 00000000..dad8bde4 --- /dev/null +++ b/_build/lib/diskcache.js @@ -0,0 +1,101 @@ +"use strict"; +var Q = require('q'); +var fs = require('fs'); +var osHomedir = require('os-homedir'); +var path = require('path'); +var shell = require('shelljs'); +var trace = require('./trace'); +var DiskCache = (function () { + function DiskCache(appName) { + this.appName = appName; + } + DiskCache.prototype.getFilePath = function (store, name) { + var storeFolder = path.join(osHomedir(), '.' + this.appName, store); + shell.mkdir('-p', storeFolder); + return path.join(storeFolder, '.' + name); + }; + DiskCache.prototype.itemExists = function (store, name) { + var defer = Q.defer(); + fs.exists(this.getFilePath(store, name), function (exists) { + defer.resolve(exists); + }); + return defer.promise; + }; + DiskCache.prototype.getItem = function (store, name) { + trace.debug('cache.getItem'); + var defer = Q.defer(); + var fp = this.getFilePath(store, name); + trace.debugArea('read: ' + store + ':' + name, 'CACHE'); + trace.debugArea(fp, 'CACHE'); + fs.readFile(fp, function (err, contents) { + if (err) { + defer.reject(err); + return; + } + var str = contents.toString(); + trace.debugArea(str, 'CACHE'); + defer.resolve(str); + }); + return defer.promise; + }; + DiskCache.prototype.setItem = function (store, name, data) { + trace.debug('cache.setItem'); + var defer = Q.defer(); + var fp = this.getFilePath(store, name); + trace.debugArea('write: ' + store + ':' + name + ':' + data, 'CACHE'); + trace.debugArea(fp, 'CACHE'); + fs.writeFile(fp, data, { flag: 'w' }, function (err) { + if (err) { + defer.reject(err); + return; + } + trace.debugArea('written', 'CACHE'); + defer.resolve(null); + }); + return defer.promise; + }; + DiskCache.prototype.deleteItem = function (store, name) { + var _this = this; + return Q.Promise(function (resolve, reject) { + fs.unlink(_this.getFilePath(store, name), function (err) { + if (err) { + reject(err); + } + else { + resolve(null); + } + }); + }); + }; + return DiskCache; +}()); +exports.DiskCache = DiskCache; +function parseSettingsFile(settingsPath, noWarn) { + trace.debug("diskcache.parseSettings"); + trace.debug("reading settings from %s", settingsPath); + return Q.Promise(function (resolve, reject, notify) { + try { + if (fs.existsSync(settingsPath)) { + var settingsStr = fs.readFileSync(settingsPath, "utf8").replace(/^\uFEFF/, ""); + var settingsJSON = void 0; + try { + resolve(JSON.parse(settingsStr)); + } + catch (err) { + trace.warn("Could not parse settings file as JSON. No settings were read from %s.", settingsPath); + resolve({}); + } + } + else { + if (!noWarn) { + trace.warn("No settings file found at %s.", settingsPath); + } + resolve({}); + } + } + catch (err) { + reject(err); + } + }); +} +exports.parseSettingsFile = parseSettingsFile; diff --git a/_build/lib/errorhandler.js b/_build/lib/errorhandler.js new file mode 100644 index 00000000..29f5d444 --- /dev/null +++ b/_build/lib/errorhandler.js @@ -0,0 +1,66 @@ +"use strict"; +var trace = require("./trace"); +function httpErr(obj) { + var errorAsObj = obj; + if (typeof errorAsObj === "string") { + try { + errorAsObj = JSON.parse(errorAsObj); + } + catch (parseError) { + throw errorAsObj; + } + } + var statusCode = errorAsObj.statusCode; + if (statusCode === 401) { + throw "Received response 401 (Not Authorized). Check that your personal access token is correct and hasn't expired."; + } + if (statusCode === 403) { + throw "Received response 403 (Forbidden). Check that you have access to this resource. Message from server: " + errorAsObj.message; + } + var errorBodyObj = errorAsObj.body; + if (errorBodyObj) { + if (typeof errorBodyObj === "string") { + try { + errorBodyObj = JSON.parse(errorBodyObj); + } + catch (parseError) { + throw errorBodyObj; + } + } + if (errorBodyObj.message) { + var message = errorBodyObj.message; + if (message) { + throw message; + } + else { + throw errorBodyObj; + } + } + } + else { + throw errorAsObj.message || "Encountered an unknown failure issuing an HTTP request."; + } +} +exports.httpErr = httpErr; +function errLog(arg) { + if (typeof arg === "string") { + trace.error(arg); + } + else if (typeof arg.toString === "function") { + trace.debug(arg.stack); + trace.error(arg.toString()); + } + else if (typeof arg === "object") { + try { + trace.error(JSON.parse(arg)); + } + catch (e) { + trace.error(arg); + } + } + else { + trace.error(arg); + } + process.exit(-1); +} +exports.errLog = errLog; diff --git a/_build/lib/jsonvalidate.js b/_build/lib/jsonvalidate.js new file mode 100644 index 00000000..72516a5c --- /dev/null +++ b/_build/lib/jsonvalidate.js @@ -0,0 +1,74 @@ +"use strict"; +var Q = require('q'); +var fs = require('fs'); +var shell = require('shelljs'); +var check = require('validator'); +var trace = require('./trace'); +/* + * Checks a json file for correct formatting against some validation function + * @param jsonFilePath path to the json file being validated + * @param jsonValidationFunction function that validates parsed json data against some criteria + * @return the parsed json file + * @throws InvalidDirectoryException if json file doesn't exist, InvalidJsonException on failed parse or *first* invalid field in json +*/ +function validate(jsonFilePath, jsonMissingErrorMessage) { + trace.debug('Validating task json...'); + var deferred = Q.defer(); + var jsonMissingErrorMsg = jsonMissingErrorMessage || 'specified json file does not exist.'; + this.exists(jsonFilePath, jsonMissingErrorMsg); + var taskJson; + try { + taskJson = require(jsonFilePath); + } + catch (jsonError) { + trace.debug('Invalid task json: %s', jsonError); + throw new Error("Invalid task json: " + jsonError); + } + var issues = this.validateTask(jsonFilePath, taskJson); + if (issues.length > 0) { + var output = "Invalid task json:"; + for (var i = 0; i < issues.length; i++) { + output += "\n\t" + issues[i]; + } + trace.debug(output); + deferred.reject(new Error(output)); + } + trace.debug('Json is valid.'); + deferred.resolve(taskJson); + return deferred.promise; +} +exports.validate = validate; +/* + * Wrapper for fs.existsSync that includes a user-specified errorMessage in an InvalidDirectoryException + */ +function exists(path, errorMessage) { + if (!fs.existsSync(path)) { + trace.debug(errorMessage); + throw new Error(errorMessage); + } +} +exports.exists = exists; +/* + * Validates a parsed json file describing a build task + * @param taskPath the path to the original json file + * @param taskData the parsed json file + * @return list of issues with the json file + */ +function validateTask(taskPath, taskData) { + var vn = (taskData.name || taskPath); + var issues = []; + if (!taskData.id || !check.isUUID(taskData.id)) { + issues.push(vn + ': id is a required guid'); + } + if (!taskData.name || !check.isAlphanumeric(taskData.name)) { + issues.push(vn + ': name is a required alphanumeric string'); + } + if (!taskData.friendlyName || !check.isLength(taskData.friendlyName, 1, 40)) { + issues.push(vn + ': friendlyName is a required string <= 40 chars'); + } + if (!taskData.instanceNameFormat) { + issues.push(vn + ': instanceNameFormat is required'); + } + return issues; +} +exports.validateTask = validateTask; diff --git a/_build/lib/loader.js b/_build/lib/loader.js new file mode 100644 index 00000000..b0e508b0 --- /dev/null +++ b/_build/lib/loader.js @@ -0,0 +1,49 @@ +"use strict"; +var common = require("../lib/common"); +var qfs = require("../lib/qfs"); +var path = require("path"); +var trace = require("../lib/trace"); +/** + * Load the module given by execPath and instantiate a TfCommand using args. + * @param {string[]} execPath: path to the module to load. This module must implement CommandFactory. + * @param {string[]} args: args to pass to the command factory to instantiate the TfCommand + * @return {Q.Promise} Promise that is resolved with the module's command + */ +function load(execPath, args) { + trace.debug('loader.load'); + var commandModulePath = path.resolve(common.APP_ROOT, "exec", execPath.join("/")); + return qfs.exists(commandModulePath).then(function (exists) { + var resolveDefaultPromise = Promise.resolve(commandModulePath); + if (exists) { + // If this extensionless path exists, it should be a directory. + // If the path doesn't exist, for now we assume that a file with a .js extension + // exists (if it doens't, we will find out below). + resolveDefaultPromise = qfs.lstat(commandModulePath).then(function (stats) { + if (stats.isDirectory()) { + return path.join(commandModulePath, "default"); + } + return commandModulePath; + }); + } + return resolveDefaultPromise.then(function (commandModulePath) { + var commandModule; + return qfs.exists(path.resolve(commandModulePath + ".js")).then(function (exists) { + if (!exists) { + throw new Error(commandModulePath + " is not a recognized command. Run with --help to see available commands."); + } + try { + commandModule = require(commandModulePath); + } + catch (e) { + trace.error(commandModulePath + " could not be fully loaded as a tfx command."); + throw e; + } + if (!commandModule.getCommand) { + throw new Error("Command modules must export a function, getCommand, that takes no arguments and returns an instance of TfCommand"); + } + return Promise.resolve(commandModule.getCommand(args)); + }); + }); + }); +} +exports.load = load; diff --git a/_build/lib/qfs.js b/_build/lib/qfs.js new file mode 100644 index 00000000..2a8bde88 --- /dev/null +++ b/_build/lib/qfs.js @@ -0,0 +1,67 @@ +"use strict"; +var fs = require("fs"); +var Q = require("q"); +// This is an fs lib that uses Q instead of callbacks. +exports.W_OK = fs.W_OK; +exports.R_OK = fs.R_OK; +exports.X_OK = fs.X_OK; +exports.F_OK = fs.F_OK; +function readdir(path) { + return Q.nfcall(fs.readdir, path); +} +exports.readdir = readdir; +function exists(path) { + return Q.Promise(function (resolve, reject, notify) { + fs.exists(path, function (fileExists) { + resolve(fileExists); + }); + }); +} +exports.exists = exists; +function lstat(path) { + return Q.nfcall(fs.lstat, path); +} +exports.lstat = lstat; +function readFile(filename, options) { + return Q.nfcall(fs.readFile, filename, options); +} +exports.readFile = readFile; +function writeFile(filename, data) { + return Q.nfcall(fs.writeFile, filename, data); +} +exports.writeFile = writeFile; +; +//export function writeFile(filename: string, data: any, callback?: (err: NodeJS.ErrnoException) => void): void; +/** + * Returns a promise resolved true or false if a file is accessible + * with the given mode (F_OK, R_OK, W_OK, X_OK) + */ +function fileAccess(path, mode) { + if (mode === void 0) { mode = exports.F_OK; } + return Q.Promise(function (resolve) { + fs.access(path, mode, function (err) { + if (err) { + resolve(false); + } + else { + resolve(true); + } + }); + }); +} +exports.fileAccess = fileAccess; +/** + * Given a valid path, resolves true if the file represented by the path + * can be written to. Files that do not exist are assumed writable. + */ +function canWriteTo(path) { + return exists(path).then(function (exists) { + if (exists) { + return fileAccess(path, fs.W_OK); + } + else { + return true; + } + }); +} +exports.canWriteTo = canWriteTo; diff --git a/_build/lib/qread.js b/_build/lib/qread.js new file mode 100644 index 00000000..75ac4799 --- /dev/null +++ b/_build/lib/qread.js @@ -0,0 +1,35 @@ +"use strict"; +var prompt = require("prompt"); +var Q = require("q"); +prompt.delimiter = ""; +prompt.message = "> "; +var queue = []; +// This is the read lib that uses Q instead of callbacks. +function read(name, message, silent) { + if (silent === void 0) { silent = false; } + var promise = Q.Promise(function (resolve, reject) { + var schema = { + properties: {} + }; + schema.properties[name] = { + required: true, + description: message + ":", + hidden: silent + }; + Promise.all(queue.filter(function (x) { return x !== promise; })).then(function () { + prompt.start(); + prompt.get(schema, function (err, result) { + if (err) { + reject(err); + } + else { + resolve(result[name]); + } + queue.shift(); + }); + }); + }); + queue.unshift(promise); + return promise; +} +exports.read = read; diff --git a/_build/lib/tfcommand.js b/_build/lib/tfcommand.js new file mode 100644 index 00000000..e67878cb --- /dev/null +++ b/_build/lib/tfcommand.js @@ -0,0 +1,500 @@ +"use strict"; +var diskcache_1 = require("../lib/diskcache"); +var credstore_1 = require("../lib/credstore"); +var common_1 = require("../lib/common"); +var connection_1 = require("../lib/connection"); +var WebApi_1 = require("vso-node-api/WebApi"); +var os_1 = require("os"); +var _ = require("lodash"); +var args = require("./arguments"); +var colors_1 = require("colors"); +var common = require("./common"); +var copypaste = require("copy-paste"); +var loader = require("../lib/loader"); +var path = require("path"); +var Q = require("q"); +var qfs = require("./qfs"); +var trace = require("./trace"); +var version = require("./version"); +var TfCommand = (function () { + /** + * @param serverCommand True to initialize the WebApi object during init phase. + */ + function TfCommand(passedArgs, serverCommand) { + if (serverCommand === void 0) { serverCommand = true; } + this.passedArgs = passedArgs; + this.serverCommand = serverCommand; + this.commandArgs = {}; + this.description = "A suite of command line tools to interact with Visual Studio Team Services."; + this.setCommandArgs(); + } + /** + * Returns a promise that is resolved when this command is initialized and + * ready to be executed. + */ + TfCommand.prototype.ensureInitialized = function () { + return this.initialized || this.initialize(); + }; + TfCommand.prototype.initialize = function () { + var _this = this; + this.initialized = this.commandArgs.help.val().then(function (needHelp) { + if (needHelp) { + return _this.run.bind(_this, _this.getHelp.bind(_this)); + } + else { + // Set the fiddler proxy + return _this.commandArgs.fiddler.val().then(function (useProxy) { + if (useProxy) { + process.env.HTTP_PROXY = "http://127.0.0.1:8888"; + } + }).then(function () { + // Set custom proxy + return _this.commandArgs.proxy.val(true).then(function (proxy) { + if (proxy) { + process.env.HTTP_PROXY = proxy; + } + }); + }).then(function () { + // Set the no-prompt flag + return _this.commandArgs.noPrompt.val(true).then(function (noPrompt) { + common.NO_PROMPT = noPrompt; + }); + }).then(function () { + // Set the cached service url + return _this.commandArgs.serviceUrl.val(true).then(function (serviceUrl) { + if (!serviceUrl && !process.env["TFX_BYPASS_CACHE"] && common.EXEC_PATH.join("") !== "login") { + var diskCache_1 = new diskcache_1.DiskCache("tfx"); + return diskCache_1.itemExists("cache", "connection").then(function (isConnection) { + var connectionUrlPromise; + if (!isConnection) { + connectionUrlPromise = Promise.resolve(null); + } + else { + connectionUrlPromise = diskCache_1.getItem("cache", "connection"); + } + return connectionUrlPromise.then(function (url) { + if (url) { + _this.commandArgs.serviceUrl.setValue(url); + } + }); + }); + } + else { + return Promise.resolve(null); + } + }); + }).then(function () { + var apiPromise = Promise.resolve(null); + if (_this.serverCommand) { + apiPromise = _this.getWebApi().then(function (_) { }); + } + return apiPromise.then(function () { + return _this.run.bind(_this, _this.exec.bind(_this)); + }); + }); + } + }); + return this.initialized; + }; + TfCommand.prototype.getGroupedArgs = function () { + if (!this.groupedArgs) { + var group_1 = {}; + var currentArg_1 = null; + this.passedArgs.forEach(function (arg) { + if (_.startsWith(arg, "--")) { + currentArg_1 = _.camelCase(arg.substr(2)); + group_1[currentArg_1] = []; + return; + } + if (currentArg_1) { + group_1[currentArg_1].push(arg); + } + }); + this.groupedArgs = group_1; + } + return this.groupedArgs; + }; + TfCommand.prototype.registerCommandArgument = function (name, friendlyName, description, ctor, defaultValue) { + var groupedArgs = this.getGroupedArgs(); + if (groupedArgs[name]) { + this.commandArgs[name] = new ctor(name, friendlyName, description, groupedArgs[name]); + } + else { + this.commandArgs[name] = new ctor(name, friendlyName, description, defaultValue, true); + } + }; + /** + * Register arguments that may be used with this command. + */ + TfCommand.prototype.setCommandArgs = function () { + this.registerCommandArgument("project", "Project name", null, args.StringArgument); + this.registerCommandArgument("root", "Root directory", null, args.ExistingDirectoriesArgument, "."); + this.registerCommandArgument("authType", "Authentication Method", "Method of authentication ('pat' or 'basic').", args.StringArgument, "pat"); + this.registerCommandArgument("serviceUrl", "Service URL", "URL to the service you will connect to, e.g. https://youraccount.visualstudio.com/DefaultCollection.", args.StringArgument); + this.registerCommandArgument("password", "Password", "Password to use for basic authentication.", args.SilentStringArgument); + this.registerCommandArgument("token", "Personal access token", null, args.SilentStringArgument); + this.registerCommandArgument("save", "Save settings", "Save arguments for the next time a command in this command group is run.", args.BooleanArgument, "false"); + this.registerCommandArgument("username", "Username", "Username to use for basic authentication.", args.StringArgument); + this.registerCommandArgument("output", "Output destination", "Method to use for output. Options: friendly, json, clipboard.", args.StringArgument, "friendly"); + this.registerCommandArgument("json", "Output as JSON", "Alias for --output json.", args.BooleanArgument, "false"); + this.registerCommandArgument("fiddler", "Use Fiddler proxy", "Set up the fiddler proxy for HTTP requests (for debugging purposes).", args.BooleanArgument, "false"); + this.registerCommandArgument("proxy", "Proxy server", "Use the specified proxy server for HTTP traffic.", args.StringArgument, null); + this.registerCommandArgument("help", "Help", "Get help for any command.", args.BooleanArgument, "false"); + this.registerCommandArgument("noPrompt", "No Prompt", "Do not prompt the user for input (instead, raise an error).", args.BooleanArgument, "false"); + }; + /** + * Return a list of registered arguments that should be displayed when help is emitted. + */ + TfCommand.prototype.getHelpArgs = function () { + return []; + }; + /** + * Get a BasicCredentialHandler based on the command arguments: + * If username & password are passed in, use those. + * If token is passed in, use that. + * Else, check the authType - if it is "pat", prompt for a token + * If it is "basic", prompt for username and password. + */ + TfCommand.prototype.getCredentials = function (serviceUrl, useCredStore) { + var _this = this; + if (useCredStore === void 0) { useCredStore = true; } + return Promise.all([ + this.commandArgs.authType.val(), + this.commandArgs.token.val(true), + this.commandArgs.username.val(true), + this.commandArgs.password.val(true) + ]).then(function (values) { + var authType = values[0], token = values[1], username = values[2], password = values[3]; + if (username && password) { + return WebApi_1.getBasicHandler(username, password); + } + else { + if (token) { + return WebApi_1.getBasicHandler("OAuth", token); + } + else { + var getCredentialPromise = void 0; + if (useCredStore) { + getCredentialPromise = credstore_1.getCredentialStore("tfx").getCredential(serviceUrl, "allusers"); + } + else { + getCredentialPromise = Q.reject(); + } + return getCredentialPromise.then(function (credString) { + if (credString.length <= 6) { + throw "Could not get credentials from credential store."; + } + if (credString.substr(0, 3) === "pat") { + return WebApi_1.getBasicHandler("OAuth", credString.substr(4)); + } + else if (credString.substr(0, 5) === "basic") { + var rest = credString.substr(6); + var unpwDividerIndex = rest.indexOf(":"); + var username_1 = rest.substr(0, unpwDividerIndex); + var password_1 = rest.substr(unpwDividerIndex + 1); + if (username_1 && password_1) { + return WebApi_1.getBasicHandler(username_1, password_1); + } + else { + throw "Could not get credentials from credential store."; + } + } + }).catch(function () { + if (authType.toLowerCase() === "pat") { + return _this.commandArgs.token.val().then(function (token) { + return WebApi_1.getBasicHandler("OAuth", token); + }); + } + else if (authType.toLowerCase() === "basic") { + return _this.commandArgs.username.val().then(function (username) { + return _this.commandArgs.password.val().then(function (password) { + return WebApi_1.getBasicHandler(username, password); + }); + }); + } + else { + throw new Error("Unsupported auth type. Currently, 'pat' and 'basic' auth are supported."); + } + }); + } + } + }); + }; + TfCommand.prototype.getWebApi = function () { + var _this = this; + return this.commandArgs.serviceUrl.val().then(function (url) { + return _this.getCredentials(url).then(function (handler) { + _this.connection = new connection_1.TfsConnection(url); + _this.webApi = new WebApi_1.WebApi(url, handler); + return _this.webApi; + }); + }); + }; + TfCommand.prototype.run = function (main, cmd) { + var _this = this; + return main(cmd).then(function (result) { + return _this.output(result).then(function () { + return _this.dispose(); + }); + }); + }; + /** + * Should be called after exec. In here we will write settings to fs if necessary. + */ + TfCommand.prototype.dispose = function () { + var _this = this; + var newToCache = {}; + return this.commandArgs.save.val().then(function (shouldSave) { + if (shouldSave) { + var cacheKey_1 = path.resolve().replace("/\.\[\]/g", "-") + "." + + common.EXEC_PATH.slice(0, common.EXEC_PATH.length - 1).join("/"); + var getValuePromises_1 = []; + Object.keys(_this.commandArgs).forEach(function (arg) { + var argObj = _this.commandArgs[arg]; + if (!argObj.hasDefaultValue) { + var pr = argObj.val().then(function (value) { + // don"t cache these 5 options. + if (["username", "password", "save", "token", "help"].indexOf(arg) < 0) { + _.set(newToCache, cacheKey_1 + "." + arg, value); + } + }); + getValuePromises_1.push(pr); + } + }); + return Promise.all(getValuePromises_1).then(function () { + return args.getOptionsCache().then(function (existingCache) { + // custom shallow-ish merge of cache properties. + var newInThisCommand = _.get(newToCache, cacheKey_1); + if (!_.get(existingCache, cacheKey_1)) { + _.set(existingCache, cacheKey_1, {}); + } + if (newInThisCommand) { + Object.keys(newInThisCommand).forEach(function (key) { + _.set(existingCache, cacheKey_1 + "." + key, newInThisCommand[key]); + }); + new diskcache_1.DiskCache("tfx").setItem("cache", "command-options", JSON.stringify(existingCache, null, 4).replace(/\n/g, os_1.EOL)); + } + }); + }); + } + else { + return Promise.resolve(null); + } + }); + }; + /** + * Gets help (as a string) for the given command + */ + TfCommand.prototype.getHelp = function (cmd) { + var _this = this; + this.commandArgs.output.setValue("help"); + var result = os_1.EOL; + result += [" fTfs ", + " fSSSSSSSs ", + " fSSSSSSSSSS ", + " TSSf fSSSSSSSSSSSS ", + " SSSSSF fSSSSSSST SSSSS ", + " SSfSSSSSsfSSSSSSSt SSSSS ", + " SS tSSSSSSSSSs SSSSS ", + " SS fSSSSSSST SSSSS ", + " SS fSSSSSFSSSSSSf SSSSS ", + " SSSSSST FSSSSSSFt SSSSS ", + " SSSSt FSSSSSSSSSSSS ", + " FSSSSSSSSSS ", + " FSSSSSSs ", + " FSFs (TM) "]. + map(function (l) { return colors_1.magenta(l); }).join(os_1.EOL) + os_1.EOL + os_1.EOL; + var continuedHierarchy = cmd.commandHierarchy; + cmd.execPath.forEach(function (segment) { + continuedHierarchy = continuedHierarchy[segment]; + }); + if (continuedHierarchy === null) { + // Need help with a particular command + var singleArgData_1 = function (argName, maxArgLen) { + var argKebab = _.kebabCase(argName); + var argObj = _this.commandArgs[argName]; + return " --" + + argKebab + " " + + common_1.repeatStr(" ", maxArgLen - argKebab.length) + + colors_1.gray((argObj.description || (argObj.friendlyName + "."))) + os_1.EOL; + }; + var commandName_1 = cmd.execPath[cmd.execPath.length - 1]; + result += colors_1.cyan("Syntax: ") + os_1.EOL + + colors_1.cyan("tfx ") + colors_1.yellow(cmd.execPath.join(" ")) + + colors_1.green(" --arg1 arg1val1 arg1val2[...]") + + colors_1.gray(" --arg2 arg2val1 arg2val2[...]") + os_1.EOL + os_1.EOL; + return loader.load(cmd.execPath, []).then(function (tfCommand) { + result += colors_1.cyan("Command: ") + commandName_1 + os_1.EOL; + result += tfCommand.description + os_1.EOL + os_1.EOL; + result += colors_1.cyan("Arguments: ") + os_1.EOL; + var uniqueArgs = _this.getHelpArgs(); + uniqueArgs = _.uniq(uniqueArgs); + var maxArgLen = uniqueArgs.map(function (a) { return _.kebabCase(a); }).reduce(function (a, b) { return Math.max(a, b.length); }, 0); + if (uniqueArgs.length === 0) { + result += "[No arguments for this command]" + os_1.EOL; + } + uniqueArgs.forEach(function (arg) { + result += singleArgData_1(arg, maxArgLen); + }); + if (_this.serverCommand) { + result += os_1.EOL + colors_1.cyan("Global server command arguments:") + os_1.EOL; + ["authType", "username", "password", "token", "serviceUrl", "fiddler", "proxy"].forEach(function (arg) { + result += singleArgData_1(arg, 11); + }); + } + result += os_1.EOL + colors_1.cyan("Global arguments:") + os_1.EOL; + ["help", "save", "noPrompt", "output", "json"].forEach(function (arg) { + result += singleArgData_1(arg, 9); + }); + result += os_1.EOL + colors_1.gray("To see more commands, type " + colors_1.reset("tfx " + cmd.execPath.slice(0, cmd.execPath.length - 1).join(" ") + " --help")); + }).then(function () { + return result; + }); + } + else { + // Need help with a suite of commands + // There is a weird coloring bug when colors are nested, so we don"t do that. + result += colors_1.cyan("Available ") + + "commands" + + colors_1.cyan(" and ") + + colors_1.yellow("command groups") + + colors_1.cyan(" in " + ["tfx"].concat(cmd.execPath).join(" / ") + ":") + os_1.EOL; + var commandDescriptionPromises_1 = []; + Object.keys(continuedHierarchy).forEach(function (command) { + if (command === "default") { + return; + } + var pr = loader.load(cmd.execPath.concat([command]), []).then(function (tfCommand) { + var coloredCommand = command; + if (continuedHierarchy[command] !== null) { + coloredCommand = colors_1.yellow(command); + } + result += " - " + coloredCommand + colors_1.gray(": " + tfCommand.description) + os_1.EOL; + }); + commandDescriptionPromises_1.push(pr); + }); + return Promise.all(commandDescriptionPromises_1).then(function () { + result += os_1.EOL + os_1.EOL + colors_1.gray("For help with an individual command, type ") + colors_1.reset("tfx " + cmd.execPath.join(" ") + " --help") + os_1.EOL; + }).then(function () { + return result; + }); + } + }; + /** + * Display a copyright banner. + */ + TfCommand.prototype.showBanner = function () { + var _this = this; + return this.commandArgs.json.val(true).then(function (useJson) { + if (useJson) { + _this.commandArgs.output.setValue("json"); + } + }).then(function () { + return _this.commandArgs.output.val(true).then(function (outputType) { + return version.getTfxVersion().then(function (semVer) { + trace.outputType = outputType; + if (outputType === "friendly") { + trace.info(colors_1.gray("TFS Cross Platform Command Line Interface v" + semVer.toString())); + trace.info(colors_1.gray("Copyright Microsoft Corporation")); + } + }); + }); + }); + }; + /** + * Takes data and pipes it to the appropriate output mechanism + */ + TfCommand.prototype.output = function (data) { + var _this = this; + return this.commandArgs.output.val().then(function (outputDestination) { + switch (outputDestination.toLowerCase()) { + case "friendly": + _this.friendlyOutput(data); + break; + case "json": + _this.jsonOutput(data); + break; + case "help": + _this.friendlyOutputConstant(data); + break; + case "clip": + case "clipboard": + var clipboardText = _this.getClipboardOutput(data); + return Q.nfcall(copypaste.copy, clipboardText); + default: + return qfs.canWriteTo(path.resolve(outputDestination)).then(function (canWrite) { + if (canWrite) { + var fileContents = _this.getFileOutput(data); + return qfs.writeFile(outputDestination, fileContents); + } + else { + throw new Error("Cannot write output to " + outputDestination); + } + }); + } + return Promise.resolve(null); + }); + }; + /** + * Given the output object, gets the string that is copied to the clipboard when + * clipboard output is requested. + */ + TfCommand.prototype.getClipboardOutput = function (data) { + return this.getOutputString(data); + }; + /** + * Given the output object, gets the string that is written to a destination + * file when a file name is given as the output destination + */ + TfCommand.prototype.getFileOutput = function (data) { + return this.getOutputString(data); + }; + TfCommand.prototype.getOutputString = function (data) { + var outputString = ""; + try { + outputString = JSON.stringify(data, null, 4); + } + catch (e) { + if (data && data.toString) { + outputString = data.toString(); + } + else { + outputString = data + ""; + } + } + return outputString; + }; + /** + * Gets a nicely formatted output string for friendly output + */ + TfCommand.prototype.friendlyOutput = function (data) { + this.friendlyOutputConstant(data); + }; + TfCommand.prototype.friendlyOutputConstant = function (data) { + if (typeof data === "string") { + console.log(data); + } + else { + try { + console.log(JSON.stringify(data, null, 4)); + } + catch (e) { + console.log(data + ""); + } + } + }; + /** + * Gets a string of valid JSON when JSON output is requested. + * Probably no need to override this one. + */ + TfCommand.prototype.jsonOutput = function (data) { + try { + console.log(colors_1.reset(JSON.stringify(data, null, 4))); + } + catch (e) { + throw new Error("Could not stringify JSON output."); + } + }; + return TfCommand; +}()); +exports.TfCommand = TfCommand; diff --git a/_build/lib/trace.js b/_build/lib/trace.js new file mode 100644 index 00000000..b82e4ffc --- /dev/null +++ b/_build/lib/trace.js @@ -0,0 +1,98 @@ +"use strict"; +var colors = require("colors"); +var os = require('os'); +var traceEnabled = process.env['TFX_TRACE']; +function println() { + info(''); +} +exports.println = println; +function error(msg) { + var replacements = []; + for (var _i = 1; _i < arguments.length; _i++) { + replacements[_i - 1] = arguments[_i]; + } + log('', msg, colors.bgRed, replacements, console.error); +} +exports.error = error; +function success(msg) { + var replacements = []; + for (var _i = 1; _i < arguments.length; _i++) { + replacements[_i - 1] = arguments[_i]; + } + log('', msg, colors.green, replacements); +} +exports.success = success; +function info(msg) { + var replacements = []; + for (var _i = 1; _i < arguments.length; _i++) { + replacements[_i - 1] = arguments[_i]; + } + if (exports.outputType === "friendly" || traceEnabled) { + log('', msg, colors.white, replacements); + } +} +exports.info = info; +function warn(msg) { + var replacements = []; + for (var _i = 1; _i < arguments.length; _i++) { + replacements[_i - 1] = arguments[_i]; + } + log('', msg, colors.bgYellow.black, replacements); +} +exports.warn = warn; +function debugArea(msg, area) { + traceEnabled = process.env['TFX_TRACE_' + area.toUpperCase()]; + if (traceEnabled) { + log(colors.cyan(new Date().toISOString() + ' : '), msg, colors.grey, []); + } + traceEnabled = process.env['TFX_TRACE']; +} +exports.debugArea = debugArea; +function debug(msg) { + var replacements = []; + for (var _i = 1; _i < arguments.length; _i++) { + replacements[_i - 1] = arguments[_i]; + } + if (traceEnabled) { + log(colors.cyan(new Date().toISOString() + ' : '), msg, colors.grey, replacements); + } +} +exports.debug = debug; +function log(prefix, msg, color, replacements, method) { + if (method === void 0) { method = console.log; } + var t = typeof (msg); + if (t === 'string') { + write(prefix, msg, color, replacements, method); + } + else if (msg instanceof Array) { + msg.forEach(function (line) { + if (typeof (line) === 'string') { + write(prefix, line, color, replacements, method); + } + }); + } + else if (t === 'object') { + write(prefix, JSON.stringify(msg, null, 2), color, replacements, method); + } +} +function write(prefix, msg, color, replacements, method) { + if (method === void 0) { method = console.log; } + var toLog = doReplacements(msg, replacements); + toLog = toLog.split(/\n|\r\n/).map(function (line) { return prefix + line; }).join(os.EOL); + method(color(toLog)); +} +function doReplacements(str, replacements) { + var lcRepl = str.replace(/%S/g, "%s"); + var split = lcRepl.split("%s"); + if (split.length - 1 !== replacements.length) { + throw new Error("The number of replacements (" + replacements.length + ") does not match the number of placeholders (" + (split.length - 1) + ")"); + } + var resultArr = []; + split.forEach(function (piece, index) { + resultArr.push(piece); + if (index < split.length - 1) { + resultArr.push(replacements[index]); + } + }); + return resultArr.join(""); +} diff --git a/_build/lib/version.js b/_build/lib/version.js new file mode 100644 index 00000000..8c4b17f4 --- /dev/null +++ b/_build/lib/version.js @@ -0,0 +1,73 @@ +"use strict"; +var common = require("./common"); +var path = require("path"); +var Q = require("q"); +var SemanticVersion = (function () { + function SemanticVersion(major, minor, patch) { + this.major = major; + this.minor = minor; + this.patch = patch; + if (major < 0 || minor < 0 || patch < 0) { + throw "Version numbers must be positive."; + } + if (major === 0 && minor === 0 && patch === 0) { + throw "Version must be greater than 0.0.0"; + } + } + /** + * Parse a Semantic Version from a string. + */ + SemanticVersion.parse = function (version) { + try { + var spl = version.split(".").map(function (v) { return parseInt(v); }); + if (spl.length === 3 && !spl.some(function (e) { return isNaN(e); })) { + return new SemanticVersion(spl[0], spl[1], spl[2]); + } + else { + throw ""; + } + } + catch (e) { + throw "Could not parse '" + version + "' as a Semantic Version."; + } + }; + /** + * Return a string-representation of this semantic version, e.g. 2.10.5 + */ + SemanticVersion.prototype.toString = function () { + return [this.major, this.minor, this.patch].join("."); + }; + /** + * Return -1 if this version is less than other, + * 1 if this version is greater than other, + * and 0 if they are equal. + */ + SemanticVersion.prototype.compareTo = function (other) { + if (this.major < other.major) { + return -1; + } + if (this.major > other.major) { + return 1; + } + if (this.minor < other.minor) { + return -1; + } + if (this.minor > other.minor) { + return 1; + } + if (this.patch < other.patch) { + return -1; + } + if (this.patch > other.patch) { + return 1; + } + return 0; + }; + return SemanticVersion; +}()); +exports.SemanticVersion = SemanticVersion; +function getTfxVersion() { + var packageJson = require(path.join(common.APP_ROOT, "package.json")); + return Q.resolve(SemanticVersion.parse(packageJson.version)); +} +exports.getTfxVersion = getTfxVersion; diff --git a/_build/package.json b/_build/package.json new file mode 100644 index 00000000..39e560f6 --- /dev/null +++ b/_build/package.json @@ -0,0 +1,61 @@ +{ + "name": "tfx-cli", + "version": "0.3.30", + "description": "CLI for Visual Studio Team Services and Team Foundation Server", + "repository": { + "type": "git", + "url": "https://github.com/Microsoft/tfs-cli" + }, + "main": "./_build/tfx-cli.js", + "preferGlobal": true, + "bin": { + "tfx": "./_build/tfx-cli.js" + }, + "scripts": { + "clean": "rimraf _build", + "prebuild": "npm run clean", + "build": "tsc -p . && ncp app/tfx-cli.js _build/tfx-cli.js && ncp package.json _build/package.json" + }, + "dependencies": { + "app-root-path": "1.0.0", + "archiver": "0.14.4", + "async": "^1.4.0", + "colors": "^1.1.2", + "copy-paste": "^1.1.3", + "glob": "5.0.10", + "inquirer": "0.8.5", + "json-in-place": "^1.0.1", + "jszip": "2.5.0", + "lodash": "^4.15.0", + "minimist": "^1.1.2", + "mkdirp": "^0.5.1", + "node-uuid": "^1.4.3", + "onecolor": "^2.5.0", + "os-homedir": "^1.0.1", + "prompt": "^0.2.14", + "q": "^1.4.1", + "read": "^1.0.6", + "request": "2.58.0", + "shelljs": "^0.5.1", + "tmp": "0.0.26", + "tracer": "0.7.4", + "validator": "^3.43.0", + "vso-node-api": "^5.0.0", + "winreg": "0.0.12", + "xml2js": "^0.4.16" + }, + "devDependencies": { + "del": "^1.2.0", + "gulp": "^3.9.0", + "gulp-filter": "^3.0.1", + "gulp-mocha": "2.0.0", + "gulp-tsb": "^1.10.2", + "minimatch": "^2.0.8", + "mocha": "^2.2.5", + "ncp": "^2.0.0", + "rimraf": "^2.5.4", + "typescript": "^2.0.2" + }, + "author": "", + "license": "MIT" +} diff --git a/_build/tfx-cli.js b/_build/tfx-cli.js new file mode 100644 index 00000000..120d8204 --- /dev/null +++ b/_build/tfx-cli.js @@ -0,0 +1,3 @@ +#!/usr/bin/env node + +require('./app'); \ No newline at end of file diff --git a/package.json b/package.json index d1e8150e..39e560f6 100644 --- a/package.json +++ b/package.json @@ -17,18 +17,6 @@ "build": "tsc -p . && ncp app/tfx-cli.js _build/tfx-cli.js && ncp package.json _build/package.json" }, "dependencies": { - "@types/colors": "^0.6.31", - "@types/copy-paste": "^1.1.29", - "@types/glob": "^5.0.29", - "@types/jszip": "0.0.30", - "@types/lodash": "^4.14.34", - "@types/mkdirp": "^0.3.28", - "@types/node": "^6.0.38", - "@types/node-uuid": "0.0.27", - "@types/shelljs": "^0.3.30", - "@types/validator": "^4.5.27", - "@types/winreg": "^1.2.29", - "@types/xml2js": "0.0.27", "app-root-path": "1.0.0", "archiver": "0.14.4", "async": "^1.4.0",