@pixlcore/xycli 1.0.1 → 1.0.3
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 +26 -0
- package/README.md +20 -9
- package/docs/help.md +17 -6
- package/docs/sync.md +28 -8
- package/docs/two-way-git-tutorial.md +262 -0
- package/lib/config.js +29 -7
- package/lib/sync.js +247 -54
- package/lib/utils.js +27 -1
- package/package.json +4 -2
- package/test/README.md +1 -1
- package/test/config.test.js +51 -11
- package/test/helpers/common.js +1 -0
- package/test/sync-unit.test.js +278 -8
- package/test/sync.test.js +3 -2
- package/test/utils-unit.test.js +19 -0
- package/test.js +2 -1
- package/tools/changelog.js +86 -0
package/test/sync-unit.test.js
CHANGED
|
@@ -13,28 +13,31 @@ const script = `
|
|
|
13
13
|
const cli = require('pixl-cli');
|
|
14
14
|
cli.global();
|
|
15
15
|
const options = JSON.parse(process.argv[1]);
|
|
16
|
-
cli.args.quiet =
|
|
16
|
+
cli.args.quiet = options.quiet !== false;
|
|
17
|
+
const sync = require('./lib/sync.js');
|
|
18
|
+
if (options.setupCwd) process.chdir(options.setupCwd);
|
|
17
19
|
const calls = [];
|
|
18
20
|
const requests = [];
|
|
19
21
|
const app = {
|
|
20
|
-
...
|
|
22
|
+
...sync,
|
|
21
23
|
args: options.args,
|
|
22
24
|
config: {
|
|
23
25
|
base_url: 'https://fixture.invalid',
|
|
24
26
|
sync: options.config || {},
|
|
25
27
|
ui: {
|
|
26
28
|
list_list: ['categories', 'events', 'plugins'].map(id => ({ id })),
|
|
27
|
-
data_types: { category: { list: 'categories' }, plugin: { list: 'plugins' } }
|
|
29
|
+
data_types: { category: { list: 'categories' }, event: { list: 'events' }, plugin: { list: 'plugins' } }
|
|
28
30
|
}
|
|
29
31
|
},
|
|
30
32
|
state: {},
|
|
31
33
|
categories: options.categories || [],
|
|
32
|
-
events: [],
|
|
34
|
+
events: options.events || [],
|
|
33
35
|
plugins: options.plugins || [],
|
|
34
36
|
version: 'test',
|
|
35
37
|
dry: !!options.args.dry,
|
|
36
38
|
color() { return cli.chalk.white; },
|
|
37
39
|
markdown(text) { return text; },
|
|
40
|
+
printBoxList() {},
|
|
38
41
|
compareVersions() { return 0; },
|
|
39
42
|
async getMultiple() {},
|
|
40
43
|
die(message) { throw new Error(message); },
|
|
@@ -58,6 +61,7 @@ function runSync(t, options) {
|
|
|
58
61
|
const dir = fs.mkdtempSync(Path.join(os.tmpdir(), 'xycli-sync-unit-'));
|
|
59
62
|
t.after(() => fs.rmSync(dir, { recursive: true, force: true }));
|
|
60
63
|
const args = { other: [dir], ...options.args };
|
|
64
|
+
if (options.setup) args.other = ['setup', ...options.setup];
|
|
61
65
|
|
|
62
66
|
if (options.source) {
|
|
63
67
|
fs.writeFileSync(Path.join(dir, 'Category.json'), JSON.stringify({
|
|
@@ -79,17 +83,41 @@ function runSync(t, options) {
|
|
|
79
83
|
fs.copyFileSync(Path.join(dir, 'Category.json'), Path.join(dir, 'Duplicate.json'));
|
|
80
84
|
}
|
|
81
85
|
}
|
|
86
|
+
if (options.existingSources) {
|
|
87
|
+
options.existingSources.forEach(source => {
|
|
88
|
+
const file = Path.join(dir, source.path);
|
|
89
|
+
fs.mkdirSync(Path.dirname(file), { recursive: true });
|
|
90
|
+
const contents = source.contents !== undefined ? source.contents : JSON.stringify({
|
|
91
|
+
type: 'xypdf',
|
|
92
|
+
version: '1.0',
|
|
93
|
+
items: [{ type: source.type, data: source.data }]
|
|
94
|
+
});
|
|
95
|
+
fs.writeFileSync(file, contents);
|
|
96
|
+
});
|
|
97
|
+
}
|
|
82
98
|
if (options.missingDir) args.other = [Path.join(dir, 'missing')];
|
|
83
99
|
|
|
84
|
-
const result = spawnSync(process.execPath, ['-e', script, JSON.stringify({
|
|
100
|
+
const result = spawnSync(process.execPath, ['-e', script, JSON.stringify({
|
|
101
|
+
...options,
|
|
102
|
+
args,
|
|
103
|
+
setupCwd: options.setup ? dir : undefined
|
|
104
|
+
})], {
|
|
85
105
|
cwd: Path.join(__dirname, '..'),
|
|
86
106
|
encoding: 'utf8',
|
|
87
|
-
timeout: 10000
|
|
107
|
+
timeout: 10000,
|
|
108
|
+
windowsHide: true
|
|
88
109
|
});
|
|
89
110
|
assert.ifError(result.error);
|
|
90
111
|
assert.equal(result.signal, null);
|
|
91
112
|
const report = JSON.parse(result.stdout.trim().split('\n').pop());
|
|
92
|
-
return { ...result, report };
|
|
113
|
+
return { ...result, report, dir };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function getSetupMarkerCommand(filename) {
|
|
117
|
+
// Use the current Node.js binary so the completion-command fixture is
|
|
118
|
+
// portable and does not depend on a separate shell utility being installed.
|
|
119
|
+
const code = `require('node:fs').writeFileSync(${JSON.stringify(filename)}, process.cwd())`;
|
|
120
|
+
return `${JSON.stringify(process.execPath)} -e ${JSON.stringify(code)}`;
|
|
93
121
|
}
|
|
94
122
|
|
|
95
123
|
test('sync update guard requires confirmation only for the exact managed object', async () => {
|
|
@@ -217,6 +245,247 @@ test('sync API errors preserve failure status while still sending both notificat
|
|
|
217
245
|
}
|
|
218
246
|
});
|
|
219
247
|
|
|
248
|
+
test('sync setup uses a custom extension only when detection falls back to txt', t => {
|
|
249
|
+
const result = runSync(t, {
|
|
250
|
+
setup: ['plugins'],
|
|
251
|
+
plugins: [
|
|
252
|
+
{ id: 'fallback', title: 'Fallback', command: 'custom-runner', script: 'Write-Host "Hello"\n' },
|
|
253
|
+
{ id: 'detected', title: 'Detected', command: 'pwsh -File', script: 'Write-Host "Hello"\n' },
|
|
254
|
+
{ id: 'json', title: 'JSON Data', command: '', script: '{ "hello": true }\n' }
|
|
255
|
+
],
|
|
256
|
+
args: { file_props: 'script', default_ext: '.code' }
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
assert.equal(result.status, 0);
|
|
260
|
+
assert.equal(fs.existsSync(Path.join(result.dir, 'plugins', 'Fallback-script.code')), true);
|
|
261
|
+
assert.equal(fs.existsSync(Path.join(result.dir, 'plugins', 'Detected-script.ps1')), true);
|
|
262
|
+
assert.equal(fs.existsSync(Path.join(result.dir, 'plugins', 'JSON-Data-script.json')), true);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
test('sync setup rejects unsafe or malformed default extensions before writing', t => {
|
|
266
|
+
for (const defaultExt of [true, '', '.', '../ps1', 'ps/1', 'ps-1', 'tar.gz']) {
|
|
267
|
+
const result = runSync(t, {
|
|
268
|
+
setup: ['plugins'],
|
|
269
|
+
plugins: [{ id: 'fixture', title: 'Fixture', command: 'custom-runner', script: 'Example\n' }],
|
|
270
|
+
args: { file_props: 'script', default_ext: defaultExt }
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
assert.equal(result.status, 1);
|
|
274
|
+
assert.match(result.stderr, /Invalid --default_ext value/);
|
|
275
|
+
assert.equal(fs.existsSync(Path.join(result.dir, 'plugins')), false);
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
test('sync setup new exports only remote definitions missing from the local tree', t => {
|
|
280
|
+
const existing = { id: 'existing', title: 'Existing', command: 'node', script: 'console.log("Existing");\n' };
|
|
281
|
+
const added = { id: 'added', title: 'Added Plugin', command: 'custom-runner', script: 'Write-Host "Added"\n' };
|
|
282
|
+
const result = runSync(t, {
|
|
283
|
+
setup: ['plugins'],
|
|
284
|
+
plugins: [existing, added],
|
|
285
|
+
existingSources: [{
|
|
286
|
+
path: 'custom/layout/Renamed-Existing.json',
|
|
287
|
+
type: 'plugin',
|
|
288
|
+
data: existing
|
|
289
|
+
}],
|
|
290
|
+
args: { new: true, file_props: 'script', default_ext: 'ps1' }
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
assert.equal(result.status, 0);
|
|
294
|
+
assert.equal(fs.existsSync(Path.join(result.dir, 'plugins', 'Added-Plugin.json')), true);
|
|
295
|
+
assert.equal(fs.existsSync(Path.join(result.dir, 'plugins', 'Added-Plugin-script.ps1')), true);
|
|
296
|
+
assert.equal(fs.existsSync(Path.join(result.dir, 'plugins', 'Existing.json')), false);
|
|
297
|
+
assert.equal(fs.existsSync(Path.join(result.dir, 'custom', 'layout', 'Renamed-Existing.json')), true);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
test('sync setup runs down_cmd after writing files from the setup directory', t => {
|
|
301
|
+
const marker = 'setup-complete.txt';
|
|
302
|
+
const result = runSync(t, {
|
|
303
|
+
setup: ['plugins'],
|
|
304
|
+
plugins: [{ id: 'added', title: 'Added', command: 'node', script: 'Added\n' }],
|
|
305
|
+
args: {
|
|
306
|
+
file_props: 'script',
|
|
307
|
+
down_cmd: getSetupMarkerCommand(marker)
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
assert.equal(result.status, 0);
|
|
312
|
+
assert.equal(fs.readFileSync(Path.join(result.dir, marker), 'utf8'), fs.realpathSync(result.dir));
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
test('sync setup new runs down_cmd only when it writes new files', t => {
|
|
316
|
+
const existing = { id: 'existing', title: 'Existing', command: 'node', script: 'Existing\n' };
|
|
317
|
+
const marker = 'setup-new-complete.txt';
|
|
318
|
+
const command = getSetupMarkerCommand(marker);
|
|
319
|
+
const added = runSync(t, {
|
|
320
|
+
setup: ['plugins'],
|
|
321
|
+
plugins: [existing, { id: 'added', title: 'Added', command: 'node', script: 'Added\n' }],
|
|
322
|
+
existingSources: [{ path: 'Existing.json', type: 'plugin', data: existing }],
|
|
323
|
+
args: { new: true, down_cmd: command }
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
assert.equal(added.status, 0);
|
|
327
|
+
assert.equal(fs.existsSync(Path.join(added.dir, marker)), true);
|
|
328
|
+
|
|
329
|
+
const unchanged = runSync(t, {
|
|
330
|
+
setup: ['plugins'],
|
|
331
|
+
plugins: [existing],
|
|
332
|
+
existingSources: [{ path: 'Existing.json', type: 'plugin', data: existing }],
|
|
333
|
+
args: { new: true, down_cmd: command }
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
assert.equal(unchanged.status, 0);
|
|
337
|
+
assert.equal(fs.existsSync(Path.join(unchanged.dir, marker)), false);
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
test('sync setup does not run down_cmd during a dry run', t => {
|
|
341
|
+
const marker = 'setup-dry-complete.txt';
|
|
342
|
+
const result = runSync(t, {
|
|
343
|
+
setup: ['plugins'],
|
|
344
|
+
plugins: [{ id: 'added', title: 'Added', command: 'node', script: 'Added\n' }],
|
|
345
|
+
args: { dry: true, down_cmd: getSetupMarkerCommand(marker) }
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
assert.equal(result.status, 0);
|
|
349
|
+
assert.equal(fs.existsSync(Path.join(result.dir, marker)), false);
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
test('sync setup completion-command errors exit nonzero', t => {
|
|
353
|
+
const result = runSync(t, {
|
|
354
|
+
setup: ['plugins'],
|
|
355
|
+
plugins: [{ id: 'added', title: 'Added', command: 'node', script: 'Added\n' }],
|
|
356
|
+
args: { down_cmd: `${JSON.stringify(process.execPath)} -e "process.exit(2)"` }
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
assert.equal(result.status, 1);
|
|
360
|
+
assert.match(result.report.errors[0], /Command failed/);
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
test('sync setup new dry run discovers additions without writing them', t => {
|
|
364
|
+
const existing = { id: 'existing', title: 'Existing', command: 'node', script: 'Existing\n' };
|
|
365
|
+
const result = runSync(t, {
|
|
366
|
+
setup: ['plugins'],
|
|
367
|
+
plugins: [existing, { id: 'added', title: 'Added', command: 'node', script: 'Added\n' }],
|
|
368
|
+
existingSources: [{ path: 'Existing-Renamed.json', type: 'plugin', data: existing }],
|
|
369
|
+
args: { new: true, dry: true, file_props: 'script' }
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
assert.equal(result.status, 0);
|
|
373
|
+
assert.equal(fs.existsSync(Path.join(result.dir, 'plugins')), false);
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
test('sync setup new preserves existing Events and writes new workflows separately', t => {
|
|
377
|
+
const category = { id: 'operations', title: 'Operations Team' };
|
|
378
|
+
const existing = { id: 'existing-event', title: 'Existing Event', type: 'event', category: category.id };
|
|
379
|
+
const workflow = { id: 'new-workflow', title: 'New Workflow', type: 'workflow', category: category.id };
|
|
380
|
+
const result = runSync(t, {
|
|
381
|
+
setup: ['events'],
|
|
382
|
+
categories: [category],
|
|
383
|
+
events: [existing, workflow],
|
|
384
|
+
existingSources: [{ path: 'custom/Event.json', type: 'event', data: existing }],
|
|
385
|
+
args: { new: true }
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
assert.equal(result.status, 0);
|
|
389
|
+
assert.equal(fs.existsSync(Path.join(result.dir, 'events')), false);
|
|
390
|
+
assert.equal(fs.existsSync(Path.join(result.dir, 'workflows', 'Operations-Team', 'New-Workflow.json')), true);
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
test('sync setup groups Events and workflows into Category folders', t => {
|
|
394
|
+
const operations = { id: 'operations', title: 'Operations Team' };
|
|
395
|
+
const limited = { id: 'limited', title: 'Limited Run' };
|
|
396
|
+
const engineering = { id: 'engineering', title: 'Engineering & QA' };
|
|
397
|
+
const result = runSync(t, {
|
|
398
|
+
setup: ['events'],
|
|
399
|
+
categories: [operations, limited, engineering],
|
|
400
|
+
events: [
|
|
401
|
+
{ id: 'nightly', title: 'Nightly Backup', type: 'event', category: operations.id, params: { script: '#!/bin/sh\necho backup\n' } },
|
|
402
|
+
{ id: 'limited', title: 'Limited Event', type: 'event', category: limited.id },
|
|
403
|
+
{ id: 'release', title: 'Release Pipeline', type: 'workflow', category: engineering.id },
|
|
404
|
+
{ id: 'ondemand', title: 'On Demand Backup', type: 'event', category: operations.id }
|
|
405
|
+
],
|
|
406
|
+
args: { file_props: 'params.script' },
|
|
407
|
+
quiet: false
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
assert.equal(result.status, 0);
|
|
411
|
+
assert.equal(fs.existsSync(Path.join(result.dir, 'events', 'Operations-Team', 'Nightly-Backup.json')), true);
|
|
412
|
+
assert.equal(fs.existsSync(Path.join(result.dir, 'events', 'Operations-Team', 'Nightly-Backup-params.script.sh')), true);
|
|
413
|
+
assert.equal(fs.existsSync(Path.join(result.dir, 'events', 'Operations-Team', 'On-Demand-Backup.json')), true);
|
|
414
|
+
assert.equal(fs.existsSync(Path.join(result.dir, 'events', 'Limited-Run', 'Limited-Event.json')), true);
|
|
415
|
+
assert.equal(fs.existsSync(Path.join(result.dir, 'workflows', 'Engineering-QA', 'Release-Pipeline.json')), true);
|
|
416
|
+
assert.equal(fs.existsSync(Path.join(result.dir, 'events', 'Nightly-Backup.json')), false);
|
|
417
|
+
assert.equal(fs.existsSync(Path.join(result.dir, 'workflows', 'Release-Pipeline.json')), false);
|
|
418
|
+
|
|
419
|
+
// The source order deliberately returns to Operations after Limited Run.
|
|
420
|
+
// Its two Events must still print beneath one folder heading.
|
|
421
|
+
const operationsHeader = result.stdout.indexOf('events/Operations-Team/');
|
|
422
|
+
const nightlyFile = result.stdout.indexOf('Nightly-Backup.json');
|
|
423
|
+
const onDemandFile = result.stdout.indexOf('On-Demand-Backup.json');
|
|
424
|
+
const limitedHeader = result.stdout.indexOf('events/Limited-Run/');
|
|
425
|
+
const limitedFile = result.stdout.indexOf('Limited-Event.json');
|
|
426
|
+
const workflowHeader = result.stdout.indexOf('workflows/Engineering-QA/');
|
|
427
|
+
assert.ok(operationsHeader < nightlyFile);
|
|
428
|
+
assert.ok(nightlyFile < onDemandFile);
|
|
429
|
+
assert.ok(onDemandFile < limitedHeader);
|
|
430
|
+
assert.ok(limitedHeader < limitedFile);
|
|
431
|
+
assert.ok(limitedFile < workflowHeader);
|
|
432
|
+
assert.equal(result.stdout.indexOf('events/Operations-Team/', operationsHeader + 1), -1);
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
test('sync setup validates all Event Categories before writing any files', t => {
|
|
436
|
+
const result = runSync(t, {
|
|
437
|
+
setup: ['plugins', 'events'],
|
|
438
|
+
plugins: [{ id: 'plugin', title: 'Plugin', script: 'Hello\n' }],
|
|
439
|
+
events: [{ id: 'orphan', title: 'Orphan Event', type: 'event', category: 'missing' }],
|
|
440
|
+
args: {}
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
assert.equal(result.status, 1);
|
|
444
|
+
assert.match(result.stderr, /Cannot find Category for Event: Orphan Event \(missing\)/);
|
|
445
|
+
assert.equal(fs.existsSync(Path.join(result.dir, 'plugins')), false);
|
|
446
|
+
assert.equal(fs.existsSync(Path.join(result.dir, 'events')), false);
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
test('sync setup new rejects overwrite mode and malformed values before writing', t => {
|
|
450
|
+
for (const args of [
|
|
451
|
+
{ new: true, force: true },
|
|
452
|
+
{ new: 'yes' }
|
|
453
|
+
]) {
|
|
454
|
+
const result = runSync(t, {
|
|
455
|
+
setup: ['plugins'],
|
|
456
|
+
plugins: [{ id: 'added', title: 'Added', command: 'node', script: 'Added\n' }],
|
|
457
|
+
args
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
assert.equal(result.status, 1);
|
|
461
|
+
assert.match(result.stderr, /--new/);
|
|
462
|
+
assert.equal(fs.existsSync(Path.join(result.dir, 'plugins')), false);
|
|
463
|
+
}
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
test('sync setup new aborts on duplicate or malformed existing sources before writing', t => {
|
|
467
|
+
const existing = { id: 'existing', title: 'Existing', command: 'node', script: 'Existing\n' };
|
|
468
|
+
for (const existingSources of [
|
|
469
|
+
[
|
|
470
|
+
{ path: 'one.json', type: 'plugin', data: existing },
|
|
471
|
+
{ path: 'nested/two.json', type: 'plugin', data: existing }
|
|
472
|
+
],
|
|
473
|
+
[{ path: 'broken.json', contents: '{ nope' }],
|
|
474
|
+
[{ path: 'orphan.json', type: 'plugin', data: { id: 'missing', title: 'Missing' } }]
|
|
475
|
+
]) {
|
|
476
|
+
const result = runSync(t, {
|
|
477
|
+
setup: ['plugins'],
|
|
478
|
+
plugins: [existing, { id: 'added', title: 'Added', command: 'node', script: 'Added\n' }],
|
|
479
|
+
existingSources,
|
|
480
|
+
args: { new: true }
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
assert.equal(result.status, 1);
|
|
484
|
+
assert.match(result.stderr, /Duplicate source item|Failed to parse JSON file|Cannot find plugin in xyOps/);
|
|
485
|
+
assert.equal(fs.existsSync(Path.join(result.dir, 'plugins')), false);
|
|
486
|
+
}
|
|
487
|
+
});
|
|
488
|
+
|
|
220
489
|
test('sync skips the entire delete pass after an update or state API failure', t => {
|
|
221
490
|
const category = { id: 'fixture', title: 'Fixture', notes: 'Remote' };
|
|
222
491
|
const orphan = { id: 'orphan', title: 'Orphan', notes: 'Delete candidate' };
|
|
@@ -391,7 +660,8 @@ test('sync PID lock rejects an overlapping run and cleans up afterward', async t
|
|
|
391
660
|
`;
|
|
392
661
|
const holder = spawn(process.execPath, ['-e', holderScript, lockFile], {
|
|
393
662
|
cwd: Path.join(__dirname, '..'),
|
|
394
|
-
stdio: ['ignore', 'pipe', 'pipe']
|
|
663
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
664
|
+
windowsHide: true
|
|
395
665
|
});
|
|
396
666
|
|
|
397
667
|
await new Promise((resolve, reject) => {
|
package/test/sync.test.js
CHANGED
|
@@ -171,8 +171,9 @@ test('sync', async t => {
|
|
|
171
171
|
const categoryExport = Path.join(temp, 'categories', slugify(categoryTitle) + '.json');
|
|
172
172
|
const pluginExport = Path.join(temp, 'plugins', slugify(pluginTitle) + '.json');
|
|
173
173
|
const pluginScriptExport = Path.join(temp, 'plugins', slugify(pluginTitle) + '-script.js');
|
|
174
|
-
const
|
|
175
|
-
const
|
|
174
|
+
const eventDir = Path.join(temp, 'events', slugify(categoryTitle));
|
|
175
|
+
const eventExport = Path.join(eventDir, slugify(eventTitle) + '.json');
|
|
176
|
+
const eventScriptExport = Path.join(eventDir, slugify(eventTitle) + '-params.script.sh');
|
|
176
177
|
|
|
177
178
|
await check('setup writes valid XYPDF and external property files', () => {
|
|
178
179
|
const category = readXYPDF(categoryExport);
|
|
@@ -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 => {
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// Build xyCLI CHANGELOG.md file
|
|
4
|
+
|
|
5
|
+
var fs = require('fs');
|
|
6
|
+
var Path = require('path');
|
|
7
|
+
var cp = require('child_process');
|
|
8
|
+
var Tools = require('pixl-tools');
|
|
9
|
+
|
|
10
|
+
var debug = (process.argv[2] == 'debug');
|
|
11
|
+
|
|
12
|
+
// make sure we're in the correct dir
|
|
13
|
+
process.chdir( Path.dirname( __dirname ) );
|
|
14
|
+
|
|
15
|
+
// make sure git sandbox is clean
|
|
16
|
+
var porcelain = cp.execSync('git status --porcelain', { encoding: 'utf8', windowsHide: true }).trim();
|
|
17
|
+
if (!debug && porcelain.length) {
|
|
18
|
+
console.error("\nERROR: Git sandbox has local changes. Please commit these before updating the changelog.\n");
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// get list of tags
|
|
23
|
+
var tags = cp.execSync('git tag --list --sort=version:refname', { encoding: 'utf8', windowsHide: true }).trim().split(/\n/).reverse();
|
|
24
|
+
|
|
25
|
+
var md = '';
|
|
26
|
+
md += "# xyCLI Changelog\n";
|
|
27
|
+
|
|
28
|
+
var last_tag = tags.pop();
|
|
29
|
+
if (!tags.length) {
|
|
30
|
+
console.error("\nERROR: No tags found after popping first one. Please push another tag before running changelog.\n");
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
tags.forEach( function(tag, idx) {
|
|
35
|
+
var prev_tag = tags[idx + 1] || last_tag;
|
|
36
|
+
md += `\n## Version ${tag}\n\n`;
|
|
37
|
+
|
|
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', windowsHide: true }).trim();
|
|
40
|
+
var commits = output.split('----PX----');
|
|
41
|
+
var first = true;
|
|
42
|
+
|
|
43
|
+
// 029a96a 029a96aebd721fe565b1b5c8f2b661564c9017f3 2025-12-30 Version 0.9.2
|
|
44
|
+
commits.forEach( function(chunk) {
|
|
45
|
+
var lines = chunk.trim().split(/\n/);
|
|
46
|
+
var line = lines.shift();
|
|
47
|
+
|
|
48
|
+
var matches = line.trim().match(/^(\w+)\s+(\w+)\s+([\d\-]+)\s+(.+)$/);
|
|
49
|
+
if (!matches) return;
|
|
50
|
+
if (matches[4].match(/\b(CHANGELOG)\b/)) return;
|
|
51
|
+
|
|
52
|
+
var url = 'https://github.com/pixlcore/xycli/commit/' + matches[2];
|
|
53
|
+
|
|
54
|
+
if (first) {
|
|
55
|
+
md += `> ` + Tools.formatDate( matches[3] + ' 00:00:00', '[mmmm] [mday], [yyyy]' ) + "\n\n";
|
|
56
|
+
first = false;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
md += `- [\`${matches[1]}\`](${url}): ` + matches[4] + "\n";
|
|
60
|
+
|
|
61
|
+
if (matches[4].match(/^Version/)) lines.forEach( function(extra) {
|
|
62
|
+
if (!extra.match(/\S/)) return;
|
|
63
|
+
extra = extra.trim();
|
|
64
|
+
if (!extra.match(/^\-/)) extra = '- ' + extra;
|
|
65
|
+
md += "\t" + extra + "\n";
|
|
66
|
+
} );
|
|
67
|
+
} );
|
|
68
|
+
} );
|
|
69
|
+
|
|
70
|
+
md += `\n## Version ${last_tag}\n\n> September 20, 2026\n\n- Initial release!\n`;
|
|
71
|
+
|
|
72
|
+
if (debug) {
|
|
73
|
+
console.log(md);
|
|
74
|
+
process.exit(0);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
fs.writeFileSync( 'CHANGELOG.md', md );
|
|
78
|
+
|
|
79
|
+
// make sure log has actually changed
|
|
80
|
+
porcelain = cp.execSync('git status --porcelain', { encoding: 'utf8', windowsHide: true }).trim();
|
|
81
|
+
if (!porcelain.length) {
|
|
82
|
+
console.error("\nWarning: Changelog has not changed since last run. Skipping actions.\n");
|
|
83
|
+
process.exit(1);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
cp.execSync('git add CHANGELOG.md && git commit -m "Update CHANGELOG" && git push', { stdio: 'inherit', windowsHide: true } );
|