losant-cli
Advanced tools
Comparing version 1.3.1 to 1.3.2
@@ -10,2 +10,8 @@ const helpLines = ` | ||
$ losant experience version v1.0.1 -d "updated home page" | ||
Create a new experience version with a description | ||
$ losant experience version v1.0.1 -d "updated home page" | ||
Create a new experience version associated with specific domain IDs or names | ||
$ losant experience version v1.0.1 -o "653981225c401f279a221eaa,653981225c401f279a221eab,*.foo.bar" | ||
Create a new experience version associated with specific slug IDs or names | ||
$ losant experience version v1.0.1 -s "653981225c401f279a221ebb,mypersonalslug,653981225c401f279a221ebc" | ||
`; | ||
@@ -19,3 +25,5 @@ | ||
.option('-d, --description <description>', 'a description to attach to this version') | ||
.option('-o --domainIds <domainIds>', 'a comma separated list of domain IDs or names') | ||
.option('-s --slugIds <slugIds>', 'a comma separated list of slug IDs or names') | ||
.action(require('../../lib/experience-version')); | ||
}; |
@@ -34,3 +34,3 @@ const error = require('error/typed'); | ||
{ type: 'password', name: 'password', message: 'Enter Losant password:' }, | ||
{ type: 'input', name: 'twoFactorCode', message: 'Enter two-factor auth code (if applicable):' } | ||
{ type: 'input', name: 'twoFactorCode', message: 'Enter multi-factor auth code (if applicable):' } | ||
]); | ||
@@ -37,0 +37,0 @@ if (!email || !password) { |
@@ -12,8 +12,13 @@ const p = require('commander'); | ||
.description('Create a User API Token in your Losant account, then set it here to configure the command line tool.') | ||
.argument('[token]', 'The API token to set (if not passed, user will be prompted)') | ||
.showHelpAfterError() | ||
.action(async () => { | ||
// prompt the user to input a token | ||
const { token: apiToken } = await inquirer.prompt([ | ||
{ type: 'input', name: 'token', message: 'Enter a Losant User API token:' } | ||
]); | ||
.action(async (token) => { | ||
let apiToken = token; | ||
if (!apiToken) { | ||
// prompt the user to input a token | ||
const res = await inquirer.prompt([ | ||
{ type: 'input', name: 'token', message: 'Enter a Losant User API token:' } | ||
]); | ||
apiToken = res.token; | ||
} | ||
try { | ||
@@ -20,0 +25,0 @@ const api = await getApi({ apiToken }); |
const inquirer = require('inquirer'); | ||
const { | ||
capitalize, keys, compact | ||
capitalize, keys, compact, map, trim, isNotNil, uniq | ||
} = require('omnibelt'); | ||
@@ -12,2 +12,3 @@ const { loadConfig, logResult, logError, log } = require('./utils'); | ||
const mapTrim = map(trim); | ||
@@ -84,2 +85,46 @@ const listVersions = async (api, applicationId, filter, endpointDomain) => { | ||
const promptForSlugsAndDomains = async (currentDomains, currentSlugs) => { | ||
const domainChoiceMappings = choices('domainName', 'version', currentDomains); | ||
const domainChoices = keys(domainChoiceMappings); | ||
const slugChoiceMappings = choices('slug', 'version', currentSlugs); | ||
const slugChoices = keys(slugChoiceMappings); | ||
const questions = []; | ||
if (domainChoices.length) { | ||
questions.push({ type: 'checkbox', name: 'domains', message: 'Select Experience Domains to point at this version', choices: domainChoices }); | ||
} | ||
if (slugChoices.length) { | ||
questions.push({ type: 'checkbox', name: 'slugs', message: 'Select Experience Slugs to point at this version', choices: slugChoices }); | ||
} | ||
let domainIds = [], slugIds = []; | ||
if (questions.length) { | ||
const { domains, slugs } = await inquirer.prompt(questions) || {}; | ||
if (domains?.length) { | ||
domainIds = domains.map((domain) => domainChoiceMappings[domain]); | ||
} | ||
if (slugs?.length) { | ||
slugIds = slugs.map((slug) => slugChoiceMappings[slug]); | ||
} | ||
} | ||
return { domainIds, slugIds }; | ||
}; | ||
const createIdAndNameMap = (data, fieldName) => { | ||
const idOrNameMap = new Map(); | ||
data.forEach((d) => { | ||
idOrNameMap.set(d[fieldName], d.id); | ||
idOrNameMap.set(d.id, d.id); | ||
}); | ||
return idOrNameMap; | ||
}; | ||
const givenNamesToIds = (given, nameOrIdMap, errMsgs, type) => { | ||
return given.map((idOrName) => { | ||
if (!nameOrIdMap.has(idOrName)) { | ||
errMsgs.push(`${type} ${idOrName} was not found in the current list.`); | ||
return; | ||
} | ||
return nameOrIdMap.get(idOrName); | ||
}).filter(isNotNil); | ||
}; | ||
module.exports = async (version, opts = {}) => { | ||
@@ -91,25 +136,26 @@ const { apiToken, applicationId, api, endpointDomain } = await loadConfig(); | ||
} else { | ||
const domainChoiceMappings = choices('domainName', 'version', await getExperiencePart(api, 'domain', { applicationId })); | ||
const domainChoices = keys(domainChoiceMappings); | ||
const slugChoiceMappings = choices('slug', 'version', await getExperiencePart(api, 'slug', { applicationId })); | ||
const slugChoices = keys(slugChoiceMappings); | ||
const questions = []; | ||
if (domainChoices.length) { | ||
questions.push({ type: 'checkbox', name: 'domains', message: 'Select Experience Domains to point at this version', choices: domainChoices }); | ||
} | ||
if (slugChoices.length) { | ||
questions.push({ type: 'checkbox', name: 'slugs', message: 'Select Experience Slugs to point at this version', choices: slugChoices }); | ||
} | ||
let domainIds, slugIds; | ||
if (questions.length) { | ||
const { domains, slugs } = await inquirer.prompt(questions) || {}; | ||
if (domains?.length) { | ||
domainIds = domains.map((domain) => domainChoiceMappings[domain]); | ||
const currentDomains = await getExperiencePart(api, 'domain', { applicationId }); | ||
const currentSlugs = await getExperiencePart(api, 'slug', { applicationId }); | ||
const versionInfo = { description: opts.description }; | ||
if (isNotNil(opts.slugIds) || isNotNil(opts.domainIds)) { | ||
const domainMap = createIdAndNameMap(currentDomains, 'domainName'); | ||
const slugMap = createIdAndNameMap(currentSlugs, 'slug'); | ||
const givenSlugs = opts.slugIds?.length ? mapTrim(opts.slugIds.split(',')) : []; | ||
const givenDomains = opts.domainIds?.length ? mapTrim(opts.domainIds.split(',')) : []; | ||
const errorMsgs = []; | ||
versionInfo.slugIds = uniq(givenNamesToIds(givenSlugs, slugMap, errorMsgs, 'Slug')); | ||
versionInfo.domainIds = uniq(givenNamesToIds(givenDomains, domainMap, errorMsgs, 'Domain')); | ||
if (errorMsgs.length) { | ||
errorMsgs.forEach((msg) => { | ||
logError(msg); | ||
}); | ||
return; | ||
} | ||
if (slugs?.length) { | ||
slugIds = slugs.map((slug) => slugChoiceMappings[slug]); | ||
} | ||
} else { | ||
const selected = await promptForSlugsAndDomains(currentDomains, currentSlugs); | ||
versionInfo.domainIds = selected.domainIds; | ||
versionInfo.slugIds = selected.slugIds; | ||
} | ||
await createVersion(api, applicationId, version, { description: opts.description, domainIds, slugIds }); | ||
await createVersion(api, applicationId, version, versionInfo); | ||
} | ||
}; |
@@ -46,3 +46,3 @@ const paginateRequest = require('./paginate-request'); | ||
if (!apiToken || !applicationId) { return; } | ||
let meta = await loadLocalMeta(commandType) || {}; | ||
let meta = await loadLocalMeta(commandType); | ||
if (opts.reset && !opts.dryRun) { | ||
@@ -49,0 +49,0 @@ // currently this can only be used by experience |
@@ -18,3 +18,2 @@ const paginateRequest = require('./paginate-request'); | ||
const { isEmpty, mergeRight, values, keys } = require('omnibelt'); | ||
const { buildMetaDataObj } = require('./meta-data-helpers'); | ||
@@ -29,3 +28,3 @@ const getUploader = ({ | ||
const api = config.api; | ||
const meta = await loadLocalMeta(commandType) || {}; | ||
const meta = await loadLocalMeta(commandType); | ||
let items; | ||
@@ -68,30 +67,37 @@ try { | ||
} | ||
const uploadResults = await allSettledSerial(async (stat) => { | ||
const statsByType = { | ||
unmodified: [], | ||
deleted: [], | ||
potentialUpdates: [] | ||
}; | ||
values(localStatusByFile).forEach((stat) => { | ||
if (statsByType[stat.status]) { | ||
return statsByType[stat.status].push(stat); | ||
} | ||
statsByType.potentialUpdates.push(stat); | ||
}); | ||
statsByType.unmodified.forEach((stat) => { | ||
logProcessing(stat.file); | ||
if (stat.status === 'unmodified') { | ||
return logResult('unmodified', stat.file); | ||
} | ||
if (stat.status === 'deleted') { | ||
if (!opts.dryRun) { | ||
try { | ||
return logResult('unmodified', stat.file); | ||
}); | ||
const deleteResults = await allSettledSerial(async (stat) => { | ||
logProcessing(stat.file); | ||
const remoteStatus = remoteStatusByFile[stat.file]; | ||
if (!opts.dryRun) { | ||
try { | ||
if (remoteStatus.status !== 'deleted') { | ||
await api[singular(apiType)].delete(await getDeleteQuery(stat, config)); | ||
} catch (err) { | ||
const message = `An error ocurred when trying to remove ${stat.file} with the message ${err.message}`; | ||
return logError(message); | ||
} | ||
delete meta[stat.file]; | ||
} catch (err) { | ||
const message = `An error ocurred when trying to remove ${stat.file} with the message ${err.message}`; | ||
return logError(message); | ||
} | ||
return logResult('deleted', stat.file, 'yellow'); | ||
delete meta[stat.file]; | ||
} | ||
const remoteStatus = remoteStatusByFile[stat.file] || {}; | ||
if ( | ||
(stat === 'added' && remoteStatus === 'added') || | ||
(stat === 'modified' && remoteStatus === 'modified') | ||
) { | ||
if (stat.localMd5 === remoteStatus.remoteMd5) { | ||
return logResult('deleted', stat.file, 'yellow'); | ||
}, statsByType.deleted); | ||
meta[stat.file] = buildMetaDataObj({ remoteStatus, localStatus: stat }); | ||
return logResult('uploaded', stat.file, 'green'); | ||
} | ||
} | ||
const uploadResults = await allSettledSerial(async (stat) => { | ||
logProcessing(stat.file); | ||
if (!opts.dryRun) { | ||
@@ -125,4 +131,4 @@ let result; | ||
return logResult('uploaded', stat.file, 'green'); | ||
}, values(localStatusByFile)); | ||
uploadResults.forEach((result) => { | ||
}, statsByType.potentialUpdates); | ||
[ ...deleteResults, ...uploadResults ].forEach((result) => { | ||
// this should only occur on unhandled rejections any api error should have already logged and resolved the promise | ||
@@ -129,0 +135,0 @@ if (result.state !== 'fulfilled') { |
@@ -238,3 +238,11 @@ const getApi = require('./get-api'); | ||
const mapFile = path.resolve(dir, `${type}.yml`); | ||
return (await pathExists(mapFile)) ? yaml.safeLoad(await readFile(mapFile)) : null; | ||
if (await pathExists(mapFile)) { | ||
return yaml.safeLoad(await readFile(mapFile)); | ||
} | ||
if (process.env.NODE_ENV !== 'test') { | ||
utils.logError(`Could not find meta data file ${type}.yml, please re-configure your directory.`); | ||
process.exit(1); | ||
} else { | ||
return {}; | ||
} | ||
}, | ||
@@ -267,3 +275,3 @@ | ||
const statusByFile = {}; | ||
const meta = await utils.loadLocalMeta(type) || {}; | ||
const meta = await utils.loadLocalMeta(type); | ||
const metaFiles = new Set(keys(meta)); | ||
@@ -338,3 +346,3 @@ const dirPattern = path.resolve(path.join(dir, globPattern)); | ||
const statusByFile = {}; | ||
const meta = await utils.loadLocalMeta(type) || {}; | ||
const meta = await utils.loadLocalMeta(type); | ||
const metaByFile = new Map(); | ||
@@ -417,4 +425,4 @@ const metaFiles = new Set(); | ||
if ( | ||
(localStatus === 'added' && (remoteStatus === 'removed' || remoteStatus === 'unmodified' || remoteStatus === 'modified')) || | ||
((localStatus === 'removed' || localStatus === 'unmodified' || localStatus === 'modified') && (remoteStatus === 'added' || remoteStatus === 'missing')) || | ||
(localStatus === 'added' && (remoteStatus === 'deleted' || remoteStatus === 'unmodified' || remoteStatus === 'modified')) || | ||
((localStatus === 'deleted' || localStatus === 'unmodified' || localStatus === 'modified') && (remoteStatus === 'added' || remoteStatus === 'missing')) || | ||
(localStatus === 'missing' && remoteStatus !== 'added') | ||
@@ -463,3 +471,2 @@ ) { | ||
allFiles.forEach((file) => { | ||
// console.log(`${file} matching dirPattern ${dirPattern} :: ${!minimatch(file, dirPattern)}`); | ||
if (!minimatch(file.split(path.sep).join(path.posix.sep), dirPattern.split(path.sep).join(path.posix.sep))) { | ||
@@ -466,0 +473,0 @@ delete remoteStatusByFile[file]; |
{ | ||
"name": "losant-cli", | ||
"version": "1.3.1", | ||
"version": "1.3.2", | ||
"description": "Losant Command Line Interface", | ||
@@ -20,3 +20,3 @@ "license": "MIT", | ||
"engines": { | ||
"node": ">=14" | ||
"node": ">=16" | ||
}, | ||
@@ -38,3 +38,3 @@ "scripts": { | ||
"@rjhilgefort/export-dir": "^2.0.0", | ||
"axios": "^1.4.0", | ||
"axios": "^1.5.1", | ||
"chalk": "^4.1.1", | ||
@@ -44,3 +44,3 @@ "chokidar": "^3.5.3", | ||
"commander": "^10.0.1", | ||
"csv-stringify": "^6.3.3", | ||
"csv-stringify": "^6.4.4", | ||
"death": "^1.1.0", | ||
@@ -51,15 +51,15 @@ "error": "^7.2.0", | ||
"fs-extra": "^11.1.1", | ||
"glob": "^10.2.2", | ||
"glob": "10.2.2", | ||
"inquirer": "^8.2.3", | ||
"js-yaml": "^3.14.1", | ||
"jsonwebtoken": "^9.0.0", | ||
"jsonwebtoken": "^9.0.2", | ||
"lodash-template": "^1.0.0", | ||
"losant-rest": "2.14.0", | ||
"losant-rest": "2.14.1", | ||
"mime-types": "^2.1.30", | ||
"minimatch": "^9.0.0", | ||
"minimatch": "^9.0.3", | ||
"moment": "^2.29.1", | ||
"omnibelt": "^3.1.1", | ||
"omnibelt": "^3.1.2", | ||
"pad": "^3.2.0", | ||
"proper-lockfile": "^4.1.2", | ||
"rollbar": "^2.21.1", | ||
"rollbar": "^2.26.2", | ||
"sanitize-filename": "^1.6.2", | ||
@@ -72,5 +72,5 @@ "single-line-log": "^1.1.2", | ||
"husky": "^8.0.3", | ||
"lint-staged": "^13.2.2", | ||
"lint-staged": "13.2.2", | ||
"mocha": "^10.2.0", | ||
"nock": "^13.3.1", | ||
"nock": "^13.3.6", | ||
"should": "^13.2.3", | ||
@@ -77,0 +77,0 @@ "sinon": "^15.0.4" |
@@ -35,4 +35,3 @@ # Losant CLI | ||
Before you run any other commands, you must run `losant login` to authenticate with your Losant account. This command checks to see if your account is linked to a Single Sign-On (SSO) provider. If so, the command will prompt for a User Token; otherwise it will prompt for the password (and optionally your two-factor code) for your Losant account. After either is given successfully, the command will store | ||
the authentication token on your computer. With this command, you can optionally set `LOSANT_API_URL` as an environment variable; e.g. `LOSANT_API_URL=<api.private.install> losant login`. By default the CLI will use `https://api.losant.com` as the API URL. This will allow you to log in across Losant installations. If you are logged in to multiple Losant installations when you configure a directory, you will be asked which API token to use to access the application. From then on, any request for that application will use the same API URL. | ||
Before you run any other commands, you must run `losant login` to authenticate with your Losant account. This command checks to see if your account is linked to a Single Sign-On (SSO) provider. If so, the command will prompt for a User Token; otherwise it will prompt for the password (and optionally your multi-factor code) for your Losant account. After either is given successfully, the command will store the authentication token on your computer. With this command, you can optionally set `LOSANT_API_URL` as an environment variable; e.g. `LOSANT_API_URL=<api.private.install> losant login`. By default the CLI will use `https://api.losant.com` as the API URL. This will allow you to log in across Losant installations. If you are logged in to multiple Losant installations when you configure a directory, you will be asked which API token to use to access the application. From then on, any request for that application will use the same API URL. | ||
@@ -39,0 +38,0 @@ ### Set-token |
94852
2298
148
17
+ Addedglob@10.2.2(transitive)
+ Addedjackspeak@2.3.6(transitive)
+ Addedlosant-rest@2.14.1(transitive)
+ Addedminipass@5.0.0(transitive)
- Removedglob@10.4.5(transitive)
- Removedjackspeak@3.4.3(transitive)
- Removedlosant-rest@2.14.0(transitive)
- Removedminipass@7.1.2(transitive)
- Removedpackage-json-from-dist@1.0.1(transitive)
Updatedaxios@^1.5.1
Updatedcsv-stringify@^6.4.4
Updatedglob@10.2.2
Updatedjsonwebtoken@^9.0.2
Updatedlosant-rest@2.14.1
Updatedminimatch@^9.0.3
Updatedomnibelt@^3.1.2
Updatedrollbar@^2.26.2