@pixlcore/xycli 1.2.0 → 1.2.2
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/.github/workflows/docker.yml +1 -0
- package/.github/workflows/offline-tests.yml +1 -1
- package/CHANGELOG.md +65 -0
- package/LICENSE.md +28 -0
- package/README.md +1 -1
- package/docs/help.md +2 -0
- package/index.js +2 -1
- package/lib/config.js +36 -0
- package/lib/jobs.js +21 -3
- package/lib/mcp.js +3 -0
- package/package.json +2 -2
- package/test/README.md +1 -0
- package/test/config.test.js +62 -3
- package/test/jobs-follow-unit.test.js +107 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,70 @@
|
|
|
1
1
|
# xyCLI Changelog
|
|
2
2
|
|
|
3
|
+
## Version v1.2.1
|
|
4
|
+
|
|
5
|
+
> October 29, 2026
|
|
6
|
+
|
|
7
|
+
- [`c5b6e1c`](https://github.com/pixlcore/xycli/commit/c5b6e1c43296ba8d58d68ebee245b7d27dc4fbfb): Version 1.2.1
|
|
8
|
+
- [`a2fca91`](https://github.com/pixlcore/xycli/commit/a2fca9138e5748d70b1e139b6d0b84f082ec7406): Feature: Follow live job retries automatically
|
|
9
|
+
- [`6b3686d`](https://github.com/pixlcore/xycli/commit/6b3686d18c72139e32d18e1c90e10f8910565224): Feature: Set custom process title for MCP server.
|
|
10
|
+
- [`099444b`](https://github.com/pixlcore/xycli/commit/099444bd87765c1a1e97a65bae6a2b2ea7f4f757): Add LICENSE.md file
|
|
11
|
+
- [`2d6c1c4`](https://github.com/pixlcore/xycli/commit/2d6c1c44a73e265fba38b4feb1c2a7aaea843487): Meta: Fix offline test trigger to only run on tag pushes
|
|
12
|
+
|
|
13
|
+
## Version v1.2.0
|
|
14
|
+
|
|
15
|
+
> October 25, 2026
|
|
16
|
+
|
|
17
|
+
- [`3f99851`](https://github.com/pixlcore/xycli/commit/3f9985186f3cba6e04c2fb0cc7b6eae3a7fc7fcb): Fix Windows MCP shutdown tests and update Node.js CI matrix
|
|
18
|
+
- [`2034c48`](https://github.com/pixlcore/xycli/commit/2034c48626f999ffb969d4cebb7343cb233d565d): Version 1.2.0
|
|
19
|
+
- [`e9693fb`](https://github.com/pixlcore/xycli/commit/e9693fbc527135b45ac2fc39659adb96923d9272): Feature: Expand MCP tools with tickets, alerts, snapshots and servers
|
|
20
|
+
- [`1c12b76`](https://github.com/pixlcore/xycli/commit/1c12b76b747d97ccf358501d438e6deb8b97ca4e): Feature: Add Docker image and automated GHCR publishing for xyCLI
|
|
21
|
+
- [`fa33af6`](https://github.com/pixlcore/xycli/commit/fa33af6b793786da33d6ae0bf832bcdea182e6b0): Feature: Add MCP server with shared STDIO and HTTP transports
|
|
22
|
+
|
|
23
|
+
## Version v1.1.1
|
|
24
|
+
|
|
25
|
+
> October 2, 2026
|
|
26
|
+
|
|
27
|
+
- [`5f924c6`](https://github.com/pixlcore/xycli/commit/5f924c69ad8c5ed3c04be40d600b95fa3eee5f66): Version 1.1.1
|
|
28
|
+
- [`759f8db`](https://github.com/pixlcore/xycli/commit/759f8db93f1763b7e876d16066b7dd04a9681ec3): Doc: Update xyOps version to v1.1.4 in two-way delete tutorial, and add in sys activity exclusions.
|
|
29
|
+
- [`551be24`](https://github.com/pixlcore/xycli/commit/551be2413b4e584daebb2ed57710a620a7006372): Dep: Override "basic-ftp" (sub-sub-sub-sub-dep of pixl-request) to v6.2.1 for vuln fix, that isn't fixed in any of the parents.
|
|
30
|
+
|
|
31
|
+
## Version v1.1.0
|
|
32
|
+
|
|
33
|
+
> September 30, 2026
|
|
34
|
+
|
|
35
|
+
- [`50ff6e6`](https://github.com/pixlcore/xycli/commit/50ff6e6f5f11a29287bcf0ffe2d8f7d966d34ee3): Version 1.1.0
|
|
36
|
+
- [`5bf07a8`](https://github.com/pixlcore/xycli/commit/5bf07a87cfdc649abd8058c10f73c44b037c8cf4): Fix ticket unit test (due to API key bug fix over in xyOps)
|
|
37
|
+
- [`5493608`](https://github.com/pixlcore/xycli/commit/5493608d71dbf8cfd9fbb3f2fef958d1bb107ee1): Feature: Down-sync delete mode, and a new two-way delete setup tut.
|
|
38
|
+
|
|
39
|
+
## Version v1.0.9
|
|
40
|
+
|
|
41
|
+
> September 28, 2026
|
|
42
|
+
|
|
43
|
+
- [`9cbc6c7`](https://github.com/pixlcore/xycli/commit/9cbc6c753e9351243e13a942a3ad19c2012884cb): Version 1.0.9
|
|
44
|
+
- [`6a0d739`](https://github.com/pixlcore/xycli/commit/6a0d739c3d85f61758bc9ea25c77a3b00968c79d): Feature: New system hook (syshook) deletion system, for active deletes from xyOps.
|
|
45
|
+
- [`eeae786`](https://github.com/pixlcore/xycli/commit/eeae7863361c1f343dedae301a00d99db3fccdbb): Feature: New "--ignore_missing" parameter to suppress missing item warnings. Fixes #9
|
|
46
|
+
- [`80e77d4`](https://github.com/pixlcore/xycli/commit/80e77d40edfdb1c7fb4285235c6cc9fb2f0bcb2a): Bug Fix: Always print all errors and warnings, even in quiet mode. Fixes #8
|
|
47
|
+
|
|
48
|
+
## Version v1.0.8
|
|
49
|
+
|
|
50
|
+
> September 25, 2026
|
|
51
|
+
|
|
52
|
+
- [`3cc0914`](https://github.com/pixlcore/xycli/commit/3cc0914dd00211e6215ef3f2e5f7b2467570c9d0): Version 1.0.8
|
|
53
|
+
- [`04d0f31`](https://github.com/pixlcore/xycli/commit/04d0f31eaee9b89bb5f2518c9bf813d24b5235eb): Meta: Fix offline unit tests on Windows (was using colon in filename)
|
|
54
|
+
- [`5570628`](https://github.com/pixlcore/xycli/commit/557062875ab99a4cca41ae419204bc4992962ef1): Bug Fix: Remove some redundant dry run toasts.
|
|
55
|
+
- [`536685e`](https://github.com/pixlcore/xycli/commit/536685eae72bc9bea1be24f4ba7e4998f17386fb): Feature: Allow buckets, bucket data and bucket files to all be synced. Fixes #6
|
|
56
|
+
- [`3cc2ccb`](https://github.com/pixlcore/xycli/commit/3cc2ccb93202bce52de6fe09dead7854ca274640): Docs: Added instructions for deleting items with two-way sync
|
|
57
|
+
- [`38d336b`](https://github.com/pixlcore/xycli/commit/38d336b20797e435664924623e94d877a74c3c0a): Bug Fix: Allow unknown object properties to sync without error (i.e. for legacy Cronicle imports).
|
|
58
|
+
- [`dc1e994`](https://github.com/pixlcore/xycli/commit/dc1e994c3b55eb4040210c8882cf01cd47b0defb): Bug FIx: For printing Emoji, do not use cursor save/restore width-fix when stdout is not a TTY.
|
|
59
|
+
|
|
60
|
+
## Version v1.0.7
|
|
61
|
+
|
|
62
|
+
> September 23, 2026
|
|
63
|
+
|
|
64
|
+
- [`0cce2eb`](https://github.com/pixlcore/xycli/commit/0cce2eb70bc3f17e54fb19c8db43ce937701b68c): Version 1.0.7
|
|
65
|
+
- [`4089e78`](https://github.com/pixlcore/xycli/commit/4089e78e9dc67894afa9d5c325af0e7c37e762a6): Bug Fix: Typo in custom job user HTML content when trying to rendering the caption. Fixes #7.
|
|
66
|
+
- [`76a247c`](https://github.com/pixlcore/xycli/commit/76a247c9abadeb9ee6f9c7bfb89d06cd375c41dd): Feature: Sync: Support workflow node sub-property neighbor files. Fixes #5.
|
|
67
|
+
|
|
3
68
|
## Version v1.0.6
|
|
4
69
|
|
|
5
70
|
> September 22, 2026
|
package/LICENSE.md
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 PixlCore LLC & CONTRIBUTORS.
|
|
4
|
+
|
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
|
6
|
+
modification, are permitted provided that the following conditions are met:
|
|
7
|
+
|
|
8
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
9
|
+
list of conditions and the following disclaimer.
|
|
10
|
+
|
|
11
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
and/or other materials provided with the distribution.
|
|
14
|
+
|
|
15
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
16
|
+
contributors may be used to endorse or promote products derived from
|
|
17
|
+
this software without specific prior written permission.
|
|
18
|
+
|
|
19
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
20
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
21
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
22
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
23
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
24
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
25
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
26
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
27
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
28
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
package/README.md
CHANGED
|
@@ -243,7 +243,7 @@ xy job JOB_ID
|
|
|
243
243
|
xy job log JOB_ID --download ./job-output.log
|
|
244
244
|
```
|
|
245
245
|
|
|
246
|
-
You can also rerun a previous job with `xy job run JOB_ID --follow`, or resume a workflow job where supported. Job reports bring together results, performance, output, files, and workflow details.
|
|
246
|
+
You can also rerun a previous job with `xy job run JOB_ID --follow`, or resume a workflow job where supported. When a job you are following fails and is retried, the CLI automatically follows the retry, continuing until the job succeeds or no further retry is scheduled. Job reports bring together results, performance, output, files, and workflow details.
|
|
247
247
|
|
|
248
248
|
- **Command reference:** [Job searches](docs/help.md#jobs) and [Job operations](docs/help.md#job).
|
|
249
249
|
- **Learn more about xyOps:** [xyOps Events and job execution](https://docs.xyops.io/events) and [xyOps Job data](https://docs.xyops.io/data#job).
|
package/docs/help.md
CHANGED
|
@@ -394,6 +394,8 @@ xy config --base_url http://localhost:5522 --api_key YOUR_API_KEY
|
|
|
394
394
|
|
|
395
395
|
Updates load the user file separately and apply only the settings specified on the command line, preserving other user settings. System settings and environment overrides are not copied into the file. If the user file does not exist, it is created with only the supplied settings.
|
|
396
396
|
|
|
397
|
+
Unknown top-level configuration keys are rejected before saving any changes, with a spelling suggestion when available. Use underscores in names such as `--base_url` and `--api_key`. Structured settings still support nested updates, such as `xy config --sync.down false` or `xy config --mcp.port 8080`.
|
|
398
|
+
|
|
397
399
|
Configuration loads the machine file first, then the user file, then `XYOPS_`-prefixed environment variables. The machine and user files are `/etc/xyops/cli.json` and `~/.config/xyops/cli.json` on Unix and macOS, or `%PROGRAMDATA%\xyops\cli.json` and `%USERPROFILE%\.config\xyops\cli.json` on Windows. Later sources override matching top-level settings. Environment overrides still take precedence over saved user settings. Common keys include `api_key`, `base_url`, `temp_dir`, `color`, `suggest`, `items_per_page`, `cache_ttl`, `raw`, and `invisible`.
|
|
398
400
|
|
|
399
401
|
Environment values that look like JSON objects or arrays are parsed automatically for any setting. For example, `XYOPS_SYNC='{"up":["events"],"down":false}'` supplies a structured `sync` setting. Boolean and numeric values keep their usual conversions. Malformed JSON-looking values report the environment variable name. Objects and arrays are serialized as JSON when the CLI exports its effective configuration back into the environment.
|
package/index.js
CHANGED
|
@@ -203,7 +203,8 @@ const app = {
|
|
|
203
203
|
delete args.format;
|
|
204
204
|
|
|
205
205
|
this.raw = args.raw || this.config.raw || false;
|
|
206
|
-
|
|
206
|
+
// The config command saves --raw as a preference for future commands.
|
|
207
|
+
if (cmd != 'config') delete args.raw;
|
|
207
208
|
|
|
208
209
|
this.dry = args.dry || false;
|
|
209
210
|
delete args.dry;
|
package/lib/config.js
CHANGED
|
@@ -7,6 +7,27 @@ const cli = require('pixl-cli');
|
|
|
7
7
|
const Tools = cli.Tools;
|
|
8
8
|
const chalk = cli.chalk;
|
|
9
9
|
|
|
10
|
+
// Settings consumed by the CLI, MCP server, or SDK. Keep this inventory
|
|
11
|
+
// explicit so a typo already saved in a config file cannot become a valid key.
|
|
12
|
+
const CONFIG_KEYS = [
|
|
13
|
+
'api_key', 'base_url', 'temp_dir', 'color', 'suggest', 'items_per_page',
|
|
14
|
+
'cache_ttl', 'raw', 'invisible', 'tz', 'log_columns', 'new_event_template',
|
|
15
|
+
'sync', 'mcp', 'user_agent', 'timeout', 'connect_timeout', 'idle_timeout',
|
|
16
|
+
'retries', 'retry_delay', 'retry_delay_max', 'allow_unauthorized',
|
|
17
|
+
'mcp_host', 'mcp_port', 'mcp_read_only', 'mcp_max_page_size',
|
|
18
|
+
'mcp_max_result_bytes', 'mcp_max_request_bytes', 'mcp_tls_cert', 'mcp_tls_key',
|
|
19
|
+
'ui', 'default_user_privileges', 'max_servers_per_group', 'quick_monitors',
|
|
20
|
+
'job_universal_actions', 'job_universal_limits'
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
// Structured settings also support the existing dot and slash path syntax.
|
|
24
|
+
// Their contents may include arbitrary plugin parameters or server UI data.
|
|
25
|
+
const CONFIG_PATH_KEYS = [
|
|
26
|
+
'log_columns', 'new_event_template', 'sync', 'mcp', 'ui',
|
|
27
|
+
'default_user_privileges', 'quick_monitors', 'job_universal_actions',
|
|
28
|
+
'job_universal_limits'
|
|
29
|
+
];
|
|
30
|
+
|
|
10
31
|
function getConfigFiles(platform, env, home_dir) {
|
|
11
32
|
// Keep the existing Unix paths, but use Windows's shared application-data
|
|
12
33
|
// directory instead of resolving /etc against whichever drive is current.
|
|
@@ -66,6 +87,9 @@ var config_layer = module.exports = {
|
|
|
66
87
|
// allow user to view config, and also modify it
|
|
67
88
|
delete this.args.other;
|
|
68
89
|
|
|
90
|
+
// These flags control this invocation, rather than saved preferences.
|
|
91
|
+
for (var key of ['debug', 'echo', 'quiet', 'verbose']) delete this.args[key];
|
|
92
|
+
|
|
69
93
|
if (!Tools.firstKey(this.args)) {
|
|
70
94
|
// just print config and exit
|
|
71
95
|
println( "\n " + this.color('theme').bold( 'CONFIGURATION SOURCES:' ) + "\n" );
|
|
@@ -81,6 +105,18 @@ var config_layer = module.exports = {
|
|
|
81
105
|
return;
|
|
82
106
|
}
|
|
83
107
|
|
|
108
|
+
// Validate the entire update before printing any values or touching the
|
|
109
|
+
// file. In particular, a misspelled --api-key must never echo its secret.
|
|
110
|
+
for (var key of Object.keys(this.args)) {
|
|
111
|
+
var root = key.split(/[.\/]/)[0];
|
|
112
|
+
if (CONFIG_KEYS.includes(key) || ((root != key) && CONFIG_PATH_KEYS.includes(root))) continue;
|
|
113
|
+
|
|
114
|
+
var suggestion = this.findClosestString(root, (root == key) ? CONFIG_KEYS : CONFIG_PATH_KEYS);
|
|
115
|
+
var message = 'Unknown configuration key "' + key + '".';
|
|
116
|
+
if (suggestion) message += ' Did you mean "' + suggestion + ((root != key) ? key.slice(root.length) : '') + '"?';
|
|
117
|
+
return this.die(message);
|
|
118
|
+
}
|
|
119
|
+
|
|
84
120
|
// Reload only the user's settings before applying changes. The effective
|
|
85
121
|
// configuration also contains system settings and environment overrides,
|
|
86
122
|
// which must not be copied into the user's file when saving preferences.
|
package/lib/jobs.js
CHANGED
|
@@ -173,7 +173,7 @@ module.exports = {
|
|
|
173
173
|
// check if job is still active
|
|
174
174
|
if (job.final) {
|
|
175
175
|
// job must have completed, hop over to job details
|
|
176
|
-
return await this.cmd_get_job();
|
|
176
|
+
return await this.cmd_get_job(true);
|
|
177
177
|
}
|
|
178
178
|
|
|
179
179
|
// print def list of job summary data
|
|
@@ -231,10 +231,10 @@ module.exports = {
|
|
|
231
231
|
|
|
232
232
|
// this.printJobCompletionBanner(job);
|
|
233
233
|
// delete this.activeJobs[this.args.id];
|
|
234
|
-
await this.cmd_get_job();
|
|
234
|
+
await this.cmd_get_job(true);
|
|
235
235
|
},
|
|
236
236
|
|
|
237
|
-
async cmd_get_job() {
|
|
237
|
+
async cmd_get_job(follow = false) {
|
|
238
238
|
// get job progress or result
|
|
239
239
|
if (this.args.other && this.args.other.length && !this.args.id) {
|
|
240
240
|
this.args.id = this.args.other.shift();
|
|
@@ -255,6 +255,24 @@ module.exports = {
|
|
|
255
255
|
return await this.cmd_stream_job();
|
|
256
256
|
}
|
|
257
257
|
|
|
258
|
+
// A live follow continues through retries using the completed record,
|
|
259
|
+
// which includes additional jobs that may be absent from stream updates.
|
|
260
|
+
// Ordinary lookups of completed jobs still display the requested attempt.
|
|
261
|
+
if (follow && job.code && Array.isArray(job.jobs)) {
|
|
262
|
+
var retry = job.jobs.find( item => item && item.id && (item.reason === 'retry') );
|
|
263
|
+
if (retry) {
|
|
264
|
+
if (!this.format.match(/json/)) {
|
|
265
|
+
this.printJobCompletionBanner(job);
|
|
266
|
+
println( "\n " + yellow.bold("Following retry: ") + this.getNiceJob(retry.id) );
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Reuse the stream entry point so queued retries and attempts that
|
|
270
|
+
// already finished before we attached are handled the same way.
|
|
271
|
+
this.args.id = retry.id;
|
|
272
|
+
return await this.cmd_stream_job();
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
258
276
|
if (this.format.match(/json/)) {
|
|
259
277
|
return this.jsonOutput(job);
|
|
260
278
|
}
|
package/lib/mcp.js
CHANGED
|
@@ -44,6 +44,9 @@ function configuration(config, args) {
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
async function main(argv) {
|
|
47
|
+
// Give the long-running MCP server a recognizable name in process lists.
|
|
48
|
+
process.title = 'xyOps MCP Server';
|
|
49
|
+
|
|
47
50
|
const { values, positionals } = parseArgs({ args: argv, allowPositionals: true, options: {
|
|
48
51
|
port: { type: 'string' }, host: { type: 'string' }, 'read-only': { type: 'boolean' },
|
|
49
52
|
'tls-cert': { type: 'string' }, 'tls-key': { type: 'string' },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pixlcore/xycli",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.2",
|
|
4
4
|
"description": "A complete command-line interface for the xyOps workflow automation system.",
|
|
5
5
|
"author": "Joseph Huckaby <jhuckaby@pixlcore.com>",
|
|
6
6
|
"homepage": "https://github.com/pixlcore/xycli",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
],
|
|
22
22
|
"scripts": {
|
|
23
23
|
"test": "node test.js",
|
|
24
|
-
"test:offline": "node --test test/config.test.js test/help.test.js test/markdown.test.js test/sync-buckets-unit.test.js test/sync-syshook-unit.test.js test/sync-unit.test.js test/toast.test.js test/upgrade.test.js test/utils-unit.test.js test/mcp-tools-unit.test.js test/mcp-stdio-unit.test.js test/mcp-http-unit.test.js",
|
|
24
|
+
"test:offline": "node --test test/config.test.js test/help.test.js test/jobs-follow-unit.test.js test/markdown.test.js test/sync-buckets-unit.test.js test/sync-syshook-unit.test.js test/sync-unit.test.js test/toast.test.js test/upgrade.test.js test/utils-unit.test.js test/mcp-tools-unit.test.js test/mcp-stdio-unit.test.js test/mcp-http-unit.test.js",
|
|
25
25
|
"changelog": "node tools/changelog.js"
|
|
26
26
|
},
|
|
27
27
|
"overrides": {
|
package/test/README.md
CHANGED
|
@@ -59,6 +59,7 @@ node --test test/config.test.js test/sync-buckets-unit.test.js test/sync-unit.te
|
|
|
59
59
|
| `config` | Offline user-file updates, inherited-setting isolation, missing-file creation, dotted edits, permissions, and read-only display |
|
|
60
60
|
| `events` | CRUD, dependency filters, nested edits, triggers, actions, limits, fields, validation, and help |
|
|
61
61
|
| `jobs` | Event launch, parameter and input overrides, completed reports, output logs, search, rerun, deletion, validation, and help |
|
|
62
|
+
| `jobs-follow-unit` | Offline live-follow retry chains, queued and already-completed retries, completed-job lookups, and stopping without a retry |
|
|
62
63
|
| `log` | Log searches, latest rows, matching, column selection, native output |
|
|
63
64
|
| `markdown` | Terminal rendering, inline formatting inside list items, typographic bullets, and output margins |
|
|
64
65
|
| `monitors` | CRUD, groups, evaluator requests, type conversions, validation, help |
|
package/test/config.test.js
CHANGED
|
@@ -32,7 +32,7 @@ function createConfigFixture(t, initial) {
|
|
|
32
32
|
|
|
33
33
|
return {
|
|
34
34
|
file,
|
|
35
|
-
run(args) {
|
|
35
|
+
run(args, expected_status = 0) {
|
|
36
36
|
const env = {
|
|
37
37
|
...process.env,
|
|
38
38
|
HOME: home_dir,
|
|
@@ -58,8 +58,8 @@ function createConfigFixture(t, initial) {
|
|
|
58
58
|
// Keep captured CLI output out of assertion messages, since a config
|
|
59
59
|
// command may display settings inherited from the real system file.
|
|
60
60
|
assert.ifError(result.error);
|
|
61
|
-
assert.equal(result.status,
|
|
62
|
-
return result.stdout;
|
|
61
|
+
assert.equal(result.status, expected_status, 'Config command should exit with the expected status');
|
|
62
|
+
return result.stdout + result.stderr;
|
|
63
63
|
},
|
|
64
64
|
read() { return JSON.parse(fs.readFileSync(file, 'utf8')); }
|
|
65
65
|
};
|
|
@@ -112,6 +112,65 @@ test('config creates a missing user file with only supplied settings', t => {
|
|
|
112
112
|
assertPrivateMode(fixture.file);
|
|
113
113
|
});
|
|
114
114
|
|
|
115
|
+
test('config rejects misspelled keys without creating a file or exposing secrets', t => {
|
|
116
|
+
const fixture = createConfigFixture(t);
|
|
117
|
+
|
|
118
|
+
for (const [key, suggestion] of [['base-url', 'base_url'], ['api-key', 'api_key'], ['items_per_pag', 'items_per_page']]) {
|
|
119
|
+
const output = fixture.run(['--' + key, 'private-fixture-value'], 1);
|
|
120
|
+
assert.ok(output.includes('Unknown configuration key "' + key + '".'));
|
|
121
|
+
assert.ok(output.includes('Did you mean "' + suggestion + '"?'));
|
|
122
|
+
assert.doesNotMatch(output, /private-fixture-value|Setting key:|Writing to:/);
|
|
123
|
+
assert.equal(fs.existsSync(fixture.file), false);
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test('config rejects mixed valid and unknown updates without changing the user file', t => {
|
|
128
|
+
// Previously saved typos must not be accepted as supported config keys.
|
|
129
|
+
const fixture = createConfigFixture(t, { items_per_page: 10, 'api-key': 'old-value' });
|
|
130
|
+
const original = fs.readFileSync(fixture.file, 'utf8');
|
|
131
|
+
const output = fixture.run(['--items_per_page', '25', '--api-key', 'private-fixture-value'], 1);
|
|
132
|
+
|
|
133
|
+
assert.match(output, /Unknown configuration key "api-key"/);
|
|
134
|
+
assert.doesNotMatch(output, /private-fixture-value|Setting key:|Writing to:/);
|
|
135
|
+
assert.equal(fs.readFileSync(fixture.file, 'utf8'), original);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test('config rejects unrelated keys and nested paths under scalar settings', t => {
|
|
139
|
+
const fixture = createConfigFixture(t);
|
|
140
|
+
|
|
141
|
+
for (const key of ['unrelated_option', 'api_key.child', 'base_url/child']) {
|
|
142
|
+
const output = fixture.run(['--' + key, 'private-fixture-value'], 1);
|
|
143
|
+
assert.match(output, /Unknown configuration key/);
|
|
144
|
+
assert.doesNotMatch(output, /private-fixture-value|Setting key:|Writing to:/);
|
|
145
|
+
assert.equal(fs.existsSync(fixture.file), false);
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test('config saves supported connection, SDK, MCP, and structured settings', t => {
|
|
150
|
+
const fixture = createConfigFixture(t);
|
|
151
|
+
const output = fixture.run([
|
|
152
|
+
'--base_url', 'https://saved.invalid', '--api_key', 'private-fixture-value',
|
|
153
|
+
'--timeout', '5000', '--mcp_port', '8080', '--mcp.read_only', 'true',
|
|
154
|
+
'--new_event_template/params/custom', 'value', '--raw', 'false'
|
|
155
|
+
]);
|
|
156
|
+
|
|
157
|
+
assert.doesNotMatch(output, /private-fixture-value/);
|
|
158
|
+
assert.deepEqual(fixture.read(), {
|
|
159
|
+
base_url: 'https://saved.invalid', api_key: 'private-fixture-value',
|
|
160
|
+
timeout: 5000, mcp_port: 8080, mcp: { read_only: true },
|
|
161
|
+
new_event_template: { params: { custom: 'value' } }, raw: false
|
|
162
|
+
});
|
|
163
|
+
assertPrivateMode(fixture.file);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test('config ignores invocation flags when displaying and saving settings', t => {
|
|
167
|
+
const fixture = createConfigFixture(t);
|
|
168
|
+
fixture.run(['--quiet']);
|
|
169
|
+
assert.equal(fs.existsSync(fixture.file), false);
|
|
170
|
+
fixture.run(['--verbose', '--items_per_page', '25']);
|
|
171
|
+
assert.deepEqual(fixture.read(), { items_per_page: 25 });
|
|
172
|
+
});
|
|
173
|
+
|
|
115
174
|
test('config dotted updates preserve sibling user settings and explicit false or zero values', t => {
|
|
116
175
|
const fixture = createConfigFixture(t, {
|
|
117
176
|
sync: { up: ['events'], down: ['plugins'], error_email: 'ops@example.com' },
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
const { test } = require('node:test');
|
|
2
|
+
const assert = require('node:assert/strict');
|
|
3
|
+
const cli = require('pixl-cli');
|
|
4
|
+
const jobs = require('../lib/jobs.js');
|
|
5
|
+
|
|
6
|
+
cli.global();
|
|
7
|
+
|
|
8
|
+
function makeFollower(t, records) {
|
|
9
|
+
// Exercise the real command transitions with an offline SDK stub. Streaming
|
|
10
|
+
// finishes an attempt, but only getJob exposes its additional-job links.
|
|
11
|
+
const streamed = [];
|
|
12
|
+
const results = [];
|
|
13
|
+
const context = {
|
|
14
|
+
...jobs,
|
|
15
|
+
args: { id: records[0].id },
|
|
16
|
+
format: 'json',
|
|
17
|
+
async getMultiple() {},
|
|
18
|
+
api: {
|
|
19
|
+
async getJob({ id }) {
|
|
20
|
+
const job = records.find( item => item.id === id );
|
|
21
|
+
assert.ok(job, 'Unexpected job lookup: ' + id);
|
|
22
|
+
return { data: { job: { ...job } } };
|
|
23
|
+
},
|
|
24
|
+
async streamJob({ id }, callback) {
|
|
25
|
+
const job = records.find( item => item.id === id );
|
|
26
|
+
streamed.push(id);
|
|
27
|
+
job.final = true;
|
|
28
|
+
callback({ id, final: true, code: job.code });
|
|
29
|
+
return {};
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
jsonOutput(job) { results.push(job.id); },
|
|
33
|
+
die(message) { throw new Error(message); },
|
|
34
|
+
color() { return cli.chalk.white; },
|
|
35
|
+
printBoxList() {}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// Keep terminal rendering out of these tests, including progress timers.
|
|
39
|
+
for (const name of ['start', 'update', 'end']) t.mock.method(cli.progress, name, () => {});
|
|
40
|
+
for (const name of ['print', 'println', 'verboseln']) t.mock.method(global, name, () => {});
|
|
41
|
+
for (const name of ['getNiceJob', 'getNiceJobEvent', 'getNiceCategory', 'getNicePlugin',
|
|
42
|
+
'getNiceTargets', 'getNiceServer', 'getNiceAlgo', 'getNiceJobSource',
|
|
43
|
+
'getRelativeDateTime', 'getNiceJobState']) {
|
|
44
|
+
context[name] = () => '';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return { context, streamed, results };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
test('live follow crosses completed and queued retries, ignoring unrelated jobs', async t => {
|
|
51
|
+
const { context, streamed, results } = makeFollower(t, [
|
|
52
|
+
{ id: 'initial', code: 1, jobs: [{ id: 'other', reason: 'action' }, { id: 'retry1', reason: 'retry' }] },
|
|
53
|
+
{ id: 'retry1', final: true, code: 'warning', jobs: [{ id: 'retry2', reason: 'retry' }] },
|
|
54
|
+
{ id: 'retry2', state: 'queued', code: 0 }
|
|
55
|
+
]);
|
|
56
|
+
|
|
57
|
+
await context.cmd_stream_job();
|
|
58
|
+
assert.deepEqual(streamed, ['initial', 'retry2']);
|
|
59
|
+
assert.deepEqual(results, ['retry2']);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('directly opening an active job also follows its retry', async t => {
|
|
63
|
+
const { context, streamed, results } = makeFollower(t, [
|
|
64
|
+
{ id: 'initial', code: 1, jobs: [{ id: 'retry', reason: 'retry' }] },
|
|
65
|
+
{ id: 'retry', code: 2 }
|
|
66
|
+
]);
|
|
67
|
+
|
|
68
|
+
await context.cmd_get_job();
|
|
69
|
+
assert.deepEqual(streamed, ['initial', 'retry']);
|
|
70
|
+
assert.deepEqual(results, ['retry']);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('follow catches retries when the initial job finishes before streaming starts', async t => {
|
|
74
|
+
const { context, streamed, results } = makeFollower(t, [
|
|
75
|
+
{ id: 'initial', final: true, code: 1, jobs: [{ id: 'retry', reason: 'retry' }] },
|
|
76
|
+
{ id: 'retry', code: 0 }
|
|
77
|
+
]);
|
|
78
|
+
|
|
79
|
+
await context.cmd_stream_job();
|
|
80
|
+
assert.deepEqual(streamed, ['retry']);
|
|
81
|
+
assert.deepEqual(results, ['retry']);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('opening a completed job preserves the requested attempt', async t => {
|
|
85
|
+
const { context, streamed, results } = makeFollower(t, [
|
|
86
|
+
{ id: 'initial', final: true, code: 1, jobs: [{ id: 'retry', reason: 'retry' }] }
|
|
87
|
+
]);
|
|
88
|
+
|
|
89
|
+
await context.cmd_get_job();
|
|
90
|
+
assert.deepEqual(streamed, []);
|
|
91
|
+
assert.deepEqual(results, ['initial']);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('follow stops on success or failure without a valid retry link', async t => {
|
|
95
|
+
for (const record of [
|
|
96
|
+
{ code: 0, jobs: [{ id: 'other', reason: 'retry' }] },
|
|
97
|
+
{ code: 1 },
|
|
98
|
+
{ code: 1, jobs: [] },
|
|
99
|
+
{ code: 1, jobs: {} },
|
|
100
|
+
{ code: 1, jobs: [null, { reason: 'retry' }, { id: 'other', reason: 'action' }] }
|
|
101
|
+
]) {
|
|
102
|
+
const { context, streamed, results } = makeFollower(t, [{ id: 'initial', ...record }]);
|
|
103
|
+
await context.cmd_stream_job();
|
|
104
|
+
assert.deepEqual(streamed, ['initial']);
|
|
105
|
+
assert.deepEqual(results, ['initial']);
|
|
106
|
+
}
|
|
107
|
+
});
|