@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.
Files changed (63) hide show
  1. package/README.md +543 -0
  2. package/docs/help.md +2587 -0
  3. package/docs/screenshots.md +219 -0
  4. package/docs/sync.md +920 -0
  5. package/docs/tutorial.md +204 -0
  6. package/index.js +444 -0
  7. package/lib/alerts.js +735 -0
  8. package/lib/apikey.js +425 -0
  9. package/lib/buckets.js +607 -0
  10. package/lib/categories.js +323 -0
  11. package/lib/channels.js +290 -0
  12. package/lib/config.js +86 -0
  13. package/lib/dashboard.js +237 -0
  14. package/lib/doc.js +131 -0
  15. package/lib/events.js +1314 -0
  16. package/lib/groups.js +1038 -0
  17. package/lib/jobs.js +1254 -0
  18. package/lib/log.js +125 -0
  19. package/lib/marketplace.js +386 -0
  20. package/lib/monitors.js +412 -0
  21. package/lib/plugins.js +491 -0
  22. package/lib/secrets.js +441 -0
  23. package/lib/servers.js +1502 -0
  24. package/lib/snapshots.js +498 -0
  25. package/lib/sync.js +963 -0
  26. package/lib/system.js +612 -0
  27. package/lib/tags.js +250 -0
  28. package/lib/tickets.js +681 -0
  29. package/lib/transfer.js +447 -0
  30. package/lib/utils.js +2745 -0
  31. package/lib/webhook.js +414 -0
  32. package/logo.webp +0 -0
  33. package/package.json +38 -0
  34. package/test/README.md +97 -0
  35. package/test/api-validation.test.js +170 -0
  36. package/test/categories.test.js +246 -0
  37. package/test/channels.test.js +209 -0
  38. package/test/config.test.js +96 -0
  39. package/test/doc.test.js +84 -0
  40. package/test/groups.test.js +211 -0
  41. package/test/help.test.js +26 -0
  42. package/test/helpers/common.js +127 -0
  43. package/test/helpers/pagination-preload.cjs +18 -0
  44. package/test/log.test.js +103 -0
  45. package/test/markdown.test.js +61 -0
  46. package/test/marketplace.test.js +229 -0
  47. package/test/monitors.test.js +227 -0
  48. package/test/pagination.test.js +138 -0
  49. package/test/plugins.test.js +351 -0
  50. package/test/secrets.test.js +271 -0
  51. package/test/servers.test.js +303 -0
  52. package/test/snapshots.test.js +167 -0
  53. package/test/sync-unit.test.js +446 -0
  54. package/test/sync.test.js +542 -0
  55. package/test/system.test.js +94 -0
  56. package/test/tags.test.js +230 -0
  57. package/test/tickets.test.js +261 -0
  58. package/test/toast.test.js +73 -0
  59. package/test/transfer-apikey.test.js +96 -0
  60. package/test/transfer-workflow.test.js +122 -0
  61. package/test/transfer.test.js +314 -0
  62. package/test/webhook.test.js +332 -0
  63. package/test.js +49 -0
