@pixlcore/xycli 1.0.2 → 1.0.4
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/offline-tests.yml +39 -0
- package/CHANGELOG.md +19 -0
- package/README.md +15 -7
- package/docs/help.md +3 -3
- package/docs/sync.md +1 -1
- package/index.js +28 -16
- package/lib/config.js +29 -7
- package/lib/sync.js +2 -1
- package/lib/utils.js +27 -1
- package/package.json +2 -1
- package/test/config.test.js +51 -11
- package/test/help.test.js +18 -4
- package/test/helpers/common.js +1 -0
- package/test/sync-unit.test.js +4 -2
- package/test/utils-unit.test.js +19 -0
- package/test.js +2 -1
- package/tools/changelog.js +5 -5
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
name: Offline Tests
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
pull_request:
|
|
6
|
+
|
|
7
|
+
permissions:
|
|
8
|
+
contents: read
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
test:
|
|
12
|
+
name: ${{ matrix.os }} / Node.js ${{ matrix.node }}
|
|
13
|
+
runs-on: ${{ matrix.os }}
|
|
14
|
+
|
|
15
|
+
strategy:
|
|
16
|
+
fail-fast: false
|
|
17
|
+
matrix:
|
|
18
|
+
os:
|
|
19
|
+
- ubuntu-latest
|
|
20
|
+
- windows-latest
|
|
21
|
+
node:
|
|
22
|
+
- 20.19.0
|
|
23
|
+
- 24
|
|
24
|
+
|
|
25
|
+
steps:
|
|
26
|
+
- name: Check out repository
|
|
27
|
+
uses: actions/checkout@v7
|
|
28
|
+
|
|
29
|
+
- name: Use Node.js ${{ matrix.node }}
|
|
30
|
+
uses: actions/setup-node@v7
|
|
31
|
+
with:
|
|
32
|
+
node-version: ${{ matrix.node }}
|
|
33
|
+
cache: npm
|
|
34
|
+
|
|
35
|
+
- name: Install dependencies
|
|
36
|
+
run: npm ci
|
|
37
|
+
|
|
38
|
+
- name: Run offline tests
|
|
39
|
+
run: npm run test:offline
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# xyCLI Changelog
|
|
2
2
|
|
|
3
|
+
## Version v1.0.2
|
|
4
|
+
|
|
5
|
+
> September 20, 2026
|
|
6
|
+
|
|
7
|
+
- [`7b9023b`](https://github.com/pixlcore/xycli/commit/7b9023b390ea45b0615ff43b4d5a8d3daa3a68f3): Version 1.0.2
|
|
8
|
+
- [`2114287`](https://github.com/pixlcore/xycli/commit/2114287dc98ca2cedcc6cf1b15eb7e164f697851): Feature: Sync Setup: Write out events and workflows into category sub-folders. Fixes #2.
|
|
9
|
+
- [`3f3e8a5`](https://github.com/pixlcore/xycli/commit/3f3e8a515be030e551c33a701c43e2bb3ff72569): Doc: Add two-way git tutorial
|
|
10
|
+
- [`4336dc2`](https://github.com/pixlcore/xycli/commit/4336dc25924616038213b77c5892990c09fa22f5): Feature: Sync setup now supports its own "--down_cmd" for running a custom command if and only if files were written out.
|
|
11
|
+
- [`7b94e63`](https://github.com/pixlcore/xycli/commit/7b94e632e2c0d86eaef5211325b259195cb30f3c): Feature: Add optional "--new" flag to sync setup, to add newly added items not already in sync set
|
|
12
|
+
- [`90d065f`](https://github.com/pixlcore/xycli/commit/90d065f941578ed83783cc79bebbbdc7018a6664): Allow "--default_ext" switch to override default ".txt" file extension for neighbor property files when language cannot be detected. Ref #1.
|
|
13
|
+
|
|
14
|
+
## Version v1.0.1
|
|
15
|
+
|
|
16
|
+
> September 20, 2026
|
|
17
|
+
|
|
18
|
+
- [`ee12585`](https://github.com/pixlcore/xycli/commit/ee12585b36e7237d8514b9c34ce0411debf16494): Version 1.0.1
|
|
19
|
+
- [`c18090a`](https://github.com/pixlcore/xycli/commit/c18090a03ebdb91772fc565b59eec22e4378df3f): Fix: Do not start delete pass if any warnings or errors occurred during the sync.
|
|
20
|
+
- [`ef4e68b`](https://github.com/pixlcore/xycli/commit/ef4e68b229b8e0114c0af4a44dc1b61ba81e9e39): Test: Add more event unit tests
|
|
21
|
+
|
|
3
22
|
## Version v1.0.0
|
|
4
23
|
|
|
5
24
|
> September 20, 2026
|
package/README.md
CHANGED
|
@@ -49,7 +49,7 @@ Save your xyOps URL and API Key:
|
|
|
49
49
|
xy config --base_url https://xyops.example.com --api_key YOUR_API_KEY
|
|
50
50
|
```
|
|
51
51
|
|
|
52
|
-
Use the base URL of your xyOps instance, without the `/api` endpoint suffix.
|
|
52
|
+
Use the base URL of your xyOps instance, without the `/api` endpoint suffix. On Unix and macOS, the command creates `~/.config/xyops/cli.json`. On Windows, it creates `%USERPROFILE%\.config\xyops\cli.json`.
|
|
53
53
|
|
|
54
54
|
Now check your connection:
|
|
55
55
|
|
|
@@ -63,11 +63,11 @@ You should see the dashboard. If you need to set up xyOps itself, start with its
|
|
|
63
63
|
|
|
64
64
|
xyCLI loads optional JSON configuration files on every invocation, then applies environment variables. Settings are merged in this order:
|
|
65
65
|
|
|
66
|
-
| Order |
|
|
67
|
-
| --- | --- | --- |
|
|
68
|
-
| 1 | `/etc/xyops/cli.json` | Shared settings for the machine. |
|
|
69
|
-
| 2 | `~/.config/xyops/cli.json` | Settings for your user account, overriding matching system settings. |
|
|
70
|
-
| 3 | `XYOPS_` environment variables | Overrides for the current shell or command. |
|
|
66
|
+
| Order | Unix and macOS | Windows | Purpose |
|
|
67
|
+
| --- | --- | --- | --- |
|
|
68
|
+
| 1 | `/etc/xyops/cli.json` | `%PROGRAMDATA%\xyops\cli.json` | Shared settings for the machine. |
|
|
69
|
+
| 2 | `~/.config/xyops/cli.json` | `%USERPROFILE%\.config\xyops\cli.json` | Settings for your user account, overriding matching system settings. |
|
|
70
|
+
| 3 | `XYOPS_` environment variables | `XYOPS_` environment variables | Overrides for the current shell or command. |
|
|
71
71
|
|
|
72
72
|
Neither file is required if you supply your connection settings through the environment. Missing files are skipped; files you create must contain valid JSON. Matching top-level properties are replaced by later sources, so a user-level object such as `sync` replaces the system-level object of the same name.
|
|
73
73
|
|
|
@@ -92,7 +92,7 @@ xy config --suggest false
|
|
|
92
92
|
xy config --color false
|
|
93
93
|
```
|
|
94
94
|
|
|
95
|
-
`xy config` masks your API Key when displaying it. Updates load the **user configuration file** separately, apply only the settings specified on the command line, and save it with owner-only read/write permissions. Other user settings are preserved; settings inherited from the system file or environment are not copied into the user file. If the user file does not exist, it is created with only the settings you supply. Environment overrides still take precedence on the next invocation.
|
|
95
|
+
`xy config` masks your API Key when displaying it. Updates load the **user configuration file** separately, apply only the settings specified on the command line, and save it with owner-only read/write permissions on Unix and macOS. On Windows, the file inherits the access controls of your user profile because Windows does not implement Unix permission modes. Other user settings are preserved; settings inherited from the system file or environment are not copied into the user file. If the user file does not exist, it is created with only the settings you supply. Environment overrides still take precedence on the next invocation.
|
|
96
96
|
|
|
97
97
|
Every `XYOPS_` variable maps to a lowercase configuration key. For example, `XYOPS_BASE_URL` sets `base_url` and `XYOPS_ITEMS_PER_PAGE` sets `items_per_page`. Literal `true` and `false` become booleans, and numeric strings become numbers.
|
|
98
98
|
|
|
@@ -104,6 +104,14 @@ export XYOPS_API_KEY="YOUR_API_KEY"
|
|
|
104
104
|
xy
|
|
105
105
|
```
|
|
106
106
|
|
|
107
|
+
In Windows PowerShell, use the equivalent environment syntax:
|
|
108
|
+
|
|
109
|
+
```powershell
|
|
110
|
+
$env:XYOPS_BASE_URL = "https://xyops.example.com"
|
|
111
|
+
$env:XYOPS_API_KEY = "YOUR_API_KEY"
|
|
112
|
+
xy
|
|
113
|
+
```
|
|
114
|
+
|
|
107
115
|
Or override a display setting for one command:
|
|
108
116
|
|
|
109
117
|
```sh
|
package/docs/help.md
CHANGED
|
@@ -258,7 +258,7 @@ Every sync and setup command uses a built-in host-local PID lock keyed by `base_
|
|
|
258
258
|
|
|
259
259
|
**Saved defaults**
|
|
260
260
|
|
|
261
|
-
Put sync defaults under a `sync` object in `/etc/xyops/cli.json`
|
|
261
|
+
Put sync defaults under a `sync` object in the machine or user configuration file. These 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:
|
|
262
262
|
|
|
263
263
|
```json
|
|
264
264
|
{
|
|
@@ -341,7 +341,7 @@ Type `.exit` or hit `Ctrl-C` to exit.
|
|
|
341
341
|
|
|
342
342
|
## config
|
|
343
343
|
|
|
344
|
-
View the effective CLI configuration or save one or more user settings. API keys are masked when displayed
|
|
344
|
+
View the effective CLI configuration or save one or more user settings. API keys are masked when displayed. Updated values are written to `~/.config/xyops/cli.json` on Unix and macOS, or `%USERPROFILE%\.config\xyops\cli.json` on Windows.
|
|
345
345
|
|
|
346
346
|
```sh
|
|
347
347
|
xy config
|
|
@@ -353,7 +353,7 @@ xy config --base_url http://localhost:5522 --api_key YOUR_API_KEY
|
|
|
353
353
|
|
|
354
354
|
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.
|
|
355
355
|
|
|
356
|
-
Configuration loads `/etc/xyops/cli.json`
|
|
356
|
+
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`.
|
|
357
357
|
|
|
358
358
|
## doc
|
|
359
359
|
|
package/docs/sync.md
CHANGED
|
@@ -308,7 +308,7 @@ After changes begin, individual API or write failures are logged and the engine
|
|
|
308
308
|
|
|
309
309
|
### Save defaults in a CLI config file
|
|
310
310
|
|
|
311
|
-
Put a `sync` object in
|
|
311
|
+
Put a `sync` object in the machine configuration file for machine or service defaults, or the user configuration file for your own account. These 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:
|
|
312
312
|
|
|
313
313
|
```json
|
|
314
314
|
{
|
package/index.js
CHANGED
|
@@ -198,6 +198,34 @@ const app = {
|
|
|
198
198
|
return;
|
|
199
199
|
}
|
|
200
200
|
|
|
201
|
+
// Validate local command routing before requiring a configured server. This
|
|
202
|
+
// keeps help and typo diagnostics available during initial CLI setup. Export
|
|
203
|
+
// is excluded because it also supports portable resource types that do not
|
|
204
|
+
// have their own complete command routers yet.
|
|
205
|
+
if (!('export' in args) && !this['cmd_' + cmd]) {
|
|
206
|
+
// Allow the user to swap the first two args if the second is a known command.
|
|
207
|
+
if (this['cmd_' + args.other[0]]) {
|
|
208
|
+
var new_cmd = args.other[0];
|
|
209
|
+
args.other[0] = cmd;
|
|
210
|
+
cmd = new_cmd;
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
var suggestion = this.findClosestString(cmd, TOP_LEVEL_COMMANDS);
|
|
214
|
+
this.die(
|
|
215
|
+
"Unknown command: " + cmd + (suggestion ? '. Did you mean "' + suggestion + '"?' : ''),
|
|
216
|
+
"Available Commands: " + TOP_LEVEL_COMMANDS.join(', ') + "\n\n"
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Help is entirely local, so it should work before a user has configured an
|
|
222
|
+
// API key or server URL, and should never make a network request.
|
|
223
|
+
if (cmd == 'help') {
|
|
224
|
+
await this.cmd_help();
|
|
225
|
+
print("\n");
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
|
|
201
229
|
delete args.debug;
|
|
202
230
|
delete args.echo;
|
|
203
231
|
delete args.quiet;
|
|
@@ -273,22 +301,6 @@ const app = {
|
|
|
273
301
|
return;
|
|
274
302
|
}
|
|
275
303
|
|
|
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
304
|
// merge in config from xyops
|
|
293
305
|
await this.cacheConfig();
|
|
294
306
|
|
package/lib/config.js
CHANGED
|
@@ -1,21 +1,37 @@
|
|
|
1
1
|
// Config Layer
|
|
2
2
|
|
|
3
3
|
const fs = require('fs');
|
|
4
|
+
const os = require('os');
|
|
4
5
|
const Path = require('path');
|
|
5
6
|
const cli = require('pixl-cli');
|
|
6
7
|
const Tools = cli.Tools;
|
|
7
8
|
const chalk = cli.chalk;
|
|
8
9
|
|
|
9
|
-
|
|
10
|
+
function getConfigFiles(platform, env, home_dir) {
|
|
11
|
+
// Keep the existing Unix paths, but use Windows's shared application-data
|
|
12
|
+
// directory instead of resolving /etc against whichever drive is current.
|
|
13
|
+
// ProgramData should always exist in an ordinary Windows login. If a custom
|
|
14
|
+
// environment omits it, skip the optional machine file rather than guessing.
|
|
15
|
+
var path_api = (platform == 'win32') ? Path.win32 : Path.posix;
|
|
16
|
+
var config_files = [];
|
|
17
|
+
var system_dir = (platform == 'win32') ?
|
|
18
|
+
(env.ProgramData || env.PROGRAMDATA || env.ALLUSERSPROFILE) : '/etc';
|
|
19
|
+
|
|
20
|
+
if (system_dir) config_files.push( path_api.join(system_dir, 'xyops', 'cli.json') );
|
|
21
|
+
|
|
22
|
+
// os.homedir() uses USERPROFILE on Windows and has an operating-system
|
|
23
|
+
// fallback, unlike reading HOME or HOMEPATH directly.
|
|
24
|
+
config_files.push( path_api.join(home_dir, '.config', 'xyops', 'cli.json') );
|
|
25
|
+
return config_files;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
var config_layer = module.exports = {
|
|
10
29
|
|
|
11
30
|
loadConfig() {
|
|
12
31
|
// handle loading config at startup
|
|
13
32
|
// optional config files, sync with env, two-way
|
|
14
33
|
var config = {};
|
|
15
|
-
var config_files = this.configFiles =
|
|
16
|
-
Path.join( '/etc', 'xyops', 'cli.json' ),
|
|
17
|
-
Path.join( process.env.HOME, '.config', 'xyops', 'cli.json' )
|
|
18
|
-
];
|
|
34
|
+
var config_files = this.configFiles = getConfigFiles(process.platform, process.env, os.homedir());
|
|
19
35
|
|
|
20
36
|
config_files.forEach( function(file) {
|
|
21
37
|
if (!fs.existsSync(file)) return;
|
|
@@ -80,7 +96,13 @@ module.exports = {
|
|
|
80
96
|
Tools.mkdirpSync( Path.dirname(dest_file) );
|
|
81
97
|
println( "\n " + bold.cyan("Writing to: ") + dest_file );
|
|
82
98
|
fs.writeFileSync( dest_file, JSON.stringify(user_config, null, "\t") + "\n", { mode: 0o600 } );
|
|
83
|
-
|
|
99
|
+
|
|
100
|
+
// Windows inherits an ACL from the user's profile directory and does not
|
|
101
|
+
// implement Unix owner/group/other mode distinctions.
|
|
102
|
+
if (process.platform != 'win32') fs.chmodSync(dest_file, 0o600);
|
|
84
103
|
}
|
|
85
|
-
|
|
86
104
|
};
|
|
105
|
+
|
|
106
|
+
// Expose the pure path selector to unit tests without merging another command
|
|
107
|
+
// method into the runtime app object.
|
|
108
|
+
Object.defineProperty(config_layer, 'getConfigFiles', { value: getConfigFiles });
|
package/lib/sync.js
CHANGED
package/lib/utils.js
CHANGED
|
@@ -254,7 +254,21 @@ module.exports = {
|
|
|
254
254
|
// This mirrors the xyOps UI helper, while avoiding slow source auto-detection
|
|
255
255
|
// for common interpreters.
|
|
256
256
|
if ((typeof(bin) != 'string') || !bin.trim()) return null;
|
|
257
|
-
|
|
257
|
+
|
|
258
|
+
// Normalize both path styles because Plugin definitions may come from a
|
|
259
|
+
// different operating system than the CLI displaying them. Remove command
|
|
260
|
+
// arguments before taking the basename, including quoted paths with spaces.
|
|
261
|
+
var cmdline = bin.trim().replace(/\\/g, '/');
|
|
262
|
+
var cmd = cmdline;
|
|
263
|
+
if (cmd.match(/^(?:.*\/)?env(?:\.exe)?\s+(?:\-S\s+)?(\S+)/i)) cmd = RegExp.$1;
|
|
264
|
+
else if (cmd.match(/^"([^"]+)"/)) cmd = RegExp.$1;
|
|
265
|
+
else if (cmd.match(/^'([^']+)'/)) cmd = RegExp.$1;
|
|
266
|
+
else cmd = cmd.replace(/\s+.+$/, '');
|
|
267
|
+
cmd = Path.posix.basename(cmd);
|
|
268
|
+
|
|
269
|
+
// Windows executables conventionally include .exe. Version suffixes are
|
|
270
|
+
// also common for interpreters such as python3.12 and node24.
|
|
271
|
+
cmd = cmd.replace(/\.exe$/i, '').replace(/\d+(?:\.\d+)*$/, '').toLowerCase();
|
|
258
272
|
|
|
259
273
|
switch (cmd) {
|
|
260
274
|
case 'sh':
|
|
@@ -268,14 +282,26 @@ module.exports = {
|
|
|
268
282
|
break;
|
|
269
283
|
|
|
270
284
|
case 'node':
|
|
285
|
+
case 'nodejs':
|
|
271
286
|
case 'deno':
|
|
272
287
|
case 'bun':
|
|
273
288
|
cmd = 'javascript';
|
|
274
289
|
break;
|
|
275
290
|
|
|
276
291
|
case 'pwsh':
|
|
292
|
+
case 'powershell':
|
|
277
293
|
cmd = 'powershell';
|
|
278
294
|
break;
|
|
295
|
+
|
|
296
|
+
case 'cmd':
|
|
297
|
+
cmd = 'dos';
|
|
298
|
+
break;
|
|
299
|
+
|
|
300
|
+
case 'py':
|
|
301
|
+
case 'pythonw':
|
|
302
|
+
case 'pypy':
|
|
303
|
+
cmd = 'python';
|
|
304
|
+
break;
|
|
279
305
|
}
|
|
280
306
|
|
|
281
307
|
return HIGHLIGHT_LANGUAGES.includes(cmd) ? cmd : null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pixlcore/xycli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
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,6 +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
25
|
"changelog": "node tools/changelog.js"
|
|
25
26
|
},
|
|
26
27
|
"dependencies": {
|
package/test/config.test.js
CHANGED
|
@@ -4,12 +4,25 @@ const fs = require('node:fs');
|
|
|
4
4
|
const os = require('node:os');
|
|
5
5
|
const Path = require('node:path');
|
|
6
6
|
const { spawnSync } = require('node:child_process');
|
|
7
|
+
const config = require('../lib/config.js');
|
|
8
|
+
|
|
9
|
+
function assertPrivateMode(file) {
|
|
10
|
+
// Windows permissions are represented by ACLs, not Unix owner/group/other
|
|
11
|
+
// mode bits. The file still has to exist, but an exact 0600 assertion only
|
|
12
|
+
// describes the Unix behavior that fs.chmodSync can enforce.
|
|
13
|
+
assert.equal(fs.existsSync(file), true);
|
|
14
|
+
if (process.platform != 'win32') assert.equal(fs.statSync(file).mode & 0o777, 0o600);
|
|
15
|
+
}
|
|
7
16
|
|
|
8
17
|
function createConfigFixture(t, initial) {
|
|
9
18
|
// Give each CLI process a disposable home directory. Config commands exit
|
|
10
19
|
// before contacting xyOps, so these checks need no development server.
|
|
11
20
|
const home_dir = fs.mkdtempSync(Path.join(os.tmpdir(), 'xycli-config-'));
|
|
12
|
-
|
|
21
|
+
const program_data = fs.mkdtempSync(Path.join(os.tmpdir(), 'xycli-program-data-'));
|
|
22
|
+
t.after(() => {
|
|
23
|
+
fs.rmSync(home_dir, { recursive: true, force: true });
|
|
24
|
+
fs.rmSync(program_data, { recursive: true, force: true });
|
|
25
|
+
});
|
|
13
26
|
const file = Path.join(home_dir, '.config', 'xyops', 'cli.json');
|
|
14
27
|
|
|
15
28
|
if (initial) {
|
|
@@ -20,17 +33,26 @@ function createConfigFixture(t, initial) {
|
|
|
20
33
|
return {
|
|
21
34
|
file,
|
|
22
35
|
run(args) {
|
|
36
|
+
const env = {
|
|
37
|
+
...process.env,
|
|
38
|
+
HOME: home_dir,
|
|
39
|
+
USERPROFILE: home_dir,
|
|
40
|
+
ProgramData: program_data,
|
|
41
|
+
XYOPS_API_KEY: 'fixture-key',
|
|
42
|
+
XYOPS_BASE_URL: 'https://fixture.invalid',
|
|
43
|
+
XYOPS_ITEMS_PER_PAGE: '99',
|
|
44
|
+
XYOPS_COLOR: 'false'
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// Prove the Windows code path does not receive a compatibility HOME
|
|
48
|
+
// variable from Git Bash, WSL, or the parent test environment.
|
|
49
|
+
if (process.platform == 'win32') delete env.HOME;
|
|
50
|
+
|
|
23
51
|
const result = spawnSync(process.execPath, [Path.join(__dirname, '..', 'index.js'), 'config', ...args], {
|
|
24
52
|
encoding: 'utf8',
|
|
25
53
|
timeout: 10000,
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
HOME: home_dir,
|
|
29
|
-
XYOPS_API_KEY: 'fixture-key',
|
|
30
|
-
XYOPS_BASE_URL: 'https://fixture.invalid',
|
|
31
|
-
XYOPS_ITEMS_PER_PAGE: '99',
|
|
32
|
-
XYOPS_COLOR: 'false'
|
|
33
|
-
}
|
|
54
|
+
windowsHide: true,
|
|
55
|
+
env
|
|
34
56
|
});
|
|
35
57
|
|
|
36
58
|
// Keep captured CLI output out of assertion messages, since a config
|
|
@@ -43,6 +65,24 @@ function createConfigFixture(t, initial) {
|
|
|
43
65
|
};
|
|
44
66
|
}
|
|
45
67
|
|
|
68
|
+
test('config resolves native Unix and Windows source paths', () => {
|
|
69
|
+
assert.deepEqual(config.getConfigFiles('linux', {}, '/home/nick'), [
|
|
70
|
+
'/etc/xyops/cli.json',
|
|
71
|
+
'/home/nick/.config/xyops/cli.json'
|
|
72
|
+
]);
|
|
73
|
+
assert.deepEqual(config.getConfigFiles('win32', { ProgramData: 'C:\\ProgramData' }, 'C:\\Users\\Nick'), [
|
|
74
|
+
'C:\\ProgramData\\xyops\\cli.json',
|
|
75
|
+
'C:\\Users\\Nick\\.config\\xyops\\cli.json'
|
|
76
|
+
]);
|
|
77
|
+
assert.deepEqual(config.getConfigFiles('win32', { ALLUSERSPROFILE: 'D:\\SharedData' }, 'C:\\Users\\Nick'), [
|
|
78
|
+
'D:\\SharedData\\xyops\\cli.json',
|
|
79
|
+
'C:\\Users\\Nick\\.config\\xyops\\cli.json'
|
|
80
|
+
]);
|
|
81
|
+
assert.deepEqual(config.getConfigFiles('win32', {}, 'C:\\Users\\Nick'), [
|
|
82
|
+
'C:\\Users\\Nick\\.config\\xyops\\cli.json'
|
|
83
|
+
]);
|
|
84
|
+
});
|
|
85
|
+
|
|
46
86
|
test('config updates only requested user settings without saving inherited overrides', t => {
|
|
47
87
|
const fixture = createConfigFixture(t, {
|
|
48
88
|
items_per_page: 10,
|
|
@@ -61,7 +101,7 @@ test('config updates only requested user settings without saving inherited overr
|
|
|
61
101
|
suggest: true,
|
|
62
102
|
sync: { up: ['events'], down: false }
|
|
63
103
|
});
|
|
64
|
-
|
|
104
|
+
assertPrivateMode(fixture.file);
|
|
65
105
|
});
|
|
66
106
|
|
|
67
107
|
test('config creates a missing user file with only supplied settings', t => {
|
|
@@ -69,7 +109,7 @@ test('config creates a missing user file with only supplied settings', t => {
|
|
|
69
109
|
fixture.run(['--items_per_page', '25']);
|
|
70
110
|
|
|
71
111
|
assert.deepEqual(fixture.read(), { items_per_page: 25 });
|
|
72
|
-
|
|
112
|
+
assertPrivateMode(fixture.file);
|
|
73
113
|
});
|
|
74
114
|
|
|
75
115
|
test('config dotted updates preserve sibling user settings and explicit false or zero values', t => {
|
package/test/help.test.js
CHANGED
|
@@ -2,25 +2,39 @@ const { test } = require('node:test');
|
|
|
2
2
|
const assert = require('node:assert/strict');
|
|
3
3
|
const { xy } = require('./helpers/common.js');
|
|
4
4
|
|
|
5
|
+
// Deliberately shadow any developer-machine configuration. Help and command
|
|
6
|
+
// diagnostics must remain useful before the user connects the CLI to xyOps.
|
|
7
|
+
const offlineEnv = {
|
|
8
|
+
XYOPS_API_KEY: '',
|
|
9
|
+
XYOPS_BASE_URL: ''
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const offlineFailure = { fail: true, env: offlineEnv };
|
|
13
|
+
|
|
14
|
+
test('help is available without server credentials', () => {
|
|
15
|
+
const output = xy(['help'], { env: offlineEnv });
|
|
16
|
+
assert.match(output, /HELP: OVERVIEW/);
|
|
17
|
+
});
|
|
18
|
+
|
|
5
19
|
test('help suggests the closest chapter for a typo', () => {
|
|
6
|
-
const output = xy(['help', 'event', 'udpate'],
|
|
20
|
+
const output = xy(['help', 'event', 'udpate'], offlineFailure);
|
|
7
21
|
assert.match(output, /Could not find help chapter for "event udpate"\. Did you mean "event update"\?/);
|
|
8
22
|
});
|
|
9
23
|
|
|
10
24
|
test('help does not suggest an unrelated chapter', () => {
|
|
11
|
-
const output = xy(['help', 'completely', 'unrelated', 'words'],
|
|
25
|
+
const output = xy(['help', 'completely', 'unrelated', 'words'], offlineFailure);
|
|
12
26
|
assert.match(output, /Could not find help chapter for "completely unrelated words"\./);
|
|
13
27
|
assert.doesNotMatch(output, /Did you mean/);
|
|
14
28
|
});
|
|
15
29
|
|
|
16
30
|
test('unknown top-level command suggests the closest command', () => {
|
|
17
|
-
const output = xy(['evnets'],
|
|
31
|
+
const output = xy(['evnets'], offlineFailure);
|
|
18
32
|
assert.match(output, /Unknown command: evnets\. Did you mean "events"\?/);
|
|
19
33
|
assert.match(output, /Available Commands:.*events/);
|
|
20
34
|
});
|
|
21
35
|
|
|
22
36
|
test('unknown top-level command does not suggest an unrelated command', () => {
|
|
23
|
-
const output = xy(['completely_unrelated'],
|
|
37
|
+
const output = xy(['completely_unrelated'], offlineFailure);
|
|
24
38
|
assert.match(output, /Unknown command: completely_unrelated/);
|
|
25
39
|
assert.doesNotMatch(output, /Did you mean/);
|
|
26
40
|
});
|
package/test/helpers/common.js
CHANGED
package/test/sync-unit.test.js
CHANGED
|
@@ -104,7 +104,8 @@ function runSync(t, options) {
|
|
|
104
104
|
})], {
|
|
105
105
|
cwd: Path.join(__dirname, '..'),
|
|
106
106
|
encoding: 'utf8',
|
|
107
|
-
timeout: 10000
|
|
107
|
+
timeout: 10000,
|
|
108
|
+
windowsHide: true
|
|
108
109
|
});
|
|
109
110
|
assert.ifError(result.error);
|
|
110
111
|
assert.equal(result.signal, null);
|
|
@@ -659,7 +660,8 @@ test('sync PID lock rejects an overlapping run and cleans up afterward', async t
|
|
|
659
660
|
`;
|
|
660
661
|
const holder = spawn(process.execPath, ['-e', holderScript, lockFile], {
|
|
661
662
|
cwd: Path.join(__dirname, '..'),
|
|
662
|
-
stdio: ['ignore', 'pipe', 'pipe']
|
|
663
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
664
|
+
windowsHide: true
|
|
663
665
|
});
|
|
664
666
|
|
|
665
667
|
await new Promise((resolve, reject) => {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
const { test } = require('node:test');
|
|
2
|
+
const assert = require('node:assert/strict');
|
|
3
|
+
const utils = require('../lib/utils.js');
|
|
4
|
+
|
|
5
|
+
test('executable language detection accepts Unix and Windows command forms', () => {
|
|
6
|
+
// Plugin definitions are portable, so exercise both path styles regardless of
|
|
7
|
+
// the operating system running this test.
|
|
8
|
+
assert.equal(utils.getLangFromBinary('/usr/bin/node --use-strict'), 'javascript');
|
|
9
|
+
assert.equal(utils.getLangFromBinary('/usr/bin/python3.12 -u'), 'python');
|
|
10
|
+
assert.equal(utils.getLangFromBinary('/bin/bash'), 'shell');
|
|
11
|
+
assert.equal(utils.getLangFromBinary('/usr/bin/env -S node --use-strict'), 'javascript');
|
|
12
|
+
assert.equal(utils.getLangFromBinary('"C:\\Program Files\\nodejs\\node.exe" --use-strict'), 'javascript');
|
|
13
|
+
assert.equal(utils.getLangFromBinary('C:\\Python312\\python.exe -u'), 'python');
|
|
14
|
+
assert.equal(utils.getLangFromBinary('pwsh.exe -File'), 'powershell');
|
|
15
|
+
assert.equal(utils.getLangFromBinary('powershell.exe -File'), 'powershell');
|
|
16
|
+
assert.equal(utils.getLangFromBinary('cmd.exe /c'), 'dos');
|
|
17
|
+
assert.equal(utils.getLangFromBinary('unknown-binary.exe'), null);
|
|
18
|
+
assert.equal(utils.getLangFromBinary(null), null);
|
|
19
|
+
});
|
package/test.js
CHANGED
|
@@ -30,7 +30,8 @@ else {
|
|
|
30
30
|
// suite does not prevent the remaining suites from running and reporting.
|
|
31
31
|
const child = spawn(process.execPath, ['--test', '--test-concurrency=1', ...files], {
|
|
32
32
|
cwd: __dirname,
|
|
33
|
-
stdio: 'inherit'
|
|
33
|
+
stdio: 'inherit',
|
|
34
|
+
windowsHide: true
|
|
34
35
|
});
|
|
35
36
|
|
|
36
37
|
child.on('error', error => {
|
package/tools/changelog.js
CHANGED
|
@@ -13,14 +13,14 @@ var debug = (process.argv[2] == 'debug');
|
|
|
13
13
|
process.chdir( Path.dirname( __dirname ) );
|
|
14
14
|
|
|
15
15
|
// make sure git sandbox is clean
|
|
16
|
-
var porcelain = cp.execSync('git status --porcelain', { encoding: 'utf8' }).trim();
|
|
16
|
+
var porcelain = cp.execSync('git status --porcelain', { encoding: 'utf8', windowsHide: true }).trim();
|
|
17
17
|
if (!debug && porcelain.length) {
|
|
18
18
|
console.error("\nERROR: Git sandbox has local changes. Please commit these before updating the changelog.\n");
|
|
19
19
|
process.exit(1);
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
// get list of tags
|
|
23
|
-
var tags = cp.execSync('git tag --list --sort=version:refname', { encoding: 'utf8' }).trim().split(/\n/).reverse();
|
|
23
|
+
var tags = cp.execSync('git tag --list --sort=version:refname', { encoding: 'utf8', windowsHide: true }).trim().split(/\n/).reverse();
|
|
24
24
|
|
|
25
25
|
var md = '';
|
|
26
26
|
md += "# xyCLI Changelog\n";
|
|
@@ -36,7 +36,7 @@ tags.forEach( function(tag, idx) {
|
|
|
36
36
|
md += `\n## Version ${tag}\n\n`;
|
|
37
37
|
|
|
38
38
|
var cmd = `git log ${prev_tag}..${tag} --no-merges --pretty=format:'%h %H %ad %s%n%n%b%n----PX----' --date=short`;
|
|
39
|
-
var output = cp.execSync(cmd, { encoding: 'utf8' }).trim();
|
|
39
|
+
var output = cp.execSync(cmd, { encoding: 'utf8', windowsHide: true }).trim();
|
|
40
40
|
var commits = output.split('----PX----');
|
|
41
41
|
var first = true;
|
|
42
42
|
|
|
@@ -77,10 +77,10 @@ if (debug) {
|
|
|
77
77
|
fs.writeFileSync( 'CHANGELOG.md', md );
|
|
78
78
|
|
|
79
79
|
// make sure log has actually changed
|
|
80
|
-
porcelain = cp.execSync('git status --porcelain', { encoding: 'utf8' }).trim();
|
|
80
|
+
porcelain = cp.execSync('git status --porcelain', { encoding: 'utf8', windowsHide: true }).trim();
|
|
81
81
|
if (!porcelain.length) {
|
|
82
82
|
console.error("\nWarning: Changelog has not changed since last run. Skipping actions.\n");
|
|
83
83
|
process.exit(1);
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
-
cp.execSync('git add CHANGELOG.md && git commit -m "Update CHANGELOG" && git push', { stdio: 'inherit' } );
|
|
86
|
+
cp.execSync('git add CHANGELOG.md && git commit -m "Update CHANGELOG" && git push', { stdio: 'inherit', windowsHide: true } );
|