@pixlcore/xycli 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +543 -0
- package/docs/help.md +2587 -0
- package/docs/screenshots.md +219 -0
- package/docs/sync.md +920 -0
- package/docs/tutorial.md +204 -0
- package/index.js +444 -0
- package/lib/alerts.js +735 -0
- package/lib/apikey.js +425 -0
- package/lib/buckets.js +607 -0
- package/lib/categories.js +323 -0
- package/lib/channels.js +290 -0
- package/lib/config.js +86 -0
- package/lib/dashboard.js +237 -0
- package/lib/doc.js +131 -0
- package/lib/events.js +1314 -0
- package/lib/groups.js +1038 -0
- package/lib/jobs.js +1254 -0
- package/lib/log.js +125 -0
- package/lib/marketplace.js +386 -0
- package/lib/monitors.js +412 -0
- package/lib/plugins.js +491 -0
- package/lib/secrets.js +441 -0
- package/lib/servers.js +1502 -0
- package/lib/snapshots.js +498 -0
- package/lib/sync.js +963 -0
- package/lib/system.js +612 -0
- package/lib/tags.js +250 -0
- package/lib/tickets.js +681 -0
- package/lib/transfer.js +447 -0
- package/lib/utils.js +2745 -0
- package/lib/webhook.js +414 -0
- package/logo.webp +0 -0
- package/package.json +38 -0
- package/test/README.md +97 -0
- package/test/api-validation.test.js +170 -0
- package/test/categories.test.js +246 -0
- package/test/channels.test.js +209 -0
- package/test/config.test.js +96 -0
- package/test/doc.test.js +84 -0
- package/test/groups.test.js +211 -0
- package/test/help.test.js +26 -0
- package/test/helpers/common.js +127 -0
- package/test/helpers/pagination-preload.cjs +18 -0
- package/test/log.test.js +103 -0
- package/test/markdown.test.js +61 -0
- package/test/marketplace.test.js +229 -0
- package/test/monitors.test.js +227 -0
- package/test/pagination.test.js +138 -0
- package/test/plugins.test.js +351 -0
- package/test/secrets.test.js +271 -0
- package/test/servers.test.js +303 -0
- package/test/snapshots.test.js +167 -0
- package/test/sync-unit.test.js +446 -0
- package/test/sync.test.js +542 -0
- package/test/system.test.js +94 -0
- package/test/tags.test.js +230 -0
- package/test/tickets.test.js +261 -0
- package/test/toast.test.js +73 -0
- package/test/transfer-apikey.test.js +96 -0
- package/test/transfer-workflow.test.js +122 -0
- package/test/transfer.test.js +314 -0
- package/test/webhook.test.js +332 -0
- package/test.js +49 -0
package/lib/plugins.js
ADDED
|
@@ -0,0 +1,491 @@
|
|
|
1
|
+
// Plugins Layer
|
|
2
|
+
|
|
3
|
+
const cli = require('pixl-cli');
|
|
4
|
+
const Tools = cli.Tools;
|
|
5
|
+
|
|
6
|
+
module.exports = {
|
|
7
|
+
|
|
8
|
+
async cmd_plugins() {
|
|
9
|
+
// The plural command lists Plugin definitions with optional filters.
|
|
10
|
+
await this.cmd_get_plugins();
|
|
11
|
+
},
|
|
12
|
+
|
|
13
|
+
async cmd_plugin() {
|
|
14
|
+
// Route the singular command, while keeping a bare ID or title convenient.
|
|
15
|
+
var cmd = this.args.other.shift();
|
|
16
|
+
if (!cmd) return this.dieUsage('plugin');
|
|
17
|
+
|
|
18
|
+
switch (cmd) {
|
|
19
|
+
case 'list': await this.cmd_get_plugins(); break;
|
|
20
|
+
case 'get': await this.cmd_get_plugin(); break;
|
|
21
|
+
case 'create': await this.cmd_create_plugin(); break;
|
|
22
|
+
case 'update': await this.cmd_update_plugin(); break;
|
|
23
|
+
case 'delete': await this.cmd_delete_plugin(); break;
|
|
24
|
+
|
|
25
|
+
default:
|
|
26
|
+
this.args.other.unshift(cmd);
|
|
27
|
+
await this.cmd_get_plugin();
|
|
28
|
+
break;
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
async cmd_get_plugins() {
|
|
33
|
+
// Plugin data is cached by getMultiple(), so filtering and pagination are
|
|
34
|
+
// performed locally without making a separate API request for each page.
|
|
35
|
+
this.prepSearchArgs();
|
|
36
|
+
await this.getMultiple();
|
|
37
|
+
|
|
38
|
+
var plugins = this.plugins.slice(0);
|
|
39
|
+
var is_filtered = !!this.args.other.length;
|
|
40
|
+
|
|
41
|
+
if (is_filtered) {
|
|
42
|
+
var search = this.args.other.join(' ');
|
|
43
|
+
plugins = this.findObjectsFuzzy(plugins, {
|
|
44
|
+
id: search,
|
|
45
|
+
title: search,
|
|
46
|
+
command: search,
|
|
47
|
+
script: search,
|
|
48
|
+
notes: search
|
|
49
|
+
}, 1);
|
|
50
|
+
}
|
|
51
|
+
delete this.args.other;
|
|
52
|
+
|
|
53
|
+
if ('enabled' in this.args) {
|
|
54
|
+
var enabled = this.parsePluginBoolean(this.args.enabled, 'enabled');
|
|
55
|
+
plugins = plugins.filter( plugin => !!plugin.enabled === enabled );
|
|
56
|
+
delete this.args.enabled;
|
|
57
|
+
is_filtered = true;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if ('type' in this.args) {
|
|
61
|
+
var type = this.parsePluginType(this.args.type);
|
|
62
|
+
plugins = plugins.filter( plugin => plugin.type === type );
|
|
63
|
+
delete this.args.type;
|
|
64
|
+
is_filtered = true;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (Tools.numKeys(this.args)) {
|
|
68
|
+
plugins = this.findObjectsFuzzy(plugins, this.args);
|
|
69
|
+
is_filtered = true;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
plugins.sort( (a, b) => String(a.title).toLowerCase().localeCompare(String(b.title).toLowerCase()) );
|
|
73
|
+
if (this.format.match(/json/)) return this.jsonOutput(plugins);
|
|
74
|
+
|
|
75
|
+
this.printPaginatedBoxTable({
|
|
76
|
+
title: is_filtered ? 'Filtered Plugins' : 'All Plugins',
|
|
77
|
+
header: ['Plugin ID', 'Title', 'Type', 'Status', 'Parameters', 'Modified'],
|
|
78
|
+
rows: plugins.slice(this.offset, this.offset + this.limit),
|
|
79
|
+
list: { length: plugins.length },
|
|
80
|
+
offset: this.offset,
|
|
81
|
+
limit: this.limit
|
|
82
|
+
}, plugin => [
|
|
83
|
+
this.color('theme').bold(plugin.id),
|
|
84
|
+
bold(this.getNicePlugin(plugin)),
|
|
85
|
+
this.getNicePluginType(plugin.type),
|
|
86
|
+
this.getNiceEnabled(plugin.enabled),
|
|
87
|
+
(plugin.type == 'monitor') ? gray('(Monitor)') : Tools.commify((plugin.params || []).length),
|
|
88
|
+
this.getRelativeDateTime(plugin.modified, true)
|
|
89
|
+
]);
|
|
90
|
+
|
|
91
|
+
this.printSuggestedCommands({
|
|
92
|
+
"View Plugin details": "xy plugin PLUGIN_ID_OR_TITLE",
|
|
93
|
+
"Create an Event Plugin": 'xy plugin create --title "My Plugin" --type event --command node --script @plugin.js',
|
|
94
|
+
"List Monitor Plugins": "xy plugins --type monitor",
|
|
95
|
+
"List disabled Plugins": "xy plugins --enabled false"
|
|
96
|
+
});
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
async cmd_get_plugin() {
|
|
100
|
+
// Exact IDs take precedence over fuzzy title matching for read-only access.
|
|
101
|
+
await this.getMultiple();
|
|
102
|
+
|
|
103
|
+
var selector = this.args.other.join(' ') || this.args.id || this.args.title;
|
|
104
|
+
if (!selector) return this.dieUsage('plugin get');
|
|
105
|
+
|
|
106
|
+
var plugin = await this.findPlugin(selector);
|
|
107
|
+
if (this.format.match(/json/)) return this.jsonOutput(plugin);
|
|
108
|
+
|
|
109
|
+
var script = plugin.script || '';
|
|
110
|
+
// Do not count the empty element created by a conventional trailing EOL.
|
|
111
|
+
var script_lines = script ? script.replace(/\r?\n$/, '').split(/\r?\n/).length : 0;
|
|
112
|
+
var source = plugin.marketplace ? 'Marketplace' : (plugin.stock ? 'xyOps Default' : (plugin.username || '(Unknown)'));
|
|
113
|
+
|
|
114
|
+
this.printBoxList({
|
|
115
|
+
title: 'Plugin Summary',
|
|
116
|
+
rows: [
|
|
117
|
+
[ 'Plugin ID', gray(plugin.id) ],
|
|
118
|
+
[ 'Title', this.color('theme').bold(plugin.title) ],
|
|
119
|
+
[ 'Status', this.getNiceEnabled(plugin.enabled) ],
|
|
120
|
+
[ 'Type', this.getNicePluginType(plugin.type) ],
|
|
121
|
+
[ 'Icon', plugin.icon || gray('(None)') ],
|
|
122
|
+
[ 'Command', plugin.command ],
|
|
123
|
+
[ 'Script', script ? Tools.commify(script_lines) + ' ' + Tools.pluralize('line', script_lines) + ', ' + Tools.getTextFromBytes(Buffer.byteLength(script)) : gray('(None)') ],
|
|
124
|
+
(plugin.type == 'monitor') ? [ 'Groups', this.getNicePluginGroups(plugin.groups) ] : null,
|
|
125
|
+
(plugin.type == 'monitor') ? [ 'Format', String(plugin.format || 'text').toUpperCase() ] : null,
|
|
126
|
+
(plugin.type == 'monitor') ? [ 'Quick Monitor', plugin.quick ? green('Yes') : gray('No') ] : null,
|
|
127
|
+
(plugin.type == 'event') ? [ 'Abort Policy', plugin.kill || 'parent' ] : null,
|
|
128
|
+
(plugin.type == 'event') ? [ 'Remote Runner', plugin.runner ? green('Yes') : gray('No') ] : null,
|
|
129
|
+
[ 'Run as User', ('uid' in plugin) && (plugin.uid !== '') ? plugin.uid : gray('(Default)') ],
|
|
130
|
+
[ 'Run as Group', ('gid' in plugin) && (plugin.gid !== '') ? plugin.gid : gray('(Default)') ],
|
|
131
|
+
[ 'Source', source ],
|
|
132
|
+
[ 'Created', this.getNiceDateTime(plugin.created, true, true) ],
|
|
133
|
+
[ 'Modified', this.getNiceDateTime(plugin.modified, true, true) ],
|
|
134
|
+
[ 'Revision', plugin.revision || 1 ]
|
|
135
|
+
]
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// User notes may contain multiple lines, so keep them out of the summary box.
|
|
139
|
+
if (plugin.notes) {
|
|
140
|
+
this.printUserNotes('PLUGIN NOTES', plugin.notes);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (plugin.type != 'monitor') {
|
|
144
|
+
this.printParamFields({
|
|
145
|
+
title: 'Plugin Parameters',
|
|
146
|
+
fields: plugin.params || []
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Keep potentially large source behind verbose mode in the normal detail view.
|
|
151
|
+
this.printPluginScript(plugin, { verboseOnly: true });
|
|
152
|
+
|
|
153
|
+
this.printSuggestedCommands({
|
|
154
|
+
"Update Plugin": `xy plugin update ${plugin.id} --notes "My notes"`,
|
|
155
|
+
"Export Plugin": `xy plugin ${plugin.id} --export plugin.json`,
|
|
156
|
+
"Disable Plugin": plugin.enabled ? `xy plugin update ${plugin.id} --enabled false` : '',
|
|
157
|
+
"Delete Plugin": `xy plugin delete ${plugin.id}`
|
|
158
|
+
});
|
|
159
|
+
},
|
|
160
|
+
|
|
161
|
+
async cmd_create_plugin() {
|
|
162
|
+
// These defaults match a fresh Plugin in the xyOps web editor. Type-specific
|
|
163
|
+
// fields are normalized below after all user input has been applied.
|
|
164
|
+
if (this.args.other.length) return this.dieUsage('plugin create');
|
|
165
|
+
delete this.args.other;
|
|
166
|
+
|
|
167
|
+
var params = this.preparePluginParams({
|
|
168
|
+
enabled: true,
|
|
169
|
+
type: 'event',
|
|
170
|
+
icon: '',
|
|
171
|
+
command: '',
|
|
172
|
+
script: '',
|
|
173
|
+
params: [],
|
|
174
|
+
groups: [],
|
|
175
|
+
format: '',
|
|
176
|
+
uid: '',
|
|
177
|
+
gid: '',
|
|
178
|
+
kill: 'parent',
|
|
179
|
+
runner: false,
|
|
180
|
+
notes: ''
|
|
181
|
+
}, this.args, true);
|
|
182
|
+
|
|
183
|
+
if (!params.title || !params.command) return this.dieUsage('plugin create');
|
|
184
|
+
|
|
185
|
+
var data = await this.callStandardAPI('createPlugin', params, { text: 'Creating Plugin...' });
|
|
186
|
+
if (this.dry) return;
|
|
187
|
+
if (this.format.match(/json/)) return this.jsonOutput(data.plugin);
|
|
188
|
+
|
|
189
|
+
this.toast('✅', 'green', "Successfully created Plugin: #" + data.plugin.id);
|
|
190
|
+
this.printSuggestedCommands({
|
|
191
|
+
"View Plugin details": `xy plugin ${data.plugin.id}`,
|
|
192
|
+
"Update Plugin": `xy plugin update ${data.plugin.id} --notes "My notes"`,
|
|
193
|
+
"Add a parameter": data.plugin.type == 'monitor' ? '' : `xy plugin update ${data.plugin.id} --param '{ "id":"name", "title":"Name", "type":"text", "value":"World" }'`,
|
|
194
|
+
"List all Plugins": "xy plugins"
|
|
195
|
+
});
|
|
196
|
+
},
|
|
197
|
+
|
|
198
|
+
async cmd_update_plugin() {
|
|
199
|
+
// Fetch the authoritative definition so indexed parameter/group changes can
|
|
200
|
+
// preserve all untouched entries while the outgoing API request stays sparse.
|
|
201
|
+
var id = this.consumePluginID();
|
|
202
|
+
var plugin = await this.fetchPlugin(id);
|
|
203
|
+
|
|
204
|
+
this.printMutationSummary({
|
|
205
|
+
title: 'Update Plugin',
|
|
206
|
+
rows: [
|
|
207
|
+
[ 'Plugin ID', gray(plugin.id) ],
|
|
208
|
+
[ 'Title', this.color('theme').bold(plugin.title) ]
|
|
209
|
+
]
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
if (!await this.confirmSyncUpdate('plugin', plugin.id, 'Plugin')) return;
|
|
213
|
+
if (!Tools.numKeys(this.args)) return this.die("No updates specified for Plugin.");
|
|
214
|
+
this.printUpdateData(this.args);
|
|
215
|
+
|
|
216
|
+
var params = this.preparePluginParams(plugin, this.args, false);
|
|
217
|
+
params.id = id;
|
|
218
|
+
|
|
219
|
+
var data = await this.callStandardAPI('updatePlugin', params, { text: 'Updating Plugin...' });
|
|
220
|
+
if (this.dry) return;
|
|
221
|
+
if (this.format.match(/json/)) return this.jsonOutput(data);
|
|
222
|
+
|
|
223
|
+
this.toast('✅', 'green', "Successfully updated Plugin: #" + id);
|
|
224
|
+
},
|
|
225
|
+
|
|
226
|
+
async cmd_delete_plugin() {
|
|
227
|
+
var id = this.consumePluginID();
|
|
228
|
+
var plugin = await this.fetchPlugin(id);
|
|
229
|
+
|
|
230
|
+
this.printMutationSummary({
|
|
231
|
+
title: 'Delete Plugin',
|
|
232
|
+
rows: [
|
|
233
|
+
[ 'Plugin ID', gray(plugin.id) ],
|
|
234
|
+
[ 'Title', this.color('theme').bold(plugin.title) ]
|
|
235
|
+
]
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
if (this.args.confirm !== true) {
|
|
239
|
+
this.toast('⚠️', 'orange', "Please confirm the Plugin delete by adding '--confirm'.");
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
delete this.args.confirm;
|
|
244
|
+
if (Tools.numKeys(this.args)) return this.die("Unsupported Plugin delete option: --" + Tools.firstKey(this.args));
|
|
245
|
+
|
|
246
|
+
var data = await this.callStandardAPI('deletePlugin', { id: id }, { text: 'Deleting Plugin...' });
|
|
247
|
+
if (this.dry) return;
|
|
248
|
+
if (this.format.match(/json/)) return this.jsonOutput(data);
|
|
249
|
+
|
|
250
|
+
this.toast('✅', 'green', "Successfully deleted Plugin: #" + id);
|
|
251
|
+
},
|
|
252
|
+
|
|
253
|
+
async findPlugin(selector) {
|
|
254
|
+
var match = Tools.findObject(this.plugins, { id: selector }) || this.findObjectFuzzy(this.plugins, { title: selector });
|
|
255
|
+
if (!match) this.die("Could not find Plugin based on your criteria: " + selector);
|
|
256
|
+
return await this.fetchPlugin(match.id);
|
|
257
|
+
},
|
|
258
|
+
|
|
259
|
+
async fetchPlugin(id) {
|
|
260
|
+
cli.progress.start({ amount: 1, pct: false, text: gray('→ Loading Plugin...') });
|
|
261
|
+
var { err, data } = await this.api.getPlugin({ id: id });
|
|
262
|
+
cli.progress.end();
|
|
263
|
+
if (err) this.die(err);
|
|
264
|
+
return data.plugin;
|
|
265
|
+
},
|
|
266
|
+
|
|
267
|
+
consumePluginID() {
|
|
268
|
+
// Mutations always require the exact internal ID, never a fuzzy title.
|
|
269
|
+
var id = this.args.other.shift() || this.args.id;
|
|
270
|
+
if (!id) this.die("Missing required Plugin ID argument.");
|
|
271
|
+
if (this.args.other.length) this.die("Unexpected argument after Plugin ID: " + this.args.other[0]);
|
|
272
|
+
if (this.args.id && (this.args.id !== id)) this.die("Conflicting Plugin ID arguments.");
|
|
273
|
+
if ((typeof(id) != 'string') || !id.match(/^[a-z0-9_]+$/)) this.die("Invalid Plugin ID: " + id);
|
|
274
|
+
delete this.args.id;
|
|
275
|
+
delete this.args.other;
|
|
276
|
+
return id;
|
|
277
|
+
},
|
|
278
|
+
|
|
279
|
+
preparePluginParams(plugin, input, creating) {
|
|
280
|
+
// Whole arrays replace first, dotted paths edit existing indexes next, and
|
|
281
|
+
// singular --param / --group options append last. This is the same model used
|
|
282
|
+
// by event fields and other CLI resources with editable arrays.
|
|
283
|
+
//
|
|
284
|
+
// --format is reserved globally for CLI output, so expose the Monitor Plugin
|
|
285
|
+
// property as --plugin_format for ordinary command-line use. Complete JSON
|
|
286
|
+
// request objects may still use the native `format` property.
|
|
287
|
+
if ('plugin_format' in input) {
|
|
288
|
+
if ('format' in input) this.die("Use either Plugin format or plugin_format, not both.");
|
|
289
|
+
input.format = input.plugin_format;
|
|
290
|
+
delete input.plugin_format;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
var params = creating ? Tools.copyHash(plugin, true) : {};
|
|
294
|
+
var fields = ['title', 'enabled', 'icon', 'command', 'script', 'params', 'param', 'groups', 'group', 'format', 'uid', 'gid', 'kill', 'runner', 'quick', 'notes'];
|
|
295
|
+
if (creating) fields.push('id', 'type');
|
|
296
|
+
|
|
297
|
+
Object.keys(input).forEach( key => {
|
|
298
|
+
var root = key.split('.')[0];
|
|
299
|
+
if (!fields.includes(root)) {
|
|
300
|
+
if (!creating && (root == 'type')) this.die("Plugin type cannot be changed after creation.");
|
|
301
|
+
this.die("Unsupported Plugin option: --" + key);
|
|
302
|
+
}
|
|
303
|
+
if (key.includes('.') && !['params', 'groups'].includes(root)) this.die("Invalid argument path: " + key);
|
|
304
|
+
|
|
305
|
+
// Clone only the saved arrays needed for dotted edits or appends.
|
|
306
|
+
if (!creating && ['params', 'param'].includes(root) && !('params' in params)) {
|
|
307
|
+
params.params = Tools.copyHash({ value: plugin.params || [] }, true).value;
|
|
308
|
+
}
|
|
309
|
+
if (!creating && ['groups', 'group'].includes(root) && !('groups' in params)) {
|
|
310
|
+
params.groups = Tools.copyHash({ value: plugin.groups || [] }, true).value;
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
// Apply ordinary options before dotted edits, independent of option order.
|
|
315
|
+
Object.keys(input).filter( key => !key.includes('.') ).forEach( key => { params[key] = input[key]; } );
|
|
316
|
+
var dotted = {};
|
|
317
|
+
Object.keys(input).filter( key => key.includes('.') ).forEach( key => { dotted[key] = input[key]; } );
|
|
318
|
+
this.mergeDotArgs(params, dotted);
|
|
319
|
+
|
|
320
|
+
if ('param' in params) {
|
|
321
|
+
if (!('params' in params)) params.params = [];
|
|
322
|
+
if (!Array.isArray(params.params)) this.die("Plugin params must be a JSON array.");
|
|
323
|
+
this.parsePluginObjects(params.param, 'param').forEach( item => { params.params.push(item); } );
|
|
324
|
+
delete params.param;
|
|
325
|
+
}
|
|
326
|
+
if ('params' in params) {
|
|
327
|
+
if (!Array.isArray(params.params)) this.die("Plugin params must be a JSON array.");
|
|
328
|
+
params.params.forEach( (param, idx) => { this.validatePluginParam(param, idx); } );
|
|
329
|
+
this.validatePluginParamIDs(params.params);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if ('group' in params) {
|
|
333
|
+
if (!('groups' in params)) params.groups = [];
|
|
334
|
+
params.groups = this.parsePluginGroups(params.groups).concat(this.parsePluginGroups(params.group));
|
|
335
|
+
params.groups = Array.from(new Set(params.groups));
|
|
336
|
+
delete params.group;
|
|
337
|
+
}
|
|
338
|
+
if ('groups' in params) params.groups = Array.from(new Set(this.parsePluginGroups(params.groups)));
|
|
339
|
+
|
|
340
|
+
if ('title' in params) {
|
|
341
|
+
if ((typeof(params.title) != 'string') || !params.title.trim()) this.die("Plugin title cannot be empty and must be a string.");
|
|
342
|
+
params.title = params.title.trim();
|
|
343
|
+
}
|
|
344
|
+
if ('command' in params) {
|
|
345
|
+
if ((typeof(params.command) != 'string') || !params.command.trim()) this.die("Plugin command cannot be empty and must be a string.");
|
|
346
|
+
params.command = params.command.trim();
|
|
347
|
+
}
|
|
348
|
+
if ('enabled' in params) params.enabled = this.parsePluginBoolean(params.enabled, 'enabled');
|
|
349
|
+
['icon', 'script', 'notes'].forEach( key => {
|
|
350
|
+
if ((key in params) && (typeof(params[key]) != 'string')) this.die("Plugin " + key + " must be a string.");
|
|
351
|
+
});
|
|
352
|
+
['uid', 'gid'].forEach( key => {
|
|
353
|
+
if ((key in params) && !['string', 'number'].includes(typeof(params[key]))) this.die("Plugin " + key + " must be a string or number.");
|
|
354
|
+
});
|
|
355
|
+
if (('id' in params) && ((typeof(params.id) != 'string') || !params.id.match(/^[a-z0-9_]+$/))) this.die("Invalid Plugin ID: " + params.id);
|
|
356
|
+
|
|
357
|
+
var type = creating ? this.parsePluginType(params.type) : plugin.type;
|
|
358
|
+
if (creating) params.type = type;
|
|
359
|
+
|
|
360
|
+
// Keep the wire object aligned with the web editor and reject settings that
|
|
361
|
+
// have no meaning for the selected Plugin type.
|
|
362
|
+
if (type == 'monitor') {
|
|
363
|
+
if (('params' in input) && input.params.length) this.die("Monitor Plugins do not support parameter definitions.");
|
|
364
|
+
if ('param' in input) this.die("Monitor Plugins do not support parameter definitions.");
|
|
365
|
+
if (('kill' in input) || ('runner' in input)) this.die("Monitor Plugins do not support Event Plugin process options.");
|
|
366
|
+
|
|
367
|
+
if (creating) params.params = [];
|
|
368
|
+
if (!('groups' in params) && creating) params.groups = [];
|
|
369
|
+
if (creating && !params.format) params.format = 'text';
|
|
370
|
+
if (!('quick' in params) && creating) params.quick = false;
|
|
371
|
+
if (('format' in params) && !['text', 'json', 'xml'].includes(params.format)) this.die("Monitor Plugin format must be text, json, or xml.");
|
|
372
|
+
if ('quick' in params) params.quick = this.parsePluginBoolean(params.quick, 'quick');
|
|
373
|
+
delete params.kill;
|
|
374
|
+
delete params.runner;
|
|
375
|
+
}
|
|
376
|
+
else {
|
|
377
|
+
if (('groups' in input) || ('group' in input) || ('format' in input) || ('quick' in input)) {
|
|
378
|
+
this.die(Tools.ucfirst(type) + " Plugins do not support Monitor Plugin options.");
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (creating) {
|
|
382
|
+
params.groups = [];
|
|
383
|
+
params.format = '';
|
|
384
|
+
}
|
|
385
|
+
delete params.quick;
|
|
386
|
+
|
|
387
|
+
if (type == 'event') {
|
|
388
|
+
if (('kill' in params) && !['none', 'parent', 'all'].includes(params.kill)) this.die("Event Plugin kill must be none, parent, or all.");
|
|
389
|
+
if ('runner' in params) params.runner = this.parsePluginBoolean(params.runner, 'runner');
|
|
390
|
+
}
|
|
391
|
+
else {
|
|
392
|
+
if (('kill' in input) || ('runner' in input)) this.die(Tools.ucfirst(type) + " Plugins do not support Event Plugin process options.");
|
|
393
|
+
delete params.kill;
|
|
394
|
+
delete params.runner;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
return params;
|
|
399
|
+
},
|
|
400
|
+
|
|
401
|
+
parsePluginObjects(value, name) {
|
|
402
|
+
// Repeated command-line options arrive as an array of JSON strings. A single
|
|
403
|
+
// parsed object and repeated objects both become a clean object array here.
|
|
404
|
+
return Tools.alwaysArray(value).map( item => {
|
|
405
|
+
if (typeof(item) == 'string') {
|
|
406
|
+
try { item = JSON.parse(item); }
|
|
407
|
+
catch (err) { this.die("Plugin --" + name + " must contain a JSON object."); }
|
|
408
|
+
}
|
|
409
|
+
if (!Tools.isaHash(item)) this.die("Plugin --" + name + " must contain a JSON object.");
|
|
410
|
+
return item;
|
|
411
|
+
});
|
|
412
|
+
},
|
|
413
|
+
|
|
414
|
+
validatePluginParam(param, idx) {
|
|
415
|
+
// Perform the common schema checks locally for good dry-run feedback. The
|
|
416
|
+
// xyOps API remains authoritative for specialized toolset configuration.
|
|
417
|
+
var prefix = 'Plugin params.' + idx;
|
|
418
|
+
if (!Tools.isaHash(param)) this.die(prefix + " must be a JSON object.");
|
|
419
|
+
if ((typeof(param.id) != 'string') || !param.id.match(/^[A-Za-z_][\w.\-]*$/) || param.id.match(Tools.MATCH_BAD_KEY)) this.die(prefix + ".id is invalid.");
|
|
420
|
+
if ((typeof(param.title) != 'string') || !param.title.trim() || param.title.match(/[<>]/)) this.die(prefix + ".title is invalid.");
|
|
421
|
+
if ((typeof(param.type) != 'string') || !this.config.ui.control_type_labels[param.type]) this.die(prefix + ".type is invalid.");
|
|
422
|
+
if (('caption' in param) && (typeof(param.caption) != 'string')) this.die(prefix + ".caption must be a string.");
|
|
423
|
+
if (param.caption && param.caption.match(/[<>]/)) this.die(prefix + ".caption contains illegal characters.");
|
|
424
|
+
|
|
425
|
+
['required', 'locked'].forEach( key => {
|
|
426
|
+
if (key in param) param[key] = this.parsePluginBoolean(param[key], prefix + '.' + key);
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
if ((param.type == 'text') && param.variant) {
|
|
430
|
+
var variants = (this.config.ui.text_field_variants || []).map( item => item.id );
|
|
431
|
+
if (!variants.includes(param.variant)) this.die(prefix + ".variant is invalid.");
|
|
432
|
+
}
|
|
433
|
+
if (param.regex) {
|
|
434
|
+
try { new RegExp(param.regex); }
|
|
435
|
+
catch (err) { this.die(prefix + ".regex is invalid: " + err.message); }
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
if (param.type.match(/^(checkbox|code|hidden|json|select|text|textarea)$/) && !('value' in param)) this.die(prefix + ".value is required for type " + param.type + '.');
|
|
439
|
+
if ((param.type == 'checkbox') && ('value' in param)) param.value = this.parsePluginBoolean(param.value, prefix + '.value');
|
|
440
|
+
if ((param.type == 'json') && (typeof(param.value) != 'object')) this.die(prefix + ".value must be a JSON object.");
|
|
441
|
+
if ((param.type == 'text') && (param.variant == 'number') && (param.value !== null) && !Number.isFinite(param.value)) this.die(prefix + ".value must be a number or null.");
|
|
442
|
+
if (param.type.match(/^(code|hidden|select|textarea)$/) && (typeof(param.value) != 'string')) this.die(prefix + ".value must be a string.");
|
|
443
|
+
if ((param.type == 'text') && (param.variant != 'number') && (typeof(param.value) != 'string')) this.die(prefix + ".value must be a string.");
|
|
444
|
+
},
|
|
445
|
+
|
|
446
|
+
validatePluginParamIDs(params) {
|
|
447
|
+
var ids = {};
|
|
448
|
+
params.forEach( param => {
|
|
449
|
+
if (ids[param.id]) this.die("Duplicate Plugin parameter ID: " + param.id);
|
|
450
|
+
ids[param.id] = true;
|
|
451
|
+
});
|
|
452
|
+
},
|
|
453
|
+
|
|
454
|
+
parsePluginGroups(value) {
|
|
455
|
+
var groups = Array.isArray(value) ? value.slice(0) : ((typeof(value) == 'string') ? value.split(',') : null);
|
|
456
|
+
if (!groups) this.die("Plugin groups must be a JSON array or comma-separated group IDs.");
|
|
457
|
+
return groups.map( id => {
|
|
458
|
+
if ((typeof(id) != 'string') || !id.trim().match(/^[a-z0-9_]+$/)) this.die("Invalid server group ID: " + id);
|
|
459
|
+
return id.trim();
|
|
460
|
+
});
|
|
461
|
+
},
|
|
462
|
+
|
|
463
|
+
parsePluginType(value) {
|
|
464
|
+
if ((typeof(value) != 'string') || !['event', 'monitor', 'action', 'scheduler'].includes(value)) {
|
|
465
|
+
this.die("Plugin type must be event, monitor, action, or scheduler.");
|
|
466
|
+
}
|
|
467
|
+
return value;
|
|
468
|
+
},
|
|
469
|
+
|
|
470
|
+
parsePluginBoolean(value, key) {
|
|
471
|
+
if ([true, 1, '1', 'true', 'yes', 'on'].includes(value)) return true;
|
|
472
|
+
if ([false, 0, '0', 'false', 'no', 'off'].includes(value)) return false;
|
|
473
|
+
this.die("Invalid boolean value for '" + key + "': " + value);
|
|
474
|
+
},
|
|
475
|
+
|
|
476
|
+
getNicePluginType(type) {
|
|
477
|
+
var labels = {
|
|
478
|
+
event: 'Event',
|
|
479
|
+
monitor: 'Monitor',
|
|
480
|
+
action: 'Action',
|
|
481
|
+
scheduler: 'Scheduler'
|
|
482
|
+
};
|
|
483
|
+
return labels[type] || Tools.ucfirst(type || 'Unknown');
|
|
484
|
+
},
|
|
485
|
+
|
|
486
|
+
getNicePluginGroups(groups) {
|
|
487
|
+
if (!groups || !groups.length) return gray('All Groups');
|
|
488
|
+
return groups.map( id => this.getNiceGroup(id) + gray(' (' + id + ')') ).join(', ');
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
}; // module.exports
|