Skip to content

Commit

Permalink
feat(cli): add au config command
Browse files Browse the repository at this point in the history
The `au config` command gets or sets configuration for the Aurelia application.
`au config <key> <value> --get --set --clear --add --remove --no-save --no-backup`

Closes #629.
  • Loading branch information
jwx committed Oct 5, 2017
1 parent e12d70d commit 5cd16f6
Show file tree
Hide file tree
Showing 10 changed files with 557 additions and 1 deletion.
47 changes: 47 additions & 0 deletions lib/commands/config/command.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
'use strict';

const UI = require('../../ui').UI;
const CLIOptions = require('../../cli-options').CLIOptions;
const Container = require('aurelia-dependency-injection').Container;
const os = require('os');

const Configuration = require('./configuration');
const ConfigurationUtilities = require('./util');

module.exports = class {
static inject() { return [Container, UI, CLIOptions]; }

constructor(container, ui, options) {
this.container = container;
this.ui = ui;
this.options = options;
}

execute(args) {
this.config = new Configuration(this.options);
this.util = new ConfigurationUtilities(this.options, args);
let key = this.util.getArg(0) || '';
let value = this.util.getValue(this.util.getArg(1));
let save = !CLIOptions.hasFlag('no-save');
let backup = !CLIOptions.hasFlag('no-backup');
let action = this.util.getAction(value);

this.displayInfo(`Performing configuration action '${action}' on '${key}'`, (value ? `with '${value}'` : ''));
this.displayInfo(this.config.execute(action, key, value));

if (action !== 'get') {
if (save) {
this.config.save(backup).then((name) => {
this.displayInfo('Configuration saved. ' + (backup ? `Backup file '${name}' created.` : 'No backup file was created.'));
});
}
else {
this.displayInfo(`Action was '${action}', but no save was performed!`);
}
}
}

displayInfo(message) {
return this.ui.log(message + os.EOL);
}
};
53 changes: 53 additions & 0 deletions lib/commands/config/command.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
{
"name": "config",
"description": "Gets or sets configuration for the Aurelia application.",
"parameters": [
{
"name": "key",
"optional": true,
"description": "The key you want to get or set. Supports hierarchies and array indexes, for example build.targets[0] and arrayWithArray[2].[1]"
},
{
"name": "value",
"optional": true,
"description": "The value you want to set the key to. Supports json, for example \"{ \\\"myKey\\\": \\\"myValue\\\" }\""
}
],
"flags": [
{
"name": "get",
"description": "Gets the content of key, ignoring value parameter (the same as not specifying a value).",
"type": "boolean"
},
{
"name": "set",
"description": "Sets the content of key to value, replacing any existing content.",
"type": "boolean"
},
{
"name": "clear",
"description": "Deletes the key and all its content from the configuration.",
"type": "boolean"
},
{
"name": "add",
"description": "If value or existing content of the key is an array, adds value(s) to existing content. If value is an object, merges it into existing content of key.",
"type": "boolean"
},
{
"name": "remove",
"description": "If value or existing content of the key is an array, removes value(s) from existing content. If value or existing content of the key is an object, removes key(s) from existing content of key.",
"type": "boolean"
},
{
"name": "no-save",
"description": "Don't save the changes in the configuration file.",
"type": "boolean"
},
{
"name": "no-backup",
"description": "Don't create a backup configuration file before saving changes.",
"type": "boolean"
}
]
}
165 changes: 165 additions & 0 deletions lib/commands/config/configuration.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
'use strict';

const os = require('os');
const copySync = require('../../file-system').copySync;
const readFileSync = require('../../file-system').readFileSync;
const writeFile = require('../../file-system').writeFile;

