rally-tools
Version:
The rally tools cli interface
1,997 lines (1,713 loc) • 149 kB
JavaScript
#!/usr/bin/env node
/*----------------------------------------------
* Generated by rollup. Written by John Schmidt.
* Rally Tools CLI v3.4.1
*--------------------------------------------*/
const importLazy = require("import-lazy")(require);
'use strict';
function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; }
var os = require('os');
var fs = require('fs');
var fs__default = _interopDefault(fs);
var child_process = require('child_process');
var perf_hooks = require('perf_hooks');
var chalk$1 = _interopDefault(require('chalk'));
var rp = _interopDefault(require('request-promise'));
var path = require('path');
var path__default = _interopDefault(path);
var moment = _interopDefault(require('moment'));
var argparse = _interopDefault(require('minimist'));
function _asyncIterator(iterable) {
var method;
if (typeof Symbol === "function") {
if (Symbol.asyncIterator) {
method = iterable[Symbol.asyncIterator];
if (method != null) return method.call(iterable);
}
if (Symbol.iterator) {
method = iterable[Symbol.iterator];
if (method != null) return method.call(iterable);
}
}
throw new TypeError("Object is not async iterable");
}
function _AwaitValue(value) {
this.wrapped = value;
}
function _AsyncGenerator(gen) {
var front, back;
function send(key, arg) {
return new Promise(function (resolve, reject) {
var request = {
key: key,
arg: arg,
resolve: resolve,
reject: reject,
next: null
};
if (back) {
back = back.next = request;
} else {
front = back = request;
resume(key, arg);
}
});
}
function resume(key, arg) {
try {
var result = gen[key](arg);
var value = result.value;
var wrappedAwait = value instanceof _AwaitValue;
Promise.resolve(wrappedAwait ? value.wrapped : value).then(function (arg) {
if (wrappedAwait) {
resume("next", arg);
return;
}
settle(result.done ? "return" : "normal", arg);
}, function (err) {
resume("throw", err);
});
} catch (err) {
settle("throw", err);
}
}
function settle(type, value) {
switch (type) {
case "return":
front.resolve({
value: value,
done: true
});
break;
case "throw":
front.reject(value);
break;
default:
front.resolve({
value: value,
done: false
});
break;
}
front = front.next;
if (front) {
resume(front.key, front.arg);
} else {
back = null;
}
}
this._invoke = send;
if (typeof gen.return !== "function") {
this.return = undefined;
}
}
if (typeof Symbol === "function" && Symbol.asyncIterator) {
_AsyncGenerator.prototype[Symbol.asyncIterator] = function () {
return this;
};
}
_AsyncGenerator.prototype.next = function (arg) {
return this._invoke("next", arg);
};
_AsyncGenerator.prototype.throw = function (arg) {
return this._invoke("throw", arg);
};
_AsyncGenerator.prototype.return = function (arg) {
return this._invoke("return", arg);
};
function _wrapAsyncGenerator(fn) {
return function () {
return new _AsyncGenerator(fn.apply(this, arguments));
};
}
function _awaitAsyncGenerator(value) {
return new _AwaitValue(value);
}
function _asyncGeneratorDelegate(inner, awaitWrap) {
var iter = {},
waiting = false;
function pump(key, value) {
waiting = true;
value = new Promise(function (resolve) {
resolve(inner[key](value));
});
return {
done: false,
value: awaitWrap(value)
};
}
if (typeof Symbol === "function" && Symbol.iterator) {
iter[Symbol.iterator] = function () {
return this;
};
}
iter.next = function (value) {
if (waiting) {
waiting = false;
return value;
}
return pump("next", value);
};
if (typeof inner.throw === "function") {
iter.throw = function (value) {
if (waiting) {
waiting = false;
throw value;
}
return pump("throw", value);
};
}
if (typeof inner.return === "function") {
iter.return = function (value) {
return pump("return", value);
};
}
return iter;
}
function _applyDecoratedDescriptor(target, property, decorators, descriptor, context) {
var desc = {};
Object['ke' + 'ys'](descriptor).forEach(function (key) {
desc[key] = descriptor[key];
});
desc.enumerable = !!desc.enumerable;
desc.configurable = !!desc.configurable;
if ('value' in desc || desc.initializer) {
desc.writable = true;
}
desc = decorators.slice().reverse().reduce(function (desc, decorator) {
return decorator(target, property, desc) || desc;
}, desc);
if (context && desc.initializer !== void 0) {
desc.value = desc.initializer ? desc.initializer.call(context) : void 0;
desc.initializer = undefined;
}
if (desc.initializer === void 0) {
Object['define' + 'Property'](target, property, desc);
desc = null;
}
return desc;
}
let configFile = null;
if (os.homedir) {
configFile = os.homedir() + "/.rallyconfig";
}
let configObject;
function loadConfig(file) {
if (file) configFile = file;
if (!configFile) return;
configObject = {
hasConfig: true
};
try {
let json = fs.readFileSync(configFile);
configObject = JSON.parse(json);
configObject.hasConfig = true;
} catch (e) {
if (e.code == "ENOENT") {
configObject.hasConfig = false; //ok, they should probably make a config
} else {
throw e;
}
}
}
function loadConfigFromArgs(args) {
let tempConfig = {
hasConfig: true,
...args.config
};
configObject = tempConfig;
}
function setConfig(obj) {
configObject = obj;
}
//these are the help entries for each command
let helpEntries = {};
let helpEntry = name => helpEntries[name] ? helpEntries[name] : helpEntries[name] = {
name
}; //short description
function helpText(text) {
return function (func, name) {
helpEntry(name).text = text;
return func;
};
} //flag type argument like -f or --file
function arg(long, short, desc) {
return function (func, name) {
let args = helpEntry(name).args = helpEntry(name).args || [];
args.unshift({
long,
short,
desc
});
return func;
};
} //normal argument
function param(param, desc) {
return function (func, name) {
let params = helpEntry(name).params = helpEntry(name).params || [];
params.unshift({
param,
desc
});
return func;
};
} //usage string
function usage(usage) {
return function (func, name) {
usage = usage.replace(/[\[<](\w+)[\]>]/g, chalk`[{blue $1}]`);
helpEntry(name).usage = usage;
return func;
};
}
//function retuns obj.a.b.c
function deepAccess(obj, path$$1) {
let o = obj;
for (let key of path$$1) {
if (!o) return [];
o = o[key];
}
return o;
} //This takes a class as the first argument, then adds a getter/setter pair that
//corresponds to an object in this.data
function defineAssoc(classname, shortname, path$$1) {
path$$1 = path$$1.split(".");
let lastKey = path$$1.pop();
Object.defineProperty(classname.prototype, shortname, {
get() {
return deepAccess(this, path$$1)[lastKey];
},
set(val) {
deepAccess(this, path$$1)[lastKey] = val;
}
});
}
function spawn(options, ...args) {
if (typeof options !== "object") {
args.unshift(options);
options = {};
} //todo options
return new Promise((resolve, reject) => {
let start = perf_hooks.performance.now();
let stdout = "";
let stderr = "";
let cp = child_process.spawn(...args);
let write = global.write;
if (options.noecho) {
write = () => {};
}
if (cp.stdout) cp.stdout.on("data", chunk => {
stdout += chunk;
write(chunk);
});
if (cp.stderr) cp.stderr.on("data", chunk => {
stderr += chunk;
write(chunk);
});
if (options.stdin) {
options.stdin(cp.stdin);
}
cp.on("error", reject);
cp.on("close", code => {
let end = perf_hooks.performance.now();
let time = end - start;
let timestr = time > 1000 ? (time / 100 | 0) / 10 + "s" : (time | 0) + "ms";
resolve({
stdout,
stderr,
exitCode: code,
time,
timestr
});
});
});
}
global.chalk = chalk$1;
global.log = (...text) => console.log(...text);
global.write = (...text) => process.stdout.write(...text);
global.elog = (...text) => console.error(...text);
global.ewrite = (...text) => process.stderr.write(...text);
global.errorLog = (...text) => log(...text.map(chalk$1.red));
class lib {
//This function takes 2 required arguemnts:
// env: the enviornment you wish to use
// and either:
// 'path', the short path to the resource. ex '/presets/'
// 'path_full', the full path to the resource like 'https://discovery-dev.sdvi.com/presets'
//
// If the method is anything but GET, either payload or body should be set.
// payload should be a javascript object to be turned into json as the request body
// body should be a string that is passed as the body. for example: the python code of a preset.
//
// qs are the querystring parameters, in a key: value object.
// {filter: "name=test name"} becomes something like 'filter=name=test+name'
//
// headers are the headers of the request. "Content-Type" is already set if
// payload is given as a parameter
//
// fullResponse should be true if you want to receive the request object,
// not just the returned data.
static async makeAPIRequest({
env,
path: path$$1,
path_full,
fullPath,
payload,
body,
method = "GET",
qs,
headers = {},
fullResponse = false,
timeout = configObject.timeout || 20000
}) {
var _configObject$api;
//backwards compatability from ruby script
if (fullPath) path_full = fullPath; //Keys are defined in enviornment variables
let config = configObject === null || configObject === void 0 ? void 0 : (_configObject$api = configObject.api) === null || _configObject$api === void 0 ? void 0 : _configObject$api[env];
if (!config) {
throw new UnconfiguredEnvError(env);
}
if (method !== "GET" && !configObject.dangerModify) {
if (env === "UAT" && configObject.restrictUAT || env === "PROD") {
throw new ProtectedEnvError(env);
}
}
let rally_api_key = config.key;
let rally_api = config.url;
if (path$$1 && path$$1.startsWith("/v1.0/")) {
rally_api = rally_api.replace("/api/v2", "/api");
}
path$$1 = path_full || rally_api + path$$1;
if (payload) {
body = JSON.stringify(payload, null, 4);
}
if (payload) {
headers["Content-Type"] = "application/vnd.api+json";
}
let fullHeaders = {
//SDVI ignores this header sometimes.
Accept: "application/vnd.api+json",
"X-SDVI-Client-Application": "Discovery-rtlib-" + (configObject.appName || "commandline"),
...headers
};
if (configObject.vvverbose) {
log(`${method} @ ${path$$1}`);
log(JSON.stringify(fullHeaders, null, 4));
if (body) {
log(body);
} else {
log("(No body");
}
}
let requestOptions = {
method,
body,
qs,
uri: path$$1,
timeout,
auth: {
bearer: rally_api_key
},
headers: fullHeaders,
simple: false,
resolveWithFullResponse: true
};
let response;
try {
response = await rp(requestOptions);
if (configObject.vverbose || configObject.vvverbose) {
log(chalk$1`${method} @ ${response.request.uri.href}`);
}
} catch (e) {
if ((e === null || e === void 0 ? void 0 : e.cause.code) === "ESOCKETTIMEDOUT") {
throw new APIError(response || {}, requestOptions, body);
} else {
throw e;
}
} //Throw an error for any 5xx or 4xx
if (!fullResponse && ![200, 201, 202, 203, 204].includes(response.statusCode)) {
throw new APIError(response, requestOptions, body);
}
let contentType = response.headers["content-type"];
let isJSONResponse = contentType === "application/vnd.api+json" || contentType === "application/json";
if (configObject.vvverbose) {
log(response.body);
}
if (fullResponse) {
return response;
} else if (isJSONResponse) {
var _response, _response$body;
if ([200, 201, 202, 203, 204].includes(response.statusCode) && !((_response = response) === null || _response === void 0 ? void 0 : (_response$body = _response.body) === null || _response$body === void 0 ? void 0 : _response$body.trim())) return {};
try {
return JSON.parse(response.body);
} catch (e) {
log(response.body);
throw new AbortError("Body is not valid json: ");
}
} else {
return response.body;
}
} //Index a json endpoint that returns a {links} field.
//This function returns the merged data objects as an array
//
//Additonal options (besides makeAPIRequest options):
// - Observe: function to be called for each set of data from the api
static async indexPath(env, path$$1) {
let opts = typeof env === "string" ? {
env,
path: path$$1
} : env;
opts.maxParallelRequests = 1;
let index = new IndexObject(opts);
return await index.fullResults();
}
static clearProgress(size = 30) {
if (!configObject.globalProgress) return;
process.stderr.write(`\r${" ".repeat(size + 15)}\r`);
}
static async drawProgress(i, max, size = process.stdout.columns - 15 || 15) {
if (!configObject.globalProgress) return;
if (size > 45) size = 45;
let pct = Number(i) / Number(max); //clamp between 0 and 1
pct = pct < 0 ? 0 : pct > 1 ? 1 : pct;
let numFilled = Math.floor(pct * size);
let numEmpty = size - numFilled;
this.clearProgress(size);
process.stderr.write(`[${"*".repeat(numFilled)}${" ".repeat(numEmpty)}] ${i} / ${max}`);
}
static async keepalive(funcs) {
for (let f of funcs) {
await f();
}
} //Index a json endpoint that returns a {links} field.
//
//This function is faster than indexPath because it can guess the pages it
//needs to retreive so that it can request all assets at once.
//
//This function assumes that the content from the inital request is the
//first page, so starting on another page may cause issues. Consider
//indexPath for that.
//
//Additional opts, besides default indexPath opts:
// - chunksize[10]: How often to break apart concurrent requests
static async indexPathFast(env, path$$1) {
let opts = typeof env === "string" ? {
env,
path: path$$1
} : env;
let index = new IndexObject(opts);
return await index.fullResults();
}
static isLocalEnv(env) {
return !env || env === "LOCAL" || env === "LOC";
}
static envName(env) {
if (this.isLocalEnv(env)) return "LOCAL";
return env;
}
}
class AbortError extends Error {
constructor(message) {
super(message);
Error.captureStackTrace(this, this.constructor);
this.name = "AbortError";
}
}
class APIError extends Error {
constructor(response, opts, body) {
super(chalk$1`
{reset Request returned} {yellow ${response === null || response === void 0 ? void 0 : response.statusCode}}{
{green ${JSON.stringify(opts, null, 4)}}
{green ${body}}
{reset ${response.body}}
===============================
{red ${response.body ? "Request timed out" : "Bad response from API"}}
===============================
`);
this.response = response;
this.opts = opts;
this.body = body;
Error.captureStackTrace(this, this.constructor);
this.name = "ApiError";
}
}
class UnconfiguredEnvError extends AbortError {
constructor(env) {
super("Unconfigured enviornment: " + env);
this.name = "Unconfigured Env Error";
}
}
class ProtectedEnvError extends AbortError {
constructor(env) {
super("Protected enviornment: " + env);
this.name = "Protected Env Error";
}
}
class FileTooLargeError extends Error {
constructor(file) {
super(`File ${file.parentAsset ? file.parentAsset.name : "(unknown)"}/${file.name} size is: ${file.sizeGB}g (> ~.2G)`);
this.name = "File too large error";
}
}
class Collection {
constructor(arr) {
this.arr = arr;
}
[Symbol.iterator]() {
return this.arr[Symbol.iterator]();
}
findById(id) {
return this.arr.find(x => x.id == id);
}
findByName(name) {
return this.arr.find(x => x.name == name);
}
findByNameContains(name) {
return this.arr.find(x => x.name.includes(name));
}
log() {
for (let d of this) {
if (d) {
log(d.chalkPrint(true));
} else {
log(chalk$1`{red (None)}`);
}
}
}
get length() {
return this.arr.length;
}
}
class RallyBase {
static handleCaching() {
if (!this.cache) this.cache = [];
}
static isLoaded(env) {
if (!this.hasLoadedAll) return;
return this.hasLoadedAll[env];
}
static async getById(env, id, qs) {
this.handleCaching();
for (let item of this.cache) {
if (item.id == id && item.remote === env || `${env}-${id}` === item.metastring) return item;
}
let data = await lib.makeAPIRequest({
env,
path: `/${this.endpoint}/${id}`,
qs
});
if (data.data) {
let o = new this({
data: data.data,
remote: env,
included: data.included
});
this.cache.push(o);
return o;
}
}
static async getByName(env, name, qs) {
this.handleCaching();
for (let item of this.cache) {
if (item.name === name && item.remote === env) return item;
}
let data = await lib.makeAPIRequest({
env,
path: `/${this.endpoint}`,
qs: { ...qs,
filter: `name=${name}` + (qs ? qs.filter : "")
}
}); //TODO included might not wokr correctly here
if (data.data[0]) {
let o = new this({
data: data.data[0],
remote: env,
included: data.included
});
this.cache.push(o);
return o;
}
}
static async getAllPreCollect(d) {
return d;
}
static async getAll(env) {
this.handleCaching();
let datas = await lib.indexPathFast({
env,
path: `/${this.endpoint}`,
pageSize: "50",
qs: {
sort: "id"
}
});
datas = await this.getAllPreCollect(datas);
let all = new Collection(datas.map(data => new this({
data,
remote: env
})));
this.cache = [...this.cache, ...all.arr];
return all;
}
static async removeCache(env) {
this.handleCaching();
this.cache = this.cache.filter(x => x.remote !== env);
} //Specific turns name into id based on env
//Generic turns ids into names
async resolveApply(type, dataObj, direction) {
let obj;
if (direction == "generic") {
obj = await type.getById(this.remote, dataObj.id);
if (obj) {
dataObj.name = obj.name;
}
} else if (direction == "specific") {
obj = await type.getByName(this.remote, dataObj.name);
if (obj) {
dataObj.id = obj.id;
}
}
return obj;
} //Type is the baseclass you are looking for (should extend RallyBase)
//name is the name of the field
//isArray is true if it has multiple cardinailty, false if it is single
//direction gets passed directly to resolveApply
async resolveField(type, name, isArray = false, direction = "generic") {
// ignore empty fields
let field = this.relationships[name];
if (!(field === null || field === void 0 ? void 0 : field.data)) return;
if (isArray) {
return await Promise.all(field.data.map(o => this.resolveApply(type, o, direction)));
} else {
return await this.resolveApply(type, field.data, direction);
}
}
cleanup() {
for (let [key, val] of Object.entries(this.relationships)) {
//Remove ids from data
if (val.data) {
if (val.data.id) {
delete val.data.id;
} else if (val.data[0]) {
for (let x of val.data) delete x.id;
}
}
delete val.links;
} // organization is unused (?)
delete this.relationships.organization; // id is specific to envs
// but save source inside meta string in case we need it
this.metastring = this.remote + "-" + this.data.id;
delete this.data.id; // links too
delete this.data.links;
}
}
function sleep(time = 1000) {
return new Promise(resolve => setTimeout(resolve, time));
}
function* zip(...items) {
let iters = items.map(x => x[Symbol.iterator]());
for (;;) {
let r = [];
for (let i of iters) {
let next = i.next();
if (next.done) return;
r.push(next.value);
}
yield r;
}
}
function unordered(_x) {
return _unordered.apply(this, arguments);
}
function _unordered() {
_unordered = _wrapAsyncGenerator(function* (proms) {
let encapsulatedPromises = proms.map(async (x, i) => [i, await x]);
while (encapsulatedPromises.length > 0) {
let [ind, result] = yield _awaitAsyncGenerator(Promise.race(encapsulatedPromises.filter(x => x)));
yield result;
encapsulatedPromises[ind] = undefined;
}
});
return _unordered.apply(this, arguments);
}
function* range(start, end) {
if (end === undefined) {
end = start;
start = 0;
}
while (start < end) yield start++;
}
class IndexObject {
//normal opts from any makeAPIRequest
//Note that full_response and pages won't work.
//
//if you want to start from another page, use `opts.start`
//opts.observe: async function(jsonData) => jsonData. Transform the data from the api
//opts.maxParallelRequests: number of max api requests to do at once
//opts.noCollect: return [] instead of the full data
constructor(opts) {
this.opts = opts;
}
linkToPage(page) {
return this.baselink.replace(`page=1p`, `page=${page}p`);
}
async initializeFirstRequest() {
//Create a copy of the options in case we need to have a special first request
this.start = this.opts.start || 1;
let initOpts = { ...this.opts
};
if (this.opts.pageSize) {
initOpts.qs = { ...this.opts.qs
};
initOpts.qs.page = `${this.start}p${this.opts.pageSize}`;
}
this.allResults = []; //we make 1 non-parallel request to the first page so we know how to
//format the next requests
let json = await lib.makeAPIRequest(initOpts);
if (this.opts.observe) json = await this.opts.observe(json);
if (!this.opts.noCollect) this.allResults.push(json);
this.baselink = json.links.first;
this.currentPageRequest = this.start;
this.hasHit404 = false;
}
getNextRequestLink() {
this.currentPageRequest++;
return [this.currentPageRequest, this.linkToPage(this.currentPageRequest)];
} ///promiseID is the id in `currentPromises`, so that it can be marked as
///done inside the promise array. promiseID is a number from 0 to
///maxparallel-1
async getNextRequestPromise(promiseID) {
let [page, path_full] = this.getNextRequestLink();
return [promiseID, page, await lib.makeAPIRequest({ ...this.opts,
path_full,
fullResponse: true
})];
}
cancel() {
this.willCancel = true;
}
async fullResults() {
await this.initializeFirstRequest();
let maxParallelRequests = this.opts.maxParallelRequests || this.opts.chunksize || 20;
let currentPromises = []; //generate the first set of requests. Everything after this will re-use these i promiseIDs
for (let i = 0; i < maxParallelRequests; i++) {
currentPromises.push(this.getNextRequestPromise(currentPromises.length));
}
for (;;) {
let [promiseID, page, requestResult] = await Promise.race(currentPromises.filter(x => x));
if (this.willCancel) {
return null;
}
if (requestResult.statusCode === 404) {
this.hasHit404 = true;
} else if (requestResult.statusCode === 200) {
let json = JSON.parse(requestResult.body);
if (this.opts.observe) json = await this.opts.observe(json);
if (!this.opts.noCollect) this.allResults.push(json);
if (json.data.length === 0) this.hasHit404 = true;
} else {
throw new APIError(requestResult, `(unknown args) page ${page}`, null);
}
if (this.hasHit404) {
currentPromises[promiseID] = null;
} else {
currentPromises[promiseID] = this.getNextRequestPromise(promiseID);
}
if (currentPromises.filter(x => x).length === 0) break;
}
let all = [];
for (let result of this.allResults) {
for (let item of result.data) {
all.push(item);
}
}
return all;
}
}
const inquirer = importLazy("inquirer");
const readdir = importLazy("recursive-readdir");
let hasAutoCompletePrompt = false;
function addAutoCompletePrompt() {
if (hasAutoCompletePrompt) return;
hasAutoCompletePrompt = true;
inquirer.registerPrompt("autocomplete", require("inquirer-autocomplete-prompt"));
}
async function $api(propArray) {
let q;
q = await inquirer.prompt([{
type: "input",
name: "company",
message: `What is your company?`,
default: `discovery`
}]);
let company = q.company;
const defaults = {
DEV: `https://${company}-dev.sdvi.com/api/v2`,
UAT: `https://${company}-uat.sdvi.com/api/v2`,
QA: `https://${company}-qa.sdvi.com/api/v2`,
PROD: `https://${company}.sdvi.com/api/v2`
};
if (propArray && propArray[1]) {
q = {
envs: [propArray[1]]
};
} else {
//Create a checkbox prompt to choose enviornments
q = await inquirer.prompt([{
type: "checkbox",
name: "envs",
message: `What enviornments would you like to configure?`,
choices: Object.keys(defaults).map(name => ({
name,
checked: true
}))
}]);
} //Each env should ask 2 for two things: The url and the key.
let questions = q.envs.map(env => {
let defaultKey = process.env[`rally_api_key_${env}`];
if (configObject && configObject.api && configObject.api[env]) {
defaultKey = configObject.api[env].key;
}
return [{
type: "input",
name: `api.${env}.url`,
message: `What is the api endpoint for ${env}?`,
default: defaults[env]
}, {
type: "input",
name: `api.${env}.key`,
message: `What is your api key for ${env}?`,
default: defaultKey
}];
}); //flatten and ask
questions = [].concat(...questions);
q = await inquirer.prompt(questions);
if (propArray) {
q.api = { ...configObject.api,
...q.api
};
}
return q;
}
async function $chalk(propArray) {
return {
chalk: await askQuestion("Would you like chalk enabled (Adds coloring)?")
};
}
async function $restrictUAT(propArray) {
return {
restrictUAT: await askQuestion("Would you like to protect UAT?")
};
}
async function $repodir(propArray) {
return await inquirer.prompt([{
type: "input",
name: `repodir`,
message: `Where is your rally repository (empty for N/A)?`,
default: process.env["rally_repo_path"]
}]);
}
async function $appName(propArray) {
let defaultAppName = "cmdline-" + (process.env.USERNAME || process.env.LOGNAME);
let project = await askInput("Application name?", defaultAppName);
if (project === "none" || project === "-" || project === "" || !project) {
project = null;
}
return {
appName: project
};
}
async function $project(propArray) {
let project = await askInput("Subproject directory?");
if (project === "none" || project === "-" || project === "" || !project) {
project = null;
}
return {
project
};
}
async function $defaultEnv(propArray) {
return await inquirer.prompt([{
type: "input",
name: `defaultEnv`,
message: `Default enviornment?`,
default: "DEV"
}]);
} //Internal usage/testing
async function selectProvider(providers, autoDefault = false) {
addAutoCompletePrompt();
let defaultProvider = providers.findByName("SdviEvaluate");
if (autoDefault) {
return defaultProvider;
} else {
let choices = providers.arr.map(x => ({
name: x.chalkPrint(true),
value: x
}));
let q = await inquirer.prompt([{
type: "autocomplete",
name: "provider",
default: defaultProvider,
source: async (sofar, input) => {
return choices.filter(x => input ? x.value.name.toLowerCase().includes(input.toLowerCase()) : true);
}
}]);
return q.provider;
}
}
async function loadLocals(path$$1, Class) {
let basePath = configObject.repodir;
let objs = (await readdir(basePath)).filter(name => name.includes(path$$1)).filter(name => !path.basename(name).startsWith(".")).map(name => new Class({
path: name
}));
return objs;
}
async function selectLocal(path$$1, typeName, Class, canSelectNone = true) {
addAutoCompletePrompt();
let objs = await loadLocals(path$$1, Class);
let objsMap = objs.map(x => ({
name: x.chalkPrint(true),
value: x
}));
let none = {
name: chalk` {red None}: {red None}`,
value: null
};
if (canSelectNone) objsMap.unshift(none);
let q = await inquirer.prompt([{
type: "autocomplete",
name: "obj",
message: `What ${typeName} do you want?`,
source: async (sofar, input) => {
return objsMap.filter(x => input ? x.name.toLowerCase().includes(input.toLowerCase()) : true);
}
}]);
return q.obj;
}
async function selectPreset({
purpose = "preset",
canSelectNone
}) {
return selectLocal("silo-presets", purpose, Preset, canSelectNone);
}
async function selectRule({
purpose = "rule",
canSelectNone
}) {
return selectLocal("silo-rules", purpose, Rule, canSelectNone);
}
async function askInput(question, def) {
return (await inquirer.prompt([{
type: "input",
name: "ok",
message: question,
default: def
}])).ok;
}
async function askQuestion(question) {
return (await inquirer.prompt([{
type: "confirm",
name: "ok",
message: question
}])).ok;
}
async function saveConfig(newConfigObject, {
ask = true,
print = true
} = {}) {
//Create readable json and make sure the user is ok with it
let newConfig = JSON.stringify(newConfigObject, null, 4);
if (print) log(newConfig); //-y or --set will make this not prompt
if (ask && !(await askQuestion("Write config to disk?"))) return;
fs.writeFileSync(configFile, newConfig, {
mode: 0o600
});
log(chalk`Created file {green ${configFile}}.`);
}
var configHelpers = /*#__PURE__*/Object.freeze({
inquirer: inquirer,
addAutoCompletePrompt: addAutoCompletePrompt,
$api: $api,
$chalk: $chalk,
$restrictUAT: $restrictUAT,
$repodir: $repodir,
$appName: $appName,
$project: $project,
$defaultEnv: $defaultEnv,
selectProvider: selectProvider,
loadLocals: loadLocals,
selectLocal: selectLocal,
selectPreset: selectPreset,
selectRule: selectRule,
askInput: askInput,
askQuestion: askQuestion,
saveConfig: saveConfig
});
class Provider extends RallyBase {
constructor({
data,
remote
}) {
super();
this.data = data;
this.meta = {};
this.remote = remote;
} //cached
async getEditorConfig() {
if (this.editorConfig) return this.editorConfig;
this.editorConfig = await lib.makeAPIRequest({
env: this.remote,
path_full: this.data.links.editorConfig
});
this.editorConfig.fileExt = await this.getFileExtension();
return this.editorConfig;
}
static async getAllPreCollect(providers) {
return providers.sort((a, b) => {
return a.attributes.category.localeCompare(b.attributes.category) || a.attributes.name.localeCompare(b.attributes.name);
});
}
async getFileExtension() {
let config = await this.getEditorConfig();
let map = {
python: "py",
text: "txt",
getmap(key) {
if (this.name === "Aurora") return "zip";
if (this[key]) return this[key];
return key;
}
};
return map.getmap(config.lang);
}
chalkPrint(pad = true) {
let id = String(this.id);
if (pad) id = id.padStart(4);
return chalk`{green ${id}}: {blue ${this.category}} - {green ${this.name}}`;
}
}
defineAssoc(Provider, "id", "data.id");
defineAssoc(Provider, "name", "data.attributes.name");
defineAssoc(Provider, "category", "data.attributes.category");
defineAssoc(Provider, "remote", "meta.remote");
defineAssoc(Provider, "editorConfig", "meta.editorConfig");
Provider.endpoint = "providerTypes";
class File extends RallyBase {
constructor({
data,
remote,
included,
parent
}) {
super();
this.data = data;
this.meta = {};
this.remote = remote;
this.parentAsset = parent;
}
chalkPrint(pad = false) {
let id = String("F-" + (this.remote && this.remote + "-" + this.id || "LOCAL"));
if (pad) id = id.padStart(15);
return chalk`{green ${id}}: {blue ${this.data.attributes ? this.name : "(lite file)"}} {red ${this.sizeHR}}`;
}
canBeDownloaded() {
return this.sizeGB <= .2;
}
async getContent(force = false) {
if (!this.canBeDownloaded() && !force) {
throw new FileTooLargeError(this);
}
return lib.makeAPIRequest({
env: this.remote,
fullPath: this.contentLink
});
}
async delete(remove = true) {
return lib.makeAPIRequest({
env: this.remote,
fullPath: this.selfLink,
method: "DELETE"
});
}
get size() {
return Object.values(this.data.attributes.instances)[0].size;
}
get sizeGB() {
return Math.round(this.size / 1024 / 1024 / 1024 * 10) / 10;
}
get sizeHR() {
let units = ["B", "K", "M", "G", "T"];
let unitIdx = 0;
let size = this.size;
while (size > 1000) {
size /= 1024;
unitIdx++;
}
if (size > 100) {
size = Math.round(size);
} else {
size = Math.round(size * 10) / 10;
}
return size + units[unitIdx];
}
get instancesList() {
let instances = [];
for (let [key, val] of Object.entries(this.instances)) {
let n = {
id: key
};
Object.assign(n, val);
instances.push(n);
}
return instances;
}
static rslURL(instance) {
return `rsl://${instance.storageLocationName}/${instance.name}`;
}
}
defineAssoc(File, "id", "data.id");
defineAssoc(File, "name", "data.attributes.label");
defineAssoc(File, "contentLink", "data.links.content");
defineAssoc(File, "selfLink", "data.links.self");
defineAssoc(File, "label", "data.attributes.label");
defineAssoc(File, "md5", "data.attributes.md5");
defineAssoc(File, "sha512", "data.attributes.sha512");
defineAssoc(File, "tags", "data.attributes.tagList");
defineAssoc(File, "instances", "data.attributes.instances");
File.endpoint = null;
async function findLineInFile(renderedPreset, lineNumber) {
let trueFileLine = lineNumber;
let linedRenderedPreset = renderedPreset.split("\n").slice(2, -2);
renderedPreset = renderedPreset.split("\n").slice(2, -2).join("\n");
let includeLocation = renderedPreset.split("\n").filter(x => x.includes("@include"));
let endIncludeNumber = -1,
addTabDepth = 2;
let lineBeforeIncludeStatement = '';
let withinInclude = true;
if (lineNumber > linedRenderedPreset.indexOf(includeLocation[includeLocation.length - 1])) {
addTabDepth = 0;
withinInclude = false;
}
for (let index = includeLocation.length - 1; index >= 0; index--) {
let currIncludeIndex = linedRenderedPreset.indexOf(includeLocation[index]);
let tabDepth = includeLocation[index].split(" ").length;
if (lineNumber > currIncludeIndex) {
if (includeLocation[index].split(" ").filter(Boolean)[1] != "ERROR:") {
if (lineBeforeIncludeStatement.split(" ").length == tabDepth && withinInclude) {
trueFileLine = trueFileLine - currIncludeIndex;
break;
} else if (lineBeforeIncludeStatement.split(" ").length + addTabDepth == tabDepth && endIncludeNumber == -1) {
endIncludeNumber = currIncludeIndex;
} else if (lineBeforeIncludeStatement.split(" ").length + addTabDepth == tabDepth) {
trueFileLine = trueFileLine - (endIncludeNumber - currIncludeIndex);
endIncludeNumber = -1;
}
}
} else {
lineBeforeIncludeStatement = includeLocation[index];
}
}
let funcLine = "";
for (let line of linedRenderedPreset.slice(0, lineNumber).reverse()) {
let match = /def (\w+)/.exec(line);
if (match) {
funcLine = match[1];
break;
}
}
let includeFilename;
if (lineBeforeIncludeStatement != "") {
includeFilename = lineBeforeIncludeStatement.slice(1).trim().slice(14, -1);
} else {
includeFilename = null;
}
if (includeLocation.length !== 0) {
trueFileLine -= 1;
lineNumber -= 1;
}
return {
lineNumber: trueFileLine,
includeFilename,
line: linedRenderedPreset[lineNumber],
funcLine
};
}
function printOutLine(eLine) {
return log(chalk`{blue ${eLine.includeFilename || "Main"}}:{green ${eLine.lineNumber}} in ${eLine.funcLine}
${eLine.line}`);
}
async function getArtifact(env, artifact, jobid) {
let path$$1 = `/jobs/${jobid}/artifacts/${artifact}`;
let art = lib.makeAPIRequest({
env,
path: path$$1
}).catch(_ => null);
return await art;
}
async function getInfo(env, jobid) {
let trace = getArtifact(env, "trace", jobid);
let renderedPreset = getArtifact(env, "preset", jobid);
let result = getArtifact(env, "result", jobid);
let error = getArtifact(env, "error", jobid);
let output = getArtifact(env, "output", jobid);
[trace, renderedPreset, result, output, error] = await Promise.all([trace, renderedPreset, result, output, error]);
return {
trace,
renderedPreset,
result,
output,
error
};
}
const tracelineRegex = /^(?:[\d.]+) ([\w ]+):(\d+): (.+)/;
function parseTraceLine(line) {
let info = tracelineRegex.exec(line);
if (!info) {
return {
full: line,
parsed: false,
content: line
};
}
return {
absoluteTime: info[0],
presetName: info[1],
lineNumber: info[2],
text: info[3],
content: info[3],
full: line,
parsed: true
};
}
async function parseTrace(env, jobid) {
let {
trace,
renderedPreset
} = await getInfo(env, jobid);
let errorLines = [];
let shouldBreak = 0;
for (let tr of trace.split("\n\n").reverse()) {
errorLines.push(tr);
shouldBreak--;
if (tr.includes("Exception")) shouldBreak = 1;
if (tr.includes("raised")) shouldBreak = 1;
if (!shouldBreak) break;
}
let errorList = [];
for (let errLine of errorLines) {
let info = parseTraceLine(errLine);
if (!info.parsed) {
errorList.push((await findLineInFile(renderedPreset, info.lineNumber)));
} else {
errorList.push(errLine);
}
}
return errorList;
}
const Trace = {
parseTrace,
printOutLine,
getInfo,
findLineInFile,
getArtifact
};
class Asset extends RallyBase {
constructor({
data,
remote,
included,
lite
}) {
super();
this.data = data;
this.meta = {};
this.remote = remote;
if (included) {
this.meta.metadata = Asset.normalizeMetadata(included);
}
this.lite = !!lite;
}
static normalizeMetadata(payload) {
let newMetadata = {};
for (let md of payload) {
if (md.type !== "metadata") continue;
newMetadata[md.attributes.usage] = md.attributes.metadata;
}
return newMetadata;
}
async getMetadata(forceRefresh = false) {
if (this.meta.metadata && !forceRefresh) return this.meta.metadata;
let req = await lib.makeAPIRequest({
env: this.remote,
path: `/movies/${this.id}/metadata?page=1p100`
});
return this.meta.metadata = Asset.normalizeMetadata(req.data);
}
async patchMetadata(metadata) {
if (metadata.Workflow) {
//FIXME
//Currently, WORKFLOW_METADATA cannot be patched via api: we need to
//start a ephemeral eval to upload it
let md = JSON.stringify(JSON.stringify(metadata.Workflow));
let fakePreset = {
code: `WORKFLOW_METADATA.update(json.loads(${md}))`
};
await this.startEphemeralEvaluateIdeal(fakePreset);
log("WFMD Patched using ephemeralEval");
}
if (metadata.Metadata) {
let req = await lib.makeAPIRequest({
env: this.remote,
path: `/movies/${this.id}/metadata/Metadata`,
method: "PATCH",
payload: {
"data": {
"type": "metadata",
"attributes": {
"metadata": metadata.Metadata
}
}
}
});
log("MD Patched");
}
}
static lite(id, remote) {
return new this({
data: {
id
},
remote,
lite: true
});
}
chalkPrint(pad = false) {
let id = String("A-" + (this.remote && this.remote + "-" + this.id || "LOCAL"));
if (pad) id = id.padStart(15);
return chalk`{green ${id}}: {blue ${this.data.attributes ? this.name : "(lite asset)"}}`;
}
static async createNew(name, env) {
let req = await lib.makeAPIRequest({
env,
path: "/assets",
method: "POST",
payload: {
data: {
attributes: {
name
},
type: "assets"
}
}
});
return new this({
data: req.data,
remote: env
});
}
async delete() {
let req = await lib.makeAPIRequest({
env: this.remote,
path: "/assets/" + this.id,
method: "DELETE"
});
}
async getFiles(refresh = false) {
if (this._files && !refresh) return this._files;
let req = await lib.indexPathFast({
env: this.remote,
path: `/assets/${this.id}/files`,
method: "GET"
}); //return req;
return this._files = new Collection(req.map(x => new File({
data: x,
remote: this.remote,
parent: this
})));
}
async addFile(label, fileuris) {
if (!Array.isArray(fileuris)) fileuris = [fileuris];
let instances = {};
for (let i = 0; i < fileuris.length; i++) {
instances[String(i + 1)] = {
uri: fileuris[i]
};
}
let req = await lib.makeAPIRequest({
env: this.remote,
path: "/files",
method: "POST",
payload: {
"data": {
"attributes": {
label,
instances
},
"relationships": {
"asset": {
"data": {
id: this.id,
"type": "assets"
}
}
},
"type": "files"
}
}
});
return req;
}
async startWorkflow(jobName, {
initData,
priority
} = {}) {
let attributes = {};
if (initData) {
//Convert init data to string
initData = typeof initData === "string" ? initData : JSON.stringify(initData);
attributes.initData = initData;
}
if (priority) {
attributes.priority = priority;
}
let req = await lib.makeAPIRequest({
env: this.remote,
path: "/workflows",
method: "POST",
payload: {
"data": {
"type": "workflows",
attributes,
"relationships": {
"movie": {
"data": {
id: this.id,
"type": "movies"
}
},
"rule": {
"data": {
"attributes": {
"name": jobName
},
"type": "rules"
}
}
}
}
}
});
return req;
}
static async startAnonWorkflow(env, jobName, {
initData,
priority
} = {}) {
let attributes = {};
if (initData) {
//Convert init data to string
initData = typeof initData === "string" ? initData : JSON.stringify(initData);
attributes.initData = initData;
}
if (priority) {
attributes.priority = priority;
}
let req = await lib.makeAPIRequest({
env,
path: "/workflows",
method: "POST",
payload: {
"data": {
"type": "workflows",
attributes,
"relationships": {
"rule": {
"data": {
"attributes": {
"name": jobName
},
"type": "rules"
}
}
}
}
}
});
return req;
}
async startEphemeralEvaluateIdeal(preset, dynamicPresetData, isBinary = false) {
let res;
const env = this.remote;
let provider = await Provider.getByName(this.remote, "SdviEvaluate");
write(chalk`Starting ephemeral evaluate on ${this.chalkPrint(false)}...`); // Fire and forget.
let evalInfo = await lib.makeAPIRequest({
env: this.remote,
path: "/jobs",
method: "POST",
payload: {
data: {
attributes: {
category: provider.category,
providerTypeName: provider.name,
rallyConfiguration: {},
//we need to strip invalid utf8 characters from the
//buffer before we encode it or the sdvi backend dies
providerData: Buffer.from(preset.code, isBinary && "binary" || "utf8").toString("base64"),
dynamicPresetData
},
type: "jobs",
relationships: {
movie: {
data: {
id: this.id,
type: "movies"
}
}
}
}
}
});
write(" Waiting for finish...\n");
let dots = 0;
for (;;) {
res = await lib.makeAPIRequest({
env,
path_full: evalInfo.data.links.self
});
write(`\r${res.data.attributes.state}${".".repeat(dots++)} `);
if (dots === 5) {
dots = 1;
}
if (res.data.attributes.state == "Complete") {
write(chalk`{green Done}...\n`);
break;
}
await sleep(500);
}
return;
}
async startEvaluate(presetid, dynamicPresetData) {
// Fire and forget.
let data = await lib.makeAPIRequest({
env: this.remote,
path: "/jobs",
method: "POST",
payload: {
data: {
type: "jobs",
attributes: {
dynamicPresetData
},
relationships: {
movie: {
data: {
id: this.id,
type: "movies"
}
},
preset: {
data: {
id: presetid,
type: "presets"
}
}
}
}
}
});
return data;
}
async rename(newName) {
let req = await lib.makeAPIRequest({
env: this.remote,
path: `/assets/${this.id}`,
method: "PATCH",
payload: {
data: {
attributes: {
name: newName
},
type: "assets"
}
}
});
this.name = newName;
return req;
}
async migrate(targetEnv) {
configObject.globalProgress = false;
log(`Creating paired file in ${targetEnv}`); //Fetch metadata in parallel, we await it later
let _mdPromise = this.getMetadata();
let targetAsset = await Asset.getByName(targetEnv, this.name);
if (targetAsset) {
log(`Asset already exists ${targetAsset.chalkPrint()}`); //if(configObject.script) process.exit(10);
} else {
targetAsset = await Asset.createNew(this.name, targetEnv);
log(`Asset created ${targetAsset.chalkPrint()}`);
} //wait for metadata to be ready before patching
await _mdPromise;
log("Adding asset metadata");
await targetAsset.patchMetadata(this.md);
let fileCreations = [];
for (let file of await this.getFiles()) {
let possibleInstances = {}; //Check for any valid copy-able instances
for (let inst of file.instancesList) {
//We need to skip internal files
if (inst.storageLocationName === "Rally Platform Bucket") continue;
log(`Adding file: ${file.chalkPrint()}`);
possibleInstances[inst.storageLocationName] = () => targetAsset.addFileInstance(file, inst);
}
if (Object.values(possibleInstances).length > 1) {
//prioritize archive is possible
if (possibleInstances["Archive"]) {
log("Hit archive prioritizer");
fileCreations.push(possibleInstances["Archive"]);
} else {
fileCreations.push(...Object.values(possibleInstances));
}
} else {
fileCreations.push(...Object.values(possibleInstances));
}
}
await Promise.all(fileCreations.map(x => x()));
}
async addFileInstance(file, inst, tagList = []) {
let newInst = {
uri: File.rslURL(inst),
name: inst.name,
size: inst.size,
lastModified: inst.lastModified,
storageLocationName: inst.storageLocationName
};
let instances = {};
instances[String(Math.floor(Math.random() * 100000 + 1))] = newInst;
let request = lib.makeAPIRequest({
env: this.remote,
path: `/files`,
method: "POST",
payload: {
data: {
type: "files",
attributes: {
label: file.label,
tagList,
instances
},
relationships: {
asset: {
data: {
id: this.id,
type: "assets"
}
}
}
}
}
});
try {
let fileData = await request;
let newFile = new File({
data: fileData.data,
remote: this.remote,
parent: this
});
if (configObject.scrip