@pixlcore/xycli 1.0.7 → 1.0.9
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/README.md +2 -2
- package/docs/help.md +22 -3
- package/docs/sync.md +71 -9
- package/docs/two-way-git-tutorial.md +19 -3
- package/index.js +32 -5
- package/lib/apikey.js +1 -1
- package/lib/buckets.js +1 -5
- package/lib/marketplace.js +1 -1
- package/lib/sync-buckets.js +315 -0
- package/lib/sync-syshook.js +207 -0
- package/lib/sync.js +128 -43
- package/lib/transfer.js +3 -3
- package/lib/utils.js +8 -3
- package/package.json +2 -2
- package/test/README.md +5 -3
- package/test/help.test.js +56 -0
- package/test/sync-buckets-unit.test.js +188 -0
- package/test/sync-buckets.test.js +133 -0
- package/test/sync-syshook-unit.test.js +231 -0
- package/test/sync-unit.test.js +118 -2
- package/test/toast.test.js +3 -3
- package/test/utils-unit.test.js +50 -0
package/test/sync-unit.test.js
CHANGED
|
@@ -18,6 +18,7 @@ const script = `
|
|
|
18
18
|
if (options.setupCwd) process.chdir(options.setupCwd);
|
|
19
19
|
const calls = [];
|
|
20
20
|
const requests = [];
|
|
21
|
+
const apiOptions = [];
|
|
21
22
|
const app = {
|
|
22
23
|
...sync,
|
|
23
24
|
args: options.args,
|
|
@@ -41,9 +42,10 @@ const script = `
|
|
|
41
42
|
compareVersions() { return 0; },
|
|
42
43
|
async getMultiple() {},
|
|
43
44
|
die(message) { throw new Error(message); },
|
|
44
|
-
async callStandardAPI(method, data) {
|
|
45
|
+
async callStandardAPI(method, data, opts) {
|
|
45
46
|
calls.push(method);
|
|
46
47
|
requests.push({ method, data });
|
|
48
|
+
apiOptions.push({ method, opts });
|
|
47
49
|
if (options.failAPI === method) throw new Error('Fixture API failure');
|
|
48
50
|
}
|
|
49
51
|
};
|
|
@@ -51,7 +53,7 @@ const script = `
|
|
|
51
53
|
console.error(err.message);
|
|
52
54
|
process.exitCode = 1;
|
|
53
55
|
}).finally(() => {
|
|
54
|
-
console.log(JSON.stringify({ calls, requests, errors: app.errors || [], warnings: app.warnings || [] }));
|
|
56
|
+
console.log(JSON.stringify({ calls, requests, apiOptions, errors: app.errors || [], warnings: app.warnings || [] }));
|
|
55
57
|
});
|
|
56
58
|
`;
|
|
57
59
|
|
|
@@ -225,6 +227,87 @@ test('sync errors exit nonzero even in quiet mode or a dry run', t => {
|
|
|
225
227
|
}
|
|
226
228
|
});
|
|
227
229
|
|
|
230
|
+
test('sync ignore-missing skips only absent remote IDs and continues updates', t => {
|
|
231
|
+
const remote = { id: 'present', title: 'Present', notes: 'Remote' };
|
|
232
|
+
const local = { ...remote, notes: 'Local' };
|
|
233
|
+
const missing = { id: 'gone', title: 'Gone', notes: 'Keep this file' };
|
|
234
|
+
const result = runSync(t, {
|
|
235
|
+
categories: [remote],
|
|
236
|
+
existingSources: [
|
|
237
|
+
{ path: 'Gone.json', type: 'category', data: missing },
|
|
238
|
+
{ path: 'Present.json', type: 'category', data: local }
|
|
239
|
+
],
|
|
240
|
+
args: { up: 'categories', down: 'categories', delete: false, ignore_missing: true }
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
assert.equal(result.status, 0);
|
|
244
|
+
assert.deepEqual(result.report.warnings, []);
|
|
245
|
+
assert.equal(result.report.calls.includes('update_category'), true);
|
|
246
|
+
assert.equal(fs.existsSync(Path.join(result.dir, 'Gone.json')), true);
|
|
247
|
+
assert.equal(JSON.parse(fs.readFileSync(Path.join(result.dir, 'Gone.json'), 'utf8')).items[0].data.notes, 'Keep this file');
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
test('sync ignore-missing does not suppress malformed source warnings', t => {
|
|
251
|
+
const result = runSync(t, {
|
|
252
|
+
source: { id: 'missing', title: 'Missing' },
|
|
253
|
+
emptyItems: true,
|
|
254
|
+
args: { up: 'categories', ignore_missing: true }
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
assert.equal(result.status, 1);
|
|
258
|
+
assert.match(result.report.warnings[0], /empty, malformed or missing items array/);
|
|
259
|
+
assert.deepEqual(result.report.calls, []);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
test('sync ignore-missing still validates neighbors of missing sources', t => {
|
|
263
|
+
const result = runSync(t, {
|
|
264
|
+
source: { id: 'missing', title: 'Missing' },
|
|
265
|
+
invalidNeighbor: true,
|
|
266
|
+
args: { up: 'categories', ignore_missing: true }
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
assert.equal(result.status, 1);
|
|
270
|
+
assert.match(result.report.warnings[0], /Neighbor file does not match/);
|
|
271
|
+
assert.deepEqual(result.report.calls, []);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
test('sync ignore-missing still rejects duplicate missing sources', t => {
|
|
275
|
+
const result = runSync(t, {
|
|
276
|
+
source: { id: 'missing', title: 'Missing' },
|
|
277
|
+
duplicateSource: true,
|
|
278
|
+
args: { up: 'categories', ignore_missing: true }
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
assert.equal(result.status, 1);
|
|
282
|
+
assert.match(result.report.warnings[0], /Duplicate source item found/);
|
|
283
|
+
assert.deepEqual(result.report.calls, []);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
test('sync ignore-missing can be disabled over a saved setting', t => {
|
|
287
|
+
const result = runSync(t, {
|
|
288
|
+
source: { id: 'missing', title: 'Missing' },
|
|
289
|
+
config: { up: 'categories', ignore_missing: true },
|
|
290
|
+
args: { ignore_missing: false }
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
assert.equal(result.status, 1);
|
|
294
|
+
assert.match(result.report.warnings[0], /Cannot find category in xyOps/);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
test('sync sends existing Plugin properties through the sync API path', t => {
|
|
298
|
+
const remote = { id: 'legacy', title: 'Legacy', type: 'event', command: 'node', cwd: '/opt/legacy', custom_field: { keep: true }, notes: 'Remote' };
|
|
299
|
+
const local = { ...remote, notes: 'Local' };
|
|
300
|
+
const result = runSync(t, {
|
|
301
|
+
plugins: [remote],
|
|
302
|
+
existingSources: [{ path: 'Legacy.json', type: 'plugin', data: local }],
|
|
303
|
+
args: { up: 'plugins' }
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
assert.equal(result.status, 0);
|
|
307
|
+
assert.deepEqual(result.report.requests.find(request => request.method === 'update_plugin').data, local);
|
|
308
|
+
assert.equal(result.report.apiOptions.find(call => call.method === 'update_plugin').opts.sync, true);
|
|
309
|
+
});
|
|
310
|
+
|
|
228
311
|
test('sync API errors preserve failure status while still sending both notifications', t => {
|
|
229
312
|
const category = { id: 'fixture', title: 'Fixture', notes: 'Remote' };
|
|
230
313
|
for (const failAPI of ['update_category', 'update_global_state', 'delete_category']) {
|
|
@@ -297,6 +380,39 @@ test('sync setup new exports only remote definitions missing from the local tree
|
|
|
297
380
|
assert.equal(fs.existsSync(Path.join(result.dir, 'custom', 'layout', 'Renamed-Existing.json')), true);
|
|
298
381
|
});
|
|
299
382
|
|
|
383
|
+
test('sync setup new ignore-missing leaves stale files and exports new definitions', t => {
|
|
384
|
+
const missing = { id: 'gone', title: 'Gone' };
|
|
385
|
+
const added = { id: 'added', title: 'Added Plugin', command: 'node' };
|
|
386
|
+
const result = runSync(t, {
|
|
387
|
+
setup: ['plugins'],
|
|
388
|
+
plugins: [added],
|
|
389
|
+
existingSources: [{ path: 'old/Gone.json', type: 'plugin', data: missing }],
|
|
390
|
+
args: { new: true, ignore_missing: true }
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
assert.equal(result.status, 0);
|
|
394
|
+
assert.deepEqual(result.report.warnings, []);
|
|
395
|
+
assert.equal(fs.existsSync(Path.join(result.dir, 'old', 'Gone.json')), true);
|
|
396
|
+
assert.equal(fs.existsSync(Path.join(result.dir, 'plugins', 'Added-Plugin.json')), true);
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
test('sync setup new ignore-missing still rejects duplicate missing sources', t => {
|
|
400
|
+
const missing = { id: 'gone', title: 'Gone' };
|
|
401
|
+
const result = runSync(t, {
|
|
402
|
+
setup: ['plugins'],
|
|
403
|
+
plugins: [{ id: 'added', title: 'Added Plugin', command: 'node' }],
|
|
404
|
+
existingSources: [
|
|
405
|
+
{ path: 'old/Gone.json', type: 'plugin', data: missing },
|
|
406
|
+
{ path: 'other/Gone.json', type: 'plugin', data: missing }
|
|
407
|
+
],
|
|
408
|
+
args: { new: true, ignore_missing: true }
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
assert.equal(result.status, 1);
|
|
412
|
+
assert.match(result.stderr, /Duplicate source item found/);
|
|
413
|
+
assert.equal(fs.existsSync(Path.join(result.dir, 'plugins', 'Added-Plugin.json')), false);
|
|
414
|
+
});
|
|
415
|
+
|
|
300
416
|
test('sync setup runs down_cmd after writing files from the setup directory', t => {
|
|
301
417
|
const marker = 'setup-complete.txt';
|
|
302
418
|
const result = runSync(t, {
|
package/test/toast.test.js
CHANGED
|
@@ -7,14 +7,14 @@ cli.global();
|
|
|
7
7
|
|
|
8
8
|
test('toast wraps within the terminal width without ellipsis', () => {
|
|
9
9
|
const originalWidth = cli.width;
|
|
10
|
-
const originalWrite = process.
|
|
10
|
+
const originalWrite = process.stderr.write;
|
|
11
11
|
const originalColor = cli.chalk.enabled;
|
|
12
12
|
let output = '';
|
|
13
13
|
|
|
14
14
|
try {
|
|
15
15
|
cli.width = function() { return 40; };
|
|
16
16
|
cli.chalk.enabled = false;
|
|
17
|
-
process.
|
|
17
|
+
process.stderr.write = function(chunk) {
|
|
18
18
|
output += chunk;
|
|
19
19
|
return true;
|
|
20
20
|
};
|
|
@@ -27,7 +27,7 @@ test('toast wraps within the terminal width without ellipsis', () => {
|
|
|
27
27
|
finally {
|
|
28
28
|
cli.width = originalWidth;
|
|
29
29
|
cli.chalk.enabled = originalColor;
|
|
30
|
-
process.
|
|
30
|
+
process.stderr.write = originalWrite;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
const box_lines = output.split('\n').filter( line => line.match(/[┌│└]/) );
|
package/test/utils-unit.test.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
const { test } = require('node:test');
|
|
2
2
|
const assert = require('node:assert/strict');
|
|
3
|
+
const cli = require('pixl-cli');
|
|
3
4
|
const utils = require('../lib/utils.js');
|
|
4
5
|
|
|
6
|
+
cli.global();
|
|
7
|
+
|
|
5
8
|
test('executable language detection accepts Unix and Windows command forms', () => {
|
|
6
9
|
// Plugin definitions are portable, so exercise both path styles regardless of
|
|
7
10
|
// the operating system running this test.
|
|
@@ -17,3 +20,50 @@ test('executable language detection accepts Unix and Windows command forms', ()
|
|
|
17
20
|
assert.equal(utils.getLangFromBinary('unknown-binary.exe'), null);
|
|
18
21
|
assert.equal(utils.getLangFromBinary(null), null);
|
|
19
22
|
});
|
|
23
|
+
|
|
24
|
+
test('sync API updates preserve stored properties while manual updates retain typo checks', async () => {
|
|
25
|
+
const calls = [];
|
|
26
|
+
const context = {
|
|
27
|
+
...utils,
|
|
28
|
+
dry: false,
|
|
29
|
+
verbose: false,
|
|
30
|
+
api: {
|
|
31
|
+
async update_plugin(request, opts) {
|
|
32
|
+
calls.push({ method: 'update_plugin', request, opts });
|
|
33
|
+
return { err: null, data: { code: 0 } };
|
|
34
|
+
},
|
|
35
|
+
async update_event(request, opts) {
|
|
36
|
+
calls.push({ method: 'update_event', request, opts });
|
|
37
|
+
return { err: null, data: { code: 0 } };
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
die(message) { throw new Error(message); }
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
for (const [method, request] of [
|
|
44
|
+
['update_plugin', { id: 'legacy_plugin', cwd: '/tmp/legacy' }],
|
|
45
|
+
['update_event', { id: 'legacy_event', custom_field: { enabled: true } }]
|
|
46
|
+
]) {
|
|
47
|
+
// A hand-typed update still rejects an unknown top-level property.
|
|
48
|
+
const prior_calls = calls.length;
|
|
49
|
+
await assert.rejects(context.callStandardAPI(method, request), /Unsupported property/);
|
|
50
|
+
assert.equal(calls.length, prior_calls);
|
|
51
|
+
|
|
52
|
+
// Sync forwards the complete stored object to xyOps for its own validation.
|
|
53
|
+
await context.callStandardAPI(method, request, { sync: true });
|
|
54
|
+
assert.equal(calls.length, prior_calls + 1);
|
|
55
|
+
assert.deepEqual(calls.at(-1), { method, request, opts: {} });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// The original report failed during dry sync, before any xyOps request.
|
|
59
|
+
// Preview the same Plugin payload and verify that it makes no SDK call.
|
|
60
|
+
const plugin_request = { id: 'legacy_plugin', cwd: '/tmp/legacy' };
|
|
61
|
+
const previews = [];
|
|
62
|
+
const prior_calls = calls.length;
|
|
63
|
+
context.dry = true;
|
|
64
|
+
context.color = () => cli.chalk.white;
|
|
65
|
+
context.jsonOutput = request => { previews.push(request); };
|
|
66
|
+
await context.callStandardAPI('update_plugin', plugin_request, { sync: true });
|
|
67
|
+
assert.deepEqual(previews, [plugin_request]);
|
|
68
|
+
assert.equal(calls.length, prior_calls);
|
|
69
|
+
});
|