@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/utils.js
ADDED
|
@@ -0,0 +1,2745 @@
|
|
|
1
|
+
// Utils
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const cp = require('child_process');
|
|
6
|
+
const Path = require('path');
|
|
7
|
+
const { PassThrough } = require('stream');
|
|
8
|
+
|
|
9
|
+
const { convert: convertHTMLToText } = require('html-to-text');
|
|
10
|
+
const { listLanguages: listHighlightLanguages } = require('cli-highlight');
|
|
11
|
+
const { marked } = require('marked');
|
|
12
|
+
const { markedTerminal } = require('marked-terminal');
|
|
13
|
+
|
|
14
|
+
const HIGHLIGHT_LANGUAGES = listHighlightLanguages();
|
|
15
|
+
const MARKDOWN_PADDING = 1;
|
|
16
|
+
|
|
17
|
+
const terminalRenderer = markedTerminal({
|
|
18
|
+
emoji: false,
|
|
19
|
+
showSectionPrefix: false,
|
|
20
|
+
reflowText: true,
|
|
21
|
+
tab: 2,
|
|
22
|
+
// Reserve one column on each side so rendered Markdown lines up with the
|
|
23
|
+
// rest of the CLI without allowing reflowed text to touch the right edge.
|
|
24
|
+
width: Math.max(20, Math.min(process.stdout.columns || 80, 120) - (MARKDOWN_PADDING * 2))
|
|
25
|
+
}, {
|
|
26
|
+
ignoreIllegals: true,
|
|
27
|
+
languageSubset: ['json', 'javascript', 'bash', 'shell']
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// marked-terminal hardcodes an asterisk as its unordered-list marker. Wait
|
|
31
|
+
// until the outermost list is complete before swapping those markers, because
|
|
32
|
+
// its parent renderer still needs the original asterisks to identify and lay
|
|
33
|
+
// out nested lists correctly.
|
|
34
|
+
const renderTerminalList = terminalRenderer.renderer.list;
|
|
35
|
+
let terminalListDepth = 0;
|
|
36
|
+
terminalRenderer.renderer.list = function(...args) {
|
|
37
|
+
terminalListDepth++;
|
|
38
|
+
try {
|
|
39
|
+
var output = renderTerminalList.apply(this, args);
|
|
40
|
+
if (terminalListDepth == 1) output = output.replace(/^([ \t]*)\* /gm, '$1• ');
|
|
41
|
+
return output;
|
|
42
|
+
}
|
|
43
|
+
finally {
|
|
44
|
+
terminalListDepth--;
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
marked.use(terminalRenderer);
|
|
49
|
+
|
|
50
|
+
// render explicitly labeled plain-text blocks in gray
|
|
51
|
+
marked.use({
|
|
52
|
+
renderer: {
|
|
53
|
+
text(token) {
|
|
54
|
+
// Marked stores inline formatting inside child tokens for list-item text.
|
|
55
|
+
// marked-terminal 7.3.0 returns only token.text here, which exposes the
|
|
56
|
+
// original **strong** and `code` markup instead of rendering it. Parse
|
|
57
|
+
// those children ourselves, then fall through for ordinary text tokens.
|
|
58
|
+
if (token && Array.isArray(token.tokens)) {
|
|
59
|
+
return this.parser.parseInline(token.tokens);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return false;
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
code(token) {
|
|
66
|
+
if (token.lang === 'sh') {
|
|
67
|
+
var disp_value = String(token.text).trim().split(/\n/).map( line => ' ' + line ).join("\n");
|
|
68
|
+
return chalk.cyan(disp_value) + '\n\n';
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// use marked-terminal for all other code blocks
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const cli = require('pixl-cli');
|
|
78
|
+
const Tools = cli.Tools;
|
|
79
|
+
const chalk = cli.chalk;
|
|
80
|
+
|
|
81
|
+
const DEFAULT_CACHE_TTL = 300;
|
|
82
|
+
const DATE_FMT = '[yyyy]/[mm]/[dd] [hour12]:[mi] [AMPM]';
|
|
83
|
+
const DATE_FMT_SEC = '[yyyy]/[mm]/[dd] [hour12]:[mi]:[ss] [AMPM]';
|
|
84
|
+
const TIME_FMT = '[hour12]:[mi] [AMPM]';
|
|
85
|
+
const TIME_FMT_SEC = '[hour12]:[mi]:[ss] [AMPM]';
|
|
86
|
+
|
|
87
|
+
// CRUD APIs deliberately accept arbitrary top-level properties, which is useful
|
|
88
|
+
// for direct API integrations but dangerous for hand-typed CLI commands. These
|
|
89
|
+
// rules describe the final, normalized request objects that this CLI supports.
|
|
90
|
+
// Rule names match the real snake_case API endpoints and are regular expressions
|
|
91
|
+
// so create and update operations can share one property list.
|
|
92
|
+
const STANDARD_API_PROPERTY_RULES = {
|
|
93
|
+
'^(create_alert|update_alert)$': [
|
|
94
|
+
'id', 'title', 'enabled', 'icon', 'expression', 'message', 'groups',
|
|
95
|
+
'actions', 'monitor_id', 'samples', 'exclusive_actions', 'limit_jobs',
|
|
96
|
+
'abort_jobs', 'notes', 'username', 'modified', 'created', 'revision'
|
|
97
|
+
],
|
|
98
|
+
'^(create_api_key|update_api_key)$': [
|
|
99
|
+
'id', 'key', 'mask', 'active', 'privileges', 'roles', 'title', 'icon',
|
|
100
|
+
'description', 'expires', 'max_per_sec', 'username', 'modified',
|
|
101
|
+
'created', 'revision'
|
|
102
|
+
],
|
|
103
|
+
'^(create_bucket|update_bucket)$': [
|
|
104
|
+
'id', 'title', 'enabled', 'icon', 'notes', 'username', 'modified',
|
|
105
|
+
'created', 'revision', 'data'
|
|
106
|
+
],
|
|
107
|
+
'^(create_category|update_category)$': [
|
|
108
|
+
'id', 'title', 'enabled', 'icon', 'color', 'notes', 'actions', 'limits',
|
|
109
|
+
'sort_order', 'username', 'modified', 'created', 'revision'
|
|
110
|
+
],
|
|
111
|
+
'^(create_channel|update_channel)$': [
|
|
112
|
+
'id', 'title', 'enabled', 'icon', 'users', 'email', 'web_hook',
|
|
113
|
+
'run_event', 'sound', 'max_per_day', 'notes', 'username', 'modified',
|
|
114
|
+
'created', 'revision'
|
|
115
|
+
],
|
|
116
|
+
'^(create_event|update_event)$': [
|
|
117
|
+
'id', 'title', 'enabled', 'icon', 'type', 'category', 'plugin', 'params',
|
|
118
|
+
'fields', 'tags', 'targets', 'expression', 'algo', 'notes', 'actions',
|
|
119
|
+
'limits', 'triggers', 'workflow', 'username', 'modified', 'created',
|
|
120
|
+
'revision', 'update_state'
|
|
121
|
+
],
|
|
122
|
+
'^(create_group|update_group)$': [
|
|
123
|
+
'id', 'title', 'hostname_match', 'icon', 'alert_actions', 'notes',
|
|
124
|
+
'max_jobs_per_server'
|
|
125
|
+
],
|
|
126
|
+
'^(create_monitor|update_monitor)$': [
|
|
127
|
+
'id', 'title', 'display', 'icon', 'groups', 'source', 'data_match',
|
|
128
|
+
'data_type', 'min_vert_scale', 'suffix', 'delta', 'divide_by_delta',
|
|
129
|
+
'delta_min_value', 'notes', 'sort_order', 'username', 'modified',
|
|
130
|
+
'created', 'revision'
|
|
131
|
+
],
|
|
132
|
+
'^(create_plugin|update_plugin)$': [
|
|
133
|
+
'id', 'title', 'enabled', 'icon', 'type', 'command', 'script', 'params',
|
|
134
|
+
'groups', 'format', 'uid', 'gid', 'kill', 'runner', 'quick', 'stock',
|
|
135
|
+
'notes', 'marketplace', 'username', 'modified', 'created', 'revision'
|
|
136
|
+
],
|
|
137
|
+
'^(create_secret|update_secret)$': [
|
|
138
|
+
'id', 'title', 'enabled', 'icon', 'notes', 'events', 'categories',
|
|
139
|
+
'plugins', 'web_hooks', 'fields'
|
|
140
|
+
],
|
|
141
|
+
'^update_server$': [
|
|
142
|
+
'id', 'title', 'enabled', 'icon', 'groups', 'autoGroup', 'maxJobs', 'userData'
|
|
143
|
+
],
|
|
144
|
+
'^(create_tag|update_tag)$': [
|
|
145
|
+
'id', 'title', 'icon', 'notes', 'username', 'modified', 'created', 'revision'
|
|
146
|
+
],
|
|
147
|
+
'^(create_ticket|update_ticket)$': [
|
|
148
|
+
'id', 'num', 'subject', 'body', 'type', 'status', 'category', 'server',
|
|
149
|
+
'assignees', 'cc', 'notify', 'due', 'tags', 'events', 'files', 'changes',
|
|
150
|
+
'username', 'modified', 'created', 'template', 'job', 'alert'
|
|
151
|
+
],
|
|
152
|
+
'^(create_web_hook|update_web_hook)$': [
|
|
153
|
+
'id', 'title', 'enabled', 'icon', 'url', 'method', 'headers', 'body',
|
|
154
|
+
'timeout', 'retries', 'follow', 'ssl_cert_bypass', 'max_per_day', 'notes',
|
|
155
|
+
'username', 'modified', 'created', 'revision'
|
|
156
|
+
]
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
module.exports = {
|
|
160
|
+
|
|
161
|
+
async getMultiple(req) {
|
|
162
|
+
// only call once per run
|
|
163
|
+
if (this.gotMultiple) return;
|
|
164
|
+
|
|
165
|
+
if (!req) req = { lists: 'all', jobs: 1, alerts: 1, state: 1, stats: 1, servers: 1, serverCache: 1 };
|
|
166
|
+
var { err, data } = await this.api.getMultiple(req);
|
|
167
|
+
if (err) this.die(err);
|
|
168
|
+
|
|
169
|
+
["api_keys", "groups", "plugins", "categories", "events", "channels", "web_hooks", "buckets", "secrets", "monitors", "alerts", "tags", "roles"].forEach( key => {
|
|
170
|
+
this[key] = data[key] || [];
|
|
171
|
+
} );
|
|
172
|
+
|
|
173
|
+
if (!this.invisible) {
|
|
174
|
+
for (var job_id in data.activeJobs) {
|
|
175
|
+
var job = data.activeJobs[job_id];
|
|
176
|
+
if (job.invisible) delete data.activeJobs[job_id];
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
this.activeJobs = data.activeJobs || {};
|
|
181
|
+
this.internalJobs = data.internalJobs || {};
|
|
182
|
+
this.jobRateLimits = data.jobRateLimits || {};
|
|
183
|
+
this.activeAlerts = data.activeAlerts || {};
|
|
184
|
+
this.state = data.state || {};
|
|
185
|
+
this.stats = data.stats || {};
|
|
186
|
+
this.servers = data.servers || {};
|
|
187
|
+
this.serverCache = data.serverCache || {};
|
|
188
|
+
this.epoch = data.epoch || Tools.timeNow();
|
|
189
|
+
this.gotMultiple = true;
|
|
190
|
+
},
|
|
191
|
+
|
|
192
|
+
async confirmSyncUpdate(type, id, thing) {
|
|
193
|
+
// Up-only filesystem syncs are authoritative for their managed objects.
|
|
194
|
+
// The composite type-ID key matches the state written by the sync engine.
|
|
195
|
+
await this.getMultiple();
|
|
196
|
+
|
|
197
|
+
if (('confirm' in this.args) && (typeof(this.args.confirm) != 'boolean')) {
|
|
198
|
+
return this.die("Update --confirm must be true or false.");
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
var confirmed = (this.args.confirm === true);
|
|
202
|
+
delete this.args.confirm;
|
|
203
|
+
|
|
204
|
+
var state_key = type + '-' + id;
|
|
205
|
+
if (!this.state || !this.state.sync || !this.state.sync[state_key]) return true;
|
|
206
|
+
if (confirmed) return true;
|
|
207
|
+
|
|
208
|
+
this.toast('⚠️', 'yellow', "This " + thing + " is under remote management, and should not be edited here. Add '--confirm' to continue.");
|
|
209
|
+
return false;
|
|
210
|
+
},
|
|
211
|
+
|
|
212
|
+
color(name) {
|
|
213
|
+
return this.colors[name] ? chalk.rgb(...this.colors[name]) : chalk.gray;
|
|
214
|
+
},
|
|
215
|
+
|
|
216
|
+
getNiceEvent(event) {
|
|
217
|
+
if (!event) return gray('(None)');
|
|
218
|
+
if (typeof(event) == 'string') {
|
|
219
|
+
event = Tools.findObject( this.events, { id: event } ) || { id: event, title: event };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (!event.enabled) return this.raw ? gray(event.id) : gray(event.title);
|
|
223
|
+
|
|
224
|
+
var category = Tools.findObject( this.categories, { id: event.category } );
|
|
225
|
+
if (category && category.color && (category.color != 'plain')) {
|
|
226
|
+
return this.raw ? gray(event.id) : this.color(category.color).bold(event.title);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return this.raw ? gray(event.id) : event.title;
|
|
230
|
+
},
|
|
231
|
+
|
|
232
|
+
getNiceCategory(category) {
|
|
233
|
+
if (!category) return gray('(None)');
|
|
234
|
+
if (typeof(category) == 'string') {
|
|
235
|
+
category = Tools.findObject( this.categories, { id: category } ) || { id: category, title: category };
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (this.raw) return gray(category.id);
|
|
239
|
+
else if (category.color && (category.color != 'plain')) return this.color(category.color).bold(category.title);
|
|
240
|
+
else return bold(category.title);
|
|
241
|
+
},
|
|
242
|
+
|
|
243
|
+
getNicePlugin(plugin) {
|
|
244
|
+
if (!plugin) return gray('(None)');
|
|
245
|
+
if (plugin === '_workflow') return '(Workflow)';
|
|
246
|
+
if (typeof(plugin) == 'string') {
|
|
247
|
+
plugin = Tools.findObject( this.plugins, { id: plugin } ) || { id: plugin, title: plugin };
|
|
248
|
+
}
|
|
249
|
+
return this.raw ? gray(plugin.id) : plugin.title;
|
|
250
|
+
},
|
|
251
|
+
|
|
252
|
+
getLangFromBinary(bin) {
|
|
253
|
+
// Guess a cli-highlight language from an executable path and arguments.
|
|
254
|
+
// This mirrors the xyOps UI helper, while avoiding slow source auto-detection
|
|
255
|
+
// for common interpreters.
|
|
256
|
+
if ((typeof(bin) != 'string') || !bin.trim()) return null;
|
|
257
|
+
var cmd = Path.basename( bin.trim() ).replace(/\s+.+$/, '').replace(/\d+$/, '').toLowerCase();
|
|
258
|
+
|
|
259
|
+
switch (cmd) {
|
|
260
|
+
case 'sh':
|
|
261
|
+
case 'csh':
|
|
262
|
+
case 'ksh':
|
|
263
|
+
case 'tcsh':
|
|
264
|
+
case 'fish':
|
|
265
|
+
case 'zsh':
|
|
266
|
+
case 'bash':
|
|
267
|
+
cmd = 'shell';
|
|
268
|
+
break;
|
|
269
|
+
|
|
270
|
+
case 'node':
|
|
271
|
+
case 'deno':
|
|
272
|
+
case 'bun':
|
|
273
|
+
cmd = 'javascript';
|
|
274
|
+
break;
|
|
275
|
+
|
|
276
|
+
case 'pwsh':
|
|
277
|
+
cmd = 'powershell';
|
|
278
|
+
break;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return HIGHLIGHT_LANGUAGES.includes(cmd) ? cmd : null;
|
|
282
|
+
},
|
|
283
|
+
|
|
284
|
+
getNiceGroup(group) {
|
|
285
|
+
if (!group) return gray('(None)');
|
|
286
|
+
if (typeof(group) == 'string') {
|
|
287
|
+
group = Tools.findObject( this.groups, { id: group } ) || { id: group, title: group };
|
|
288
|
+
}
|
|
289
|
+
return this.raw ? gray(group.id) : group.title;
|
|
290
|
+
},
|
|
291
|
+
|
|
292
|
+
getNiceServer(server) {
|
|
293
|
+
if (!server) return gray('(None)');
|
|
294
|
+
if (typeof(server) == 'string') {
|
|
295
|
+
server = this.servers[server] || this.serverCache[server] || { id: server, hostname: server };
|
|
296
|
+
}
|
|
297
|
+
return this.raw ? gray(server.id) : (server.title || server.hostname);
|
|
298
|
+
},
|
|
299
|
+
|
|
300
|
+
getNiceTarget(target) {
|
|
301
|
+
// a target can be a group ID, or a server ID
|
|
302
|
+
if (!target) return gray('(None)');
|
|
303
|
+
if (Tools.findObject(this.groups, { id: target })) return this.getNiceGroup(target);
|
|
304
|
+
else return this.getNiceServer(target);
|
|
305
|
+
},
|
|
306
|
+
|
|
307
|
+
getNiceTargets(targets) {
|
|
308
|
+
// CSV list of targets
|
|
309
|
+
if (!targets || !targets.length) return gray('(None)');
|
|
310
|
+
return targets.map( this.getNiceTarget.bind(this) ).join(', ');
|
|
311
|
+
},
|
|
312
|
+
|
|
313
|
+
getNiceTag(tag) {
|
|
314
|
+
if (!tag) return gray('(None)');
|
|
315
|
+
if (typeof(tag) == 'string') {
|
|
316
|
+
tag = Tools.findObject( this.tags, { id: tag } ) || { id: tag, title: tag };
|
|
317
|
+
}
|
|
318
|
+
return this.raw ? gray(tag.id) : tag.title;
|
|
319
|
+
},
|
|
320
|
+
|
|
321
|
+
getNiceTagList(list) {
|
|
322
|
+
if (!list || !list.length) return gray('(None)');
|
|
323
|
+
list = list.filter( function(tag) { return !tag.match(/^_/); } ); // filter out system tags
|
|
324
|
+
if (!list.length) return '(None)';
|
|
325
|
+
return list.map( tag => this.getNiceTag(tag) ).join(', ');
|
|
326
|
+
},
|
|
327
|
+
|
|
328
|
+
getNiceAlgo(algo) {
|
|
329
|
+
if (!algo) return gray('(None)');
|
|
330
|
+
var def = Tools.findObject( this.config.ui.event_target_algo_menu, { id: algo } );
|
|
331
|
+
return def ? def.title : algo;
|
|
332
|
+
},
|
|
333
|
+
|
|
334
|
+
getNiceAlert(alert) {
|
|
335
|
+
if (!alert) return gray('(None)');
|
|
336
|
+
if (typeof(alert) == 'string') {
|
|
337
|
+
alert = Tools.findObject( this.alerts, { id: alert } ) || { id: alert, title: alert };
|
|
338
|
+
}
|
|
339
|
+
return this.raw ? gray(alert.id) : alert.title;
|
|
340
|
+
},
|
|
341
|
+
|
|
342
|
+
getNiceAlertStatus(alert) {
|
|
343
|
+
return alert.active ? `${cli.emoji('🚨')} ${red('Active')}` : `${gray('Cleared')}`;
|
|
344
|
+
},
|
|
345
|
+
|
|
346
|
+
getNiceAPIKeyStatus(api_key) {
|
|
347
|
+
// API Keys may be enabled but expired, so represent that as a distinct state.
|
|
348
|
+
if (!api_key.active) return red('Disabled');
|
|
349
|
+
if (api_key.expires && (this.epoch >= api_key.expires)) return yellow('Expired');
|
|
350
|
+
return green('Active');
|
|
351
|
+
},
|
|
352
|
+
|
|
353
|
+
getNicePrivileges(privileges) {
|
|
354
|
+
// Convert the privilege hash into the same friendly labels used by the UI.
|
|
355
|
+
var priv_list = (this.config.ui && this.config.ui.privilege_list) || [];
|
|
356
|
+
var ids = Object.keys(privileges || {}).filter( id => !!privileges[id] );
|
|
357
|
+
if (!ids.length) return gray('(None)');
|
|
358
|
+
|
|
359
|
+
return ids.map( function(id) {
|
|
360
|
+
var def = Tools.findObject(priv_list, { id: id });
|
|
361
|
+
return def ? def.title : id;
|
|
362
|
+
} ).join(', ');
|
|
363
|
+
},
|
|
364
|
+
|
|
365
|
+
getNiceRoles(roles) {
|
|
366
|
+
// Role IDs are safe to display, even if a referenced role was later deleted.
|
|
367
|
+
if (!roles || !roles.length) return gray('(None)');
|
|
368
|
+
return roles.map( id => {
|
|
369
|
+
var role = Tools.findObject(this.roles || [], { id: id });
|
|
370
|
+
return role ? role.title + gray(' (' + id + ')') : id;
|
|
371
|
+
} ).join(', ');
|
|
372
|
+
},
|
|
373
|
+
|
|
374
|
+
sanitizeAPIKey(api_key) {
|
|
375
|
+
// Never expose the stored salted hash through the friendly key commands.
|
|
376
|
+
// plain_key is only added explicitly by the create command at display time.
|
|
377
|
+
var clean = Tools.copyHash(api_key || {}, true);
|
|
378
|
+
delete clean.key;
|
|
379
|
+
delete clean.plain_key;
|
|
380
|
+
return clean;
|
|
381
|
+
},
|
|
382
|
+
|
|
383
|
+
getNiceDateTime(epoch, secs, ago) {
|
|
384
|
+
var str = Tools.formatDate(epoch, secs ? DATE_FMT_SEC : DATE_FMT);
|
|
385
|
+
if (ago) str += gray(` (${this.getTextFromSecondsShort(Math.max(0, this.epoch - epoch))} ago)`);
|
|
386
|
+
return str;
|
|
387
|
+
},
|
|
388
|
+
|
|
389
|
+
getNiceTime(epoch, secs, ago) {
|
|
390
|
+
var str = Tools.formatDate(epoch, secs ? TIME_FMT_SEC : TIME_FMT);
|
|
391
|
+
if (ago) str += gray(` (${this.getTextFromSecondsShort(Math.max(0, this.epoch - epoch))} ago)`);
|
|
392
|
+
return str;
|
|
393
|
+
},
|
|
394
|
+
|
|
395
|
+
getRelativeDateTime(epoch, secs) {
|
|
396
|
+
var dargs = Tools.getDateArgs(epoch);
|
|
397
|
+
var nargs = Tools.getDateArgs( this.epoch );
|
|
398
|
+
|
|
399
|
+
if (nargs.yyyy_mm_dd == dargs.yyyy_mm_dd) {
|
|
400
|
+
// today
|
|
401
|
+
return 'Today at ' + this.getNiceTime(epoch, secs);
|
|
402
|
+
}
|
|
403
|
+
else {
|
|
404
|
+
// some other day
|
|
405
|
+
return this.getNiceDateTime(epoch, secs);
|
|
406
|
+
}
|
|
407
|
+
},
|
|
408
|
+
|
|
409
|
+
getDateOptions(opts = {}) {
|
|
410
|
+
// get combined date/time options with user locale settings
|
|
411
|
+
var user = this.config;
|
|
412
|
+
var ropts = Intl.DateTimeFormat().resolvedOptions();
|
|
413
|
+
var [lang, reg] = ropts.locale.split(/\-/);
|
|
414
|
+
if (!reg) reg = lang.toUpperCase();
|
|
415
|
+
|
|
416
|
+
lang = user.language || lang;
|
|
417
|
+
reg = user.region || reg;
|
|
418
|
+
|
|
419
|
+
if (opts.locale === false) delete opts.locale;
|
|
420
|
+
else if (!opts.locale) opts.locale = lang + '-' + reg;
|
|
421
|
+
|
|
422
|
+
if (opts.timeZone === false) delete opts.timeZone;
|
|
423
|
+
else if (!opts.timeZone) opts.timeZone = user.timezone || ropts.timeZone;
|
|
424
|
+
|
|
425
|
+
if (opts.numberingSystem === false) delete opts.numberingSystem;
|
|
426
|
+
else if (!opts.numberingSystem) opts.numberingSystem = user.num_format || ropts.numberingSystem;
|
|
427
|
+
|
|
428
|
+
if (opts.hourCycle === false) delete opts.hourCycle;
|
|
429
|
+
else if (!opts.hourCycle) opts.hourCycle = user.hour_cycle || 'h12';
|
|
430
|
+
|
|
431
|
+
if (!opts.second) delete opts.second;
|
|
432
|
+
else {
|
|
433
|
+
// has seconds -- does user want milliseconds too?
|
|
434
|
+
if (user.milliseconds) opts.fractionalSecondDigits = 3;
|
|
435
|
+
else delete opts.fractionalSecondDigits;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
return opts;
|
|
439
|
+
},
|
|
440
|
+
|
|
441
|
+
getNiceEnabled(enabled) {
|
|
442
|
+
return enabled ? `${cli.emoji('✅')} ${green('Enabled')}` : `${cli.emoji('🚫')} ${gray('Disabled')}`;
|
|
443
|
+
},
|
|
444
|
+
|
|
445
|
+
getNiceWebHook(hook) {
|
|
446
|
+
if (!hook) return gray('(None)');
|
|
447
|
+
if (typeof(hook) == 'string') {
|
|
448
|
+
hook = Tools.findObject( this.web_hooks, { id: hook } ) || { id: hook, title: hook };
|
|
449
|
+
}
|
|
450
|
+
return this.raw ? gray(hook.id) : hook.title;
|
|
451
|
+
},
|
|
452
|
+
|
|
453
|
+
getNiceChannel(channel) {
|
|
454
|
+
if (!channel) return gray('(None)');
|
|
455
|
+
if (typeof(channel) == 'string') {
|
|
456
|
+
channel = Tools.findObject( this.channels, { id: channel } ) || { id: channel, title: channel };
|
|
457
|
+
}
|
|
458
|
+
return this.raw ? gray(channel.id) : channel.title;
|
|
459
|
+
},
|
|
460
|
+
|
|
461
|
+
getNiceBucket(bucket) {
|
|
462
|
+
if (!bucket) return gray('(None)');
|
|
463
|
+
if (typeof(bucket) == 'string') {
|
|
464
|
+
bucket = Tools.findObject( this.buckets, { id: bucket } ) || { id: bucket, title: bucket };
|
|
465
|
+
}
|
|
466
|
+
return this.raw ? gray(bucket.id) : bucket.title;
|
|
467
|
+
},
|
|
468
|
+
|
|
469
|
+
getNiceJob(job) {
|
|
470
|
+
// get formatted job id
|
|
471
|
+
if (!job) return '(None)';
|
|
472
|
+
if (typeof(job) == 'string') {
|
|
473
|
+
if (this.activeJobs[job]) job = this.activeJobs[job];
|
|
474
|
+
else job = { id: job };
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
var nice_id = job.id;
|
|
478
|
+
if (job.label && ((job.type != 'adhoc') || ('plabel' in job))) {
|
|
479
|
+
nice_id = job.label + gray(' (' + job.id + ')');
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
var prefix = '';
|
|
483
|
+
if (job.workflow && job.workflow.job) prefix = gray.bold('➥ ');
|
|
484
|
+
|
|
485
|
+
var icon = '⏱️';
|
|
486
|
+
if (job.suspended) icon = '⏸️';
|
|
487
|
+
else if (job.invisible) icon = '🫥';
|
|
488
|
+
else if (job.type == 'workflow') icon = '📋';
|
|
489
|
+
|
|
490
|
+
return prefix + cli.emoji(icon) + ' ' + nice_id;
|
|
491
|
+
},
|
|
492
|
+
|
|
493
|
+
getNiceJobElapsedTime(job, abbrev, no_secondary) {
|
|
494
|
+
// render nice elapsed time display
|
|
495
|
+
var now = Math.floor( job.completed || this.epoch );
|
|
496
|
+
var elapsed = job.elapsed || Math.max( 0, now - Math.floor(job.started) ) || 0;
|
|
497
|
+
return Tools.getTextFromSeconds( elapsed, abbrev, no_secondary );
|
|
498
|
+
},
|
|
499
|
+
|
|
500
|
+
getNiceJobRemainingTime(job, abbrev) {
|
|
501
|
+
// get nice job remaining time, using elapsed and progress
|
|
502
|
+
var now = this.epoch;
|
|
503
|
+
var elapsed = Math.floor( Math.max( 0, now - job.started ) );
|
|
504
|
+
var progress = job.progress || 0;
|
|
505
|
+
if ((elapsed >= 10) && (progress > 0) && (progress < 1.0)) {
|
|
506
|
+
var sec_remain = Math.floor(((1.0 - progress) * elapsed) / progress);
|
|
507
|
+
return Tools.getTextFromSeconds( sec_remain, abbrev, true );
|
|
508
|
+
}
|
|
509
|
+
else return 'n/a';
|
|
510
|
+
},
|
|
511
|
+
|
|
512
|
+
getNiceJobState(job) {
|
|
513
|
+
// get nice job state given job
|
|
514
|
+
// (states: queued, start_delay, retry_delay, ready, starting, active, finishing, complete)
|
|
515
|
+
var nice_state = ucfirst(job.state || 'unknown');
|
|
516
|
+
var now = this.epoch;
|
|
517
|
+
|
|
518
|
+
switch (job.state) {
|
|
519
|
+
case 'queued':
|
|
520
|
+
if (job.position && job.priority) nice_state += ` (#${job.position} - Priority)`;
|
|
521
|
+
else if (job.position) nice_state += ` (#${job.position})`;
|
|
522
|
+
else if (job.priority) nice_state += ` (Priority)`;
|
|
523
|
+
break;
|
|
524
|
+
|
|
525
|
+
case 'start_delay':
|
|
526
|
+
nice_state = 'Start Delay';
|
|
527
|
+
if (job.until && (job.until > now)) nice_state += ' (' + Tools.getTextFromSeconds(job.until - now, true, true) + ')';
|
|
528
|
+
break;
|
|
529
|
+
|
|
530
|
+
case 'retry_delay':
|
|
531
|
+
nice_state = 'Retry Delay';
|
|
532
|
+
if (job.until && (job.until > now)) nice_state += ' (' + Tools.getTextFromSeconds(job.until - now, true, true) + ')';
|
|
533
|
+
break;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// special case: If job is complete but not final, it's actually still finishing
|
|
537
|
+
if ((job.state == 'complete') && !job.final) { nice_state = 'Finishing'; }
|
|
538
|
+
|
|
539
|
+
// special case for suspended job
|
|
540
|
+
if (job.suspended && !job.final) { nice_state = 'Suspended'; }
|
|
541
|
+
|
|
542
|
+
return nice_state;
|
|
543
|
+
},
|
|
544
|
+
|
|
545
|
+
getNiceJobEvent(job) {
|
|
546
|
+
// get nice event formatted as running job, supporting workflows etc.
|
|
547
|
+
if (job.type == 'adhoc') {
|
|
548
|
+
var plugin = Tools.findObject( this.plugins, { id: job.plugin } );
|
|
549
|
+
if (!plugin) return 'n/a'; // sanity
|
|
550
|
+
|
|
551
|
+
var title = '';
|
|
552
|
+
|
|
553
|
+
if ('plabel' in job) {
|
|
554
|
+
// new
|
|
555
|
+
title = job.plabel || plugin.title;
|
|
556
|
+
}
|
|
557
|
+
else {
|
|
558
|
+
// legacy
|
|
559
|
+
title = job.label || plugin.title;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
return title;
|
|
563
|
+
}
|
|
564
|
+
else if ('plabel' in job) {
|
|
565
|
+
return job.plabel || this.getNiceEvent(job.event);
|
|
566
|
+
}
|
|
567
|
+
else return this.getNiceEvent(job.event);
|
|
568
|
+
},
|
|
569
|
+
|
|
570
|
+
getNiceJobSource(job) {
|
|
571
|
+
return Tools.ucfirst(job.source);
|
|
572
|
+
},
|
|
573
|
+
|
|
574
|
+
getJobResultArgs(job) {
|
|
575
|
+
var color = '';
|
|
576
|
+
var text = ucfirst( '' + job.code );
|
|
577
|
+
var icon = '';
|
|
578
|
+
|
|
579
|
+
if (!job.final) {
|
|
580
|
+
color = 'blue';
|
|
581
|
+
text = 'In Progress';
|
|
582
|
+
icon = '🔄';
|
|
583
|
+
}
|
|
584
|
+
else if (job.replay) {
|
|
585
|
+
color = 'cyan';
|
|
586
|
+
text = 'Replay';
|
|
587
|
+
icon = '⏮️';
|
|
588
|
+
}
|
|
589
|
+
else if (!job.code) {
|
|
590
|
+
color = 'green';
|
|
591
|
+
text = 'Success';
|
|
592
|
+
icon = '✅';
|
|
593
|
+
}
|
|
594
|
+
else if (job.retried) {
|
|
595
|
+
color = 'orange';
|
|
596
|
+
text = 'Retried';
|
|
597
|
+
icon = '✴️';
|
|
598
|
+
}
|
|
599
|
+
else switch(job.code) {
|
|
600
|
+
case 'warning': color = 'yellow'; icon = '⚠️'; break;
|
|
601
|
+
case 'critical': color = 'purple'; icon = '☢️'; break;
|
|
602
|
+
case 'abort': color = 'gray'; text = 'Aborted'; icon = '🚫'; break;
|
|
603
|
+
|
|
604
|
+
default:
|
|
605
|
+
color = 'red';
|
|
606
|
+
text = 'Error';
|
|
607
|
+
icon = '🛑';
|
|
608
|
+
break;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
return { color, text, icon };
|
|
612
|
+
},
|
|
613
|
+
|
|
614
|
+
getNiceJobResult(job) {
|
|
615
|
+
// color label + icon for job result
|
|
616
|
+
var args = this.getJobResultArgs(job);
|
|
617
|
+
return cli.emoji(args.icon) + ' ' + this.color(args.color).bold(args.text);
|
|
618
|
+
},
|
|
619
|
+
|
|
620
|
+
getJobActionDisplayArgs(action, link) {
|
|
621
|
+
// get display args for job action
|
|
622
|
+
// returns: { condition, type, text, desc }
|
|
623
|
+
var disp = Tools.copyHash({
|
|
624
|
+
condition: Tools.findObject( this.config.ui.action_condition_menu, { id: action.condition } ) ||
|
|
625
|
+
Tools.findObject( this.config.ui.alert_action_condition_menu, { id: action.condition } )
|
|
626
|
+
}, true);
|
|
627
|
+
|
|
628
|
+
if (!disp.condition && action.condition.match(/^tag:(\w+)$/)) {
|
|
629
|
+
var tag_id = RegExp.$1;
|
|
630
|
+
var tag = Tools.findObject( this.tags, { id: tag_id } ) || { title: tag_id };
|
|
631
|
+
disp.condition = { id: action.condition, title: "On " + tag.title };
|
|
632
|
+
}
|
|
633
|
+
else if (!disp.condition && (action.condition == 'instant')) {
|
|
634
|
+
disp.condition = { id: action.condition, title: "Instant" };
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
disp.condition.color = this.condition_colors[action.condition] || 'gray';
|
|
638
|
+
|
|
639
|
+
disp.note = action.source ? `(Inherited from ${action.source})` : '';
|
|
640
|
+
|
|
641
|
+
switch (action.type) {
|
|
642
|
+
case 'email':
|
|
643
|
+
disp.type = "Send Email";
|
|
644
|
+
var parts = [];
|
|
645
|
+
if (action.users && action.users.length) parts.push( '' + Tools.commify(action.users.length) + ' ' + Tools.pluralize('user', action.users.length) );
|
|
646
|
+
if (action.email) parts.push( action.email );
|
|
647
|
+
if (!parts.length) parts = [ '(None)' ];
|
|
648
|
+
disp.text = disp.desc = parts.join(', ');
|
|
649
|
+
break;
|
|
650
|
+
|
|
651
|
+
case 'web_hook':
|
|
652
|
+
disp.type = "Web Hook";
|
|
653
|
+
var web_hook = Tools.findObject( this.web_hooks, { id: action.web_hook } );
|
|
654
|
+
disp.text = web_hook ? web_hook.title : "(Web Hook not found)";
|
|
655
|
+
disp.desc = this.getNiceWebHook(action.web_hook);
|
|
656
|
+
break;
|
|
657
|
+
|
|
658
|
+
case 'run_event':
|
|
659
|
+
disp.type = "Run Event";
|
|
660
|
+
var event = Tools.findObject( this.events, { id: action.event_id } );
|
|
661
|
+
disp.text = event ? event.title : "(Event not found)";
|
|
662
|
+
disp.desc = this.getNiceEvent(event);
|
|
663
|
+
break;
|
|
664
|
+
|
|
665
|
+
case 'channel':
|
|
666
|
+
disp.type = "Notify Channel";
|
|
667
|
+
var channel = Tools.findObject( this.channels, { id: action.channel_id } );
|
|
668
|
+
disp.text = channel ? channel.title : "(Channel not found)";
|
|
669
|
+
disp.desc = this.getNiceChannel(channel, link);
|
|
670
|
+
break;
|
|
671
|
+
|
|
672
|
+
case 'snapshot':
|
|
673
|
+
disp.type = "Take Snapshot";
|
|
674
|
+
disp.text = disp.desc = "(Current Server)";
|
|
675
|
+
break;
|
|
676
|
+
|
|
677
|
+
case 'store':
|
|
678
|
+
disp.type = "Store Bucket";
|
|
679
|
+
var bucket = Tools.findObject( this.buckets, { id: action.bucket_id } );
|
|
680
|
+
disp.text = bucket ? bucket.title : "(Bucket not found)";
|
|
681
|
+
disp.desc = this.getNiceBucket(bucket, link);
|
|
682
|
+
break;
|
|
683
|
+
|
|
684
|
+
case 'fetch':
|
|
685
|
+
disp.type = "Fetch Bucket";
|
|
686
|
+
var bucket = Tools.findObject( this.buckets, { id: action.bucket_id } );
|
|
687
|
+
disp.text = bucket ? bucket.title : "(Bucket not found)";
|
|
688
|
+
disp.desc = this.getNiceBucket(bucket, link);
|
|
689
|
+
break;
|
|
690
|
+
|
|
691
|
+
case 'ticket':
|
|
692
|
+
disp.type = 'Create Ticket';
|
|
693
|
+
var ticket_type = Tools.findObject( this.config.ui.ticket_types, { id: action.ticket_type } );
|
|
694
|
+
disp.text = ticket_type.title;
|
|
695
|
+
disp.desc = disp.text;
|
|
696
|
+
break;
|
|
697
|
+
|
|
698
|
+
case 'suspend':
|
|
699
|
+
disp.type = "Suspend Job";
|
|
700
|
+
var label = "";
|
|
701
|
+
if ((action.users && action.users.length) || action.email.length) label = "Send Email";
|
|
702
|
+
if (action.web_hook) label += (label.length ? ', ' : '') + "Web Hook";
|
|
703
|
+
disp.text = disp.desc = label || 'n/a';
|
|
704
|
+
break;
|
|
705
|
+
|
|
706
|
+
case 'tag':
|
|
707
|
+
disp.type = "Apply Tags";
|
|
708
|
+
disp.text = this.getNiceTagList(action.tags);
|
|
709
|
+
disp.desc = disp.text;
|
|
710
|
+
break;
|
|
711
|
+
|
|
712
|
+
case 'label':
|
|
713
|
+
disp.type = "Apply Label";
|
|
714
|
+
disp.text = '"' + action.label + '"';
|
|
715
|
+
disp.desc = disp.text;
|
|
716
|
+
break;
|
|
717
|
+
|
|
718
|
+
case 'disable':
|
|
719
|
+
disp.type = "Disable Event";
|
|
720
|
+
disp.text = disp.desc = "(Current Event)";
|
|
721
|
+
disp.icon = 'cancel';
|
|
722
|
+
break;
|
|
723
|
+
|
|
724
|
+
case 'delete':
|
|
725
|
+
disp.type = "Delete Event";
|
|
726
|
+
disp.text = disp.desc = "(Current Event)";
|
|
727
|
+
disp.icon = 'trash-can-outline';
|
|
728
|
+
break;
|
|
729
|
+
|
|
730
|
+
case 'plugin':
|
|
731
|
+
disp.type = "Plugin";
|
|
732
|
+
var plugin = Tools.findObject( this.plugins, { id: action.plugin_id, type: 'action' } );
|
|
733
|
+
disp.text = plugin ? plugin.title : "(Plugin not found)";
|
|
734
|
+
disp.desc = this.getNicePlugin(plugin, link);
|
|
735
|
+
disp.icon = 'power-plug';
|
|
736
|
+
break;
|
|
737
|
+
|
|
738
|
+
} // switch item.type
|
|
739
|
+
|
|
740
|
+
return disp;
|
|
741
|
+
},
|
|
742
|
+
|
|
743
|
+
getResLimitDisplayArgs(item) {
|
|
744
|
+
// get nice title and description for resource limit
|
|
745
|
+
var nice_title = '';
|
|
746
|
+
var nice_desc = '';
|
|
747
|
+
var short_desc = '';
|
|
748
|
+
var note = item.source ? `(Inherited from ${item.source})` : '';
|
|
749
|
+
|
|
750
|
+
switch (item.type) {
|
|
751
|
+
case 'mem':
|
|
752
|
+
nice_title = "Max Memory";
|
|
753
|
+
nice_desc = Tools.getTextFromBytes(item.amount) + " for " + Tools.getTextFromSeconds(item.duration, false, true);
|
|
754
|
+
short_desc = Tools.getTextFromBytes(item.amount);
|
|
755
|
+
break;
|
|
756
|
+
|
|
757
|
+
case 'cpu':
|
|
758
|
+
nice_title = "Max CPU %";
|
|
759
|
+
nice_desc = item.amount + "% for " + Tools.getTextFromSeconds(item.duration, false, true);
|
|
760
|
+
short_desc = item.amount + '%';
|
|
761
|
+
break;
|
|
762
|
+
|
|
763
|
+
case 'log':
|
|
764
|
+
nice_title = "Max Log Size";
|
|
765
|
+
nice_desc = short_desc = Tools.getTextFromBytes(item.amount);
|
|
766
|
+
break;
|
|
767
|
+
|
|
768
|
+
case 'time':
|
|
769
|
+
nice_title = "Max Run Time";
|
|
770
|
+
nice_desc = Tools.getTextFromSeconds(item.duration, false, false);
|
|
771
|
+
short_desc = Tools.getTextFromSeconds(item.duration, true, false);
|
|
772
|
+
break;
|
|
773
|
+
|
|
774
|
+
case 'job':
|
|
775
|
+
nice_title = "Max Jobs";
|
|
776
|
+
if (!item.amount) nice_desc = short_desc = "Unlimited";
|
|
777
|
+
else {
|
|
778
|
+
nice_desc = "Up to " + Tools.commify(item.amount) + " concurrent " + Tools.pluralize("job", item.amount);
|
|
779
|
+
short_desc = Tools.commify(item.amount) + ' ' + Tools.pluralize("job", item.amount);
|
|
780
|
+
}
|
|
781
|
+
break;
|
|
782
|
+
|
|
783
|
+
case 'retry':
|
|
784
|
+
nice_title = "Max Retries";
|
|
785
|
+
if (!item.amount) {
|
|
786
|
+
nice_desc = "No retries will be attempted";
|
|
787
|
+
short_desc = "None";
|
|
788
|
+
}
|
|
789
|
+
else {
|
|
790
|
+
nice_desc = short_desc = "Up to " + Tools.commify(item.amount);
|
|
791
|
+
if (item.duration) nice_desc += " (" + Tools.getTextFromSeconds(item.duration, false, true) + " delay)";
|
|
792
|
+
}
|
|
793
|
+
break;
|
|
794
|
+
|
|
795
|
+
case 'queue':
|
|
796
|
+
nice_title = "Max Queue";
|
|
797
|
+
if (!item.amount) {
|
|
798
|
+
nice_desc = "No jobs allowed in queue";
|
|
799
|
+
short_desc = "None";
|
|
800
|
+
}
|
|
801
|
+
else {
|
|
802
|
+
nice_desc = "Up to " + Tools.commify(item.amount) + " " + Tools.pluralize("job", item.amount) + " allowed in queue";
|
|
803
|
+
short_desc = Tools.commify(item.amount) + ' ' + Tools.pluralize("job", item.amount);
|
|
804
|
+
}
|
|
805
|
+
break;
|
|
806
|
+
|
|
807
|
+
case 'file':
|
|
808
|
+
nice_title = "Max Files";
|
|
809
|
+
if (!item.amount) {
|
|
810
|
+
nice_desc = "No files allowed";
|
|
811
|
+
short_desc = "None";
|
|
812
|
+
}
|
|
813
|
+
else {
|
|
814
|
+
nice_desc = "Up to " + Tools.commify(item.amount) + " " + Tools.pluralize("file", item.amount);
|
|
815
|
+
if (item.size) nice_desc += " (" + Tools.getTextFromBytes(item.size) + " total)";
|
|
816
|
+
|
|
817
|
+
short_desc = Tools.commify(item.amount) + ' ' + Tools.pluralize("file", item.amount);
|
|
818
|
+
if (item.size) short_desc += ", " + Tools.getTextFromBytes(item.size);
|
|
819
|
+
}
|
|
820
|
+
break;
|
|
821
|
+
|
|
822
|
+
case 'day':
|
|
823
|
+
nice_title = "Max Daily";
|
|
824
|
+
nice_desc = "Up to " + Tools.commify(item.amount) + " “" + item.condition + "” " + Tools.pluralize("condition", item.amount);
|
|
825
|
+
short_desc = Tools.commify(item.amount) + " x " + item.condition;
|
|
826
|
+
break;
|
|
827
|
+
|
|
828
|
+
case 'tag':
|
|
829
|
+
nice_title = "Max Tags";
|
|
830
|
+
if (!item.amount) {
|
|
831
|
+
nice_desc = "No tags allowed";
|
|
832
|
+
short_desc = "None";
|
|
833
|
+
}
|
|
834
|
+
else {
|
|
835
|
+
nice_desc = "Up to " + Tools.commify(item.amount) + " " + Tools.pluralize("tag", item.amount);
|
|
836
|
+
short_desc = Tools.commify(item.amount) + ' ' + Tools.pluralize("tag", item.amount);
|
|
837
|
+
}
|
|
838
|
+
break;
|
|
839
|
+
} // switch item.type
|
|
840
|
+
|
|
841
|
+
return { nice_title, nice_desc, short_desc, note };
|
|
842
|
+
},
|
|
843
|
+
|
|
844
|
+
setPath: function(target, path, value) {
|
|
845
|
+
// set path using dir/slash/syntax or dot.path.syntax
|
|
846
|
+
// support inline dots and slashes if backslash-escaped
|
|
847
|
+
// implement strict array indexing
|
|
848
|
+
var parts = (path.indexOf("\\") > -1) ? path.replace(/\\\./g, '__PXDOT__').replace(/\\\//g, '__PXSLASH__').split(/[\.\/]/).map( function(elem) {
|
|
849
|
+
return elem.replace(/__PXDOT__/g, '.').replace(/__PXSLASH__/g, '/');
|
|
850
|
+
} ) : path.split(/[\.\/]/);
|
|
851
|
+
|
|
852
|
+
var key = parts.pop();
|
|
853
|
+
if (key.match(Tools.MATCH_BAD_KEY)) return false;
|
|
854
|
+
|
|
855
|
+
// traverse path
|
|
856
|
+
while (parts.length) {
|
|
857
|
+
var part = parts.shift();
|
|
858
|
+
if (part) {
|
|
859
|
+
if (part.match(Tools.MATCH_BAD_KEY)) {
|
|
860
|
+
// do not traverse into any bad keys
|
|
861
|
+
return false;
|
|
862
|
+
}
|
|
863
|
+
if (Array.isArray(target)) {
|
|
864
|
+
if (!part.match(/^(0|[1-9]\d*)$/)) return false;
|
|
865
|
+
if (!(parseInt(part) in target)) return false;
|
|
866
|
+
}
|
|
867
|
+
if (!(part in target)) {
|
|
868
|
+
// auto-create nodes
|
|
869
|
+
target[part] = {};
|
|
870
|
+
}
|
|
871
|
+
if (!target[part] || (typeof(target[part]) != 'object')) {
|
|
872
|
+
// path runs into null or a non-object
|
|
873
|
+
return false;
|
|
874
|
+
}
|
|
875
|
+
target = target[part];
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
if (Array.isArray(target)) {
|
|
880
|
+
if (!key.match(/^(0|[1-9]\d*)$/)) return false;
|
|
881
|
+
if (!(parseInt(key) in target)) return false;
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
target[key] = value;
|
|
885
|
+
return true;
|
|
886
|
+
},
|
|
887
|
+
|
|
888
|
+
deletePath: function(target, path) {
|
|
889
|
+
// Delete an object property using dir/slash/syntax or dot.path.syntax.
|
|
890
|
+
// This intentionally tightens pixl-tools' implementation for untrusted CLI
|
|
891
|
+
// paths, just as our local setPath() does for strict array traversal.
|
|
892
|
+
if (!target || (typeof(target) != 'object') || (typeof(path) != 'string') || !path.length) return false;
|
|
893
|
+
|
|
894
|
+
// Support literal dots and slashes when they are backslash-escaped.
|
|
895
|
+
var parts = (path.indexOf("\\") > -1) ? path.replace(/\\\./g, '__PXDOT__').replace(/\\\//g, '__PXSLASH__').split(/[\.\/]/).map( function(elem) {
|
|
896
|
+
return elem.replace(/__PXDOT__/g, '.').replace(/__PXSLASH__/g, '/');
|
|
897
|
+
} ) : path.split(/[\.\/]/);
|
|
898
|
+
|
|
899
|
+
var key = parts.pop();
|
|
900
|
+
if (!key || key.match(Tools.MATCH_BAD_KEY)) return false;
|
|
901
|
+
|
|
902
|
+
// Traverse existing containers only. Arrays may be crossed using a strict,
|
|
903
|
+
// existing numeric index, but they are never modified by this helper.
|
|
904
|
+
while (parts.length) {
|
|
905
|
+
var part = parts.shift();
|
|
906
|
+
if (!part) continue;
|
|
907
|
+
if (part.match(Tools.MATCH_BAD_KEY)) return false;
|
|
908
|
+
|
|
909
|
+
if (Array.isArray(target) && !part.match(/^(0|[1-9]\d*)$/)) return false;
|
|
910
|
+
if (!Object.prototype.hasOwnProperty.call(target, part)) return false;
|
|
911
|
+
if (!target[part] || (typeof(target[part]) != 'object')) return false;
|
|
912
|
+
target = target[part];
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
// Deleting an array index would leave a sparse hole. The final container
|
|
916
|
+
// must be an object with the requested property as its own key.
|
|
917
|
+
if (Array.isArray(target)) return false;
|
|
918
|
+
if (!Object.prototype.hasOwnProperty.call(target, key)) return false;
|
|
919
|
+
delete target[key];
|
|
920
|
+
return true;
|
|
921
|
+
},
|
|
922
|
+
|
|
923
|
+
mergeDotArgs(target, source) {
|
|
924
|
+
// merge dot paths into target cleanly
|
|
925
|
+
// target and source may be the same object
|
|
926
|
+
for (var key in source) {
|
|
927
|
+
if (key.match(/\./)) {
|
|
928
|
+
// resolve dotted paths
|
|
929
|
+
if (!this.setPath(target, key, source[key])) {
|
|
930
|
+
this.die("Invalid argument path: " + key);
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
// remove the original dotted key when operating in-place
|
|
934
|
+
if (target === source) delete target[key];
|
|
935
|
+
}
|
|
936
|
+
else if (target !== source) {
|
|
937
|
+
// copy ordinary top-level arguments
|
|
938
|
+
target[key] = source[key];
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
},
|
|
942
|
+
|
|
943
|
+
applyDeleteArgs(target, source) {
|
|
944
|
+
// Apply one or more repeated `--delete PATH` options to an update object.
|
|
945
|
+
// The caller should merge ordinary updates first so explicit deletes win.
|
|
946
|
+
if (!Object.prototype.hasOwnProperty.call(source, 'delete')) return;
|
|
947
|
+
var paths = Tools.alwaysArray(source.delete);
|
|
948
|
+
|
|
949
|
+
// `delete` is a CLI instruction, not part of the outgoing API request.
|
|
950
|
+
delete source.delete;
|
|
951
|
+
if (target !== source) delete target.delete;
|
|
952
|
+
|
|
953
|
+
paths.forEach( path => {
|
|
954
|
+
path = String(path);
|
|
955
|
+
|
|
956
|
+
// xyOps update APIs shallow-merge top-level properties. Require a nested
|
|
957
|
+
// path so deleting the leaf and sending its parent has real server effect.
|
|
958
|
+
var path_parts = path.replace(/\\\./g, '').replace(/\\\//g, '').split(/[\.\/]/).filter( part => !!part );
|
|
959
|
+
if (path_parts.length < 2) {
|
|
960
|
+
this.die("Delete path must identify a nested object property: " + path);
|
|
961
|
+
}
|
|
962
|
+
if (!this.deletePath(target, path)) {
|
|
963
|
+
this.die("Invalid or missing delete path: " + path);
|
|
964
|
+
}
|
|
965
|
+
});
|
|
966
|
+
},
|
|
967
|
+
|
|
968
|
+
jsonOutput(data) {
|
|
969
|
+
// output formatted or compact json
|
|
970
|
+
if (this.format && this.format.match(/jsonc/)) {
|
|
971
|
+
print("\n");
|
|
972
|
+
process.stdout.write( JSON.stringify(data) + "\n" );
|
|
973
|
+
}
|
|
974
|
+
else {
|
|
975
|
+
var disp_value = JSON.stringify(data, null, "\t");
|
|
976
|
+
|
|
977
|
+
if (this.format && this.format.match(/json/)) disp_value = String(disp_value).trim() + "\n";
|
|
978
|
+
else disp_value = String(disp_value).trim().split(/\n/).map( line => ' ' + line ).join("\n") + "\n";
|
|
979
|
+
|
|
980
|
+
process.stdout.write( "\n" + gray( this.highlight(disp_value, { language: 'json', ignoreIllegals: true }) ) );
|
|
981
|
+
}
|
|
982
|
+
},
|
|
983
|
+
|
|
984
|
+
async cacheConfig() {
|
|
985
|
+
// load client config from xyops and cache for a while
|
|
986
|
+
var { err, data } = await this.cacheAPI('config', { format: 'json' }, { cache_ttl: 3600 });
|
|
987
|
+
if (err) {
|
|
988
|
+
println( "\n" + bold.red("Failed to contact xyOps at: ") + this.config.base_url );
|
|
989
|
+
this.die(err);
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
// merge, but do not clobber keys already in local config
|
|
993
|
+
for (var key in data.config) {
|
|
994
|
+
if (!(key in this.config)) this.config[key] = data.config[key];
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
this.masters = data.masters;
|
|
998
|
+
this.xyopsVersion = data.version;
|
|
999
|
+
},
|
|
1000
|
+
|
|
1001
|
+
async cacheAPI(name, req, opts) {
|
|
1002
|
+
// use cached API response, or call origin and update cache
|
|
1003
|
+
if (!opts) opts = {};
|
|
1004
|
+
|
|
1005
|
+
// vary cache filename based on hash of base_url
|
|
1006
|
+
var filename = name + '-' + Tools.digestHex(this.config.base_url, 'sha256', 16) + '.json';
|
|
1007
|
+
|
|
1008
|
+
var cache_file = Path.join( this.tempDir, filename );
|
|
1009
|
+
var ttl = opts.cache_ttl || this.config.cache_ttl || DEFAULT_CACHE_TTL;
|
|
1010
|
+
var stats = null;
|
|
1011
|
+
|
|
1012
|
+
try { stats = fs.statSync(cache_file); }
|
|
1013
|
+
catch (err) { stats = null; }
|
|
1014
|
+
|
|
1015
|
+
if (stats && (stats.mtimeMs / 1000 > Tools.timeNow() - ttl)) {
|
|
1016
|
+
// cache object still fresh
|
|
1017
|
+
return { data: JSON.parse( fs.readFileSync(cache_file, 'utf8') ) };
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
// fetch from origin
|
|
1021
|
+
delete opts.cache_ttl;
|
|
1022
|
+
var res = await this.api[name](req, opts);
|
|
1023
|
+
if (res.err) return res;
|
|
1024
|
+
|
|
1025
|
+
// store in cache
|
|
1026
|
+
Tools.writeFileAtomicSync( cache_file, JSON.stringify(res.data) );
|
|
1027
|
+
|
|
1028
|
+
return res;
|
|
1029
|
+
},
|
|
1030
|
+
|
|
1031
|
+
async callStandardAPI(name, req, opts = {}) {
|
|
1032
|
+
// wrapper around APIs that make a change
|
|
1033
|
+
// indeterminate progress w/text, handle dry run, verbose req/res, etc.
|
|
1034
|
+
this.validateStandardAPIRequest(name, req);
|
|
1035
|
+
|
|
1036
|
+
// Sensitive callers may supply a redacted display copy while the original
|
|
1037
|
+
// request continues through validation and transport unchanged.
|
|
1038
|
+
var filter_request = opts.filterRequest;
|
|
1039
|
+
delete opts.filterRequest;
|
|
1040
|
+
|
|
1041
|
+
if (this.dry || this.verbose) {
|
|
1042
|
+
println( "\n " + this.color('theme').bold( this.dry ? `API REQUEST PREVIEW: ${name}` : `API REQUEST: ${name}` ) );
|
|
1043
|
+
this.jsonOutput(filter_request ? filter_request(req) : req);
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
if (this.dry) return;
|
|
1047
|
+
|
|
1048
|
+
var text = opts.text || '';
|
|
1049
|
+
if (text) text = gray('→ ' + text);
|
|
1050
|
+
delete opts.text;
|
|
1051
|
+
|
|
1052
|
+
// Callers handling one-time secrets can redact the verbose response while
|
|
1053
|
+
// retaining the original response object for their final, controlled output.
|
|
1054
|
+
var filter_response = opts.filterResponse;
|
|
1055
|
+
delete opts.filterResponse;
|
|
1056
|
+
|
|
1057
|
+
cli.progress.start({ amount: 1, pct: false, text: text });
|
|
1058
|
+
var { err, data } = await this.api[name](req, opts);
|
|
1059
|
+
cli.progress.end();
|
|
1060
|
+
|
|
1061
|
+
if (err) {
|
|
1062
|
+
if (opts.throw) throw err;
|
|
1063
|
+
else this.die(err);
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
if (this.verbose) {
|
|
1067
|
+
println( "\n " + this.color('theme').bold("API RESPONSE: " + name) );
|
|
1068
|
+
this.jsonOutput(filter_response ? filter_response(data) : data);
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
return data;
|
|
1072
|
+
},
|
|
1073
|
+
|
|
1074
|
+
compareVersions(left, right) {
|
|
1075
|
+
// Compare semantic version components without adding a dependency. Build
|
|
1076
|
+
// metadata does not affect precedence; prereleases sort below releases.
|
|
1077
|
+
var parse = value => {
|
|
1078
|
+
var match = typeof(value) == 'string' && value.match(/^(\d+)\.(\d+)\.(\d+)(?:-([\w.-]+))?(?:\+[\w.-]+)?$/);
|
|
1079
|
+
if (!match) this.die("Invalid version: " + value);
|
|
1080
|
+
return match;
|
|
1081
|
+
};
|
|
1082
|
+
|
|
1083
|
+
var a = parse(left), b = parse(right);
|
|
1084
|
+
|
|
1085
|
+
for (var idx = 1; idx <= 3; idx++) {
|
|
1086
|
+
if (Number(a[idx]) != Number(b[idx])) return Number(a[idx]) > Number(b[idx]) ? 1 : -1;
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
if (a[4] === b[4]) return 0;
|
|
1090
|
+
if (!a[4]) return 1;
|
|
1091
|
+
if (!b[4]) return -1;
|
|
1092
|
+
|
|
1093
|
+
var ap = a[4].split('.'), bp = b[4].split('.');
|
|
1094
|
+
|
|
1095
|
+
for (var idx = 0; idx < Math.max(ap.length, bp.length); idx++) {
|
|
1096
|
+
if (ap[idx] === bp[idx]) continue;
|
|
1097
|
+
if (ap[idx] === undefined) return -1;
|
|
1098
|
+
if (bp[idx] === undefined) return 1;
|
|
1099
|
+
|
|
1100
|
+
var an = /^\d+$/.test(ap[idx]), bn = /^\d+$/.test(bp[idx]);
|
|
1101
|
+
if (an && bn) return Number(ap[idx]) > Number(bp[idx]) ? 1 : -1;
|
|
1102
|
+
if (an != bn) return an ? -1 : 1;
|
|
1103
|
+
|
|
1104
|
+
return ap[idx] > bp[idx] ? 1 : -1;
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
return 0;
|
|
1108
|
+
},
|
|
1109
|
+
|
|
1110
|
+
getLevenshteinDistance(left, right) {
|
|
1111
|
+
// Compute edit distance using two rows instead of allocating a full matrix.
|
|
1112
|
+
// CRUD property names are short, but keeping memory linear makes this helper
|
|
1113
|
+
// safe and generally useful for other CLI suggestions in the future.
|
|
1114
|
+
left = String(left);
|
|
1115
|
+
right = String(right);
|
|
1116
|
+
if (left === right) return 0;
|
|
1117
|
+
if (!left.length) return right.length;
|
|
1118
|
+
if (!right.length) return left.length;
|
|
1119
|
+
|
|
1120
|
+
var previous = Array.from({ length: right.length + 1 }, (unused, idx) => idx);
|
|
1121
|
+
|
|
1122
|
+
for (var left_idx = 1; left_idx <= left.length; left_idx++) {
|
|
1123
|
+
var current = [left_idx];
|
|
1124
|
+
|
|
1125
|
+
for (var right_idx = 1; right_idx <= right.length; right_idx++) {
|
|
1126
|
+
var cost = left[left_idx - 1] == right[right_idx - 1] ? 0 : 1;
|
|
1127
|
+
current[right_idx] = Math.min(
|
|
1128
|
+
current[right_idx - 1] + 1,
|
|
1129
|
+
previous[right_idx] + 1,
|
|
1130
|
+
previous[right_idx - 1] + cost
|
|
1131
|
+
);
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
previous = current;
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
return previous[right.length];
|
|
1138
|
+
},
|
|
1139
|
+
|
|
1140
|
+
findClosestString(value, candidates) {
|
|
1141
|
+
// Avoid noisy or misleading guesses. Longer names tolerate a few more
|
|
1142
|
+
// edits, and tied candidates produce no suggestion at all.
|
|
1143
|
+
value = String(value).toLowerCase();
|
|
1144
|
+
var max_distance = Math.max(1, Math.min(3, Math.ceil(value.length / 3)));
|
|
1145
|
+
var best = '';
|
|
1146
|
+
var best_distance = Infinity;
|
|
1147
|
+
var tied = false;
|
|
1148
|
+
|
|
1149
|
+
candidates.forEach( candidate => {
|
|
1150
|
+
var distance = this.getLevenshteinDistance(value, String(candidate).toLowerCase());
|
|
1151
|
+
if (distance < best_distance) {
|
|
1152
|
+
best = candidate;
|
|
1153
|
+
best_distance = distance;
|
|
1154
|
+
tied = false;
|
|
1155
|
+
}
|
|
1156
|
+
else if (distance == best_distance) tied = true;
|
|
1157
|
+
});
|
|
1158
|
+
|
|
1159
|
+
return (!tied && (best_distance <= max_distance)) ? best : '';
|
|
1160
|
+
},
|
|
1161
|
+
|
|
1162
|
+
validateStandardAPIRequest(name, req) {
|
|
1163
|
+
// Convert the SDK's camelCase method name to the documented API endpoint.
|
|
1164
|
+
// APIs without a CRUD property rule retain their normal pass-through behavior.
|
|
1165
|
+
var api_name = String(name).replace(/([a-z0-9])([A-Z])/g, '$1_$2').toLowerCase();
|
|
1166
|
+
var pattern = Object.keys(STANDARD_API_PROPERTY_RULES).find( pattern => {
|
|
1167
|
+
return new RegExp(pattern).test(api_name);
|
|
1168
|
+
});
|
|
1169
|
+
if (!pattern || !req || (typeof(req) != 'object') || Array.isArray(req)) return;
|
|
1170
|
+
|
|
1171
|
+
var allowed = STANDARD_API_PROPERTY_RULES[pattern];
|
|
1172
|
+
var unexpected = Object.keys(req).find( key => !allowed.includes(key) );
|
|
1173
|
+
if (!unexpected) return;
|
|
1174
|
+
|
|
1175
|
+
var suggestion = this.findClosestString(unexpected, allowed);
|
|
1176
|
+
var message = 'Unsupported property for ' + api_name + ': "' + unexpected + '".';
|
|
1177
|
+
if (suggestion) message += ' Did you mean "' + suggestion + '"?';
|
|
1178
|
+
this.die(message);
|
|
1179
|
+
},
|
|
1180
|
+
|
|
1181
|
+
findObjectsFuzzy(arr, criteria, crit_count = 0) {
|
|
1182
|
+
// find objects using fuzzy matching (lower-case substring)
|
|
1183
|
+
crit_count = crit_count || Tools.numKeys(criteria);
|
|
1184
|
+
|
|
1185
|
+
return arr.filter( item => {
|
|
1186
|
+
var num_matched = 0;
|
|
1187
|
+
for (var key in criteria) {
|
|
1188
|
+
if (String(item[key]).toLowerCase().includes(String(criteria[key]).toLowerCase())) num_matched++;
|
|
1189
|
+
}
|
|
1190
|
+
return (num_matched >= crit_count);
|
|
1191
|
+
} );
|
|
1192
|
+
},
|
|
1193
|
+
|
|
1194
|
+
findObjectFuzzy(arr, criteria, crit_count = 0) {
|
|
1195
|
+
// find object using fuzzy matching (lower-case substring)
|
|
1196
|
+
crit_count = crit_count || Tools.numKeys(criteria);
|
|
1197
|
+
|
|
1198
|
+
return arr.find( item => {
|
|
1199
|
+
var num_matched = 0;
|
|
1200
|
+
for (var key in criteria) {
|
|
1201
|
+
if (String(item[key]).toLowerCase().includes(String(criteria[key]).toLowerCase())) num_matched++;
|
|
1202
|
+
}
|
|
1203
|
+
return (num_matched >= crit_count);
|
|
1204
|
+
} );
|
|
1205
|
+
},
|
|
1206
|
+
|
|
1207
|
+
includesAny: function(haystack, needles) {
|
|
1208
|
+
// return true if haystack contains any needles, false otherwise
|
|
1209
|
+
for (var idx = 0, len = needles.length; idx < len; idx++) {
|
|
1210
|
+
if (haystack.includes(needles[idx])) return true;
|
|
1211
|
+
}
|
|
1212
|
+
return false;
|
|
1213
|
+
},
|
|
1214
|
+
|
|
1215
|
+
includesAll: function(haystack, needles) {
|
|
1216
|
+
// return true if haystack contains ALL needles, false otherwise
|
|
1217
|
+
for (var idx = 0, len = needles.length; idx < len; idx++) {
|
|
1218
|
+
if (!haystack.includes(needles[idx])) return false;
|
|
1219
|
+
}
|
|
1220
|
+
return true;
|
|
1221
|
+
},
|
|
1222
|
+
|
|
1223
|
+
getTextFromSecondsShort(sec) {
|
|
1224
|
+
if (sec >= 31536000) return '' + Math.floor(sec / 31536000) + 'y';
|
|
1225
|
+
if (sec >= 2592000) return '' + Math.floor(sec / 2592000) + 'mo';
|
|
1226
|
+
if (sec >= 86400) return '' + Math.floor(sec / 86400) + 'd';
|
|
1227
|
+
if (sec >= 3600) return '' + Math.floor(sec / 3600) + 'h';
|
|
1228
|
+
if (sec >= 60) return '' + Math.floor(sec / 60) + 'm';
|
|
1229
|
+
return '' + Math.floor(sec / 1) + 's';
|
|
1230
|
+
},
|
|
1231
|
+
|
|
1232
|
+
toast(emoji, color, text) {
|
|
1233
|
+
// Toasts wrap at the visible terminal width. Redirected output has no TTY
|
|
1234
|
+
// width, so use the same conventional 80-column fallback as other displays.
|
|
1235
|
+
var final_text = cli.emoji(emoji) + ' ' + this.color(color).bold( text );
|
|
1236
|
+
|
|
1237
|
+
// Account for the indent, two borders, and two spaces of padding per side.
|
|
1238
|
+
// cli.box performs word wrapping at this content width and never ellipsizes.
|
|
1239
|
+
var available_width = Math.max( 1, (cli.width() || 80) - 7 );
|
|
1240
|
+
var max_width = Math.min( available_width, cli.stringWidth(final_text) );
|
|
1241
|
+
println( "\n" + cli.box(final_text, { width: max_width, hspace: 2, vspace: 1, indent: 1, styles: [this.color(color)] }) );
|
|
1242
|
+
},
|
|
1243
|
+
|
|
1244
|
+
printMutationSummary(opts) {
|
|
1245
|
+
// Mutation summaries are UX for interactive users. Keep explicit JSON
|
|
1246
|
+
// formats machine-readable by omitting all decorative terminal output.
|
|
1247
|
+
if (this.format && this.format.match(/json/)) return;
|
|
1248
|
+
this.printBoxList(opts);
|
|
1249
|
+
},
|
|
1250
|
+
|
|
1251
|
+
printUpdateData(data) {
|
|
1252
|
+
// Show the parsed CLI input before resource-specific normalization or merging,
|
|
1253
|
+
// so the user can verify exactly how their update arguments were understood.
|
|
1254
|
+
if (this.format && this.format.match(/json/)) return;
|
|
1255
|
+
print( "\n " + this.color('theme').bold('UPDATE DATA') );
|
|
1256
|
+
this.jsonOutput(data || {});
|
|
1257
|
+
},
|
|
1258
|
+
|
|
1259
|
+
printUserNotes(title, notes) {
|
|
1260
|
+
// print user notes (plain text, multi-line)
|
|
1261
|
+
println( "\n " + this.color('theme').bold(title) );
|
|
1262
|
+
if (cli.width()) {
|
|
1263
|
+
println( cli.box(String(notes).trim(), { indent: 1, hspace: 1, width: Math.min( cli.width() - 6, cli.widestLine(notes) ) } ) );
|
|
1264
|
+
}
|
|
1265
|
+
else {
|
|
1266
|
+
println( String(notes).trim() );
|
|
1267
|
+
}
|
|
1268
|
+
},
|
|
1269
|
+
|
|
1270
|
+
table(rows, opts) {
|
|
1271
|
+
// wrapper around cli.table, drop columns based on screen width
|
|
1272
|
+
if (!opts) opts = {};
|
|
1273
|
+
if (!('indent' in opts)) opts.indent = 1;
|
|
1274
|
+
// if (!('autoFit' in opts)) opts.autoFit = true;
|
|
1275
|
+
opts.autoFit = false;
|
|
1276
|
+
|
|
1277
|
+
var width = cli.width();
|
|
1278
|
+
if (!width) return cli.table(rows, opts);
|
|
1279
|
+
|
|
1280
|
+
// chop columns if table is too wide
|
|
1281
|
+
var orig_rows = [ ...rows ];
|
|
1282
|
+
var chopped = false;
|
|
1283
|
+
|
|
1284
|
+
while ((cli.widestLine(cli.table(Tools.copyHash(rows, true), opts)) > width) && rows[0].length) {
|
|
1285
|
+
rows.forEach( function(row) { row.pop(); } );
|
|
1286
|
+
chopped = true;
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
if (!rows[0].length) {
|
|
1290
|
+
// oops, we killed all the rows, add first one back in (will be auto-fit)
|
|
1291
|
+
rows.forEach( function(row, idx) { row.push( orig_rows[idx] ); } );
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
if (chopped) {
|
|
1295
|
+
rows.forEach( function(row) { row.push('...'); } );
|
|
1296
|
+
opts.autoFit = true;
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
return cli.table(rows, opts);
|
|
1300
|
+
},
|
|
1301
|
+
|
|
1302
|
+
printSuggestedCommands(opts) {
|
|
1303
|
+
// print formatted suggested commands for the user to copy & paste
|
|
1304
|
+
if (('suggest' in this.config) && !this.config.suggest) return;
|
|
1305
|
+
|
|
1306
|
+
var cmds = Object.keys(opts).filter( label => !!opts[label] ).map( label => {
|
|
1307
|
+
return [ label, opts[label] ];
|
|
1308
|
+
} );
|
|
1309
|
+
if (!cmds.length) return;
|
|
1310
|
+
|
|
1311
|
+
println("\n " + this.color('theme').bold("Other Commands:") );
|
|
1312
|
+
cmds.forEach( pair => {
|
|
1313
|
+
var [ label, cmd ] = pair;
|
|
1314
|
+
println( " " + this.color('green').bold('• ' + label) + ": " + bold(cmd) );
|
|
1315
|
+
} );
|
|
1316
|
+
},
|
|
1317
|
+
|
|
1318
|
+
printBoxList(opts) {
|
|
1319
|
+
// convenience wrapper around cli.defList with title and colors
|
|
1320
|
+
// args: { title, rows }
|
|
1321
|
+
println( "\n " + this.color('theme').bold( opts.title.toUpperCase() ) + "\n" +
|
|
1322
|
+
cli.defList(opts.rows.filter( row => !!row ), { labelStyles: ['yellow', 'bold'], textStyles: [], indent: 1, gap: 2 }) );
|
|
1323
|
+
},
|
|
1324
|
+
|
|
1325
|
+
printBoxTable(opts) {
|
|
1326
|
+
// convenience wrapper around cli.table with title and colors
|
|
1327
|
+
println( "\n " + this.color('theme').bold( opts.title.toUpperCase() ) + (opts.title_suffix || '') );
|
|
1328
|
+
var rows = [ opts.header ].concat( opts.rows.filter( row => !!row ) );
|
|
1329
|
+
if (rows.length < 2) {
|
|
1330
|
+
println( gray(` No ${opts.title.toLowerCase()} found.`) );
|
|
1331
|
+
return;
|
|
1332
|
+
}
|
|
1333
|
+
println( "" + this.table(rows, {}) );
|
|
1334
|
+
},
|
|
1335
|
+
|
|
1336
|
+
prepSearchArgs() {
|
|
1337
|
+
// Call only from a paginated command or variant, before building filters.
|
|
1338
|
+
// Other commands may use these names as API or resource properties.
|
|
1339
|
+
this.offset = this.args.offset || 0;
|
|
1340
|
+
this.limit = this.args.limit || this.config.items_per_page;
|
|
1341
|
+
if (this.args.page) this.offset = (this.args.page - 1) * this.limit;
|
|
1342
|
+
|
|
1343
|
+
// Keep pagination out of fuzzy criteria and server search query strings.
|
|
1344
|
+
delete this.args.offset;
|
|
1345
|
+
delete this.args.limit;
|
|
1346
|
+
delete this.args.page;
|
|
1347
|
+
},
|
|
1348
|
+
|
|
1349
|
+
printPaginatedBoxTable(opts, callback) {
|
|
1350
|
+
// print paginated box table
|
|
1351
|
+
var results = {
|
|
1352
|
+
limit: opts.limit,
|
|
1353
|
+
offset: opts.offset || 0,
|
|
1354
|
+
total: opts.list.length
|
|
1355
|
+
};
|
|
1356
|
+
|
|
1357
|
+
var num_pages = Math.floor( results.total / results.limit ) + 1;
|
|
1358
|
+
if (results.total % results.limit == 0) num_pages--;
|
|
1359
|
+
var current_page = Math.floor( results.offset / results.limit ) + 1;
|
|
1360
|
+
|
|
1361
|
+
var data_type = opts.data_type || opts.title.toLowerCase();
|
|
1362
|
+
var nice_total = Tools.commify(results.total) + ' items found';
|
|
1363
|
+
if (num_pages > 1) nice_total += ', page ' + Tools.commify(current_page) + ' of ' + Tools.commify(num_pages);
|
|
1364
|
+
|
|
1365
|
+
if (results.total) opts.title_suffix = gray(` (${nice_total})`);
|
|
1366
|
+
opts.rows = opts.rows.map( callback );
|
|
1367
|
+
|
|
1368
|
+
this.printBoxTable(opts);
|
|
1369
|
+
|
|
1370
|
+
if ((current_page == 1) && (current_page < num_pages)) {
|
|
1371
|
+
println( ' ' + gray(`(Add ${cyan.bold('--page 2')} to view the next page.)`) );
|
|
1372
|
+
}
|
|
1373
|
+
},
|
|
1374
|
+
|
|
1375
|
+
printActiveJobs(opts) {
|
|
1376
|
+
// print table of running jobs, with optional criteria
|
|
1377
|
+
if (!opts) opts = {};
|
|
1378
|
+
if (!opts.criteria) opts.criteria = {};
|
|
1379
|
+
|
|
1380
|
+
opts.title = opts.title || 'Active Jobs';
|
|
1381
|
+
opts.header = [ 'Job ID', 'Event', 'Category', 'Server', 'State', 'Elapsed', 'Progress', 'Remaining' ];
|
|
1382
|
+
|
|
1383
|
+
var jobs = opts.rows || Tools.sortBy( Tools.findObjects( Object.values(this.activeJobs), opts.criteria ), 'started', { type: 'number', dir: -1 } );
|
|
1384
|
+
var now = this.epoch;
|
|
1385
|
+
|
|
1386
|
+
opts.rows = jobs.map( job => {
|
|
1387
|
+
return [
|
|
1388
|
+
this.color('theme').bold( this.getNiceJob(job) ),
|
|
1389
|
+
this.getNiceEvent(job.event),
|
|
1390
|
+
this.getNiceCategory(job.category),
|
|
1391
|
+
this.getNiceServer(job.server),
|
|
1392
|
+
this.getNiceJobState(job),
|
|
1393
|
+
Tools.getTextFromSeconds( now - job.started, true, true ),
|
|
1394
|
+
Tools.pct( (job.progress || 0), 1.0, true ),
|
|
1395
|
+
this.getNiceJobRemainingTime(job, true)
|
|
1396
|
+
];
|
|
1397
|
+
} );
|
|
1398
|
+
|
|
1399
|
+
this.printBoxTable(opts);
|
|
1400
|
+
},
|
|
1401
|
+
|
|
1402
|
+
printInternalJobs(opts) {
|
|
1403
|
+
// print table of internal jobs, with optional criteria
|
|
1404
|
+
if (!opts) opts = {};
|
|
1405
|
+
if (!opts.criteria) opts.criteria = {};
|
|
1406
|
+
|
|
1407
|
+
// hide section entirely if no jobs
|
|
1408
|
+
if (!Tools.firstKey(this.internalJobs)) return;
|
|
1409
|
+
|
|
1410
|
+
opts.title = opts.title || 'Internal Jobs';
|
|
1411
|
+
opts.header = [ 'Job ID', 'Title', 'Type', 'Username', 'Progress', 'Elapsed', 'Remaining' ];
|
|
1412
|
+
|
|
1413
|
+
var jobs = Tools.sortBy( Tools.findObjects( Object.values(this.internalJobs), opts.criteria ), 'started', { type: 'number', dir: -1 } );
|
|
1414
|
+
var now = this.epoch;
|
|
1415
|
+
|
|
1416
|
+
opts.rows = jobs.map( job => {
|
|
1417
|
+
return [
|
|
1418
|
+
this.color('theme').bold(job.id),
|
|
1419
|
+
bold(job.title),
|
|
1420
|
+
job.type,
|
|
1421
|
+
job.username,
|
|
1422
|
+
Tools.pct( (job.progress || 0), 1.0, true ),
|
|
1423
|
+
Tools.getTextFromSeconds( now - job.started, true, true ),
|
|
1424
|
+
this.getNiceJobRemainingTime(job, true)
|
|
1425
|
+
];
|
|
1426
|
+
} );
|
|
1427
|
+
|
|
1428
|
+
this.printBoxTable(opts);
|
|
1429
|
+
},
|
|
1430
|
+
|
|
1431
|
+
printActiveAlerts(opts) {
|
|
1432
|
+
// print table of running alerts, with optional criteria
|
|
1433
|
+
if (!opts) opts = {};
|
|
1434
|
+
if (!opts.criteria) opts.criteria = {};
|
|
1435
|
+
|
|
1436
|
+
opts.title = opts.title || 'Active Alerts';
|
|
1437
|
+
opts.header = [ "Alert ID", "Title", "Message", "Server", "Status", "Started", "Duration" ];
|
|
1438
|
+
|
|
1439
|
+
var alerts = Tools.sortBy( Tools.findObjects( Object.values(this.activeAlerts), opts.criteria ), 'date', { type: 'number', dir: -1 } );
|
|
1440
|
+
var now = this.epoch;
|
|
1441
|
+
|
|
1442
|
+
opts.rows = alerts.map( alert => {
|
|
1443
|
+
return [
|
|
1444
|
+
cli.emoji('🔔') + ' ' + this.color('theme').bold(alert.id),
|
|
1445
|
+
this.getNiceAlert(alert.alert, false),
|
|
1446
|
+
alert.message,
|
|
1447
|
+
this.getNiceServer(alert.server, false),
|
|
1448
|
+
this.getNiceAlertStatus(alert),
|
|
1449
|
+
this.getNiceDateTime(alert.date),
|
|
1450
|
+
Tools.getTextFromSeconds( now - alert.date, true, true )
|
|
1451
|
+
];
|
|
1452
|
+
} );
|
|
1453
|
+
|
|
1454
|
+
this.printBoxTable(opts);
|
|
1455
|
+
},
|
|
1456
|
+
|
|
1457
|
+
async printUpcomingJobs(opts) {
|
|
1458
|
+
// predict and print upcoming jobs
|
|
1459
|
+
if (!opts) opts = {};
|
|
1460
|
+
|
|
1461
|
+
cli.progress.start();
|
|
1462
|
+
|
|
1463
|
+
this.upcomingJobs = await new Promise((resolve, reject) => {
|
|
1464
|
+
this.predictUpcomingJobs({
|
|
1465
|
+
events: opts.events || this.events,
|
|
1466
|
+
duration: 86400 * 32,
|
|
1467
|
+
burn: 16,
|
|
1468
|
+
max: 1000,
|
|
1469
|
+
progress: function(amount) { cli.progress.update( amount ); },
|
|
1470
|
+
callback: resolve
|
|
1471
|
+
});
|
|
1472
|
+
}); // promise
|
|
1473
|
+
|
|
1474
|
+
cli.progress.end();
|
|
1475
|
+
|
|
1476
|
+
opts.title = opts.title || 'Upcoming Jobs';
|
|
1477
|
+
opts.header = [ 'Event', 'Category', 'Targets', 'Source', 'Scheduled Time', 'Countdown' ];
|
|
1478
|
+
|
|
1479
|
+
opts.offset = this.offset || 0;
|
|
1480
|
+
opts.limit = this.limit || this.config.items_per_page;
|
|
1481
|
+
opts.rows = this.upcomingJobs.slice( opts.offset, opts.offset + opts.limit );
|
|
1482
|
+
opts.list = { length: this.upcomingJobs.length };
|
|
1483
|
+
|
|
1484
|
+
this.printPaginatedBoxTable( opts, job => {
|
|
1485
|
+
var type_info = Tools.findObject( this.config.ui.event_trigger_type_menu, { id: job.type } ) || { title: "Scheduler", icon: 'update' };
|
|
1486
|
+
var nice_source = type_info.title;
|
|
1487
|
+
var event = Tools.findObject( this.events, { id: job.event } ) || {};
|
|
1488
|
+
var precision = event.triggers ? Tools.findObject(event.triggers, { type: 'precision', enabled: true }) : null;
|
|
1489
|
+
var nice_date_time = '';
|
|
1490
|
+
var nice_countdown = '';
|
|
1491
|
+
var nice_skip = '';
|
|
1492
|
+
|
|
1493
|
+
// take over source if plugin modifier
|
|
1494
|
+
if (job.plugin) {
|
|
1495
|
+
var plugin = Tools.findObject( this.plugins, { id: job.plugin } ) || { title: job.plugin };
|
|
1496
|
+
nice_source = plugin.title;
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
// optionally vary date/time prediction display based on precision or interval
|
|
1500
|
+
var countdown = 0;
|
|
1501
|
+
|
|
1502
|
+
if (precision && precision.seconds && precision.seconds.length) {
|
|
1503
|
+
job.seconds = precision.seconds;
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
if (job.seconds) {
|
|
1507
|
+
// precision or interval job
|
|
1508
|
+
nice_date_time = this.getRelativeDateTime( job.epoch + job.seconds[0], true );
|
|
1509
|
+
if (job.seconds.length > 1) nice_date_time += ' (+' + Math.floor(job.seconds.length - 1) + ')';
|
|
1510
|
+
countdown = Math.max( 60, Math.abs((job.epoch + job.seconds[0]) - app.epoch) );
|
|
1511
|
+
}
|
|
1512
|
+
else {
|
|
1513
|
+
// normal scheduled job
|
|
1514
|
+
nice_date_time = this.getRelativeDateTime( job.epoch );
|
|
1515
|
+
countdown = Math.max( 60, Math.abs(job.epoch - app.epoch) );
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
nice_countdown = get_text_from_seconds_round( countdown, true );
|
|
1519
|
+
|
|
1520
|
+
return [
|
|
1521
|
+
this.color('theme').bold( this.getNiceEvent(job.event, true) ),
|
|
1522
|
+
this.getNiceCategory(event.category),
|
|
1523
|
+
this.getNiceTargets(event.targets),
|
|
1524
|
+
nice_source,
|
|
1525
|
+
nice_date_time,
|
|
1526
|
+
nice_countdown
|
|
1527
|
+
];
|
|
1528
|
+
} );
|
|
1529
|
+
},
|
|
1530
|
+
|
|
1531
|
+
async printCompletedJobs(opts) {
|
|
1532
|
+
// search for jobs, and print results
|
|
1533
|
+
if (!opts) opts = {};
|
|
1534
|
+
|
|
1535
|
+
var has_ids = Array.isArray(opts.ids);
|
|
1536
|
+
var query = opts.query || '*';
|
|
1537
|
+
var offset = ('offset' in opts) ? opts.offset : this.offset;
|
|
1538
|
+
var limit = opts.limit || this.limit || this.config.items_per_page;
|
|
1539
|
+
var rows = [];
|
|
1540
|
+
var list = { length: 0 };
|
|
1541
|
+
|
|
1542
|
+
if (has_ids) {
|
|
1543
|
+
if (!opts.ids.length) return;
|
|
1544
|
+
cli.progress.start();
|
|
1545
|
+
var { err, data } = await this.api.getJobs({
|
|
1546
|
+
ids: opts.ids
|
|
1547
|
+
});
|
|
1548
|
+
if (err) this.die(err);
|
|
1549
|
+
cli.progress.end();
|
|
1550
|
+
|
|
1551
|
+
var all_rows = data.jobs || [];
|
|
1552
|
+
list = { length: all_rows.length };
|
|
1553
|
+
rows = all_rows.slice(offset, offset + limit);
|
|
1554
|
+
}
|
|
1555
|
+
else {
|
|
1556
|
+
cli.progress.start();
|
|
1557
|
+
var { err, data } = await this.api.searchJobs({
|
|
1558
|
+
query: opts.query || '*',
|
|
1559
|
+
offset: this.offset,
|
|
1560
|
+
limit: this.limit
|
|
1561
|
+
});
|
|
1562
|
+
if (err) this.die(err);
|
|
1563
|
+
rows = data.rows;
|
|
1564
|
+
list = data.list;
|
|
1565
|
+
cli.progress.end();
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
// Embedded detail screens should omit the section when there are no associated tickets.
|
|
1569
|
+
if (opts.hide_empty && !rows.length) return;
|
|
1570
|
+
|
|
1571
|
+
opts.title = opts.title || 'Completed Jobs';
|
|
1572
|
+
opts.header = [ 'Job ID', 'Event', 'Category', 'Server', 'Source', 'Tags', 'Completed', 'Elapsed', 'Result' ];
|
|
1573
|
+
|
|
1574
|
+
opts.offset = this.offset || 0;
|
|
1575
|
+
opts.limit = this.limit || this.config.items_per_page;
|
|
1576
|
+
opts.rows = rows;
|
|
1577
|
+
opts.list = list;
|
|
1578
|
+
|
|
1579
|
+
this.printPaginatedBoxTable( opts, job => {
|
|
1580
|
+
if (!job) return false;
|
|
1581
|
+
return [
|
|
1582
|
+
this.color('theme').bold( this.getNiceJob(job) ),
|
|
1583
|
+
this.getNiceJobEvent(job, true),
|
|
1584
|
+
this.getNiceCategory(job.category, true),
|
|
1585
|
+
this.getNiceServer(job.server, true),
|
|
1586
|
+
this.getNiceJobSource(job),
|
|
1587
|
+
this.getNiceTagList( job.tags, false ),
|
|
1588
|
+
this.getRelativeDateTime( job.completed, true ),
|
|
1589
|
+
this.getNiceJobElapsedTime(job, true, true),
|
|
1590
|
+
this.getNiceJobResult(job)
|
|
1591
|
+
];
|
|
1592
|
+
});
|
|
1593
|
+
},
|
|
1594
|
+
|
|
1595
|
+
async printQueuedJobs(opts) {
|
|
1596
|
+
// Fetch queued jobs from the server and print a paginated table.
|
|
1597
|
+
if (!opts) opts = {};
|
|
1598
|
+
|
|
1599
|
+
var offset = ('offset' in opts) ? opts.offset : this.offset;
|
|
1600
|
+
var limit = opts.limit || this.limit || this.config.items_per_page;
|
|
1601
|
+
|
|
1602
|
+
// Start with any optional caller-supplied criteria, but always restrict this
|
|
1603
|
+
// helper to queued jobs. An event ID may be supplied for event detail views.
|
|
1604
|
+
var params = Object.assign({}, opts.criteria || {}, {
|
|
1605
|
+
state: 'queued',
|
|
1606
|
+
offset: offset,
|
|
1607
|
+
limit: limit,
|
|
1608
|
+
sort_by: opts.sort_by || 'started',
|
|
1609
|
+
sort_dir: ('sort_dir' in opts) ? opts.sort_dir : -1
|
|
1610
|
+
});
|
|
1611
|
+
|
|
1612
|
+
if (opts.event) params.event = opts.event;
|
|
1613
|
+
|
|
1614
|
+
cli.progress.start();
|
|
1615
|
+
var { err, data } = await this.api.getActiveJobs(params);
|
|
1616
|
+
if (err) this.die(err);
|
|
1617
|
+
cli.progress.end();
|
|
1618
|
+
|
|
1619
|
+
var rows = data.rows || [];
|
|
1620
|
+
|
|
1621
|
+
// Allow this helper to be embedded in larger detail screens without
|
|
1622
|
+
// displaying an empty section.
|
|
1623
|
+
if (opts.hide_empty && !rows.length) return;
|
|
1624
|
+
|
|
1625
|
+
opts.title = opts.title || 'Queued Jobs';
|
|
1626
|
+
opts.header = [
|
|
1627
|
+
'Job ID',
|
|
1628
|
+
'State',
|
|
1629
|
+
'Source',
|
|
1630
|
+
'Targets',
|
|
1631
|
+
'Queued',
|
|
1632
|
+
'Elapsed'
|
|
1633
|
+
];
|
|
1634
|
+
|
|
1635
|
+
opts.offset = offset;
|
|
1636
|
+
opts.limit = limit;
|
|
1637
|
+
opts.rows = rows;
|
|
1638
|
+
opts.list = data.list || { length: rows.length };
|
|
1639
|
+
|
|
1640
|
+
this.printPaginatedBoxTable(opts, job => {
|
|
1641
|
+
if (!job) return false;
|
|
1642
|
+
|
|
1643
|
+
return [
|
|
1644
|
+
this.color('theme').bold(this.getNiceJob(job)),
|
|
1645
|
+
this.getNiceJobState(job),
|
|
1646
|
+
this.getNiceJobSource(job),
|
|
1647
|
+
this.getNiceTargets(job.targets),
|
|
1648
|
+
this.getRelativeDateTime(job.started, true),
|
|
1649
|
+
this.getNiceJobElapsedTime(job, true)
|
|
1650
|
+
];
|
|
1651
|
+
});
|
|
1652
|
+
},
|
|
1653
|
+
|
|
1654
|
+
async printAlertInvocations(opts) {
|
|
1655
|
+
// Search historical or active alert invocations and print the results.
|
|
1656
|
+
// By default this shows all invocations, with the most recent first.
|
|
1657
|
+
if (!opts) opts = {};
|
|
1658
|
+
|
|
1659
|
+
var query = opts.query || '*';
|
|
1660
|
+
var offset = ('offset' in opts) ? opts.offset : this.offset;
|
|
1661
|
+
var limit = opts.limit || this.limit || this.config.items_per_page;
|
|
1662
|
+
|
|
1663
|
+
cli.progress.start();
|
|
1664
|
+
var { err, data } = await this.api.searchAlerts({
|
|
1665
|
+
query: query,
|
|
1666
|
+
offset: offset,
|
|
1667
|
+
limit: limit,
|
|
1668
|
+
sort_by: opts.sort_by || '_id',
|
|
1669
|
+
sort_dir: ('sort_dir' in opts) ? opts.sort_dir : -1
|
|
1670
|
+
});
|
|
1671
|
+
if (err) this.die(err);
|
|
1672
|
+
cli.progress.end();
|
|
1673
|
+
|
|
1674
|
+
var rows = data.rows || [];
|
|
1675
|
+
|
|
1676
|
+
// Some embedded detail screens, such as completed jobs, should omit the
|
|
1677
|
+
// section entirely when no related alerts exist.
|
|
1678
|
+
if (opts.hide_empty && !rows.length) return;
|
|
1679
|
+
|
|
1680
|
+
var is_filtered = query && (query != '*');
|
|
1681
|
+
|
|
1682
|
+
if (this.format.match(/json/)) return this.jsonOutput(rows);
|
|
1683
|
+
|
|
1684
|
+
opts.title = opts.title || (
|
|
1685
|
+
is_filtered ? 'Alert Invocation Search Results' : 'All Alert Invocations'
|
|
1686
|
+
);
|
|
1687
|
+
|
|
1688
|
+
opts.header = [
|
|
1689
|
+
'Invocation ID',
|
|
1690
|
+
'Title',
|
|
1691
|
+
'Message',
|
|
1692
|
+
'Server',
|
|
1693
|
+
'Status',
|
|
1694
|
+
'Started',
|
|
1695
|
+
'Duration'
|
|
1696
|
+
];
|
|
1697
|
+
|
|
1698
|
+
opts.offset = offset;
|
|
1699
|
+
opts.limit = limit;
|
|
1700
|
+
opts.rows = rows;
|
|
1701
|
+
opts.list = data.list || { length: rows.length };
|
|
1702
|
+
|
|
1703
|
+
this.printPaginatedBoxTable(opts, alert => {
|
|
1704
|
+
var icon = alert.active ? '🚨' : '🔔';
|
|
1705
|
+
|
|
1706
|
+
// Active alerts run through the current server time. Cleared alerts
|
|
1707
|
+
// use their final modified timestamp as the ending time.
|
|
1708
|
+
var completed = alert.active ?
|
|
1709
|
+
this.epoch :
|
|
1710
|
+
(alert.modified || alert.date);
|
|
1711
|
+
|
|
1712
|
+
var elapsed = Math.floor(
|
|
1713
|
+
Math.max(0, completed - alert.date)
|
|
1714
|
+
);
|
|
1715
|
+
|
|
1716
|
+
// Keep multiline alert messages from breaking the table layout.
|
|
1717
|
+
var message = String(alert.message || '')
|
|
1718
|
+
.replace(/\s+/g, ' ')
|
|
1719
|
+
.trim();
|
|
1720
|
+
|
|
1721
|
+
return [
|
|
1722
|
+
this.color('theme').bold(alert.id),
|
|
1723
|
+
this.getNiceAlert(alert.alert),
|
|
1724
|
+
message,
|
|
1725
|
+
this.getNiceServer(alert.server),
|
|
1726
|
+
this.getNiceAlertStatus(alert),
|
|
1727
|
+
this.getRelativeDateTime(alert.date, true),
|
|
1728
|
+
Tools.getTextFromSeconds(elapsed, true, true)
|
|
1729
|
+
];
|
|
1730
|
+
});
|
|
1731
|
+
},
|
|
1732
|
+
|
|
1733
|
+
async printSnapshots(opts) {
|
|
1734
|
+
// Search server or group snapshots and print the results.
|
|
1735
|
+
// By default this shows all snapshots, with the most recent first.
|
|
1736
|
+
if (!opts) opts = {};
|
|
1737
|
+
|
|
1738
|
+
var query = opts.query || '*';
|
|
1739
|
+
var offset = ('offset' in opts) ? opts.offset : this.offset;
|
|
1740
|
+
var limit = opts.limit || this.limit || this.config.items_per_page;
|
|
1741
|
+
|
|
1742
|
+
cli.progress.start();
|
|
1743
|
+
var { err, data } = await this.api.searchSnapshots({
|
|
1744
|
+
query: query,
|
|
1745
|
+
offset: offset,
|
|
1746
|
+
limit: limit,
|
|
1747
|
+
sort_by: opts.sort_by || '_id',
|
|
1748
|
+
sort_dir: ('sort_dir' in opts) ? opts.sort_dir : -1,
|
|
1749
|
+
verbose: !!opts.verbose
|
|
1750
|
+
});
|
|
1751
|
+
if (err) this.die(err);
|
|
1752
|
+
cli.progress.end();
|
|
1753
|
+
|
|
1754
|
+
var rows = data.rows || [];
|
|
1755
|
+
if (this.format.match(/json/)) return this.jsonOutput(rows);
|
|
1756
|
+
|
|
1757
|
+
// Embedded detail screens should omit this section when no related
|
|
1758
|
+
// snapshots were found.
|
|
1759
|
+
if (opts.hide_empty && !rows.length) return;
|
|
1760
|
+
|
|
1761
|
+
var is_filtered = query && (query != '*');
|
|
1762
|
+
|
|
1763
|
+
opts.title = opts.title || (
|
|
1764
|
+
is_filtered ? 'Snapshot Search Results' : 'All Snapshots'
|
|
1765
|
+
);
|
|
1766
|
+
|
|
1767
|
+
opts.header = [
|
|
1768
|
+
'Snapshot ID',
|
|
1769
|
+
'Source',
|
|
1770
|
+
'Target',
|
|
1771
|
+
'Uptime',
|
|
1772
|
+
'Load Avg',
|
|
1773
|
+
'Mem Avail',
|
|
1774
|
+
'Date / Time'
|
|
1775
|
+
];
|
|
1776
|
+
|
|
1777
|
+
opts.offset = offset;
|
|
1778
|
+
opts.limit = limit;
|
|
1779
|
+
opts.rows = rows;
|
|
1780
|
+
opts.list = data.list || { length: rows.length };
|
|
1781
|
+
|
|
1782
|
+
this.printPaginatedBoxTable(opts, snapshot => {
|
|
1783
|
+
var is_group = (snapshot.type == 'group');
|
|
1784
|
+
var source = String(snapshot.source || 'unknown').toLowerCase();
|
|
1785
|
+
var nice_source = Tools.ucfirst(source);
|
|
1786
|
+
|
|
1787
|
+
if ((source == 'user') && snapshot.username) {
|
|
1788
|
+
nice_source += ' (' + snapshot.username + ')';
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1791
|
+
var nice_target = 'n/a';
|
|
1792
|
+
var nice_uptime = 'n/a';
|
|
1793
|
+
var nice_load = 'n/a';
|
|
1794
|
+
var nice_memory = 'n/a';
|
|
1795
|
+
|
|
1796
|
+
if (is_group) {
|
|
1797
|
+
// Group snapshots identify their target using the first group ID.
|
|
1798
|
+
if (snapshot.groups && snapshot.groups.length) {
|
|
1799
|
+
nice_target = this.getNiceGroup(snapshot.groups[0]);
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
else {
|
|
1803
|
+
// Server snapshots contain a lightweight monitor-data object even
|
|
1804
|
+
// when verbose mode is disabled.
|
|
1805
|
+
var snap_data = snapshot.data || {};
|
|
1806
|
+
var memory = snap_data.memory || {};
|
|
1807
|
+
var load = Array.isArray(snap_data.load) ?
|
|
1808
|
+
snap_data.load : [];
|
|
1809
|
+
|
|
1810
|
+
nice_target = this.getNiceServer(snapshot.server);
|
|
1811
|
+
nice_uptime = Tools.getTextFromSeconds(
|
|
1812
|
+
snap_data.uptime_sec || 0,
|
|
1813
|
+
true,
|
|
1814
|
+
true
|
|
1815
|
+
);
|
|
1816
|
+
|
|
1817
|
+
if (load.length) {
|
|
1818
|
+
nice_load = load.map( function(value) {
|
|
1819
|
+
return Tools.shortFloat(value);
|
|
1820
|
+
} ).join(', ');
|
|
1821
|
+
}
|
|
1822
|
+
|
|
1823
|
+
nice_memory = Tools.getTextFromBytes(
|
|
1824
|
+
memory.available || 0
|
|
1825
|
+
);
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1828
|
+
return [
|
|
1829
|
+
this.color('theme').bold(snapshot.id),
|
|
1830
|
+
nice_source,
|
|
1831
|
+
nice_target,
|
|
1832
|
+
nice_uptime,
|
|
1833
|
+
nice_load,
|
|
1834
|
+
nice_memory,
|
|
1835
|
+
this.getRelativeDateTime(snapshot.date, true)
|
|
1836
|
+
];
|
|
1837
|
+
});
|
|
1838
|
+
},
|
|
1839
|
+
|
|
1840
|
+
async printTickets(opts) {
|
|
1841
|
+
// Fetch or search tickets and print the results. Explicit IDs are useful
|
|
1842
|
+
// for embedded sections such as completed-job details, while query mode
|
|
1843
|
+
// supports general ticket searches.
|
|
1844
|
+
if (!opts) opts = {};
|
|
1845
|
+
|
|
1846
|
+
var has_ids = Array.isArray(opts.ids);
|
|
1847
|
+
var query = opts.query || '*';
|
|
1848
|
+
var offset = ('offset' in opts) ? opts.offset : this.offset;
|
|
1849
|
+
var limit = opts.limit || this.limit || this.config.items_per_page;
|
|
1850
|
+
var rows = [];
|
|
1851
|
+
var list = { length: 0 };
|
|
1852
|
+
|
|
1853
|
+
if (has_ids) {
|
|
1854
|
+
// An explicitly empty ID list means there are no related tickets.
|
|
1855
|
+
if (opts.ids.length) {
|
|
1856
|
+
cli.progress.start();
|
|
1857
|
+
var { err, data } = await this.api.getTickets({
|
|
1858
|
+
ids: opts.ids
|
|
1859
|
+
});
|
|
1860
|
+
if (err) this.die(err);
|
|
1861
|
+
cli.progress.end();
|
|
1862
|
+
|
|
1863
|
+
var all_rows = data.tickets || [];
|
|
1864
|
+
list = { length: all_rows.length };
|
|
1865
|
+
rows = all_rows.slice(offset, offset + limit);
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1868
|
+
else {
|
|
1869
|
+
// No explicit IDs were supplied, so perform a normal ticket search.
|
|
1870
|
+
cli.progress.start();
|
|
1871
|
+
var { err, data } = await this.api.searchTickets({
|
|
1872
|
+
query: query,
|
|
1873
|
+
offset: offset,
|
|
1874
|
+
limit: limit,
|
|
1875
|
+
sort_by: opts.sort_by || '_id',
|
|
1876
|
+
sort_dir: ('sort_dir' in opts) ? opts.sort_dir : -1,
|
|
1877
|
+
compact: ('compact' in opts) ? !!opts.compact : true
|
|
1878
|
+
});
|
|
1879
|
+
if (err) this.die(err);
|
|
1880
|
+
cli.progress.end();
|
|
1881
|
+
|
|
1882
|
+
rows = data.rows || [];
|
|
1883
|
+
list = data.list || { length: rows.length };
|
|
1884
|
+
}
|
|
1885
|
+
|
|
1886
|
+
// Embedded detail screens should omit the section when there are no associated tickets.
|
|
1887
|
+
if (opts.hide_empty && !rows.length) return;
|
|
1888
|
+
|
|
1889
|
+
var is_filtered = !has_ids && query && (query != '*');
|
|
1890
|
+
|
|
1891
|
+
opts.title = opts.title || (
|
|
1892
|
+
has_ids ?
|
|
1893
|
+
'Tickets' :
|
|
1894
|
+
(is_filtered ? 'Ticket Search Results' : 'All Tickets')
|
|
1895
|
+
);
|
|
1896
|
+
|
|
1897
|
+
opts.header = [
|
|
1898
|
+
'#',
|
|
1899
|
+
'Subject',
|
|
1900
|
+
'Type',
|
|
1901
|
+
'Status',
|
|
1902
|
+
'Assignees',
|
|
1903
|
+
'Tags',
|
|
1904
|
+
'Created'
|
|
1905
|
+
];
|
|
1906
|
+
|
|
1907
|
+
opts.offset = offset;
|
|
1908
|
+
opts.limit = limit;
|
|
1909
|
+
opts.rows = rows;
|
|
1910
|
+
opts.list = list;
|
|
1911
|
+
|
|
1912
|
+
this.printPaginatedBoxTable(opts, ticket => {
|
|
1913
|
+
// getTickets() returns an error stub when a requested ticket was
|
|
1914
|
+
// deleted or could not be loaded.
|
|
1915
|
+
if (ticket.err) {
|
|
1916
|
+
return [
|
|
1917
|
+
cli.emoji('🗑️') + ' ' + gray(ticket.id || '#'),
|
|
1918
|
+
gray('(Ticket was deleted)'),
|
|
1919
|
+
'n/a',
|
|
1920
|
+
'n/a',
|
|
1921
|
+
'n/a',
|
|
1922
|
+
'n/a',
|
|
1923
|
+
'n/a'
|
|
1924
|
+
];
|
|
1925
|
+
}
|
|
1926
|
+
|
|
1927
|
+
var status_icons = {
|
|
1928
|
+
draft: '📝',
|
|
1929
|
+
open: '🟢',
|
|
1930
|
+
closed: '🔴'
|
|
1931
|
+
};
|
|
1932
|
+
|
|
1933
|
+
var ticket_types =
|
|
1934
|
+
(this.config.ui && this.config.ui.ticket_types) || [];
|
|
1935
|
+
var ticket_statuses =
|
|
1936
|
+
(this.config.ui && this.config.ui.ticket_statuses) || [];
|
|
1937
|
+
|
|
1938
|
+
var type_def = Tools.findObject(
|
|
1939
|
+
ticket_types,
|
|
1940
|
+
{ id: ticket.type }
|
|
1941
|
+
) || {
|
|
1942
|
+
title: Tools.ucfirst(ticket.type || 'Other')
|
|
1943
|
+
};
|
|
1944
|
+
|
|
1945
|
+
var status_def = Tools.findObject(
|
|
1946
|
+
ticket_statuses,
|
|
1947
|
+
{ id: ticket.status }
|
|
1948
|
+
) || {
|
|
1949
|
+
title: Tools.ucfirst(ticket.status || 'Unknown'),
|
|
1950
|
+
color: 'gray'
|
|
1951
|
+
};
|
|
1952
|
+
|
|
1953
|
+
var status_icon = status_icons[ticket.status] || '⚪';
|
|
1954
|
+
var nice_type = type_def.title;
|
|
1955
|
+
|
|
1956
|
+
var nice_status =
|
|
1957
|
+
cli.emoji(status_icon) + ' ' +
|
|
1958
|
+
this.color(status_def.color || 'gray').bold(status_def.title);
|
|
1959
|
+
|
|
1960
|
+
var assignees = (ticket.assignees || []).join(', ');
|
|
1961
|
+
if (!assignees) assignees = gray('(None)');
|
|
1962
|
+
|
|
1963
|
+
// Keep unexpected whitespace out of the table layout.
|
|
1964
|
+
var subject = String(ticket.subject || '(No Subject)')
|
|
1965
|
+
.replace(/\s+/g, ' ')
|
|
1966
|
+
.trim();
|
|
1967
|
+
|
|
1968
|
+
return [
|
|
1969
|
+
this.color('theme').bold('#' + ticket.num),
|
|
1970
|
+
cli.emoji('🗒️') + ' ' + subject,
|
|
1971
|
+
nice_type,
|
|
1972
|
+
nice_status,
|
|
1973
|
+
assignees,
|
|
1974
|
+
this.getNiceTagList(ticket.tags || []),
|
|
1975
|
+
this.getRelativeDateTime(ticket.created, true)
|
|
1976
|
+
];
|
|
1977
|
+
});
|
|
1978
|
+
},
|
|
1979
|
+
|
|
1980
|
+
printParamValues(opts) {
|
|
1981
|
+
// print param value with values
|
|
1982
|
+
// opts: { title, fields, values }
|
|
1983
|
+
if (!opts.fields.length) {
|
|
1984
|
+
println( "\n " + this.color('theme').bold( opts.title.toUpperCase() ) );
|
|
1985
|
+
println( gray(" No parameter definitions found.") );
|
|
1986
|
+
return;
|
|
1987
|
+
}
|
|
1988
|
+
|
|
1989
|
+
opts.header = opts.header || ['Param Title', 'ID', 'Type', 'Value'];
|
|
1990
|
+
opts.rows = [];
|
|
1991
|
+
|
|
1992
|
+
opts.fields.forEach( field => {
|
|
1993
|
+
var nice_id = field.id;
|
|
1994
|
+
var value = (field.id in opts.values) ? opts.values[field.id] : field.value;
|
|
1995
|
+
var disp_value = value;
|
|
1996
|
+
var nice_type = this.config.ui.control_type_labels[field.type] || Tools.ucfirst(field.type);
|
|
1997
|
+
|
|
1998
|
+
switch (field.type) {
|
|
1999
|
+
case 'checkbox':
|
|
2000
|
+
disp_value = value ? `${cli.emoji('✅')} ${green('Checked')}` : `${gray('Unchecked')}`
|
|
2001
|
+
break;
|
|
2002
|
+
|
|
2003
|
+
case 'text':
|
|
2004
|
+
if ((field.variant == 'password') && !this.verbose) disp_value = yellow('(Shown in verbose mode)');
|
|
2005
|
+
else disp_value = green( disp_value );
|
|
2006
|
+
break;
|
|
2007
|
+
|
|
2008
|
+
case 'textarea':
|
|
2009
|
+
case 'code':
|
|
2010
|
+
case 'json':
|
|
2011
|
+
disp_value = this.verbose ? green('(See below)') : yellow('(Shown in verbose mode)');
|
|
2012
|
+
break;
|
|
2013
|
+
|
|
2014
|
+
case 'hidden':
|
|
2015
|
+
if (!opts.hidden) return false;
|
|
2016
|
+
disp_value = green( disp_value );
|
|
2017
|
+
break;
|
|
2018
|
+
|
|
2019
|
+
case 'group':
|
|
2020
|
+
if (!opts.hidden) return false;
|
|
2021
|
+
nice_id = '-';
|
|
2022
|
+
disp_value = '-';
|
|
2023
|
+
break;
|
|
2024
|
+
|
|
2025
|
+
case 'select':
|
|
2026
|
+
if (field.multiple) nice_type += ' (Multi)';
|
|
2027
|
+
disp_value = green( disp_value );
|
|
2028
|
+
break;
|
|
2029
|
+
|
|
2030
|
+
case 'bucket':
|
|
2031
|
+
disp_value = this.getNiceBucket(field.bucket_id);
|
|
2032
|
+
if (field.multiple) nice_type += ' (Multi)';
|
|
2033
|
+
break;
|
|
2034
|
+
|
|
2035
|
+
case 'system':
|
|
2036
|
+
disp_value = this.getNiceSystemList(field.list_id);
|
|
2037
|
+
if (field.multiple) nice_type += ' (Multi)';
|
|
2038
|
+
break;
|
|
2039
|
+
|
|
2040
|
+
case 'toolset':
|
|
2041
|
+
if (opts.hidden) {
|
|
2042
|
+
if (field.data && field.data.tools && field.data.tools.length) disp_value = Tools.commify(field.data.tools.length) + " tools in set";
|
|
2043
|
+
else disp_value = "(No tools in set)";
|
|
2044
|
+
}
|
|
2045
|
+
else {
|
|
2046
|
+
disp_value = green( disp_value );
|
|
2047
|
+
}
|
|
2048
|
+
break;
|
|
2049
|
+
|
|
2050
|
+
default:
|
|
2051
|
+
disp_value = green( disp_value );
|
|
2052
|
+
break;
|
|
2053
|
+
} // switch field.type
|
|
2054
|
+
|
|
2055
|
+
opts.rows.push([
|
|
2056
|
+
bold(field.title),
|
|
2057
|
+
gray(nice_id),
|
|
2058
|
+
nice_type,
|
|
2059
|
+
disp_value
|
|
2060
|
+
]);
|
|
2061
|
+
} );
|
|
2062
|
+
|
|
2063
|
+
this.printBoxTable(opts);
|
|
2064
|
+
|
|
2065
|
+
opts.fields.filter( field => !!field.type.match(/^(toolset)$/) ).forEach( toolset => {
|
|
2066
|
+
var tools = (toolset.data && toolset.data.tools) || [];
|
|
2067
|
+
if (!tools.length) return;
|
|
2068
|
+
|
|
2069
|
+
// Definition views need to describe every available tool. Value views
|
|
2070
|
+
// only need the selected tool, because the others have no active values.
|
|
2071
|
+
var display_tools = tools;
|
|
2072
|
+
if (!opts.hidden) {
|
|
2073
|
+
var tool_sel = opts.values[toolset.id] || tools[0].id;
|
|
2074
|
+
if (tool_sel && (typeof(tool_sel) == 'object')) tool_sel = tool_sel.id;
|
|
2075
|
+
display_tools = [ Tools.findObject( tools, { id: tool_sel } ) || tools[0] ];
|
|
2076
|
+
}
|
|
2077
|
+
|
|
2078
|
+
display_tools.forEach( tool => {
|
|
2079
|
+
this.printParamValues({
|
|
2080
|
+
title: toolset.title + ": " + tool.title,
|
|
2081
|
+
fields: tool.fields || [],
|
|
2082
|
+
values: opts.values,
|
|
2083
|
+
hidden: opts.hidden
|
|
2084
|
+
});
|
|
2085
|
+
});
|
|
2086
|
+
});
|
|
2087
|
+
|
|
2088
|
+
if (this.verbose) {
|
|
2089
|
+
opts.fields.filter( field => !!field.type.match(/^(textarea|code|json)$/) ).forEach( field => {
|
|
2090
|
+
var value = (field.id in opts.values) ? opts.values[field.id] : field.value;
|
|
2091
|
+
var disp_value = (typeof(value) == 'object') ? JSON.stringify(value, null, "\t") : value;
|
|
2092
|
+
println( "\n " + this.color('theme').bold( 'PARAMETER: ' + field.title.toUpperCase() ) );
|
|
2093
|
+
|
|
2094
|
+
disp_value = String(disp_value).trim().split(/\n/).map( line => ' ' + line ).join("\n");
|
|
2095
|
+
|
|
2096
|
+
if (field.type == 'json') println( this.highlight(disp_value, { language: 'json', ignoreIllegals: true }) );
|
|
2097
|
+
else println( disp_value );
|
|
2098
|
+
} );
|
|
2099
|
+
}
|
|
2100
|
+
},
|
|
2101
|
+
|
|
2102
|
+
printParamFields(opts) {
|
|
2103
|
+
// print param field definitions
|
|
2104
|
+
// opts: { title, fields }
|
|
2105
|
+
opts.header = ['Param Title', 'ID', 'Type', 'Default Value'];
|
|
2106
|
+
opts.values = {};
|
|
2107
|
+
opts.hidden = true;
|
|
2108
|
+
|
|
2109
|
+
this.printParamValues(opts);
|
|
2110
|
+
},
|
|
2111
|
+
|
|
2112
|
+
printPluginScript(plugin, opts) {
|
|
2113
|
+
// Print embedded Plugin source consistently wherever a full definition is
|
|
2114
|
+
// shown. Prefer the executable language, then inspect the script shebang,
|
|
2115
|
+
// and only fall back to cli-highlight's slower automatic detection.
|
|
2116
|
+
opts = opts || {};
|
|
2117
|
+
var script = plugin.script || '';
|
|
2118
|
+
if (!script) return;
|
|
2119
|
+
|
|
2120
|
+
println( "\n " + this.color('theme').bold('PLUGIN SCRIPT') );
|
|
2121
|
+
if (opts.verboseOnly && !this.verbose) {
|
|
2122
|
+
println( " " + gray("(Shown in verbose mode)") );
|
|
2123
|
+
return;
|
|
2124
|
+
}
|
|
2125
|
+
|
|
2126
|
+
var language = this.getLangFromBinary(plugin.command);
|
|
2127
|
+
if (!language && script.match(/^\#\!(\/\S+)/)) language = this.getLangFromBinary(RegExp.$1);
|
|
2128
|
+
|
|
2129
|
+
var source_lines = script.trim().split(/\r?\n/).map( line => ' ' + line ).join("\n");
|
|
2130
|
+
var highlight_opts = { ignoreIllegals: true };
|
|
2131
|
+
if (language) highlight_opts.language = language;
|
|
2132
|
+
println( "\n" + this.highlight(source_lines, highlight_opts) );
|
|
2133
|
+
},
|
|
2134
|
+
|
|
2135
|
+
getNiceSystemList(list_id) {
|
|
2136
|
+
// get formatted list title with icon
|
|
2137
|
+
var item = Tools.findObject( this.getSystemListMenuItems(), { id: list_id } );
|
|
2138
|
+
if (!item) return '(None)';
|
|
2139
|
+
return item.title;
|
|
2140
|
+
},
|
|
2141
|
+
|
|
2142
|
+
getSystemListMenuItems() {
|
|
2143
|
+
// get menu of all lists (excluding a few)
|
|
2144
|
+
var items = this.config.ui.list_list.filter( function(item) {
|
|
2145
|
+
return !item.id.match(/^(api_keys|secrets)$/);
|
|
2146
|
+
} );
|
|
2147
|
+
|
|
2148
|
+
items = items.concat([
|
|
2149
|
+
{
|
|
2150
|
+
"id": "servers",
|
|
2151
|
+
"title": "Servers"
|
|
2152
|
+
},
|
|
2153
|
+
{
|
|
2154
|
+
"id": "groups",
|
|
2155
|
+
"title": "Groups"
|
|
2156
|
+
},
|
|
2157
|
+
{
|
|
2158
|
+
"id": "targets",
|
|
2159
|
+
"title": "Targets"
|
|
2160
|
+
},
|
|
2161
|
+
{
|
|
2162
|
+
"id": "algos",
|
|
2163
|
+
"title": "Algorithms"
|
|
2164
|
+
},
|
|
2165
|
+
{
|
|
2166
|
+
"id": "icons",
|
|
2167
|
+
"title": "Icons"
|
|
2168
|
+
}
|
|
2169
|
+
]);
|
|
2170
|
+
|
|
2171
|
+
Tools.sortBy( items, 'title' );
|
|
2172
|
+
return items;
|
|
2173
|
+
},
|
|
2174
|
+
|
|
2175
|
+
markdown(text) {
|
|
2176
|
+
// Convert GitHub-style Markdown into ANSI terminal output. The renderer
|
|
2177
|
+
// width above already reserves both margins, so only the visible left-side
|
|
2178
|
+
// padding needs to be added to each completed output line.
|
|
2179
|
+
var output = marked.parse(String(text)).trimEnd();
|
|
2180
|
+
if (!cli.chalk.enabled) output = Tools.stripANSI(output);
|
|
2181
|
+
return output.split('\n').map( line => ' '.repeat(MARKDOWN_PADDING) + line ).join('\n');
|
|
2182
|
+
},
|
|
2183
|
+
|
|
2184
|
+
ellipsis(text, max_len) {
|
|
2185
|
+
// add ellipsis to text if over N length
|
|
2186
|
+
text = '' + text;
|
|
2187
|
+
if (cli.stringWidth(text) >= max_len) {
|
|
2188
|
+
return text.substring(0, max_len - 1) + '…';
|
|
2189
|
+
}
|
|
2190
|
+
else return text;
|
|
2191
|
+
},
|
|
2192
|
+
|
|
2193
|
+
getLastDayInMonth: function(year, month) {
|
|
2194
|
+
// compute the last day in the month, and cache in RAM
|
|
2195
|
+
var cache_key = '' + year + '/' + month;
|
|
2196
|
+
if (cache_key in this.lastMonthDayCache) return this.lastMonthDayCache[cache_key];
|
|
2197
|
+
|
|
2198
|
+
var last_day = new Date(year, month, 0).getDate();
|
|
2199
|
+
this.lastMonthDayCache[cache_key] = last_day;
|
|
2200
|
+
|
|
2201
|
+
return last_day;
|
|
2202
|
+
},
|
|
2203
|
+
|
|
2204
|
+
predictUpcomingJobs(opts) {
|
|
2205
|
+
// simulate schedule for predicting future jobs
|
|
2206
|
+
// opts: { events, duration, burn, max, progress, callback }
|
|
2207
|
+
if (!opts.start) opts.start = Tools.normalizeTime( Math.floor(this.epoch), { sec: 0 } ) + 60; // next minute
|
|
2208
|
+
if (!opts.duration) opts.duration = 86400; // default 24 hours
|
|
2209
|
+
if (!opts.burn) opts.burn = 1000; // default cpu time per chunk
|
|
2210
|
+
if (!opts.max) opts.max = 1000; // max 1k jobs in array
|
|
2211
|
+
|
|
2212
|
+
opts.epoch = opts.start; // starting time
|
|
2213
|
+
opts.end = opts.start + opts.duration; // where to stop (inclusive)
|
|
2214
|
+
opts.jobs = []; // output jobs
|
|
2215
|
+
opts.formatters = {}; // intl formatters
|
|
2216
|
+
|
|
2217
|
+
opts.events = Tools.copyHash(opts.events, true).filter( event => {
|
|
2218
|
+
if (!event.enabled) return false;
|
|
2219
|
+
if (!event.triggers || !event.triggers.length) return false; // on-demand
|
|
2220
|
+
|
|
2221
|
+
// check for disabled category
|
|
2222
|
+
var category = Tools.findObject( this.categories, { id: event.category } );
|
|
2223
|
+
if (!category.enabled) return false;
|
|
2224
|
+
|
|
2225
|
+
// check for disabled plugin
|
|
2226
|
+
if (event.plugin && !event.plugin.match(/^_/)) {
|
|
2227
|
+
var plugin = Tools.findObject( this.plugins, { id: event.plugin } );
|
|
2228
|
+
if (plugin && !plugin.enabled) return false;
|
|
2229
|
+
}
|
|
2230
|
+
|
|
2231
|
+
// process triggers
|
|
2232
|
+
var triggers = event.triggers.filter( function(trigger) { return trigger.enabled; } );
|
|
2233
|
+
var schedules = triggers.filter( function(trigger) { return trigger.type.match(/^(schedule|single|interval)$/); } );
|
|
2234
|
+
if (!schedules.length) return false;
|
|
2235
|
+
|
|
2236
|
+
// quiet (invisible) mode
|
|
2237
|
+
var quiet = Tools.findObject( triggers, { type: 'quiet' } );
|
|
2238
|
+
if (quiet && quiet.invisible) {
|
|
2239
|
+
if (!this.invisible) return false;
|
|
2240
|
+
event.invisible = true;
|
|
2241
|
+
}
|
|
2242
|
+
|
|
2243
|
+
// setup all unique timezones (intl formatters)
|
|
2244
|
+
schedules.forEach( trigger => {
|
|
2245
|
+
if (trigger.type != 'schedule') return;
|
|
2246
|
+
var tz = trigger.timezone || this.config.tz;
|
|
2247
|
+
if (tz in opts.formatters) return; // already setup
|
|
2248
|
+
|
|
2249
|
+
opts.formatters[tz] = new Intl.DateTimeFormat('en-US',
|
|
2250
|
+
{ year: 'numeric', month: '2-digit', day: 'numeric', weekday: 'long', hour: 'numeric', minute: '2-digit', hourCycle: 'h23', timeZone: tz }
|
|
2251
|
+
);
|
|
2252
|
+
} );
|
|
2253
|
+
|
|
2254
|
+
// store some props for fast access below
|
|
2255
|
+
event.schedules = schedules;
|
|
2256
|
+
event.ranges = triggers.filter( function(trigger) { return (trigger.type == 'range') || (trigger.type == 'blackout'); } );
|
|
2257
|
+
event.plugin_trigger = Tools.findObject( triggers, { type: 'plugin', enabled: true } );
|
|
2258
|
+
event.nth_trigger = Tools.findObject( triggers, { type: 'nth', enabled: true } );
|
|
2259
|
+
event.day_limits = Tools.findObjects( event.limits || [], { type: 'day', enabled: true } ) || [];
|
|
2260
|
+
|
|
2261
|
+
// add deep copies of event stats and state, so we can simulate mutations
|
|
2262
|
+
event.stats = Tools.copyHash( Tools.getPath( this.stats.currentDay, 'events.' + event.id ) || {}, true );
|
|
2263
|
+
event.state = Tools.copyHash( Tools.getPath( this.state, 'events.' + event.id ) || {}, true );
|
|
2264
|
+
|
|
2265
|
+
// if we have day_limits, we need a formatter for the server timezone (whew!)
|
|
2266
|
+
if (event.day_limits.length && !opts.formatters[ this.config.tz ]) {
|
|
2267
|
+
opts.formatters[ this.config.tz ] = new Intl.DateTimeFormat('en-US',
|
|
2268
|
+
{ year: 'numeric', month: '2-digit', day: 'numeric', weekday: 'long', hour: 'numeric', minute: '2-digit', hourCycle: 'h23', timeZone: this.config.tz }
|
|
2269
|
+
);
|
|
2270
|
+
}
|
|
2271
|
+
|
|
2272
|
+
return true;
|
|
2273
|
+
} ); // filter events
|
|
2274
|
+
|
|
2275
|
+
if (!opts.events.length) return opts.callback([]);
|
|
2276
|
+
|
|
2277
|
+
this.currentPrediction = opts;
|
|
2278
|
+
this.predictNextChunk();
|
|
2279
|
+
},
|
|
2280
|
+
|
|
2281
|
+
predictNextChunk() {
|
|
2282
|
+
// predict one chunk of upcoming jobs (limit CPU burn)
|
|
2283
|
+
var self = this;
|
|
2284
|
+
var opts = this.currentPrediction;
|
|
2285
|
+
if (!opts) return; // sanity
|
|
2286
|
+
|
|
2287
|
+
var pstart = performance.now();
|
|
2288
|
+
var date = new Date();
|
|
2289
|
+
var tzargs = {};
|
|
2290
|
+
var days = { Sunday: 0, Monday: 1, Tuesday: 2, Wednesday: 3, Thursday: 4, Friday: 5, Saturday: 6 };
|
|
2291
|
+
|
|
2292
|
+
do {
|
|
2293
|
+
// predict jobs for current minute
|
|
2294
|
+
date.setTime( opts.epoch * 1000 );
|
|
2295
|
+
|
|
2296
|
+
// convert date to all unique timezones we care about and argify it
|
|
2297
|
+
// { month: 11, day: 29, weekday: 2, year: 2022, hour: 22, minute: 29 }
|
|
2298
|
+
for (var tz in opts.formatters) {
|
|
2299
|
+
if (!tzargs[tz]) tzargs[tz] = {};
|
|
2300
|
+
|
|
2301
|
+
opts.formatters[tz].formatToParts(date).forEach( function(part) {
|
|
2302
|
+
if (part.type == 'literal') return;
|
|
2303
|
+
if (part.type == 'weekday') tzargs[tz][ part.type ] = days[ part.value ];
|
|
2304
|
+
else tzargs[tz][ part.type ] = parseInt( part.value );
|
|
2305
|
+
} );
|
|
2306
|
+
|
|
2307
|
+
// include reverse-month-day (rday): -1 is last day of month, -2 is 2nd-to-last day, etc.
|
|
2308
|
+
tzargs[tz].rday = (tzargs[tz].day - this.getLastDayInMonth( tzargs[tz].year, tzargs[tz].month )) - 1;
|
|
2309
|
+
}
|
|
2310
|
+
|
|
2311
|
+
// keep track of day rollovers for day_limits
|
|
2312
|
+
var is_server_midnight = false;
|
|
2313
|
+
if (tzargs[this.config.tz] && (tzargs[this.config.tz].hour == 0) && (tzargs[this.config.tz].minute == 0)) is_server_midnight = true;
|
|
2314
|
+
|
|
2315
|
+
// do any events need to run this minute?
|
|
2316
|
+
// { "type": "schedule", "enabled": true, "years": [2023], "months": [3, 4, 5], "days": [1, 15], "weekdays": [1, 2, 3, 4, 5], "hours": [6, 7, 8, 9, 10], "minutes": [15, 45] }
|
|
2317
|
+
opts.events.forEach( event => {
|
|
2318
|
+
var scheduled = false;
|
|
2319
|
+
var extras = {};
|
|
2320
|
+
|
|
2321
|
+
// reset stats at server midnight (simulated)
|
|
2322
|
+
if (is_server_midnight) event.stats = {};
|
|
2323
|
+
|
|
2324
|
+
event.schedules.forEach( trigger => {
|
|
2325
|
+
if ((trigger.type == 'single') && (trigger.epoch == opts.epoch)) {
|
|
2326
|
+
scheduled = 'single';
|
|
2327
|
+
return;
|
|
2328
|
+
}
|
|
2329
|
+
if (trigger.type == 'interval') {
|
|
2330
|
+
var hits = interval_hits_per_minute(trigger, opts.epoch);
|
|
2331
|
+
if (hits.length) {
|
|
2332
|
+
scheduled = 'interval';
|
|
2333
|
+
extras.seconds = hits;
|
|
2334
|
+
return;
|
|
2335
|
+
}
|
|
2336
|
+
}
|
|
2337
|
+
|
|
2338
|
+
if (trigger.type != 'schedule') return; // sanity
|
|
2339
|
+
var tz = trigger.timezone || this.config.tz;
|
|
2340
|
+
var dargs = tzargs[tz];
|
|
2341
|
+
|
|
2342
|
+
if (trigger.years && trigger.years.length && !trigger.years.includes(dargs.year)) return;
|
|
2343
|
+
if (trigger.months && trigger.months.length && !trigger.months.includes(dargs.month)) return;
|
|
2344
|
+
if (trigger.days && trigger.days.length && !trigger.days.includes(dargs.day) && !trigger.days.includes(dargs.rday)) return;
|
|
2345
|
+
if (trigger.weekdays && trigger.weekdays.length && !trigger.weekdays.includes(dargs.weekday)) return;
|
|
2346
|
+
if (trigger.hours && trigger.hours.length && !trigger.hours.includes(dargs.hour)) return;
|
|
2347
|
+
if (trigger.minutes && trigger.minutes.length && !trigger.minutes.includes(dargs.minute)) return;
|
|
2348
|
+
|
|
2349
|
+
scheduled = 'schedule';
|
|
2350
|
+
} ); // foreach schedule
|
|
2351
|
+
|
|
2352
|
+
if (!scheduled) return;
|
|
2353
|
+
|
|
2354
|
+
// check ranges
|
|
2355
|
+
// (both start/end dates are INCLUSIVE)
|
|
2356
|
+
event.ranges.forEach( function(trigger) {
|
|
2357
|
+
switch (trigger.type) {
|
|
2358
|
+
case 'range':
|
|
2359
|
+
if (trigger.start && (opts.epoch < trigger.start)) scheduled = false;
|
|
2360
|
+
else if (trigger.end && (opts.epoch > trigger.end)) scheduled = false;
|
|
2361
|
+
break;
|
|
2362
|
+
|
|
2363
|
+
case 'blackout':
|
|
2364
|
+
if ((opts.epoch >= trigger.start) && (opts.epoch <= trigger.end)) scheduled = false;
|
|
2365
|
+
break;
|
|
2366
|
+
}
|
|
2367
|
+
} );
|
|
2368
|
+
|
|
2369
|
+
if (!scheduled) return;
|
|
2370
|
+
|
|
2371
|
+
// check day limits
|
|
2372
|
+
event.day_limits.forEach( function(limit) {
|
|
2373
|
+
var count = event.stats[ 'job_' + limit.condition ] || 0;
|
|
2374
|
+
if (limit.amount && (count >= limit.amount)) scheduled = false;
|
|
2375
|
+
} );
|
|
2376
|
+
|
|
2377
|
+
if (!scheduled) return;
|
|
2378
|
+
|
|
2379
|
+
// check every nth
|
|
2380
|
+
if (event.nth_trigger) {
|
|
2381
|
+
var nth_counter = (event.state.nth || 0) - 1;
|
|
2382
|
+
if (nth_counter <= 0) nth_counter = event.nth_trigger.every;
|
|
2383
|
+
else scheduled = false;
|
|
2384
|
+
event.state.nth = nth_counter;
|
|
2385
|
+
}
|
|
2386
|
+
|
|
2387
|
+
if (!scheduled) return;
|
|
2388
|
+
|
|
2389
|
+
// add plugin modifier if applicable
|
|
2390
|
+
if (event.plugin_trigger) extras.plugin = event.plugin_trigger.plugin_id;
|
|
2391
|
+
|
|
2392
|
+
// add invisible flag if applicable
|
|
2393
|
+
if (event.invisible) extras.invisible = true;
|
|
2394
|
+
|
|
2395
|
+
// add job!
|
|
2396
|
+
opts.jobs.push({ event: event.id, epoch: opts.epoch, type: scheduled, ...extras });
|
|
2397
|
+
|
|
2398
|
+
// simulate event stat increments (used by day_limits)
|
|
2399
|
+
event.stats.job_start = (event.stats.job_start || 0) + 1;
|
|
2400
|
+
event.stats.job_complete = (event.stats.job_complete || 0) + 1;
|
|
2401
|
+
event.stats.job_success = (event.stats.job_success || 0) + 1;
|
|
2402
|
+
} ); // foreach event
|
|
2403
|
+
|
|
2404
|
+
opts.epoch += 60; // skip to next minute
|
|
2405
|
+
}
|
|
2406
|
+
while ((performance.now() - pstart < opts.burn) && (opts.epoch <= opts.end));
|
|
2407
|
+
|
|
2408
|
+
if ((opts.epoch > opts.end) || (opts.jobs.length >= opts.max)) {
|
|
2409
|
+
// all done, reached target epoch (inclusive)
|
|
2410
|
+
if (opts.jobs.length > opts.max) opts.jobs.splice( opts.max );
|
|
2411
|
+
delete this.currentPrediction;
|
|
2412
|
+
return opts.callback(opts.jobs);
|
|
2413
|
+
}
|
|
2414
|
+
else {
|
|
2415
|
+
// not done, optionally call progress handler
|
|
2416
|
+
if (opts.progress) opts.progress( (opts.epoch - opts.start) / opts.duration, opts.jobs );
|
|
2417
|
+
|
|
2418
|
+
// schedule next chunk after a frame of sleep
|
|
2419
|
+
setTimeout( function() { self.predictNextChunk(); }, 1 );
|
|
2420
|
+
}
|
|
2421
|
+
},
|
|
2422
|
+
|
|
2423
|
+
getDateRangeQuery(key, value) {
|
|
2424
|
+
// get formatted epoch/3600 date range for DB queries
|
|
2425
|
+
// now, lasthour, today, yesterday, month, lastmonth, year, lastyear, older
|
|
2426
|
+
var query = '';
|
|
2427
|
+
var now = Math.floor(this.epoch);
|
|
2428
|
+
var dargs = get_date_args( now );
|
|
2429
|
+
|
|
2430
|
+
switch (value) {
|
|
2431
|
+
case 'now':
|
|
2432
|
+
case 'hour':
|
|
2433
|
+
query += '' + key + ':' + now;
|
|
2434
|
+
break;
|
|
2435
|
+
|
|
2436
|
+
case 'lasthour':
|
|
2437
|
+
var date_code = now - 3600;
|
|
2438
|
+
query += '' + key + ':' + date_code;
|
|
2439
|
+
break;
|
|
2440
|
+
|
|
2441
|
+
case 'today':
|
|
2442
|
+
var epoch = parse_date( dargs.year + '-' + dargs.month + '-' + dargs.day + ' 00:00:00' );
|
|
2443
|
+
query += '' + key + ':>=' + epoch;
|
|
2444
|
+
break;
|
|
2445
|
+
|
|
2446
|
+
case 'yesterday':
|
|
2447
|
+
var midnight = parse_date( dargs.year + '-' + dargs.month + '-' + dargs.day + ' 00:00:00' ); // get epoch of midnight today
|
|
2448
|
+
var noon = parse_date( dargs.year + '-' + dargs.month + '-' + dargs.day + ' 12:00:00' ); // get epoch of noon today
|
|
2449
|
+
var yesterday = noon - 86400; // subtract 1d for yesterday -- can be +/- 1 hour off
|
|
2450
|
+
dargs = get_date_args( yesterday ); // get dargs for yesterday
|
|
2451
|
+
var yesterday_midnight = parse_date( dargs.year + '-' + dargs.month + '-' + dargs.day + ' 00:00:00' ); // get epoch of midnight yesterday
|
|
2452
|
+
query += '' + key + ':' + yesterday_midnight + '..' + Math.floor(midnight - 1);
|
|
2453
|
+
break;
|
|
2454
|
+
|
|
2455
|
+
case 'month':
|
|
2456
|
+
var cur_month = parse_date( dargs.year + '-' + dargs.month + '-01 00:00:00' ); // get epoch of midnight on first month day
|
|
2457
|
+
query += '' + key + ':>=' + cur_month;
|
|
2458
|
+
break;
|
|
2459
|
+
|
|
2460
|
+
case 'lastmonth':
|
|
2461
|
+
var cur_month = parse_date( dargs.year + '-' + dargs.month + '-01 00:00:00' ); // get epoch of midnight on first month day
|
|
2462
|
+
var before = cur_month - (86400 * 15); // sometime in last month -- does not need to be exact
|
|
2463
|
+
dargs = get_date_args( before ); // get dargs for last month
|
|
2464
|
+
var last_month = parse_date( dargs.year + '-' + dargs.month + '-01 00:00:00' ); // get epoch of midnight on first day of last month
|
|
2465
|
+
query += '' + key + ':' + last_month + '..' + Math.floor(cur_month - 1);
|
|
2466
|
+
break;
|
|
2467
|
+
|
|
2468
|
+
case 'year':
|
|
2469
|
+
var cur_year = parse_date( dargs.year + '-01-01 00:00:00' ); // get epoch of midnight on first year day
|
|
2470
|
+
query += '' + key + ':>=' + cur_year;
|
|
2471
|
+
break;
|
|
2472
|
+
|
|
2473
|
+
case 'lastyear':
|
|
2474
|
+
var cur_year = parse_date( dargs.year + '-01-01 00:00:00' ); // get epoch of midnight on first year day
|
|
2475
|
+
var before = cur_year - (86400 * 180); // sometime in last year -- does not need to be exact
|
|
2476
|
+
dargs = get_date_args( before ); // get dargs for last year
|
|
2477
|
+
var last_year = parse_date( dargs.year + '-01-01 00:00:00' ); // get epoch of midnight on first day of last year
|
|
2478
|
+
query += '' + key + ':' + last_year + '..' + Math.floor(cur_year - 1);
|
|
2479
|
+
break;
|
|
2480
|
+
|
|
2481
|
+
case 'older':
|
|
2482
|
+
var cur_year = parse_date( dargs.year + '-01-01 00:00:00' ); // get epoch of midnight on first year day
|
|
2483
|
+
var before = cur_year - (86400 * 180); // sometime in last year -- does not need to be exact
|
|
2484
|
+
dargs = get_date_args( before ); // get dargs for last year
|
|
2485
|
+
var last_year = parse_date( dargs.year + '-01-01 00:00:00' ); // get epoch of midnight on first day of last year
|
|
2486
|
+
query += '' + key + ':<' + last_year;
|
|
2487
|
+
break;
|
|
2488
|
+
} // switch
|
|
2489
|
+
|
|
2490
|
+
return query;
|
|
2491
|
+
},
|
|
2492
|
+
|
|
2493
|
+
maskValue(value) {
|
|
2494
|
+
// mask value for display purposes
|
|
2495
|
+
value = String(value);
|
|
2496
|
+
if (value.length < 16) return '********';
|
|
2497
|
+
if (value.length < 32) return value.substring(0, 2) + ('*').repeat(8) + value.substring(value.length - 2);
|
|
2498
|
+
return value.substring(0, 4) + ('*').repeat(8) + value.substring(value.length - 4);
|
|
2499
|
+
},
|
|
2500
|
+
|
|
2501
|
+
printNiceHTML(html) {
|
|
2502
|
+
// Convert user-generated HTML into terminal-friendly plain text.
|
|
2503
|
+
if (!html) return;
|
|
2504
|
+
|
|
2505
|
+
var text = convertHTMLToText(String(html), {
|
|
2506
|
+
wordwrap: false,
|
|
2507
|
+
preserveNewlines: false
|
|
2508
|
+
}).trim();
|
|
2509
|
+
|
|
2510
|
+
println( text.split(/\n/).map( line => ' ' + line ).join("\n") );
|
|
2511
|
+
},
|
|
2512
|
+
|
|
2513
|
+
getOutputStream() {
|
|
2514
|
+
var download = new PassThrough();
|
|
2515
|
+
download.pipe(process.stdout, {
|
|
2516
|
+
end: false
|
|
2517
|
+
});
|
|
2518
|
+
return download;
|
|
2519
|
+
},
|
|
2520
|
+
|
|
2521
|
+
getTextFromSecondsRound(sec, abbrev) {
|
|
2522
|
+
// Public wrapper around the local duration formatting helper.
|
|
2523
|
+
return get_text_from_seconds_round(sec, abbrev);
|
|
2524
|
+
},
|
|
2525
|
+
|
|
2526
|
+
parseCrontab(raw, rand_seed) {
|
|
2527
|
+
// alias for parse_crontab
|
|
2528
|
+
return parse_crontab(raw, rand_seed);
|
|
2529
|
+
}
|
|
2530
|
+
|
|
2531
|
+
}; // module.exports
|
|
2532
|
+
|
|
2533
|
+
function interval_hits_per_minute(trigger, epoch) {
|
|
2534
|
+
// calculate when an interval should hit in the current minute (epoch)
|
|
2535
|
+
// return an array of second offsets, similar to precision.seconds
|
|
2536
|
+
// trigger: { start, duration }
|
|
2537
|
+
if (trigger.start > epoch) return []; // trigger starts in future
|
|
2538
|
+
|
|
2539
|
+
var first_idx = Math.ceil((epoch - trigger.start) / trigger.duration);
|
|
2540
|
+
var first_hit = trigger.start + first_idx * trigger.duration;
|
|
2541
|
+
var hits = [];
|
|
2542
|
+
|
|
2543
|
+
for (var t = first_hit; t < epoch + 60; t += trigger.duration) {
|
|
2544
|
+
if (t >= epoch) hits.push(t - epoch);
|
|
2545
|
+
}
|
|
2546
|
+
|
|
2547
|
+
return hits;
|
|
2548
|
+
};
|
|
2549
|
+
|
|
2550
|
+
function get_text_from_seconds_round(sec, abbrev) {
|
|
2551
|
+
// convert raw seconds to human-readable relative time
|
|
2552
|
+
// round to nearest instead of floor
|
|
2553
|
+
var neg = '';
|
|
2554
|
+
sec = Math.round(sec);
|
|
2555
|
+
if (sec < 0) { sec =- sec; neg = '-'; }
|
|
2556
|
+
|
|
2557
|
+
var suffix = abbrev ? "sec" : "second";
|
|
2558
|
+
var amt = sec;
|
|
2559
|
+
|
|
2560
|
+
if (sec > 59) {
|
|
2561
|
+
var min = Math.round(sec / 60);
|
|
2562
|
+
suffix = abbrev ? "min" : "minute";
|
|
2563
|
+
amt = min;
|
|
2564
|
+
|
|
2565
|
+
if (min > 59) {
|
|
2566
|
+
var hour = Math.round(min / 60);
|
|
2567
|
+
suffix = abbrev ? "hr" : "hour";
|
|
2568
|
+
amt = hour;
|
|
2569
|
+
|
|
2570
|
+
if (hour > 23) {
|
|
2571
|
+
var day = Math.round(hour / 24);
|
|
2572
|
+
suffix = "day";
|
|
2573
|
+
amt = day;
|
|
2574
|
+
} // hour>23
|
|
2575
|
+
} // min>59
|
|
2576
|
+
} // sec>59
|
|
2577
|
+
|
|
2578
|
+
if (abbrev === 2) suffix = suffix.substring(0, 1);
|
|
2579
|
+
|
|
2580
|
+
var text = "" + amt + " " + suffix;
|
|
2581
|
+
if ((amt != 1) && !abbrev) text += "s";
|
|
2582
|
+
if (abbrev === 2) text = text.replace(/\s+/g, '');
|
|
2583
|
+
|
|
2584
|
+
return(neg + text);
|
|
2585
|
+
};
|
|
2586
|
+
|
|
2587
|
+
function parse_date(str) {
|
|
2588
|
+
// parse date into epoch
|
|
2589
|
+
return Math.floor( ((new Date(str)).getTime() / 1000) );
|
|
2590
|
+
};
|
|
2591
|
+
|
|
2592
|
+
function get_date_args(now) {
|
|
2593
|
+
// convenience wrapper with month / day aliases
|
|
2594
|
+
var dargs = Tools.getDateArgs( now );
|
|
2595
|
+
dargs.month = dargs.mon;
|
|
2596
|
+
dargs.day = dargs.mday;
|
|
2597
|
+
return dargs;
|
|
2598
|
+
};
|
|
2599
|
+
|
|
2600
|
+
// Crontab Parsing Tools
|
|
2601
|
+
// by Joseph Huckaby, (c) 2015, BSD 3-Clause License
|
|
2602
|
+
|
|
2603
|
+
var cron_aliases = {
|
|
2604
|
+
jan: 1,
|
|
2605
|
+
feb: 2,
|
|
2606
|
+
mar: 3,
|
|
2607
|
+
apr: 4,
|
|
2608
|
+
may: 5,
|
|
2609
|
+
jun: 6,
|
|
2610
|
+
jul: 7,
|
|
2611
|
+
aug: 8,
|
|
2612
|
+
sep: 9,
|
|
2613
|
+
oct: 10,
|
|
2614
|
+
nov: 11,
|
|
2615
|
+
dec: 12,
|
|
2616
|
+
|
|
2617
|
+
sun: 0,
|
|
2618
|
+
mon: 1,
|
|
2619
|
+
tue: 2,
|
|
2620
|
+
wed: 3,
|
|
2621
|
+
thu: 4,
|
|
2622
|
+
fri: 5,
|
|
2623
|
+
sat: 6
|
|
2624
|
+
};
|
|
2625
|
+
var cron_alias_re = new RegExp("\\b(" + Object.keys(cron_aliases).join('|') + ")\\b", "g");
|
|
2626
|
+
|
|
2627
|
+
function string_hash(str) {
|
|
2628
|
+
// DJB2 algorithm
|
|
2629
|
+
var hash = 5381, i = str.length;
|
|
2630
|
+
while (i) { hash = (hash * 33) ^ str.charCodeAt(--i); }
|
|
2631
|
+
return hash >>> 0;
|
|
2632
|
+
};
|
|
2633
|
+
|
|
2634
|
+
function parse_crontab_part(trigger, raw, key, min, max, rand_seed) {
|
|
2635
|
+
// parse one crontab part, e.g. 1,2,3,5,20-25,30-35,59
|
|
2636
|
+
// can contain single number, and/or list and/or ranges and/or these things: */5 or 10-50/5
|
|
2637
|
+
if (raw == '*') { return; } // wildcard
|
|
2638
|
+
if (raw == 'h') {
|
|
2639
|
+
// unique value over accepted range, but locked to random seed
|
|
2640
|
+
// https://github.com/jhuckaby/Cronicle/issues/6
|
|
2641
|
+
raw = min + (string_hash(rand_seed) % ((max - min) + 1));
|
|
2642
|
+
raw = '' + raw;
|
|
2643
|
+
}
|
|
2644
|
+
if (!raw.match(/^[\w\-\,\/\*]+$/)) { throw new Error("Invalid crontab format: " + raw); }
|
|
2645
|
+
var values = {};
|
|
2646
|
+
var bits = raw.split(/\,/);
|
|
2647
|
+
|
|
2648
|
+
for (var idx = 0, len = bits.length; idx < len; idx++) {
|
|
2649
|
+
var bit = bits[idx];
|
|
2650
|
+
if (bit.match(/^\d+$/)) {
|
|
2651
|
+
// simple number, easy
|
|
2652
|
+
values[bit] = 1;
|
|
2653
|
+
}
|
|
2654
|
+
else if (bit.match(/^(\d+)\-(\d+)$/)) {
|
|
2655
|
+
// simple range, e.g. 25-30
|
|
2656
|
+
var start = parseInt( RegExp.$1 );
|
|
2657
|
+
var end = parseInt( RegExp.$2 );
|
|
2658
|
+
for (var idy = start; idy <= end; idy++) { values[idy] = 1; }
|
|
2659
|
+
}
|
|
2660
|
+
else if (bit.match(/^\*\/(\d+)$/)) {
|
|
2661
|
+
// simple step interval, e.g. */5
|
|
2662
|
+
var step = parseInt( RegExp.$1 );
|
|
2663
|
+
var start = min;
|
|
2664
|
+
var end = max;
|
|
2665
|
+
for (var idy = start; idy <= end; idy += step) { values[idy] = 1; }
|
|
2666
|
+
}
|
|
2667
|
+
else if (bit.match(/^(\d+)\-(\d+)\/(\d+)$/)) {
|
|
2668
|
+
// range step inverval, e.g. 1-31/5
|
|
2669
|
+
var start = parseInt( RegExp.$1 );
|
|
2670
|
+
var end = parseInt( RegExp.$2 );
|
|
2671
|
+
var step = parseInt( RegExp.$3 );
|
|
2672
|
+
for (var idy = start; idy <= end; idy += step) { values[idy] = 1; }
|
|
2673
|
+
}
|
|
2674
|
+
else {
|
|
2675
|
+
throw new Error("Invalid crontab format: " + bit + " (" + raw + ")");
|
|
2676
|
+
}
|
|
2677
|
+
}
|
|
2678
|
+
|
|
2679
|
+
// min max
|
|
2680
|
+
var to_add = {};
|
|
2681
|
+
var to_del = {};
|
|
2682
|
+
for (var value in values) {
|
|
2683
|
+
value = parseInt( value );
|
|
2684
|
+
if (value < min) {
|
|
2685
|
+
to_del[value] = 1;
|
|
2686
|
+
to_add[min] = 1;
|
|
2687
|
+
}
|
|
2688
|
+
else if (value > max) {
|
|
2689
|
+
to_del[value] = 1;
|
|
2690
|
+
value -= min;
|
|
2691
|
+
value = value % ((max - min) + 1); // max is inclusive
|
|
2692
|
+
value += min;
|
|
2693
|
+
to_add[value] = 1;
|
|
2694
|
+
}
|
|
2695
|
+
}
|
|
2696
|
+
for (var value in to_del) delete values[value];
|
|
2697
|
+
for (var value in to_add) values[value] = 1;
|
|
2698
|
+
|
|
2699
|
+
// convert to sorted array
|
|
2700
|
+
var list = Object.keys(values);
|
|
2701
|
+
for (var idx = 0, len = list.length; idx < len; idx++) {
|
|
2702
|
+
list[idx] = parseInt( list[idx] );
|
|
2703
|
+
}
|
|
2704
|
+
list = list.sort( function(a, b) { return a - b; } );
|
|
2705
|
+
if (list.length) trigger[key] = list;
|
|
2706
|
+
};
|
|
2707
|
+
|
|
2708
|
+
function parse_crontab(raw, rand_seed) {
|
|
2709
|
+
// parse standard crontab syntax, return trigger object
|
|
2710
|
+
// e.g. 1,2,3,5,20-25,30-35,59 23 31 12 * *
|
|
2711
|
+
// optional 6th element == years
|
|
2712
|
+
if (!rand_seed) rand_seed = Tools.generateShortID();
|
|
2713
|
+
var trigger = {};
|
|
2714
|
+
|
|
2715
|
+
// resolve all @shortcuts
|
|
2716
|
+
raw = String(raw).trim().toLowerCase();
|
|
2717
|
+
if (raw.match(/\@(yearly|annually)/)) raw = '0 0 1 1 *';
|
|
2718
|
+
else if (raw == '@monthly') raw = '0 0 1 * *';
|
|
2719
|
+
else if (raw == '@weekly') raw = '0 0 * * 0';
|
|
2720
|
+
else if (raw == '@daily') raw = '0 0 * * *';
|
|
2721
|
+
else if (raw == '@hourly') raw = '0 * * * *';
|
|
2722
|
+
|
|
2723
|
+
// expand all month/wday aliases
|
|
2724
|
+
raw = raw.replace(cron_alias_re, function(m_all, m_g1) {
|
|
2725
|
+
return cron_aliases[m_g1];
|
|
2726
|
+
} );
|
|
2727
|
+
|
|
2728
|
+
// at this point string should not contain any alpha characters or '@', except for 'h'
|
|
2729
|
+
if (raw.match(/([a-gi-z\@]+)/i)) throw new Error("Invalid crontab keyword: " + RegExp.$1);
|
|
2730
|
+
|
|
2731
|
+
// split into parts
|
|
2732
|
+
var parts = raw.split(/\s+/);
|
|
2733
|
+
if (parts.length > 6) throw new Error("Invalid crontab format: " + parts.slice(6).join(' '));
|
|
2734
|
+
if (!parts[0].length) throw new Error("Invalid crontab format");
|
|
2735
|
+
|
|
2736
|
+
// parse each part
|
|
2737
|
+
if ((parts.length > 0) && parts[0].length) parse_crontab_part( trigger, parts[0], 'minutes', 0, 59, rand_seed );
|
|
2738
|
+
if ((parts.length > 1) && parts[1].length) parse_crontab_part( trigger, parts[1], 'hours', 0, 23, rand_seed );
|
|
2739
|
+
if ((parts.length > 2) && parts[2].length) parse_crontab_part( trigger, parts[2], 'days', 1, 31, rand_seed );
|
|
2740
|
+
if ((parts.length > 3) && parts[3].length) parse_crontab_part( trigger, parts[3], 'months', 1, 12, rand_seed );
|
|
2741
|
+
if ((parts.length > 4) && parts[4].length) parse_crontab_part( trigger, parts[4], 'weekdays', 0, 6, rand_seed );
|
|
2742
|
+
if ((parts.length > 5) && parts[5].length) parse_crontab_part( trigger, parts[5], 'years', 1970, 3000, rand_seed );
|
|
2743
|
+
|
|
2744
|
+
return trigger;
|
|
2745
|
+
};
|