@pixlcore/xycli 1.0.5 → 1.0.6
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/CHANGELOG.md +21 -0
- package/README.md +7 -0
- package/docs/help.md +16 -0
- package/index.js +5 -3
- package/lib/sync.js +33 -4
- package/lib/upgrade.js +126 -0
- package/package.json +2 -2
- package/test/help.test.js +6 -0
- package/test/helpers/upgrade-preload.js +44 -0
- package/test/sync-unit.test.js +72 -0
- package/test/upgrade.test.js +121 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# xyCLI Changelog
|
|
2
2
|
|
|
3
|
+
## Version v1.0.5
|
|
4
|
+
|
|
5
|
+
> September 21, 2026
|
|
6
|
+
|
|
7
|
+
- [`2542921`](https://github.com/pixlcore/xycli/commit/25429212b664130bf767db85f52610c8d07c00aa): Version 1.0.5
|
|
8
|
+
- [`8818ec6`](https://github.com/pixlcore/xycli/commit/8818ec6291c0d06ec6b48174443eaf330682cff2): Bug FIx: Help system was breaking on Windows due to CRLF line endings.
|
|
9
|
+
|
|
10
|
+
## Version v1.0.4
|
|
11
|
+
|
|
12
|
+
> September 21, 2026
|
|
13
|
+
|
|
14
|
+
- [`553d4d9`](https://github.com/pixlcore/xycli/commit/553d4d95eec0dd4e966dba03d1cccadee49454fc): Version 1.0.4
|
|
15
|
+
- [`4a68d5a`](https://github.com/pixlcore/xycli/commit/4a68d5ad6ab1d2c28b5a5a933e6c6ec711fd2d90): Bug Fix: Validate local command routing before requiring a configured server.
|
|
16
|
+
|
|
17
|
+
## Version v1.0.3
|
|
18
|
+
|
|
19
|
+
> September 21, 2026
|
|
20
|
+
|
|
21
|
+
- [`00c2805`](https://github.com/pixlcore/xycli/commit/00c280596899cee1de66a4179684d1a7992ee1d9): Version 1.0.3
|
|
22
|
+
- [`82777d0`](https://github.com/pixlcore/xycli/commit/82777d01cb751769e14987a7cd86e0d71f91ee79): Feature: Add Windows support. Fixes #3.
|
|
23
|
+
|
|
3
24
|
## Version v1.0.2
|
|
4
25
|
|
|
5
26
|
> September 20, 2026
|
package/README.md
CHANGED
|
@@ -43,6 +43,13 @@ npm install -g @pixlcore/xycli
|
|
|
43
43
|
|
|
44
44
|
This installs the `xy` command. In the xyOps web interface, open **API Keys** and create a key for the CLI. Choose privileges and resource access appropriate for the work you want to do. The [tutorial](docs/tutorial.md) needs access to view Events and servers, create and edit Events, run jobs, and create and test Web Hooks. System administration commands require a full administrator key.
|
|
45
45
|
|
|
46
|
+
To check for a newer xyCLI release, use `xy upgrade`. The command displays the installed and latest npm versions without making any changes. Add `--confirm` to install the version shown.
|
|
47
|
+
|
|
48
|
+
```sh
|
|
49
|
+
xy upgrade
|
|
50
|
+
xy upgrade --confirm
|
|
51
|
+
```
|
|
52
|
+
|
|
46
53
|
Save your xyOps URL and API Key:
|
|
47
54
|
|
|
48
55
|
```sh
|
package/docs/help.md
CHANGED
|
@@ -24,6 +24,7 @@ xy tags
|
|
|
24
24
|
xy tickets
|
|
25
25
|
xy hooks
|
|
26
26
|
xy marketplace
|
|
27
|
+
xy upgrade
|
|
27
28
|
xy event EVENT_ID --export event.json
|
|
28
29
|
xy import event.json
|
|
29
30
|
xy sync ./ --up events,plugins --dry
|
|
@@ -95,6 +96,7 @@ xy help marketplace
|
|
|
95
96
|
xy help marketplace search
|
|
96
97
|
xy help marketplace get
|
|
97
98
|
xy help marketplace install
|
|
99
|
+
xy help upgrade
|
|
98
100
|
xy help export
|
|
99
101
|
xy help import
|
|
100
102
|
xy help sync
|
|
@@ -312,6 +314,20 @@ An explicitly supplied `--down_cmd` runs from the current setup directory after
|
|
|
312
314
|
|
|
313
315
|
Without `--force`, ordinary setup stops when an output file already exists. Files written before an error are not rolled back. Ensure object titles produce distinct filename slugs, especially when using `--force`, and back up any local edits before rerunning setup. For a delete-mode inventory, verify that every object eligible for deletion that you intend to keep has a source file. Stock and Marketplace objects are protected from deletion and may be omitted.
|
|
314
316
|
|
|
317
|
+
## upgrade
|
|
318
|
+
|
|
319
|
+
Check the installed xyCLI version against the latest version published to npm. This command is local and does not require an xyOps URL or API Key.
|
|
320
|
+
|
|
321
|
+
```sh
|
|
322
|
+
xy upgrade
|
|
323
|
+
xy upgrade --confirm
|
|
324
|
+
xy upgrade --confirm --dry
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
The command always displays the installed and latest versions. When an upgrade is available, add `--confirm` to install the exact version shown using npm. npm output is connected directly to the current terminal. A dry run prints the installation command without executing it.
|
|
328
|
+
|
|
329
|
+
The command never downgrades xyCLI. If the installed version is newer than npm's `latest` version, it reports that status and exits without running npm. Global npm installations may require a user-writable npm prefix; the command does not invoke `sudo` or otherwise elevate privileges.
|
|
330
|
+
|
|
315
331
|
## api
|
|
316
332
|
|
|
317
333
|
Call any API method exposed by the xyOps SDK and print its JSON request and response. Named options become request properties, and dotted names provide a convenient way to build nested objects.
|
package/index.js
CHANGED
|
@@ -54,7 +54,7 @@ const JSON_ARG_EXCEPTIONS = ['body'];
|
|
|
54
54
|
// Keep the public command inventory in one place, so typo suggestions and the
|
|
55
55
|
// fallback help text always advertise the exact same top-level commands.
|
|
56
56
|
const TOP_LEVEL_COMMANDS = [
|
|
57
|
-
'help', 'config', 'doc', 'dashboard', 'system', 'upcoming',
|
|
57
|
+
'help', 'config', 'upgrade', 'doc', 'dashboard', 'system', 'upcoming',
|
|
58
58
|
'alerts', 'alert', 'buckets', 'bucket', 'categories', 'category',
|
|
59
59
|
'channels', 'channel', 'events', 'event', 'run', 'groups', 'group',
|
|
60
60
|
'jobs', 'job', 'keys', 'key', 'log', 'monitors', 'monitor',
|
|
@@ -191,8 +191,9 @@ const app = {
|
|
|
191
191
|
|
|
192
192
|
println( "\n " + cli.emoji('🚀') + " " + this.color('theme').bold("xyOps CLI ") + gray("v" + this.version) );
|
|
193
193
|
|
|
194
|
-
//
|
|
195
|
-
|
|
194
|
+
// Call local commands early, before requiring xyOps credentials or
|
|
195
|
+
// contacting the configured server. The upgrade command only contacts npm.
|
|
196
|
+
if ((cmd == 'config') || (cmd == 'upgrade')) {
|
|
196
197
|
await this['cmd_' + cmd]();
|
|
197
198
|
print("\n");
|
|
198
199
|
return;
|
|
@@ -428,6 +429,7 @@ const app = {
|
|
|
428
429
|
|
|
429
430
|
Tools.mergeHashInto( app, require('./lib/utils.js') );
|
|
430
431
|
Tools.mergeHashInto( app, require('./lib/config.js') );
|
|
432
|
+
Tools.mergeHashInto( app, require('./lib/upgrade.js') );
|
|
431
433
|
Tools.mergeHashInto( app, require('./lib/doc.js') );
|
|
432
434
|
Tools.mergeHashInto( app, require('./lib/dashboard.js') );
|
|
433
435
|
Tools.mergeHashInto( app, require('./lib/system.js') );
|
package/lib/sync.js
CHANGED
|
@@ -100,6 +100,30 @@ module.exports = {
|
|
|
100
100
|
|
|
101
101
|
println( " " + Tools.commify(files.length) + " files found." );
|
|
102
102
|
|
|
103
|
+
// Index XYPDF basenames by directory before attaching any neighbor files.
|
|
104
|
+
// A source such as "Test-Event.json" is a prefix of both
|
|
105
|
+
// "Test-Event-V2.json" and "Test-Event-V2-params.command.ps1". Sorting
|
|
106
|
+
// longest-first lets each external property file belong to the most specific
|
|
107
|
+
// source basename instead of also being claimed by every shorter prefix.
|
|
108
|
+
var source_stems_by_dir = new Map();
|
|
109
|
+
files.filter( file => !!file.path.match(/\.json$/i) ).forEach( file => {
|
|
110
|
+
var xypdf = null;
|
|
111
|
+
try { xypdf = Tools.parseJSON( fs.readFileSync(file.path, 'utf8') ); }
|
|
112
|
+
catch (err) { return; } // the normal scan below reports malformed JSON
|
|
113
|
+
if (!xypdf || !Tools.isaHash(xypdf) || (xypdf.type !== 'xypdf')) return;
|
|
114
|
+
|
|
115
|
+
var dir = Path.dirname(file.path);
|
|
116
|
+
var stems = source_stems_by_dir.get(dir);
|
|
117
|
+
if (!stems) {
|
|
118
|
+
stems = [];
|
|
119
|
+
source_stems_by_dir.set(dir, stems);
|
|
120
|
+
}
|
|
121
|
+
stems.push( Path.basename(file.path).replace(/\.\w+$/, '') );
|
|
122
|
+
} );
|
|
123
|
+
source_stems_by_dir.forEach( stems => {
|
|
124
|
+
stems.sort( (a, b) => b.length - a.length );
|
|
125
|
+
} );
|
|
126
|
+
|
|
103
127
|
// load all xypdf files
|
|
104
128
|
var items = [];
|
|
105
129
|
|
|
@@ -196,11 +220,16 @@ module.exports = {
|
|
|
196
220
|
var nfilename = Path.basename(neighbor.path);
|
|
197
221
|
var nstem = nfilename.replace(/\.\w+$/, '');
|
|
198
222
|
if (nstem.length <= stem.length) return; // neighbor stem too short
|
|
199
|
-
if (!nstem.startsWith(stem)) return; // no
|
|
223
|
+
if (!nstem.startsWith(stem + '-')) return; // no exact basename boundary
|
|
224
|
+
|
|
225
|
+
// Multiple XYPDF sources may share a title prefix. The longest matching
|
|
226
|
+
// source stem owns this neighbor, so shorter sources must ignore it.
|
|
227
|
+
var source_stems = source_stems_by_dir.get(dir) || [];
|
|
228
|
+
var owner_stem = source_stems.find( source_stem => nstem.startsWith(source_stem + '-') );
|
|
229
|
+
if (owner_stem && (owner_stem !== stem)) return;
|
|
200
230
|
|
|
201
|
-
var
|
|
202
|
-
if (!
|
|
203
|
-
var prop_path = RegExp.$1;
|
|
231
|
+
var prop_path = nstem.substring(stem.length + 1);
|
|
232
|
+
if (!prop_path.match(/^[\w\.\-]+$/)) return; // not correct format
|
|
204
233
|
|
|
205
234
|
var content = '';
|
|
206
235
|
try {
|
package/lib/upgrade.js
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// xyCLI Self-Upgrade Layer
|
|
2
|
+
|
|
3
|
+
const cp = require('node:child_process');
|
|
4
|
+
|
|
5
|
+
const cli = require('pixl-cli');
|
|
6
|
+
const chalk = cli.chalk;
|
|
7
|
+
|
|
8
|
+
const PACKAGE_NAME = '@pixlcore/xycli';
|
|
9
|
+
const REGISTRY_URL = 'https://registry.npmjs.org/@pixlcore%2Fxycli/latest';
|
|
10
|
+
const REGISTRY_TIMEOUT_MS = 10000;
|
|
11
|
+
|
|
12
|
+
module.exports = {
|
|
13
|
+
|
|
14
|
+
async cmd_upgrade() {
|
|
15
|
+
// This is a deliberately small local command. It must remain usable before
|
|
16
|
+
// the user has configured an xyOps URL or API key.
|
|
17
|
+
if (this.args.other.length) this.dieUsage('upgrade');
|
|
18
|
+
if (('confirm' in this.args) && (typeof(this.args.confirm) != 'boolean')) {
|
|
19
|
+
this.die("Upgrade --confirm must be true or false.");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// These common switches are consumed by the main command runner after local
|
|
23
|
+
// commands return. Ignore them here, but reject command-specific typos.
|
|
24
|
+
var allowed = ['other', 'confirm', 'debug', 'echo', 'quiet', 'verbose'];
|
|
25
|
+
var unsupported = Object.keys(this.args).find( key => !allowed.includes(key) );
|
|
26
|
+
if (unsupported) this.die("Unsupported upgrade option: --" + unsupported);
|
|
27
|
+
|
|
28
|
+
var installed_version = this.version;
|
|
29
|
+
var latest_version = await this.getLatestXYCLIVersion();
|
|
30
|
+
var comparison = this.compareVersions(latest_version, installed_version);
|
|
31
|
+
var status = comparison > 0 ? this.color('blue').bold('Upgrade available') :
|
|
32
|
+
comparison < 0 ? this.color('yellow').bold('Installed version is newer than npm') :
|
|
33
|
+
this.color('green').bold('Up to date');
|
|
34
|
+
|
|
35
|
+
this.printBoxList({
|
|
36
|
+
title: 'xyCLI Version',
|
|
37
|
+
rows: [
|
|
38
|
+
[ 'Installed Version', 'v' + installed_version ],
|
|
39
|
+
[ 'Latest Version', 'v' + latest_version ],
|
|
40
|
+
[ 'Status', status ]
|
|
41
|
+
]
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// A different version is not necessarily an upgrade. In particular, local
|
|
45
|
+
// development builds must never be silently downgraded to npm's latest tag.
|
|
46
|
+
if (comparison <= 0) return;
|
|
47
|
+
|
|
48
|
+
if (this.args.confirm !== true) {
|
|
49
|
+
this.toast('ℹ️', 'blue', "A new xyCLI version is available! Add '--confirm' to perform the upgrade now.");
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Install the exact version shown above. This prevents the npm latest tag
|
|
54
|
+
// from moving between the version check and the confirmed installation.
|
|
55
|
+
var command = 'npm install --global ' + PACKAGE_NAME + '@' + latest_version;
|
|
56
|
+
println( "\n " + this.color('green').bold('Running command: ') + chalk.bold(command) );
|
|
57
|
+
|
|
58
|
+
if (this.dry) {
|
|
59
|
+
this.toast('⚠️', 'orange', "DRY RUN: The upgrade command was not executed.");
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
var result;
|
|
64
|
+
try {
|
|
65
|
+
result = await this.spawnUpgradeCommand(command);
|
|
66
|
+
}
|
|
67
|
+
catch (err) {
|
|
68
|
+
this.die("Could not start the upgrade command: " + err.message);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (result.signal) this.die("Upgrade command was terminated by signal: " + result.signal);
|
|
72
|
+
if (result.code !== 0) this.die("Upgrade command failed with exit code: " + result.code);
|
|
73
|
+
|
|
74
|
+
this.toast('✅', 'green', 'Successfully upgraded xyCLI to v' + latest_version + '.');
|
|
75
|
+
},
|
|
76
|
+
|
|
77
|
+
async getLatestXYCLIVersion() {
|
|
78
|
+
// The npm latest document is authoritative for the version that an
|
|
79
|
+
// unqualified global npm install would select.
|
|
80
|
+
this.cli.progress.start({ amount: 1, pct: false, text: this.color('gray')('Checking npm for the latest xyCLI version...') });
|
|
81
|
+
|
|
82
|
+
var response;
|
|
83
|
+
try {
|
|
84
|
+
response = await fetch(REGISTRY_URL, {
|
|
85
|
+
headers: {
|
|
86
|
+
'Accept': 'application/json',
|
|
87
|
+
'User-Agent': PACKAGE_NAME + '/v' + this.version
|
|
88
|
+
},
|
|
89
|
+
signal: AbortSignal.timeout(REGISTRY_TIMEOUT_MS)
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
this.cli.progress.end();
|
|
94
|
+
this.die("Could not contact the npm registry: " + err.message);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
this.cli.progress.end();
|
|
98
|
+
if (!response.ok) this.die("npm registry request failed with HTTP status: " + response.status);
|
|
99
|
+
|
|
100
|
+
var data;
|
|
101
|
+
try { data = await response.json(); }
|
|
102
|
+
catch (err) { this.die("Could not parse the npm registry response: " + err.message); }
|
|
103
|
+
|
|
104
|
+
if (!data || (typeof(data.version) != 'string') || !data.version.length) {
|
|
105
|
+
this.die("The npm registry response did not include a package version.");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return data.version;
|
|
109
|
+
},
|
|
110
|
+
|
|
111
|
+
spawnUpgradeCommand(command) {
|
|
112
|
+
// npm is npm.cmd on Windows, so launch the fixed, validated command through
|
|
113
|
+
// the platform shell. Inherited stdio gives npm full use of the terminal.
|
|
114
|
+
return new Promise((resolve, reject) => {
|
|
115
|
+
var child = cp.spawn(command, {
|
|
116
|
+
shell: true,
|
|
117
|
+
stdio: 'inherit',
|
|
118
|
+
windowsHide: false
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
child.once('error', reject);
|
|
122
|
+
child.once('close', (code, signal) => resolve({ code, signal }));
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pixlcore/xycli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.6",
|
|
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-unit.test.js test/toast.test.js test/utils-unit.test.js",
|
|
24
|
+
"test:offline": "node --test test/config.test.js test/help.test.js test/markdown.test.js test/sync-unit.test.js test/toast.test.js test/upgrade.test.js test/utils-unit.test.js",
|
|
25
25
|
"changelog": "node tools/changelog.js"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
package/test/help.test.js
CHANGED
|
@@ -18,6 +18,12 @@ test('help is available without server credentials', () => {
|
|
|
18
18
|
assert.match(output, /HELP: OVERVIEW/);
|
|
19
19
|
});
|
|
20
20
|
|
|
21
|
+
test('upgrade help is available without server credentials', () => {
|
|
22
|
+
const output = xy(['help', 'upgrade'], { env: offlineEnv });
|
|
23
|
+
assert.match(output, /HELP: UPGRADE/);
|
|
24
|
+
assert.match(output, /xy upgrade --confirm/);
|
|
25
|
+
});
|
|
26
|
+
|
|
21
27
|
test('help parses a document with Windows line endings', () => {
|
|
22
28
|
const output = xy(['help'], { env: offlineEnv, preload: crlfHelpPreload });
|
|
23
29
|
assert.match(output, /HELP: OVERVIEW/);
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// Replace npm registry access and the final npm process for offline CLI tests.
|
|
2
|
+
const fs = require('node:fs');
|
|
3
|
+
const cp = require('node:child_process');
|
|
4
|
+
const { EventEmitter } = require('node:events');
|
|
5
|
+
|
|
6
|
+
global.fetch = async function(url, options) {
|
|
7
|
+
if (process.env.XYCLI_TEST_REQUEST_FILE) {
|
|
8
|
+
fs.writeFileSync(process.env.XYCLI_TEST_REQUEST_FILE, JSON.stringify({
|
|
9
|
+
url: url,
|
|
10
|
+
headers: options.headers
|
|
11
|
+
}));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (process.env.XYCLI_TEST_FETCH_ERROR) {
|
|
15
|
+
throw new Error(process.env.XYCLI_TEST_FETCH_ERROR);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
var status = Number(process.env.XYCLI_TEST_HTTP_STATUS || 200);
|
|
19
|
+
return {
|
|
20
|
+
ok: (status >= 200) && (status < 300),
|
|
21
|
+
status: status,
|
|
22
|
+
async json() {
|
|
23
|
+
return { version: process.env.XYCLI_TEST_LATEST_VERSION };
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
cp.spawn = function(command, options) {
|
|
29
|
+
if (process.env.XYCLI_TEST_SPAWN_FILE) {
|
|
30
|
+
fs.writeFileSync(process.env.XYCLI_TEST_SPAWN_FILE, JSON.stringify({
|
|
31
|
+
command: command,
|
|
32
|
+
shell: options.shell,
|
|
33
|
+
stdio: options.stdio,
|
|
34
|
+
windowsHide: options.windowsHide
|
|
35
|
+
}));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
var child = new EventEmitter();
|
|
39
|
+
process.nextTick(() => {
|
|
40
|
+
if (process.env.XYCLI_TEST_SPAWN_ERROR) child.emit('error', new Error(process.env.XYCLI_TEST_SPAWN_ERROR));
|
|
41
|
+
else child.emit('close', Number(process.env.XYCLI_TEST_SPAWN_EXIT || 0), null);
|
|
42
|
+
});
|
|
43
|
+
return child;
|
|
44
|
+
};
|
package/test/sync-unit.test.js
CHANGED
|
@@ -583,6 +583,78 @@ test('a prefixed XYPDF source is never loaded as a property neighbor', t => {
|
|
|
583
583
|
assert.equal(result.report.requests.some(request => request.method === 'update_category'), false);
|
|
584
584
|
});
|
|
585
585
|
|
|
586
|
+
test('a property neighbor belongs to the longest matching XYPDF source stem', t => {
|
|
587
|
+
const shortCommand = 'Write-Host "Test Event"\n';
|
|
588
|
+
const longCommand = 'Write-Host "Test Event V2"\n';
|
|
589
|
+
const shortEvent = { id: 'short', title: 'Test Event', params: { command: shortCommand } };
|
|
590
|
+
const longEvent = { id: 'long', title: 'Test Event V2', params: { command: longCommand } };
|
|
591
|
+
const result = runSync(t, {
|
|
592
|
+
events: [shortEvent, longEvent],
|
|
593
|
+
existingSources: [
|
|
594
|
+
{
|
|
595
|
+
path: 'Test-Event.json',
|
|
596
|
+
type: 'event',
|
|
597
|
+
data: { ...shortEvent, params: { command: '(External)' } }
|
|
598
|
+
},
|
|
599
|
+
{
|
|
600
|
+
path: 'Test-Event-params.command.ps1',
|
|
601
|
+
contents: shortCommand
|
|
602
|
+
},
|
|
603
|
+
{
|
|
604
|
+
path: 'Test-Event-V2.json',
|
|
605
|
+
type: 'event',
|
|
606
|
+
data: { ...longEvent, params: { command: '(External)' } }
|
|
607
|
+
},
|
|
608
|
+
{
|
|
609
|
+
path: 'Test-Event-V2-params.command.ps1',
|
|
610
|
+
contents: longCommand
|
|
611
|
+
}
|
|
612
|
+
],
|
|
613
|
+
args: { up: 'events', dry: true }
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
assert.equal(result.status, 0);
|
|
617
|
+
assert.deepEqual(result.report.warnings, []);
|
|
618
|
+
assert.deepEqual(result.report.errors, []);
|
|
619
|
+
assert.equal(result.report.requests.some(request => request.method === 'update_event'), false);
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
test('property neighbor matching requires an exact source basename boundary', t => {
|
|
623
|
+
const singularCommand = 'Write-Host "Test Event"\n';
|
|
624
|
+
const pluralCommand = 'Write-Host "Test Events"\n';
|
|
625
|
+
const singularEvent = { id: 'singular', title: 'Test Event', params: { command: singularCommand } };
|
|
626
|
+
const pluralEvent = { id: 'plural', title: 'Test Events', params: { command: pluralCommand } };
|
|
627
|
+
const result = runSync(t, {
|
|
628
|
+
events: [singularEvent, pluralEvent],
|
|
629
|
+
existingSources: [
|
|
630
|
+
{
|
|
631
|
+
path: 'Test-Event.json',
|
|
632
|
+
type: 'event',
|
|
633
|
+
data: { ...singularEvent, params: { command: '(External)' } }
|
|
634
|
+
},
|
|
635
|
+
{
|
|
636
|
+
path: 'Test-Event-params.command.ps1',
|
|
637
|
+
contents: singularCommand
|
|
638
|
+
},
|
|
639
|
+
{
|
|
640
|
+
path: 'Test-Events.json',
|
|
641
|
+
type: 'event',
|
|
642
|
+
data: { ...pluralEvent, params: { command: '(External)' } }
|
|
643
|
+
},
|
|
644
|
+
{
|
|
645
|
+
path: 'Test-Events-params.command.ps1',
|
|
646
|
+
contents: pluralCommand
|
|
647
|
+
}
|
|
648
|
+
],
|
|
649
|
+
args: { up: 'events', dry: true }
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
assert.equal(result.status, 0);
|
|
653
|
+
assert.deepEqual(result.report.warnings, []);
|
|
654
|
+
assert.deepEqual(result.report.errors, []);
|
|
655
|
+
assert.equal(result.report.requests.some(request => request.method === 'update_event'), false);
|
|
656
|
+
});
|
|
657
|
+
|
|
586
658
|
test('a property neighbor must resolve to an existing string property', t => {
|
|
587
659
|
const category = { id: 'fixture', title: 'Fixture', notes: '' };
|
|
588
660
|
const result = runSync(t, {
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
const { test } = require('node:test');
|
|
2
|
+
const assert = require('node:assert/strict');
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const os = require('node:os');
|
|
5
|
+
const Path = require('node:path');
|
|
6
|
+
const pkg = require('../package.json');
|
|
7
|
+
const { xy } = require('./helpers/common.js');
|
|
8
|
+
|
|
9
|
+
const preload = Path.join(__dirname, 'helpers', 'upgrade-preload.js');
|
|
10
|
+
const offlineEnv = {
|
|
11
|
+
XYOPS_API_KEY: '',
|
|
12
|
+
XYOPS_BASE_URL: ''
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
function createUpgradeFixture(t, latest) {
|
|
16
|
+
// Each command gets private observation files. The preload records registry
|
|
17
|
+
// and spawn calls without contacting npm or installing anything globally.
|
|
18
|
+
const dir = fs.mkdtempSync(Path.join(os.tmpdir(), 'xycli-upgrade-'));
|
|
19
|
+
t.after(() => fs.rmSync(dir, { recursive: true, force: true }));
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
requestFile: Path.join(dir, 'request.json'),
|
|
23
|
+
spawnFile: Path.join(dir, 'spawn.json'),
|
|
24
|
+
run(args, options = {}) {
|
|
25
|
+
return xy(['upgrade', ...args], {
|
|
26
|
+
preload: preload,
|
|
27
|
+
fail: options.fail,
|
|
28
|
+
env: {
|
|
29
|
+
...offlineEnv,
|
|
30
|
+
XYCLI_TEST_LATEST_VERSION: latest,
|
|
31
|
+
XYCLI_TEST_REQUEST_FILE: this.requestFile,
|
|
32
|
+
XYCLI_TEST_SPAWN_FILE: this.spawnFile,
|
|
33
|
+
...options.env
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
test('upgrade checks npm and works without xyOps credentials', t => {
|
|
41
|
+
const fixture = createUpgradeFixture(t, pkg.version);
|
|
42
|
+
const output = fixture.run([]);
|
|
43
|
+
const request = JSON.parse(fs.readFileSync(fixture.requestFile, 'utf8'));
|
|
44
|
+
const escaped_version = pkg.version.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
45
|
+
|
|
46
|
+
assert.equal(request.url, 'https://registry.npmjs.org/@pixlcore%2Fxycli/latest');
|
|
47
|
+
assert.equal(request.headers.Accept, 'application/json');
|
|
48
|
+
assert.match(request.headers['User-Agent'], /^@pixlcore\/xycli\/v/);
|
|
49
|
+
assert.match(output, new RegExp('Installed Version:\\s+v' + escaped_version));
|
|
50
|
+
assert.match(output, new RegExp('Latest Version:\\s+v' + escaped_version));
|
|
51
|
+
assert.match(output, /Up to date/);
|
|
52
|
+
assert.equal(fs.existsSync(fixture.spawnFile), false);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('upgrade advertises a newer version without installing it', t => {
|
|
56
|
+
const fixture = createUpgradeFixture(t, '99.0.0');
|
|
57
|
+
const output = fixture.run([]);
|
|
58
|
+
|
|
59
|
+
assert.match(output, /Upgrade available/);
|
|
60
|
+
assert.match(output, /A new xyCLI version is available!/);
|
|
61
|
+
assert.match(output, /--confirm/);
|
|
62
|
+
assert.equal(fs.existsSync(fixture.spawnFile), false);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('confirmed upgrade installs the exact checked version with inherited stdio', t => {
|
|
66
|
+
const fixture = createUpgradeFixture(t, '99.0.0');
|
|
67
|
+
const output = fixture.run(['--confirm']);
|
|
68
|
+
const spawn = JSON.parse(fs.readFileSync(fixture.spawnFile, 'utf8'));
|
|
69
|
+
|
|
70
|
+
assert.match(output, /Running command:\s+npm install --global @pixlcore\/xycli@99\.0\.0/);
|
|
71
|
+
assert.match(output, /Successfully upgraded xyCLI to v99\.0\.0/);
|
|
72
|
+
assert.equal(spawn.command, 'npm install --global @pixlcore/xycli@99.0.0');
|
|
73
|
+
assert.equal(spawn.shell, true);
|
|
74
|
+
assert.equal(spawn.stdio, 'inherit');
|
|
75
|
+
assert.equal(spawn.windowsHide, false);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('upgrade dry run prints the pinned command without spawning npm', t => {
|
|
79
|
+
const fixture = createUpgradeFixture(t, '99.0.0');
|
|
80
|
+
const output = fixture.run(['--confirm', '--dry']);
|
|
81
|
+
|
|
82
|
+
assert.match(output, /npm install --global @pixlcore\/xycli@99\.0\.0/);
|
|
83
|
+
assert.match(output, /DRY RUN: The upgrade command was not executed/);
|
|
84
|
+
assert.equal(fs.existsSync(fixture.spawnFile), false);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('upgrade never downgrades a newer local version', t => {
|
|
88
|
+
const fixture = createUpgradeFixture(t, '0.0.1');
|
|
89
|
+
const output = fixture.run(['--confirm']);
|
|
90
|
+
|
|
91
|
+
assert.match(output, /Installed version is newer than npm/);
|
|
92
|
+
assert.equal(fs.existsSync(fixture.spawnFile), false);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('upgrade rejects malformed registry versions before spawning a shell', t => {
|
|
96
|
+
const fixture = createUpgradeFixture(t, '99.0.0; unwanted-command');
|
|
97
|
+
const output = fixture.run(['--confirm'], { fail: true });
|
|
98
|
+
|
|
99
|
+
assert.match(output, /Invalid version/);
|
|
100
|
+
assert.equal(fs.existsSync(fixture.spawnFile), false);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('upgrade reports registry and npm process failures', async t => {
|
|
104
|
+
await t.test('registry failure', t => {
|
|
105
|
+
const fixture = createUpgradeFixture(t, '99.0.0');
|
|
106
|
+
const output = fixture.run([], {
|
|
107
|
+
fail: true,
|
|
108
|
+
env: { XYCLI_TEST_FETCH_ERROR: 'fixture network failure' }
|
|
109
|
+
});
|
|
110
|
+
assert.match(output, /Could not contact the npm registry: fixture network failure/);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
await t.test('npm failure', t => {
|
|
114
|
+
const fixture = createUpgradeFixture(t, '99.0.0');
|
|
115
|
+
const output = fixture.run(['--confirm'], {
|
|
116
|
+
fail: true,
|
|
117
|
+
env: { XYCLI_TEST_SPAWN_EXIT: '7' }
|
|
118
|
+
});
|
|
119
|
+
assert.match(output, /Upgrade command failed with exit code: 7/);
|
|
120
|
+
});
|
|
121
|
+
});
|