package/lib/log.js ADDED
@@ -0,0 +1,125 @@
1
+ // System Log Viewer Layer
2
+
3
+ const cli = require('pixl-cli');
4
+ const Tools = cli.Tools;
5
+
6
+ module.exports = {
7
+
8
+ async cmd_log() {
9
+ // Log searches return the last N matches, not offset-based pages. Leave
10
+ // pagination helpers out of this command and use the API's rows option.
11
+ var params = Object.assign({}, this.args);
12
+ var log = params.other.shift();
13
+
14
+ if (params.other.length) return this.dieUsage('log');
15
+ if (log && params.log && (log !== params.log)) return this.die("Conflicting log names.");
16
+
17
+ params.log = log || params.log || 'xyOps';
18
+ delete params.other;
19
+ if (!('rows' in params)) params.rows = 100;
20
+
21
+ // Sorting is a display option in the web UI, not an API search parameter.
22
+ var sort = params.sort || 'date_asc';
23
+ delete params.sort;
24
+ if (!['date_asc', 'date_desc'].includes(sort)) return this.die("Log sort must be 'date_asc' or 'date_desc'.");
25
+
26
+ var fields = ['log', 'rows', 'match', 'regex', 'case', 'cols', 'date'];
27
+ var unknown = Object.keys(params).find( key => !fields.includes(key) );
28
+ if (unknown) return this.die("Unsupported log option: --" + unknown);
29
+
30
+ if ((typeof(params.log) != 'string') || !params.log.match(/^\w+$/)) return this.die("Log name must contain only letters, numbers, or underscores, without an extension.");
31
+ if (!Number.isInteger(params.rows) || (params.rows < 1) || (params.rows > 1000)) return this.die("Log rows must be an integer from 1 to 1000.");
32
+
33
+ ['regex', 'case'].forEach( key => {
34
+ if (!(key in params)) return;
35
+
36
+ if ([true, 1, '1', 'true'].includes(params[key])) params[key] = true;
37
+ else if ([false, 0, '0', 'false'].includes(params[key])) params[key] = false;
38
+ else this.die("Invalid boolean value for '" + key + "': " + params[key]);
39
+ });
40
+
41
+ if ('match' in params) {
42
+ // Numeric search text is common for log codes and process IDs.
43
+ if (!['string', 'number', 'boolean'].includes(typeof(params.match))) return this.die("Log match must be text.");
44
+ params.match = String(params.match);
45
+ }
46
+
47
+ if (params.regex && params.match) {
48
+ // The server does not always return an error for a malformed regex.
49
+ // Compile it here first so the CLI fails immediately and clearly.
50
+ try { new RegExp(params.match, params.case ? 'g' : 'ig'); }
51
+ catch (err) { return this.die("Invalid log regular expression: " + err.message); }
52
+ }
53
+
54
+ if ('date' in params) {
55
+ // An empty date selects the live log. Archive dates use server time.
56
+ if (params.date !== '') {
57
+ if ((typeof(params.date) != 'string') || !params.date.match(/^\d{4}-\d{2}-\d{2}$/)) return this.die("Log date must use YYYY-MM-DD format.");
58
+
59
+ var date = new Date(params.date + 'T00:00:00Z');
60
+ if (!Number.isFinite(date.getTime()) || (date.toISOString().slice(0, 10) !== params.date)) return this.die("Invalid log date: " + params.date);
61
+ }
62
+ }
63
+
64
+ var all_cols = this.config.log_columns || ['hires_epoch', 'date', 'hostname', 'pid', 'component', 'category', 'code', 'msg', 'data'];
65
+
66
+ if ('cols' in params) {
67
+ if (typeof(params.cols) == 'string') params.cols = params.cols.split(',').map( col => col.trim() );
68
+ if (!Array.isArray(params.cols) || !params.cols.length || params.cols.some( col => !all_cols.includes(col) )) return this.die("Log cols must contain valid column names: " + all_cols.join(', '));
69
+
70
+ params.cols = Array.from(new Set(params.cols));
71
+ }
72
+
73
+ // Reconstruct native log lines in the server's configured column order.
74
+ // An explicit column selection applies to both native and JSON output.
75
+ var cols = params.cols || all_cols;
76
+ var data = await this.callStandardAPI('adminSearchLogs', params, { text: 'Searching system log...' });
77
+ if (this.dry) return;
78
+
79
+ var rows = data.rows || [];
80
+ if (sort == 'date_desc') rows = rows.slice(0).reverse();
81
+
82
+ if (this.format.match(/json/)) return this.jsonOutput(rows);
83
+
84
+ // list.length counts every log line, including non-matches. It is not a
85
+ // match count or a pagination total. Missing archives may omit it entirely.
86
+ var summary = Tools.commify(rows.length) + ' rows returned';
87
+ if (data.list && (typeof(data.list.length) == 'number')) summary += ', ' + Tools.commify(data.list.length) + ' total log rows';
88
+
89
+ println( "\n " + gray(summary) + "\n" );
90
+
91
+ // Color values independently so every delimiter remains gray. Messages
92
+ // and data retain their full contents, with no table wrapping or truncation.
93
+ var colors = {
94
+ hires_epoch: 'teal',
95
+ date: 'cyan',
96
+ hostname: 'violet',
97
+ pid: 'orange',
98
+ component: 'blue',
99
+ category: 'green',
100
+ code: 'yellow',
101
+ data: 'gray'
102
+ };
103
+
104
+ rows.forEach( row => {
105
+ println( cols.map( col => {
106
+ var value = row[col];
107
+ if ((value === undefined) || (value === null)) value = '';
108
+ else value = String(value);
109
+
110
+ var color = colors[col];
111
+ if ((col == 'category') && /^(error|warning|fatal)$/.test(value)) color = (value == 'warning') ? 'yellow' : 'red';
112
+
113
+ return gray('[') + (color ? this.color(color)(value) : value) + gray(']');
114
+ }).join('') );
115
+ });
116
+
117
+ this.printSuggestedCommands({
118
+ "Search log text": `xy log ${params.log} --match "error"`,
119
+ "Search with a regex": `xy log ${params.log} --match "error|warning" --regex`,
120
+ "Show newest first": `xy log ${params.log} --rows 100 --sort date_desc`,
121
+ "Export JSON rows": `xy log ${params.log} --rows 100 --format json`
122
+ });
123
+ }
124
+
125
+ }; // module.exports
@@ -0,0 +1,386 @@
1
+ // xyOps Plugin Marketplace Layer
2
+
3
+ const cli = require('pixl-cli');
4
+ const Tools = cli.Tools;
5
+
6
+ module.exports = {
7
+
8
+ async cmd_marketplace() {
9
+ // Marketplace is both the collection command and the singular router. A
10
+ // bare author/repo ID opens details, while all other bare text is a search.
11
+ var cmd = this.args.other[0];
12
+
13
+ if (cmd == 'list' || cmd == 'search') {
14
+ this.args.other.shift();
15
+ return await this.cmd_search_marketplace();
16
+ }
17
+ if (cmd == 'get') {
18
+ this.args.other.shift();
19
+ return await this.cmd_get_marketplace_product();
20
+ }
21
+ if (cmd == 'install') {
22
+ this.args.other.shift();
23
+ return await this.cmd_install_marketplace_product();
24
+ }
25
+ if ((this.args.other.length == 1) && this.args.other[0].includes('/')) {
26
+ return await this.cmd_get_marketplace_product();
27
+ }
28
+
29
+ await this.cmd_search_marketplace();
30
+ },
31
+
32
+ async cmd_search_marketplace() {
33
+ // The Marketplace API performs its own filtering, sorting, and pagination.
34
+ // Product type is fixed to Plugin for v1, leaving room for future products.
35
+ this.prepSearchArgs();
36
+
37
+ if (this.args.other.length) {
38
+ if ('query' in this.args) this.die("Use either positional search text or --query, not both.");
39
+ this.args.query = this.args.other.join(' ');
40
+ }
41
+ delete this.args.other;
42
+
43
+ var allowed = ['query', 'plugin_type', 'author', 'status', 'license', 'tags', 'requires', 'sort_by', 'sort_dir'];
44
+ Object.keys(this.args).forEach( key => {
45
+ if (!allowed.includes(key)) this.die("Unsupported Marketplace search option: --" + key);
46
+ });
47
+
48
+ if ('plugin_type' in this.args) this.args.plugin_type = this.parsePluginType(this.args.plugin_type);
49
+ if (('status' in this.args) && !['installed', 'not'].includes(this.args.status)) {
50
+ this.die("Marketplace status must be installed or not.");
51
+ }
52
+ if (('sort_by' in this.args) && !['title', 'author', 'license', 'plugin_type', 'created', 'modified'].includes(this.args.sort_by)) {
53
+ this.die("Marketplace sort_by must be title, author, license, plugin_type, created, or modified.");
54
+ }
55
+ if ('sort_dir' in this.args) {
56
+ if (this.args.sort_dir == 'asc') this.args.sort_dir = 1;
57
+ else if (this.args.sort_dir == 'desc') this.args.sort_dir = -1;
58
+ else this.args.sort_dir = Number(this.args.sort_dir);
59
+ if (![1, -1].includes(this.args.sort_dir)) this.die("Marketplace sort_dir must be asc, desc, 1, or -1.");
60
+ }
61
+ ['query', 'author', 'license'].forEach( key => {
62
+ if ((key in this.args) && ((typeof(this.args[key]) != 'string') || !this.args[key].trim())) {
63
+ this.die("Marketplace " + key + " must be a non-empty string.");
64
+ }
65
+ });
66
+ ['tags', 'requires'].forEach( key => {
67
+ if (key in this.args) this.args[key] = this.normalizeMarketplaceList(this.args[key], key);
68
+ });
69
+
70
+ this.offset = this.parseMarketplaceInteger(this.offset, 'offset', 0);
71
+ this.limit = this.parseMarketplaceInteger(this.limit, 'limit', 1);
72
+
73
+ var params = Object.assign({
74
+ type: 'plugin',
75
+ offset: this.offset,
76
+ limit: this.limit
77
+ }, this.args);
78
+
79
+ await this.getMultiple({ lists: 'plugins' });
80
+ var data = await this.fetchMarketplace(params, 'Searching Marketplace...');
81
+ var rows = data.rows || [];
82
+ if (this.format.match(/json/)) return this.jsonOutput(rows);
83
+
84
+ this.printPaginatedBoxTable({
85
+ title: Tools.numKeys(this.args) ? 'Marketplace Search Results' : 'Plugin Marketplace',
86
+ header: ['Marketplace ID', 'Title', 'Type', 'Author', 'Version', 'Status'],
87
+ rows: rows,
88
+ list: data.list || { length: rows.length },
89
+ offset: this.offset,
90
+ limit: this.limit
91
+ }, product => {
92
+ var installed = this.findInstalledMarketplaceProduct(product);
93
+ return [
94
+ this.color('theme').bold(product.id),
95
+ bold(product.title),
96
+ this.getNicePluginType(product.plugin_type),
97
+ product.author,
98
+ (product.versions || [])[0] || gray('(Unknown)'),
99
+ this.getNiceMarketplaceStatus(product, installed)
100
+ ];
101
+ });
102
+
103
+ this.printSuggestedCommands({
104
+ "View Marketplace Plugin": "xy marketplace AUTHOR/REPO",
105
+ "Install Marketplace Plugin": "xy marketplace install AUTHOR/REPO",
106
+ "List Event Plugins": "xy marketplace --plugin_type event",
107
+ "List installed Plugins": "xy marketplace --status installed"
108
+ });
109
+ },
110
+
111
+ async cmd_get_marketplace_product() {
112
+ // The server returns listing metadata and the selected version's README in
113
+ // one response. README source is preserved in JSON output.
114
+ var id = this.consumeMarketplaceID();
115
+ var version = this.consumeMarketplaceVersion();
116
+ if (Tools.numKeys(this.args)) this.die("Unsupported Marketplace detail option: --" + Tools.firstKey(this.args));
117
+
118
+ await this.getMultiple({ lists: 'plugins' });
119
+ var params = { id: id, readme: true };
120
+ if (version) params.version = version;
121
+ var data = await this.fetchMarketplace(params, 'Loading Marketplace Plugin...');
122
+ this.validateMarketplaceProduct(data.item, data.version, version);
123
+
124
+ if (this.format.match(/json/)) return this.jsonOutput({
125
+ item: data.item,
126
+ version: data.version,
127
+ text: data.text
128
+ });
129
+
130
+ var product = data.item;
131
+ var installed = this.findInstalledMarketplaceProduct(product);
132
+ var repo_url = product.repo_url || ('https://github.com/' + product.id);
133
+
134
+ this.printBoxList({
135
+ title: 'Marketplace Plugin Summary',
136
+ rows: [
137
+ [ 'Marketplace ID', gray(product.id) ],
138
+ [ 'Title', this.color('theme').bold(product.title) ],
139
+ [ 'Description', product.description ],
140
+ [ 'Author', product.author ],
141
+ [ 'Plugin Type', this.getNicePluginType(product.plugin_type) ],
142
+ [ 'Status', this.getNiceMarketplaceStatus(product, installed) ],
143
+ installed ? [ 'Installed Plugin', installed.title + gray(' (' + installed.id + ')') ] : null,
144
+ installed ? [ 'Installed Version', installed.marketplace.version ] : null,
145
+ [ 'Viewed Version', data.version ],
146
+ [ 'Latest Version', product.versions[0] ],
147
+ [ 'Available Versions', product.versions.join(', ') ],
148
+ [ 'License', product.license ],
149
+ [ 'Requirements', (product.requires || []).join(', ') || gray('(None)') ],
150
+ [ 'Tags', (product.tags || []).join(', ') || gray('(None)') ],
151
+ [ 'Created', product.created ],
152
+ [ 'Modified', product.modified ],
153
+ [ 'Repository', repo_url ]
154
+ ]
155
+ });
156
+
157
+ println( "\n " + this.color('theme').bold('PLUGIN README') );
158
+ println( "\n" + this.markdown(this.cleanMarketplaceReadme(data.text)).trimEnd() );
159
+
160
+ this.printSuggestedCommands({
161
+ "Preview installation": "xy marketplace install " + product.id + (data.version == product.versions[0] ? '' : ' --version ' + data.version),
162
+ "Install latest version": "xy marketplace install " + product.id,
163
+ "List Marketplace Plugins": "xy marketplace"
164
+ });
165
+ },
166
+
167
+ async cmd_install_marketplace_product() {
168
+ // Marketplace packages are single-item XYPDF files. Preview by default,
169
+ // then use the ordinary Plugin APIs only after explicit confirmation.
170
+ var id = this.consumeMarketplaceID();
171
+ var version = this.consumeMarketplaceVersion();
172
+ if (('confirm' in this.args) && (typeof(this.args.confirm) != 'boolean')) {
173
+ this.die("Marketplace install --confirm must be true or false.");
174
+ }
175
+ if (Tools.numKeys(this.args) > (('confirm' in this.args) ? 1 : 0)) {
176
+ var unsupported = Object.keys(this.args).find( key => key != 'confirm' );
177
+ this.die("Unsupported Marketplace install option: --" + unsupported);
178
+ }
179
+
180
+ await this.getMultiple({ lists: 'plugins' });
181
+ var params = { id: id, data: true };
182
+ if (version) params.version = version;
183
+ var response = await this.fetchMarketplace(params, 'Preparing Marketplace installation...');
184
+ this.validateMarketplaceProduct(response.item, response.version, version);
185
+ this.validateMarketplacePayload(response.data, response.item);
186
+
187
+ var plugin = this.cleanTransferData(response.data.items[0].data);
188
+ plugin.marketplace = {
189
+ id: response.item.id,
190
+ version: response.version
191
+ };
192
+
193
+ // Marketplace Plugins always run under the server's configured default
194
+ // credentials. Strip these fields client-side as well as server-side.
195
+ delete plugin.uid;
196
+ delete plugin.gid;
197
+
198
+ var installed = this.findInstalledMarketplaceProduct(response.item);
199
+ if (installed && (installed.id != plugin.id)) {
200
+ this.die("Marketplace Plugin is already installed with a different Plugin ID: " + installed.id);
201
+ }
202
+
203
+ var existing = Tools.findObject(this.plugins, { id: plugin.id });
204
+ if (existing && (existing.type != plugin.type)) {
205
+ this.die("Existing Plugin #" + plugin.id + " has a different Plugin type and cannot be replaced.");
206
+ }
207
+ var operation = existing ? 'update' : 'create';
208
+ var warning = '';
209
+ if (existing && (!existing.marketplace || (existing.marketplace.id != response.item.id))) {
210
+ warning = "Existing local Plugin #" + plugin.id + " will be replaced by the Marketplace version.";
211
+ }
212
+
213
+ var preview = {
214
+ product: response.item,
215
+ marketplace: plugin.marketplace,
216
+ version: response.version,
217
+ operation: operation,
218
+ preview: true,
219
+ warnings: warning ? [warning] : [],
220
+ plugin: plugin
221
+ };
222
+
223
+ if (this.dry || this.args.confirm !== true) {
224
+ // Explicit JSON formats retain the complete machine-readable preview envelope.
225
+ // The friendly terminal preview below focuses on the Plugin definition itself.
226
+ if (this.format.match(/json/)) return this.jsonOutput(preview);
227
+
228
+ var display = this.prepareMarketplacePreviewPlugin(plugin);
229
+ this.printBoxList({
230
+ title: 'Marketplace Plugin Install Preview',
231
+ rows: [
232
+ [ 'Plugin Name', this.color('theme').bold(response.item.title) ],
233
+ [ 'Author', response.item.author ],
234
+ [ 'Version', response.version ]
235
+ ]
236
+ });
237
+
238
+ if (warning) println("\n " + yellow(warning));
239
+ println("\n This is the Plugin definition that will be " + (operation == 'create' ? 'created' : 'used to update the installed Plugin') + " when you confirm:");
240
+ this.jsonOutput(display.plugin);
241
+ if (display.script) this.printPluginScript(plugin);
242
+
243
+ this.toast('⚠️', 'orange', "Preview only. Review the Plugin data above, then use --confirm to install it.");
244
+ this.printSuggestedCommands({
245
+ "Review this installation": "xy marketplace install " + response.item.id + " --version " + response.version,
246
+ "View Marketplace Plugin": "xy marketplace " + response.item.id
247
+ });
248
+ return;
249
+ }
250
+
251
+ var method = operation == 'create' ? 'createPlugin' : 'updatePlugin';
252
+ var data = await this.callStandardAPI(method, plugin, {
253
+ text: (operation == 'create' ? 'Installing' : 'Upgrading') + ' Marketplace Plugin...'
254
+ });
255
+ var saved = data.plugin;
256
+ var result = {
257
+ code: 0,
258
+ marketplace: plugin.marketplace,
259
+ operation: operation == 'create' ? 'created' : 'updated',
260
+ plugin: saved
261
+ };
262
+
263
+ if (this.format.match(/json/)) return this.jsonOutput(result);
264
+
265
+ this.toast('✅', 'green', response.item.title + ' ' + response.version + ' was installed successfully.');
266
+ this.printSuggestedCommands({
267
+ "View installed Plugin": "xy plugin " + saved.id,
268
+ "View Marketplace details": "xy marketplace " + response.item.id,
269
+ "List installed Marketplace Plugins": "xy marketplace --status installed"
270
+ });
271
+ },
272
+
273
+ async fetchMarketplace(params, message) {
274
+ // Keep progress behavior consistent across search, details, and data fetches.
275
+ cli.progress.start({ amount: 1, pct: false, text: gray('→ ' + message) });
276
+ var { err, data } = await this.api.marketplace(params);
277
+ cli.progress.end();
278
+ if (err) this.die(err);
279
+ return data;
280
+ },
281
+
282
+ consumeMarketplaceID() {
283
+ var id = this.args.other.shift() || this.args.id;
284
+ if (!id) this.die("Missing Marketplace Plugin ID. Use AUTHOR/REPO.");
285
+ if (this.args.other.length) this.die("Unexpected argument after Marketplace Plugin ID: " + this.args.other[0]);
286
+ if (this.args.id && (this.args.id !== id)) this.die("Conflicting Marketplace Plugin ID arguments.");
287
+ if ((typeof(id) != 'string') || !id.match(/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/)) {
288
+ this.die("Invalid Marketplace Plugin ID: " + id + ". Use AUTHOR/REPO.");
289
+ }
290
+ delete this.args.id;
291
+ delete this.args.other;
292
+ return id;
293
+ },
294
+
295
+ consumeMarketplaceVersion() {
296
+ var version = this.args.version;
297
+ delete this.args.version;
298
+ if ((version !== undefined) && ((typeof(version) != 'string') || !version.trim() || !version.match(/^[A-Za-z0-9_.+-]+$/))) {
299
+ this.die("Invalid Marketplace Plugin version: " + version);
300
+ }
301
+ return version;
302
+ },
303
+
304
+ validateMarketplaceProduct(product, resolved_version, requested_version) {
305
+ if (!product || (product.type != 'plugin')) this.die("Marketplace product is not a Plugin.");
306
+ if (!Array.isArray(product.versions) || !product.versions.length) this.die("Marketplace Plugin has no published versions.");
307
+ if (!['event', 'monitor', 'action', 'scheduler'].includes(product.plugin_type)) this.die("Marketplace Plugin has an invalid Plugin type.");
308
+ if (!product.versions.includes(resolved_version)) this.die("Marketplace returned an unknown Plugin version: " + resolved_version);
309
+ if (requested_version && (resolved_version != requested_version)) this.die("Marketplace returned the wrong Plugin version.");
310
+ },
311
+
312
+ validateMarketplacePayload(payload, product) {
313
+ // Reuse the complete XYPDF validation, then narrow the accepted package to
314
+ // the single Plugin product supported by Marketplace v1.
315
+ this.validateTransferPayload(payload);
316
+ if (payload.items.length != 1 || payload.items[0].type != 'plugin') {
317
+ this.die("Marketplace package must contain exactly one Plugin item.");
318
+ }
319
+ var plugin = payload.items[0].data;
320
+ if (!plugin.id) this.die("Marketplace Plugin is missing its internal Plugin ID.");
321
+ if (plugin.type != product.plugin_type) this.die("Marketplace Plugin type does not match its listing metadata.");
322
+ },
323
+
324
+ prepareMarketplacePreviewPlugin(plugin) {
325
+ // Scripts are unreadable once escaped inside JSON, so pull non-empty source
326
+ // into a separate display section without mutating the install request.
327
+ var preview_plugin = Object.assign({}, plugin);
328
+ var script = preview_plugin.script || '';
329
+ if (script) delete preview_plugin.script;
330
+
331
+ return { plugin: preview_plugin, script: script };
332
+ },
333
+
334
+ findInstalledMarketplaceProduct(product) {
335
+ return (this.plugins || []).find( plugin => {
336
+ return !!(plugin.marketplace && (plugin.marketplace.id == product.id));
337
+ });
338
+ },
339
+
340
+ getNiceMarketplaceStatus(product, installed) {
341
+ if (!installed) return gray('Not Installed');
342
+ if (installed.marketplace.version == product.versions[0]) return green('Up to Date');
343
+ return yellow('Outdated');
344
+ },
345
+
346
+ normalizeMarketplaceList(value, key) {
347
+ var values = [];
348
+ Tools.alwaysArray(value).forEach( entry => {
349
+ if (typeof(entry) != 'string') this.die("Marketplace " + key + " must be a comma-separated string or repeated option.");
350
+ entry.split(',').forEach( item => {
351
+ item = item.trim();
352
+ if (item) values.push(item);
353
+ });
354
+ });
355
+ if (!values.length) this.die("Marketplace " + key + " cannot be empty.");
356
+ return values;
357
+ },
358
+
359
+ parseMarketplaceInteger(value, key, minimum) {
360
+ value = Number(value);
361
+ if (!Number.isInteger(value) || (value < minimum)) this.die("Marketplace " + key + " must be an integer of at least " + minimum + ".");
362
+ return value;
363
+ },
364
+
365
+ cleanMarketplaceReadme(text) {
366
+ // Terminals cannot display images, and marked-terminal otherwise leaves the
367
+ // source markup visible. Preserve linked badge labels, then remove images.
368
+ text = String(text || '').replace(/\r\n?/g, '\n');
369
+
370
+ // README content comes from third-party repositories. Remove raw terminal
371
+ // control characters before rendering while preserving tabs and newlines.
372
+ text = text.replace(/[\x00-\x08\x0B-\x1F\x7F-\x9F]/g, '');
373
+ text = text.replace(/\[!\[([^\]]*)\]\([^\n]*?\)\]\(([^\n]*?)\)/g, '[$1]($2)');
374
+ text = text.replace(/!\[[^\]]*\]\([^\n]*?\)/g, '');
375
+ text = text.replace(/<(p|div)\b[^>]*>\s*(?:<img\b[^>]*\/?\s*>\s*)+<\/\1>/gi, '');
376
+ text = text.replace(/<img\b[^>]*\/?\s*>/gi, '');
377
+
378
+ // GitHub READMEs often center their title using raw HTML. Convert those
379
+ // headings back to Markdown so they remain readable in the terminal.
380
+ text = text.replace(/<h([1-6])\b[^>]*>([\s\S]*?)<\/h\1>/gi, function(match, level, title) {
381
+ return '#'.repeat(Number(level)) + ' ' + title.replace(/<[^>]+>/g, '').trim();
382
+ });
383
+ return text.trim();
384
+ }
385
+
386
+ }; // module.exports