@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/tags.js
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
// Tag Management Layer
|
|
2
|
+
|
|
3
|
+
const cli = require('pixl-cli');
|
|
4
|
+
const Tools = cli.Tools;
|
|
5
|
+
|
|
6
|
+
module.exports = {
|
|
7
|
+
|
|
8
|
+
async cmd_tags() {
|
|
9
|
+
// The plural command lists and filters Tag definitions.
|
|
10
|
+
await this.cmd_get_tags();
|
|
11
|
+
},
|
|
12
|
+
|
|
13
|
+
async cmd_tag() {
|
|
14
|
+
// Route Tag operations, or treat a bare ID or title as a detail lookup.
|
|
15
|
+
var cmd = this.args.other.shift();
|
|
16
|
+
if (!cmd) return this.dieUsage('tag');
|
|
17
|
+
|
|
18
|
+
switch (cmd) {
|
|
19
|
+
case 'list': await this.cmd_get_tags(); break;
|
|
20
|
+
case 'get': await this.cmd_get_tag(); break;
|
|
21
|
+
case 'create': await this.cmd_create_tag(); break;
|
|
22
|
+
case 'update': await this.cmd_update_tag(); break;
|
|
23
|
+
case 'delete': await this.cmd_delete_tag(); break;
|
|
24
|
+
|
|
25
|
+
default:
|
|
26
|
+
this.args.other.unshift(cmd);
|
|
27
|
+
await this.cmd_get_tag();
|
|
28
|
+
break;
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
async cmd_get_tags() {
|
|
33
|
+
// Tags are kept in the shared getMultiple cache, so search and paginate
|
|
34
|
+
// locally like the other small definition collections.
|
|
35
|
+
this.prepSearchArgs();
|
|
36
|
+
await this.getMultiple();
|
|
37
|
+
|
|
38
|
+
var tags = this.tags.slice(0);
|
|
39
|
+
var is_filtered = !!this.args.other.length;
|
|
40
|
+
if (is_filtered) {
|
|
41
|
+
var search = this.args.other.join(' ');
|
|
42
|
+
tags = this.findObjectsFuzzy(tags, {
|
|
43
|
+
id: search,
|
|
44
|
+
title: search,
|
|
45
|
+
notes: search,
|
|
46
|
+
username: search
|
|
47
|
+
}, 1);
|
|
48
|
+
}
|
|
49
|
+
delete this.args.other;
|
|
50
|
+
|
|
51
|
+
if (Tools.numKeys(this.args)) {
|
|
52
|
+
tags = this.findObjectsFuzzy(tags, this.args);
|
|
53
|
+
is_filtered = true;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
tags.sort( (a, b) => String(a.title || '').toLowerCase().localeCompare(String(b.title || '').toLowerCase()) );
|
|
57
|
+
if (this.format.match(/json/)) return this.jsonOutput(tags);
|
|
58
|
+
|
|
59
|
+
var event_counts = {};
|
|
60
|
+
this.events.forEach( event => {
|
|
61
|
+
(event.tags || []).forEach( id => { event_counts[id] = (event_counts[id] || 0) + 1; } );
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
this.printPaginatedBoxTable({
|
|
65
|
+
title: is_filtered ? 'Filtered Tags' : 'All Tags',
|
|
66
|
+
header: ['Tag ID', 'Title', 'Icon', 'Events', 'Author', 'Modified'],
|
|
67
|
+
rows: tags.slice(this.offset, this.offset + this.limit),
|
|
68
|
+
list: { length: tags.length },
|
|
69
|
+
offset: this.offset,
|
|
70
|
+
limit: this.limit
|
|
71
|
+
}, tag => [
|
|
72
|
+
this.color('theme').bold(tag.id),
|
|
73
|
+
bold(tag.title),
|
|
74
|
+
tag.icon || gray('(None)'),
|
|
75
|
+
Tools.commify(event_counts[tag.id] || 0),
|
|
76
|
+
tag.username || gray('(Unknown)'),
|
|
77
|
+
this.getRelativeDateTime(tag.modified, true)
|
|
78
|
+
]);
|
|
79
|
+
|
|
80
|
+
this.printSuggestedCommands({
|
|
81
|
+
"View Tag details": "xy tag TAG_ID_OR_TITLE",
|
|
82
|
+
"Create a Tag": 'xy tag create --title "Production" --icon server',
|
|
83
|
+
"Find a Tag": "xy tags SEARCH_TEXT",
|
|
84
|
+
"List tagged Events": "xy events --tags TAG_ID"
|
|
85
|
+
});
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
async cmd_get_tag() {
|
|
89
|
+
// Exact IDs take priority, while fuzzy titles are safe for read-only views.
|
|
90
|
+
await this.getMultiple();
|
|
91
|
+
var selector = this.args.other.join(' ') || this.args.id || this.args.title;
|
|
92
|
+
if (!selector) return this.dieUsage('tag get');
|
|
93
|
+
|
|
94
|
+
delete this.args.other;
|
|
95
|
+
delete this.args.id;
|
|
96
|
+
delete this.args.title;
|
|
97
|
+
if (Tools.numKeys(this.args)) return this.die("Unsupported Tag get option: --" + Tools.firstKey(this.args));
|
|
98
|
+
|
|
99
|
+
var match = Tools.findObject(this.tags, { id: selector }) || this.findObjectFuzzy(this.tags, { title: selector });
|
|
100
|
+
if (!match) return this.die("Could not find Tag based on your criteria: " + selector);
|
|
101
|
+
var tag = await this.fetchTag(match.id);
|
|
102
|
+
if (this.format.match(/json/)) return this.jsonOutput(tag);
|
|
103
|
+
|
|
104
|
+
var events = this.events.filter( event => (event.tags || []).includes(tag.id) );
|
|
105
|
+
this.printBoxList({
|
|
106
|
+
title: 'Tag Summary',
|
|
107
|
+
rows: [
|
|
108
|
+
[ 'Tag ID', gray(tag.id) ],
|
|
109
|
+
[ 'Title', this.color('theme').bold(tag.title) ],
|
|
110
|
+
[ 'Icon', tag.icon || gray('(None)') ],
|
|
111
|
+
[ 'Events', Tools.commify(events.length) ],
|
|
112
|
+
[ 'Author', tag.username || gray('(Unknown)') ],
|
|
113
|
+
[ 'Created', this.getNiceDateTime(tag.created, true, true) ],
|
|
114
|
+
[ 'Modified', this.getNiceDateTime(tag.modified, true, true) ],
|
|
115
|
+
[ 'Revision', tag.revision || 1 ]
|
|
116
|
+
]
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// Notes may contain multiple lines, so keep them out of the summary box.
|
|
120
|
+
if (tag.notes) this.printUserNotes('TAG NOTES', tag.notes);
|
|
121
|
+
|
|
122
|
+
this.printSuggestedCommands({
|
|
123
|
+
"List tagged Events": `xy events --tags ${tag.id}`,
|
|
124
|
+
"Export Tag": `xy tag ${tag.id} --export tag.json`,
|
|
125
|
+
"Update Tag": `xy tag update ${tag.id} --notes "Updated notes"`,
|
|
126
|
+
"Delete Tag": `xy tag delete ${tag.id}`
|
|
127
|
+
});
|
|
128
|
+
},
|
|
129
|
+
|
|
130
|
+
async cmd_create_tag() {
|
|
131
|
+
// Defaults match the web editor. The server assigns ID and audit fields.
|
|
132
|
+
if (this.args.other.length) return this.dieUsage('tag create');
|
|
133
|
+
delete this.args.other;
|
|
134
|
+
|
|
135
|
+
var params = this.prepareTagParams({
|
|
136
|
+
icon: 'tag-outline',
|
|
137
|
+
notes: ''
|
|
138
|
+
}, this.args, true);
|
|
139
|
+
if (!params.title) return this.dieUsage('tag create');
|
|
140
|
+
|
|
141
|
+
var data = await this.callStandardAPI('createTag', params, { text: 'Creating Tag...' });
|
|
142
|
+
if (this.dry) return;
|
|
143
|
+
if (this.format.match(/json/)) return this.jsonOutput(data.tag);
|
|
144
|
+
|
|
145
|
+
this.toast('✅', 'green', "Successfully created Tag: #" + data.tag.id);
|
|
146
|
+
this.printSuggestedCommands({
|
|
147
|
+
"View Tag details": `xy tag ${data.tag.id}`,
|
|
148
|
+
"Update Tag": `xy tag update ${data.tag.id} --notes "Updated notes"`,
|
|
149
|
+
"List all Tags": "xy tags",
|
|
150
|
+
"Delete Tag": `xy tag delete ${data.tag.id}`
|
|
151
|
+
});
|
|
152
|
+
},
|
|
153
|
+
|
|
154
|
+
async cmd_update_tag() {
|
|
155
|
+
var id = this.consumeTagID();
|
|
156
|
+
var tag = await this.fetchTag(id);
|
|
157
|
+
|
|
158
|
+
this.printMutationSummary({
|
|
159
|
+
title: 'Update Tag',
|
|
160
|
+
rows: [
|
|
161
|
+
[ 'Tag ID', gray(tag.id) ],
|
|
162
|
+
[ 'Title', this.color('theme').bold(tag.title) ]
|
|
163
|
+
]
|
|
164
|
+
});
|
|
165
|
+
if (!await this.confirmSyncUpdate('tag', tag.id, 'Tag')) return;
|
|
166
|
+
if (!Tools.numKeys(this.args)) return this.die("No updates specified for Tag.");
|
|
167
|
+
this.printUpdateData(this.args);
|
|
168
|
+
|
|
169
|
+
var params = this.prepareTagParams({}, this.args, false);
|
|
170
|
+
params.id = id;
|
|
171
|
+
var data = await this.callStandardAPI('updateTag', params, { text: 'Updating Tag...' });
|
|
172
|
+
if (this.dry) return;
|
|
173
|
+
if (this.format.match(/json/)) return this.jsonOutput(data);
|
|
174
|
+
this.toast('✅', 'green', "Successfully updated Tag: #" + id);
|
|
175
|
+
},
|
|
176
|
+
|
|
177
|
+
async cmd_delete_tag() {
|
|
178
|
+
var id = this.consumeTagID();
|
|
179
|
+
var tag = await this.fetchTag(id);
|
|
180
|
+
|
|
181
|
+
this.printMutationSummary({
|
|
182
|
+
title: 'Delete Tag',
|
|
183
|
+
rows: [
|
|
184
|
+
[ 'Tag ID', gray(tag.id) ],
|
|
185
|
+
[ 'Title', this.color('theme').bold(tag.title) ]
|
|
186
|
+
]
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
if (('confirm' in this.args) && (typeof(this.args.confirm) != 'boolean')) {
|
|
190
|
+
return this.die("Tag delete --confirm must be true or false.");
|
|
191
|
+
}
|
|
192
|
+
if (this.args.confirm !== true) {
|
|
193
|
+
this.toast('⚠️', 'orange', "Please confirm the Tag delete by adding '--confirm'.");
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
delete this.args.confirm;
|
|
197
|
+
if (Tools.numKeys(this.args)) return this.die("Unsupported Tag delete option: --" + Tools.firstKey(this.args));
|
|
198
|
+
|
|
199
|
+
var data = await this.callStandardAPI('deleteTag', { id: id }, { text: 'Deleting Tag...' });
|
|
200
|
+
if (this.dry) return;
|
|
201
|
+
if (this.format.match(/json/)) return this.jsonOutput(data);
|
|
202
|
+
this.toast('✅', 'green', "Successfully deleted Tag: #" + id);
|
|
203
|
+
},
|
|
204
|
+
|
|
205
|
+
async fetchTag(id) {
|
|
206
|
+
// Use the single-resource endpoint for authoritative metadata and privileges.
|
|
207
|
+
cli.progress.start({ amount: 1, pct: false, text: gray('→ Loading Tag...') });
|
|
208
|
+
var { err, data } = await this.api.getTag({ id: id });
|
|
209
|
+
cli.progress.end();
|
|
210
|
+
if (err) this.die(err);
|
|
211
|
+
return data.tag;
|
|
212
|
+
},
|
|
213
|
+
|
|
214
|
+
consumeTagID() {
|
|
215
|
+
// Mutations require an exact internal ID and can never change that ID.
|
|
216
|
+
var id = this.args.other.shift() || this.args.id;
|
|
217
|
+
if (!id) this.die("Missing required Tag ID argument.");
|
|
218
|
+
if (this.args.other.length) this.die("Unexpected argument after Tag ID: " + this.args.other[0]);
|
|
219
|
+
if (this.args.id && (this.args.id !== id)) this.die("Conflicting Tag ID arguments.");
|
|
220
|
+
if ((typeof(id) != 'string') || !id.match(/^[a-z0-9_]+$/)) this.die("Invalid Tag ID: " + id);
|
|
221
|
+
delete this.args.id;
|
|
222
|
+
delete this.args.other;
|
|
223
|
+
return id;
|
|
224
|
+
},
|
|
225
|
+
|
|
226
|
+
prepareTagParams(defaults, input, creating) {
|
|
227
|
+
// Send only user-selected fields on update. This keeps old CLI versions
|
|
228
|
+
// compatible when newer xyOps releases add properties to Tag records.
|
|
229
|
+
var params = creating ? Tools.copyHash(defaults, true) : {};
|
|
230
|
+
var allowed = creating ? ['id', 'title', 'icon', 'notes'] : ['title', 'icon', 'notes'];
|
|
231
|
+
Object.keys(input).forEach( key => {
|
|
232
|
+
if (!allowed.includes(key)) return this.die("Unsupported Tag option: --" + key);
|
|
233
|
+
params[key] = input[key];
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
if ('title' in params) {
|
|
237
|
+
if ((typeof(params.title) != 'string') || !params.title.trim()) this.die("Tag title cannot be empty and must be a string.");
|
|
238
|
+
params.title = params.title.trim();
|
|
239
|
+
}
|
|
240
|
+
['icon', 'notes'].forEach( key => {
|
|
241
|
+
if ((key in params) && (typeof(params[key]) != 'string')) this.die("Tag " + key + " must be a string.");
|
|
242
|
+
});
|
|
243
|
+
if ('icon' in params) params.icon = params.icon.replace(/^mdi\-/, '');
|
|
244
|
+
if (('id' in params) && ((typeof(params.id) != 'string') || !params.id.match(/^[a-z0-9_]+$/))) {
|
|
245
|
+
this.die("Invalid Tag ID: " + params.id);
|
|
246
|
+
}
|
|
247
|
+
return params;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
}; // module.exports
|