class Configuration {
constructor(options) {
this.options = options;
this.aureliaJsonPath = options.originalBaseDir + '/aurelia_project/aurelia.json';
this.project = JSON.parse(readFileSync(this.aureliaJsonPath));
}

configEntry(key, createKey) {
let entry = this.project;
let keys = key.split('.');

if (!keys[0]) {
return entry;
}

while (entry && keys.length) {
key = this.parsedKey(keys.shift());
if (entry[key.value] === undefined || entry[key.value] === null) {
if (!createKey) {
return entry[key.value];
}
let checkKey = this.parsedKey(keys.length ? keys[0] : createKey);
if (checkKey.index) {
entry[key.value] = [];
}
else if (checkKey.key) {
entry[key.value] = {};
}
}
entry = entry[key.value];

// TODO: Add support for finding objects based on input values?
// TODO: Add support for finding string in array?
}

return entry;
}

parsedKey(key) {
if (/\[(\d+)\]/.test(key)) {
return { index: true, key: false, value: +(RegExp.$1) };
}
else {
return { index: false, key: true, value: key };
}
}

normalizeKey(key) {
const re = /([^.])\[/;
while (re.exec(key)) {
key = key.replace(re, RegExp.$1 + '.[');
}

let keys = key.split('.');
for (let i = 0; i < keys.length; i++) {
if (/\[(\d+)\]/.test(keys[i])) {
// console.log(`keys[${i}] is index: ${keys[i]}`);
}
else if (/\[(.+)\]/.test(keys[i])) {
// console.log(`keys[${i}] is indexed name: ${keys[i]}`);
keys[i] = RegExp.$1;
}
else {
// console.log(`keys[${i}] is name: ${keys[i]}`);
}
}

return keys.join('.');
}

execute(action, key, value) {
let originalKey = key;

key = this.normalizeKey(key);

if (action === 'get') {
return `Configuration key '${key}' is:` + os.EOL + JSON.stringify(this.configEntry(key), null, 2);
}

let keys = key.split('.');
key = this.parsedKey(keys.pop());
let parent = keys.join('.');

if (action === 'set') {
let entry = this.configEntry(parent, key.value);
if (entry) {
entry[key.value] = value;
}
else {
console.log('Failed to set property', this.normalizeKey(originalKey), '!');
}
}
else if (action === 'clear') {
let entry = this.configEntry(parent);
if (entry && (key.value in entry)) {
delete entry[key.value];
}
else {
console.log('No property', this.normalizeKey(originalKey), 'to clear!');
}
}
else if (action === 'add') {
let entry = this.configEntry(parent, key.value);
if (Array.isArray(entry[key.value]) && !Array.isArray(value)) {
value = [value];
}
if (Array.isArray(value) && !Array.isArray(entry[key.value])) {
entry[key.value] = (entry ? [entry[key.value]] : []);
}
if (Array.isArray(value)) {
entry[key.value].push(...value);
}
else if (Object(value) === value) {
if (Object(entry[key.value]) !== entry[key.value]) {
entry[key.value] = {};
}
Object.assign(entry[key.value], value);
}
else {
entry[key.value] = value;
}
}
else if (action === 'remove') {
let entry = this.configEntry(parent);

if (Array.isArray(entry) && key.index) {
entry.splice(key.value, 1);
}
else if (Object(entry) === entry && key.key) {
delete entry[key.value];
}
else if (!entry) {
console.log('No property', this.normalizeKey(originalKey), 'to remove from!');
}
else {
console.log("Can't remove value from", entry[key.value], "!");
}
}
key = this.normalizeKey(originalKey);
return `Configuration key '${key}' is now:` + os.EOL + JSON.stringify(this.configEntry(key), null, 2);
}

save(backup = true) {
const unique = new Date().toISOString().replace(/[T\D]/g, '');
let arr = this.aureliaJsonPath.split(/[\\\/]/);
const name = arr.pop();
const path = arr.join('/');
const bak = `${name}.${unique}.bak`;

if (backup) {
copySync(this.aureliaJsonPath, [path, bak].join('/'));
}
return writeFile(this.aureliaJsonPath, JSON.stringify(this.project, null, 2), 'utf8')
.then(() => { return bak });
}
}

module.exports = Configuration;
52 changes: 52 additions & 0 deletions lib/commands/config/util.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
'use strict';

class ConfigurationUtilities {
constructor(options, args) {
this.options = options;
this.args = args;
}

getArg(arg) {
let args = this.args;
if (args) {
for (let i = 0; i < args.length; i++) {
if (args[i].startsWith('--')) {
arg++;
}
if (i === arg) {
return args[i];
}
}
}
}

getValue(value) {
if (value) {
if (!value.startsWith('"') &&
!value.startsWith('[') &&
!value.startsWith('{')) {
value = `"${value}"`;
}
value = JSON.parse(value);
}
return value;
}

getAction(value) {
let actions = ['add', 'remove', 'set', 'clear', 'get'];
for (let action of actions) {
if (this.options.hasFlag(action)) {
return action;
}
}
if (!value) {
return 'get';
}
if (Array.isArray(value) || typeof value === 'object') {
return 'add';
}
return 'set';
}
}

module.exports = ConfigurationUtilities;
1 change: 1 addition & 0 deletions lib/commands/help/command.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ module.exports = class {
getLocalCommandText() {
let commands = [
require('../generate/command.json'),
require('../config/command.json'),
require('./command.json')
];

Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,10 @@
"gulp-conventional-changelog": "^1.1.3",
"gulp-eslint": "^3.0.1",
"jasmine": "^2.5.2",
"jasmine-spec-reporter": "^4.2.1",
"latest-version": "^3.1.0",
"mock-fs": "^4.2.0",
"minimatch": "^3.0.4",
"mock-fs": "^4.2.0",
"nodemon": "^1.11.0",
"require-dir": "^0.3.1",
"run-sequence": "^1.2.2",
Expand Down
Loading

0 comments on commit 5cd16f6

Please sign in to comment.