@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
@@ -0,0 +1,204 @@
1
+ # xyCLI Tutorial
2
+
3
+ Let's build a small piece of automation from your terminal. You'll explore the dashboard, create a shell Event, watch a job run, and add a Web Hook that fires when the job succeeds. Then you'll update your script and export the finished automation.
4
+
5
+ For installation and connection settings, start with [Getting started](../README.md#getting-started). The [command reference](help.md) has the full options for every command used here.
6
+
7
+ ## Before you begin
8
+
9
+ This walkthrough assumes:
10
+
11
+ - xyCLI is installed and connected to your xyOps instance.
12
+ - You have an online Linux or macOS worker with a POSIX shell, the built-in Shell Plugin (`shellplug`), and an enabled Category.
13
+ - Your API Key can create and edit Events, run jobs, and use the selected Category and Group. For the Web Hook steps, it also needs `create_web_hooks` and `edit_web_hooks`.
14
+ - You have an HTTP endpoint that accepts JSON POST requests and lets you inspect incoming requests. This could be your own application or a request-capture endpoint. The xyOps conductor must be able to reach it.
15
+
16
+ Use the IDs printed by xyCLI as you go. Replace placeholders such as `EVENT_ID` and `HOOK_ID` with those actual IDs, and replace the example Hook URL with your endpoint.
17
+
18
+ ## 1. Explore the dashboard
19
+
20
+ Run `xy` to see your system at a glance:
21
+
22
+ ```sh
23
+ xy
24
+ ```
25
+
26
+ The dashboard brings together server health, active Alerts and jobs, queued work, and rate limits. Check the upcoming schedule to see what xyOps expects to run soon:
27
+
28
+ ```sh
29
+ xy upcoming
30
+ ```
31
+
32
+ Now browse the workers and resources available for your first Event:
33
+
34
+ ```sh
35
+ xy servers
36
+ xy groups
37
+ xy categories
38
+ xy plugins --type event
39
+ ```
40
+
41
+ Choose a Group with an online, enabled worker, and note its ID. Also note an enabled Category ID and confirm that the Shell Plugin is available. You can inspect a worker or Group more closely with `xy server SERVER_ID` or `xy group GROUP_ID`.
42
+
43
+ - **Command reference:** [Dashboard](help.md#dashboard), [Servers](help.md#servers), and [Groups](help.md#groups).
44
+
45
+ ## 2. Create your first Event
46
+
47
+ An Event is a reusable definition of what to run and where to run it. Each execution of that Event becomes a separate job.
48
+
49
+ Make a local file named `hello.sh` containing:
50
+
51
+ ```sh
52
+ #!/bin/sh
53
+ echo "Hello from xyCLI!"
54
+ echo "Running on $(hostname)"
55
+ sleep 3
56
+ echo "All done."
57
+ ```
58
+
59
+ Create an enabled Event with a manual trigger. Replace `CATEGORY_ID` and `GROUP_ID` with the IDs you selected:
60
+
61
+ ```sh
62
+ xy event create --title "Hello from xyCLI" \
63
+ --enabled true --category CATEGORY_ID \
64
+ --plugin shellplug --targets '["GROUP_ID"]' \
65
+ --params.script @./hello.sh \
66
+ --triggers '[{"type":"manual","enabled":true}]'
67
+ ```
68
+
69
+ The `@./hello.sh` value reads your local script into the Event. The Group determines which workers are eligible to run it, and the manual trigger allows you to launch it on demand. There is no schedule, so this Event runs when you ask it to.
70
+
71
+ The creation command prints the new Event ID. Use it to review the saved definition:
72
+
73
+ ```sh
74
+ xy event EVENT_ID
75
+ ```
76
+
77
+ - **Command reference:** [Event creation](help.md#event-create).
78
+ - **Learn more about xyOps:** [xyOps Events](https://docs.xyops.io/events) and [xyOps Triggers](https://docs.xyops.io/triggers).
79
+
80
+ ## 3. Run a job and follow its output
81
+
82
+ Launch the Event and follow the job until it finishes:
83
+
84
+ ```sh
85
+ xy run EVENT_ID --follow
86
+ ```
87
+
88
+ You should see the greeting, the worker's hostname, and the final message. xyCLI then displays the completed job report.
89
+
90
+ Each run gets its own Job ID. Use the ID printed at launch to revisit its report or read its output log:
91
+
92
+ ```sh
93
+ xy job JOB_ID
94
+ xy job log JOB_ID
95
+ ```
96
+
97
+ Find all of today's completed runs of your Event:
98
+
99
+ ```sh
100
+ xy jobs --event EVENT_ID --date today
101
+ ```
102
+
103
+ You can also launch without `--follow` and open the Job ID afterward. `xy job JOB_ID` follows an active job or displays a completed report, depending on its state.
104
+
105
+ - **Command reference:** [Running Events](help.md#event-run), [Job searches](help.md#jobs), and [Job reports](help.md#job).
106
+
107
+ ## 4. Create a custom Web Hook
108
+
109
+ Let's send a JSON notification to your HTTP endpoint. A Web Hook stores the destination and request settings; an Event action will decide when to fire it.
110
+
111
+ Create a local file named `notification-body.txt` containing this request template:
112
+
113
+ ```text
114
+ {
115
+ "source": "xycli-tutorial",
116
+ "message": {{ stringify(text) }}
117
+ }
118
+ ```
119
+
120
+ `text` is the notification message generated by xyOps. `stringify(text)` turns it into a quoted JSON string, including escaping quotes and line breaks. Leave that expression unquoted in the template: xyOps fills it in before sending the request, producing valid JSON.
121
+
122
+ Create the Hook, replacing `https://hooks.example.com/xycli` with your endpoint:
123
+
124
+ ```sh
125
+ xy hook create --title "xyCLI Success Notification" \
126
+ --url https://hooks.example.com/xycli \
127
+ --method POST --body @./notification-body.txt
128
+ ```
129
+
130
+ xyCLI prints the new Hook ID. The default headers include `Content-Type: application/json`, and new Hooks are enabled. Creating the Hook saves its definition without sending a request.
131
+
132
+ Inspect it, then send a test request:
133
+
134
+ ```sh
135
+ xy hook HOOK_ID
136
+ xy hook test HOOK_ID
137
+ ```
138
+
139
+ The test makes a real HTTP request from xyOps and displays the request, response, and timing details. Your endpoint should receive a JSON object with `source` set to `xycli-tutorial` and a test notification in `message`. The test uses sample notification data; it does not run your Event.
140
+
141
+ If the request fails, check the URL, the endpoint's response, and network access from the conductor. If your destination needs authentication or a different payload, adjust the Hook to match its API before continuing.
142
+
143
+ - **Command reference:** [Hook creation](help.md#hook-create) and [Hook testing](help.md#hook-test).
144
+ - **Learn more about xyOps:** [xyOps Web Hooks](https://docs.xyops.io/webhooks).
145
+
146
+ ## 5. Fire the Hook when your Event succeeds
147
+
148
+ Attach an enabled Web Hook action to the Event. Replace `HOOK_ID` inside the JSON with the Hook ID from the previous step:
149
+
150
+ ```sh
151
+ xy event update EVENT_ID \
152
+ --action '{"type":"web_hook","enabled":true,"condition":"success","web_hook":"HOOK_ID","text":"Hello from xyCLI finished successfully."}'
153
+ ```
154
+
155
+ The `success` condition fires when the job completes successfully. The `web_hook` field selects the saved Hook, and `text` adds your custom message to the notification. A failed run will not fire this success action.
156
+
157
+ The singular `--action` option **appends** to the Event's existing actions. Run this update once; repeating it adds another action entry. Review the Event to confirm the new action is present:
158
+
159
+ ```sh
160
+ xy event EVENT_ID
161
+ ```
162
+
163
+ Now run the Event again:
164
+
165
+ ```sh
166
+ xy run EVENT_ID --follow
167
+ ```
168
+
169
+ After the job succeeds, check your endpoint for the notification. The same Hook template now receives the real job's notification text, including your custom message. Your Event and Hook are saved, so future successful runs use the same action automatically.
170
+
171
+ The completed job report includes the actions that fired and their results. If the notification does not arrive, revisit `xy job JOB_ID` and inspect the Web Hook action result as well as your endpoint's request log.
172
+
173
+ - **Command reference:** [Event updates](help.md#event-update).
174
+ - **Learn more about xyOps:** [xyOps Actions](https://docs.xyops.io/actions).
175
+
176
+ ## 6. Update your script from your editor
177
+
178
+ Add another message to your local `hello.sh`, then upload the revised script:
179
+
180
+ ```sh
181
+ xy event update EVENT_ID --params.script @./hello.sh
182
+ xy run EVENT_ID --follow
183
+ ```
184
+
185
+ The update changes the saved script while preserving the Event's other settings, including the success action. Editing your local file alone does not update xyOps; the update command sends the new contents.
186
+
187
+ This is a simple way to iterate on automation with your usual editor. For a larger collection of Events and Plugins, explore the [filesystem sync overview](../README.md#sync-your-automation-with-local-files).
188
+
189
+ ## 7. Export the Event and its Hook together
190
+
191
+ Save your finished Event as an xyOps Portable Data Format (XYPDF) file, including the Web Hook referenced by its action:
192
+
193
+ ```sh
194
+ xy event EVENT_ID --export ./hello-event.json --deps web_hooks
195
+ ```
196
+
197
+ The export includes the Event's saved script, manual trigger, and success action, plus the custom Hook definition. You can review the file, keep it in Git, or import it into another instance using xyCLI or the xyOps web interface.
198
+
199
+ This example selects only the Hook dependency. Importing elsewhere also requires compatible Plugins, Categories, and target Groups; use `--deps all` to include all supported portable dependencies. Individual workers are not portable.
200
+
201
+ - **Command reference:** [Export](help.md#export) and [Import](help.md#import).
202
+ - **Learn more about xyOps:** [xyOps Portable Data Format](https://docs.xyops.io/xypdf).
203
+
204
+ For more ideas, return to [Explore and manage xyOps](../README.md#explore-and-manage-xyops) or browse the [screenshot gallery](screenshots.md).
package/index.js ADDED
@@ -0,0 +1,444 @@
1
+ #!/usr/bin/env node
2
+
3
+ // xyOps CLI
4
+ // See: https://github.com/pixlcore/xyops
5
+ // Copyright (c) 2019 - 2026 PixlCore LLC, BSD 3-Clause License
6
+
7
+ // Config Keys: api_key, base_url, temp_dir, color, suggest, items_per_page
8
+
9
+ const fs = require('fs');
10
+ const os = require('os');
11
+ const cp = require('child_process');
12
+ const Path = require('path');
13
+ const Uncatch = require('uncatch');
14
+ const cli = require('pixl-cli');
15
+ const pkg = require('./package.json');
16
+
17
+ if (!process.env.XYOPS_USER_AGENT) {
18
+ process.env.XYOPS_USER_AGENT = `xyOps CLI v${pkg.version} (Node.js); ${os.hostname()} (${os.platform()}/${os.arch()})`;
19
+ }
20
+
21
+ cli.global();
22
+
23
+ cli.mapArgs({
24
+ 'v': 'verbose',
25
+ 'q': 'quiet',
26
+ 'd': 'debug',
27
+ 'f': 'format',
28
+ 'r': 'raw',
29
+ 'h': 'help'
30
+ });
31
+
32
+ process.on('SIGINT', function() { cli.progress.end(); process.exit(130); } );
33
+ process.on('SIGTERM', function() { cli.progress.end(); process.exit(143); } );
34
+ process.on('SIGHUP', function() { cli.progress.end(); process.exit(129); } );
35
+
36
+ Uncatch.on('uncaughtException', function(err) {
37
+ // execute your own application shutdown routine here
38
+ // do not call any async code, and do not call process.exit
39
+ cli.progress.end();
40
+ });
41
+
42
+ const { api } = require('@pixlcore/xyops-sdk');
43
+ var highlight = require('cli-highlight').highlight;
44
+ const Tools = cli.Tools;
45
+ const chalk = cli.chalk;
46
+
47
+ // minimum xyops version
48
+ const XYOPS_MIN_VERSION = '1.0.96';
49
+
50
+ // These API parameters are always opaque strings, even when their contents
51
+ // happen to look like JSON or come from a file with a .json extension.
52
+ const JSON_ARG_EXCEPTIONS = ['body'];
53
+
54
+ // Keep the public command inventory in one place, so typo suggestions and the
55
+ // fallback help text always advertise the exact same top-level commands.
56
+ const TOP_LEVEL_COMMANDS = [
57
+ 'help', 'config', 'doc', 'dashboard', 'system', 'upcoming',
58
+ 'alerts', 'alert', 'buckets', 'bucket', 'categories', 'category',
59
+ 'channels', 'channel', 'events', 'event', 'run', 'groups', 'group',
60
+ 'jobs', 'job', 'keys', 'key', 'log', 'monitors', 'monitor',
61
+ 'plugins', 'plugin', 'servers', 'server', 'secrets', 'secret',
62
+ 'snapshots', 'snapshot', 'tags', 'tag', 'tickets', 'ticket',
63
+ 'hooks', 'hook', 'marketplace',
64
+ 'import', 'api', 'repl'
65
+ ];
66
+
67
+ // coerce true/false into booleans
68
+ for (var key in cli.args) {
69
+ if (cli.args[key] === 'true') cli.args[key] = true;
70
+ else if (cli.args[key] === 'false') cli.args[key] = false;
71
+ }
72
+
73
+ // make our own copy of cli.args
74
+ var args = Object.assign( {}, cli.args );
75
+
76
+ if (!args.other || !args.other.length) args.other = ['dashboard'];
77
+ else if (args.help) args.other = ['help'];
78
+
79
+ var cmd = args.other.shift().toLowerCase();
80
+
81
+ const app = {
82
+
83
+ epoch: Tools.timeNow(),
84
+
85
+ colors: {
86
+ "theme": [55, 145, 245],
87
+ "gray": [127, 127, 127],
88
+
89
+ "red": [251, 44, 54],
90
+ "fire": [255, 77, 7],
91
+ "orange": [255, 105, 0],
92
+ "tangerine": [255, 130, 0],
93
+ "amber": [254, 154, 0],
94
+ "gold": [248, 165, 0],
95
+ "yellow": [240, 177, 0],
96
+ "lemon": [198, 191, 0],
97
+ "lime": [124, 207, 0],
98
+ "grass": [74, 204, 38],
99
+ "green": [0, 201, 80],
100
+ "mint": [0, 195, 106],
101
+ "emerald": [0, 188, 125],
102
+ "aqua": [0, 188, 148],
103
+ "teal": [0, 187, 167],
104
+ "turquoise": [0, 187, 195],
105
+ "cyan": [0, 184, 219],
106
+ "ice": [0, 176, 232],
107
+ "sky": [0, 166, 244],
108
+ "azure": [0, 148, 254],
109
+ "blue": [43, 127, 255],
110
+ "sapphire": [75, 111, 255],
111
+ "indigo": [97, 95, 255],
112
+ "lavender": [120, 88, 255],
113
+ "violet": [142, 81, 255],
114
+ "orchid": [158, 76, 255],
115
+ "purple": [173, 70, 255],
116
+ "magenta": [199, 59, 255],
117
+ "fuchsia": [225, 42, 251],
118
+ "hotpink": [241, 43, 201],
119
+ "pink": [246, 51, 154],
120
+ "blush": [252, 40, 122],
121
+ "rose": [255, 32, 86]
122
+ },
123
+
124
+ condition_colors: {
125
+ 'start': 'sky',
126
+ 'complete': 'emerald',
127
+ 'continue': 'gray',
128
+ 'success': 'green',
129
+ 'error': 'red',
130
+ 'user': 'orange',
131
+ 'warning': 'yellow',
132
+ 'critical': 'purple',
133
+ 'abort': 'gray'
134
+ },
135
+
136
+ events: [],
137
+ categories: [],
138
+ groups: [],
139
+ plugins: [],
140
+ tags: [],
141
+ api_keys: [],
142
+ buckets: [],
143
+ roles: [],
144
+ secrets: [],
145
+ web_hooks: [],
146
+
147
+ activeJobs: {},
148
+ activeAlerts: {},
149
+ internalJobs: {},
150
+ servers: {},
151
+ serverCache: {},
152
+ state: {},
153
+ stats: {},
154
+
155
+ lastMonthDayCache: {},
156
+
157
+ async run() {
158
+ // main entry point
159
+ var self = this;
160
+
161
+ this.loadConfig();
162
+
163
+ this.version = pkg.version;
164
+ this.args = args;
165
+ this.api = api;
166
+ this.cli = cli;
167
+ this.highlight = highlight;
168
+
169
+ this.debug = args.debug;
170
+ this.verbose = args.verbose;
171
+ this.quiet = args.quiet;
172
+
173
+ // process global common args
174
+ this.format = args.format || '';
175
+ delete args.format;
176
+
177
+ this.raw = args.raw || this.config.raw || false;
178
+ delete args.raw;
179
+
180
+ this.dry = args.dry || false;
181
+ delete args.dry;
182
+
183
+ // show invisible jobs: config prop only
184
+ this.invisible = this.config.invisible || false;
185
+
186
+ // optionally disable all ANSI color
187
+ if (("color" in this.config) && !this.config.color) {
188
+ cli.chalk.enabled = false;
189
+ highlight = this.highlight = function(text) { return text; };
190
+ }
191
+
192
+ println( "\n " + cli.emoji('🚀') + " " + this.color('theme').bold("xyOps CLI ") + gray("v" + this.version) );
193
+
194
+ // call config cmd early (before contacting xyops)
195
+ if (cmd == 'config') {
196
+ await this['cmd_' + cmd]();
197
+ print("\n");
198
+ return;
199
+ }
200
+
201
+ delete args.debug;
202
+ delete args.echo;
203
+ delete args.quiet;
204
+ delete args.verbose;
205
+
206
+ // create temp dir if needed
207
+ this.tempDir = this.config.temp_dir || Path.join( os.tmpdir(), 'xyops', 'cli' );
208
+ if (!fs.existsSync(this.tempDir)) Tools.mkdirpSync( this.tempDir );
209
+
210
+ if (!this.config.api_key) {
211
+ this.die("Missing xyOps API Key. Set an 'api_key' property in " + this.configFiles.join(', or ') + ", or set a 'XYOPS_API_KEY' environment variable.");
212
+ }
213
+ if (!this.config.base_url) {
214
+ this.die("Missing xyOps Base URL. Set a 'base_url' property in " + this.configFiles.join(', or ') + ", or set a 'XYOPS_BASE_URL' environment variable.");
215
+ }
216
+
217
+ println( ' ' + gray(this.config.base_url) );
218
+
219
+ if (this.dry) {
220
+ this.toast('⚠️', 'orange', bold("DRY RUN: ") + "No changes will actually be made.");
221
+ }
222
+
223
+ // optionally read STDIN into a named arg (use curl @- convention)
224
+ var stdin_arg_key = null;
225
+ for (var key in args) {
226
+ if (args[key] === '@-') { stdin_arg_key = key; break; }
227
+ }
228
+ if (stdin_arg_key) {
229
+ verboseln("Reading STDIN into: " + stdin_arg_key);
230
+ const chunks = [];
231
+ for await (const chunk of process.stdin) chunks.push(chunk);
232
+ args[stdin_arg_key] = chunks.join('');
233
+
234
+ // Some API parameters are opaque strings even when they look like JSON.
235
+ if (!JSON_ARG_EXCEPTIONS.includes(stdin_arg_key) && (args[stdin_arg_key].trim().match(/^\{[\s\S]*\}$/) || args[stdin_arg_key].trim().match(/^\[[\s\S]*\]$/))) {
236
+ try { args[stdin_arg_key] = JSON.parse(args[stdin_arg_key]); }
237
+ catch (err) { this.die("Failed to parse JSON from STDIN: " + err); }
238
+ }
239
+ }
240
+
241
+ // optionally read any file into any arg (use curl @FILE convention)
242
+ for (var key in args) {
243
+ if (String(args[key]).match(/^@(.+)$/)) {
244
+ var file = RegExp.$1;
245
+ if (!fs.existsSync(file)) this.die("File not found: " + file);
246
+ if (!JSON_ARG_EXCEPTIONS.includes(key) && file.match(/\.json$/i)) args[key] = JSON.parse( fs.readFileSync(file, 'utf8') );
247
+ else args[key] = fs.readFileSync(file, 'utf8');
248
+ }
249
+ }
250
+
251
+ // if 'json' arg is present and an object, merge into top-level
252
+ // (so user can pipe entire request in from STDIN or file)
253
+ if (args.json && (typeof(args.json) == 'object')) {
254
+ Tools.mergeHashInto( args, args.json );
255
+ delete args.json;
256
+ }
257
+
258
+ // any arg value that looks like json will be parsed
259
+ for (var key in args) {
260
+ if (!JSON_ARG_EXCEPTIONS.includes(key) && args[key] && (typeof(args[key]) == 'string') && (args[key].trim().match(/^\{[\s\S]*\}$/) || args[key].trim().match(/^\[[\s\S]*\]$/))) {
261
+ try { args[key] = JSON.parse( args[key] ); }
262
+ catch (err) { this.die("Failed to parse JSON from argument: " + err); }
263
+ }
264
+ }
265
+
266
+ // Export is an explicit read-only variant, including portable object types
267
+ // whose full resource routers are not yet implemented. Do not let the flag
268
+ // become a search filter or an accidental field in a create/update request.
269
+ if (('export' in args) && (cmd != 'api')) {
270
+ await this.cacheConfig();
271
+ await this.cmd_export_object(cmd);
272
+ print("\n");
273
+ return;
274
+ }
275
+
276
+ if (!this['cmd_' + cmd]) {
277
+ // allow user to swap first two args, if 2nd is known command
278
+ if (this['cmd_' + args.other[0]]) {
279
+ var new_cmd = args.other[0];
280
+ args.other[0] = cmd;
281
+ cmd = new_cmd;
282
+ }
283
+ else {
284
+ var suggestion = this.findClosestString(cmd, TOP_LEVEL_COMMANDS);
285
+ this.die(
286
+ "Unknown command: " + cmd + (suggestion ? '. Did you mean "' + suggestion + '"?' : ''),
287
+ "Available Commands: " + TOP_LEVEL_COMMANDS.join(', ') + "\n\n"
288
+ );
289
+ }
290
+ }
291
+
292
+ // merge in config from xyops
293
+ await this.cacheConfig();
294
+
295
+ // make sure we have a compatible version of xyops
296
+ if (this.compareVersions(XYOPS_MIN_VERSION, this.xyopsVersion) > 0) {
297
+ this.die("The CLI requires xyOps v" + XYOPS_MIN_VERSION + " or higher.");
298
+ }
299
+
300
+ // go go go
301
+ await this['cmd_' + cmd]();
302
+
303
+ // always end with empty line
304
+ print("\n");
305
+ },
306
+
307
+ async cmd_repl() {
308
+ // open repl for user to debug
309
+ await this.getMultiple();
310
+
311
+ println( "\n" + this.markdown(
312
+ "The REPL exposes `app`, `xy`, `config`, `cli`, and `Tools` in its context.\n\n" +
313
+ "Type `.exit` or hit `Ctrl-C` to exit."
314
+ ) );
315
+
316
+ print("\n");
317
+ var repl = this.repl = require('repl').start({ prompt: '> ', useGlobal: true, ignoreUndefined: true });
318
+
319
+ repl.context.app = this;
320
+ repl.context.config = this.config;
321
+ repl.context.cli = cli;
322
+ repl.context.Tools = Tools;
323
+ repl.context.xy = this;
324
+
325
+ await new Promise((resolve) => {
326
+ repl.once('exit', () => {
327
+ delete app.repl;
328
+ resolve();
329
+ });
330
+ });
331
+ },
332
+
333
+ async cmd_api() {
334
+ // perform arbitrary xyops api call
335
+ // e.g. "xy api getEvents"
336
+ var name = this.args.other.shift() || this.dieUsage('api');
337
+ delete this.args.other;
338
+
339
+ this.mergeDotArgs( this.args, this.args );
340
+ if (this.verbose) this.args.verbose = true;
341
+
342
+ println( "\n " + this.color('theme').bold("Calling API: " + name) );
343
+
344
+ print( "\n " + cyan.bold("Request:") );
345
+ this.jsonOutput(this.args);
346
+
347
+ if (this.dry) {
348
+ this.toast('⚠️', 'orange', bold("DRY RUN: ") + "Exiting without sending request.");
349
+ return;
350
+ }
351
+
352
+ cli.progress.start({ amount: 1, pct: false, text: gray('→ ' + name) });
353
+ var { err, data } = await this.api[name](this.args);
354
+ cli.progress.end();
355
+ if (err) this.die(err);
356
+
357
+ print( "\n " + cyan.bold("Response:") );
358
+ this.jsonOutput(data);
359
+ },
360
+
361
+ async cmd_help() {
362
+ // show help section
363
+ var parts = this.args.other.slice(0);
364
+ var heading = parts.join(' ') || 'help';
365
+ this.printHelp(heading);
366
+ },
367
+
368
+ printHelp(heading) {
369
+ // print named section from help file
370
+ heading = heading.toLowerCase();
371
+ var md = fs.readFileSync(Path.join(__dirname, 'docs', 'help.md'), 'utf8').trim() + "\n\n# end sentinel\n";
372
+ var re = new RegExp( "(^|\\n)(\\#+)\\s+(" + Tools.escapeRegExp(heading) + ")\\n([\\s\\S]*?)\\n\#+\\s+" );
373
+ var matches = md.match(re);
374
+ if (!matches) {
375
+ var msg = 'Could not find help chapter for "' + heading + '".';
376
+
377
+ // Extract the user-visible Markdown headings and reuse the same conservative
378
+ // Levenshtein matcher used for API property typo suggestions. The final
379
+ // heading is synthetic and exists only to terminate the section regex.
380
+ var headings = Array.from( md.matchAll(/^\#+\s+(.+?)\s*$/gm), match => match[1] );
381
+ headings = headings.filter( heading => heading != 'end sentinel' );
382
+ var suggestion = this.findClosestString(heading, headings);
383
+ if (suggestion) msg += ' Did you mean "' + suggestion + '"?';
384
+
385
+ this.die(msg);
386
+ }
387
+
388
+ if (heading == 'help') heading = 'overview';
389
+ println( "\n " + this.color('theme').bold( 'HELP: ' + heading.toUpperCase() ) );
390
+ println( "\n" + this.markdown( matches[4].trim() ).trimEnd() );
391
+ },
392
+
393
+ dieUsage(heading) {
394
+ // print help and exit
395
+ this.printHelp(heading);
396
+ println("");
397
+ process.exit(1);
398
+ },
399
+
400
+ die(msg, extra = "") {
401
+ // colorful die
402
+ cli.progress.end();
403
+ if (typeof(msg) == 'object') {
404
+ print("\n");
405
+ console.error(msg);
406
+ if (msg.message) msg = msg.message;
407
+ }
408
+ cli.args.quiet = false;
409
+ die( "\n ❌ " + red.bold("ERROR: ") + yellow.bold(msg) + "\n\n" + extra );
410
+ }
411
+
412
+ };
413
+
414
+ Tools.mergeHashInto( app, require('./lib/utils.js') );
415
+ Tools.mergeHashInto( app, require('./lib/config.js') );
416
+ Tools.mergeHashInto( app, require('./lib/doc.js') );
417
+ Tools.mergeHashInto( app, require('./lib/dashboard.js') );
418
+ Tools.mergeHashInto( app, require('./lib/system.js') );
419
+ Tools.mergeHashInto( app, require('./lib/events.js') );
420
+ Tools.mergeHashInto( app, require('./lib/jobs.js') );
421
+ Tools.mergeHashInto( app, require('./lib/apikey.js') );
422
+ Tools.mergeHashInto( app, require('./lib/alerts.js') );
423
+ Tools.mergeHashInto( app, require('./lib/buckets.js') );
424
+ Tools.mergeHashInto( app, require('./lib/categories.js') );
425
+ Tools.mergeHashInto( app, require('./lib/channels.js') );
426
+ Tools.mergeHashInto( app, require('./lib/log.js') );
427
+ Tools.mergeHashInto( app, require('./lib/monitors.js') );
428
+ Tools.mergeHashInto( app, require('./lib/plugins.js') );
429
+ Tools.mergeHashInto( app, require('./lib/servers.js') );
430
+ Tools.mergeHashInto( app, require('./lib/groups.js') );
431
+ Tools.mergeHashInto( app, require('./lib/snapshots.js') );
432
+ Tools.mergeHashInto( app, require('./lib/secrets.js') );
433
+ Tools.mergeHashInto( app, require('./lib/tags.js') );
434
+ Tools.mergeHashInto( app, require('./lib/tickets.js') );
435
+ Tools.mergeHashInto( app, require('./lib/webhook.js') );
436
+ Tools.mergeHashInto( app, require('./lib/transfer.js') );
437
+ Tools.mergeHashInto( app, require('./lib/marketplace.js') );
438
+ Tools.mergeHashInto( app, require('./lib/sync.js') );
439
+
440
+ global.app = app;
441
+
442
+ app.run().catch( function(err) {
443
+ app.die(err);
444
+ } );