retold-content-system 1.0.10 → 1.0.12

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "retold-content-system",
3
- "version": "1.0.10",
3
+ "version": "1.0.12",
4
4
  "description": "Retold Content System - Markdown content viewer and editor",
5
5
  "main": "source/Pict-ContentSystem-Bundle.js",
6
6
  "bin": {
@@ -17,7 +17,7 @@
17
17
  "LICENSE"
18
18
  ],
19
19
  "scripts": {
20
- "start": "node source/server/Simple-Server.js",
20
+ "start": "node source/cli/ContentSystem-CLI-Run.js serve",
21
21
  "build": "npx quack build && npx quack copy",
22
22
  "build-codemirror": "node build/build-codemirror-bundle.js",
23
23
  "build-codejar": "node build/build-codejar-bundle.js",
@@ -27,11 +27,14 @@
27
27
  },
28
28
  "author": "steven velozo <steven@velozo.com>",
29
29
  "license": "MIT",
30
+ "optionalDependencies": {
31
+ "ultravisor-beacon": "^0.0.2"
32
+ },
30
33
  "dependencies": {
31
- "fable": "^3.1.65",
34
+ "fable": "^3.1.66",
32
35
  "orator": "^6.0.4",
33
36
  "orator-serviceserver-restify": "^2.0.9",
34
- "pict": "^1.0.357",
37
+ "pict": "^1.0.359",
35
38
  "pict-application": "^1.0.33",
36
39
  "pict-docuserve": "^0.0.32",
37
40
  "pict-provider": "^1.0.12",
@@ -50,7 +53,7 @@
50
53
  "codemirror": "^6.0.2",
51
54
  "esbuild": "^0.27.4",
52
55
  "highlight.js": "^11.11.1",
53
- "quackage": "^1.0.64"
56
+ "quackage": "^1.0.65"
54
57
  },
55
58
  "copyFilesSettings": {
56
59
  "whenFileExists": "overwrite"
@@ -402,6 +402,15 @@ function setupContentSystemServer(pOptions, fCallback)
402
402
  tmpOrator.startService(
403
403
  function ()
404
404
  {
405
+ // --- Optional Ultravisor Beacon Integration ---
406
+ // When pOptions.Beacon is provided and Enabled is true,
407
+ // register this content system as a beacon worker.
408
+ let tmpBeaconConfig = pOptions.Beacon || {};
409
+ if (tmpBeaconConfig.Enabled)
410
+ {
411
+ _initializeBeacon(tmpFable, tmpContentPath, tmpBeaconConfig);
412
+ }
413
+
405
414
  return fCallback(null,
406
415
  {
407
416
  Fable: tmpFable,
@@ -412,4 +421,266 @@ function setupContentSystemServer(pOptions, fCallback)
412
421
  });
413
422
  }
414
423
 
424
+ // ═══════════════════════════════════════════════════════════════════
425
+ // Ultravisor Beacon Integration
426
+ // ═══════════════════════════════════════════════════════════════════
427
+
428
+ /**
429
+ * Initialize the Ultravisor beacon service and register ContentSystem
430
+ * capabilities. This is opt-in — only called when Beacon.Enabled is true.
431
+ *
432
+ * @param {object} pFable - Fable instance
433
+ * @param {string} pContentPath - Resolved content directory path
434
+ * @param {object} pBeaconConfig - Beacon configuration object
435
+ */
436
+ function _initializeBeacon(pFable, pContentPath, pBeaconConfig)
437
+ {
438
+ let libBeaconService;
439
+ try
440
+ {
441
+ libBeaconService = require('ultravisor-beacon');
442
+ }
443
+ catch (pError)
444
+ {
445
+ pFable.log.warn(`Content System Beacon: ultravisor-beacon not installed. Skipping beacon init.`);
446
+ return;
447
+ }
448
+
449
+ pFable.serviceManager.addAndInstantiateServiceType('UltravisorBeacon', libBeaconService, pBeaconConfig);
450
+ let tmpBeacon = pFable.services.UltravisorBeacon;
451
+
452
+ tmpBeacon.registerCapability({
453
+ Capability: 'ContentSystem',
454
+ Name: 'ContentSystemProvider',
455
+ actions:
456
+ {
457
+ 'ReadFile':
458
+ {
459
+ Description: 'Read a content file',
460
+ SettingsSchema: [
461
+ { Name: 'FilePath', DataType: 'String', Required: true }
462
+ ],
463
+ Handler: function (pWorkItem, pContext, fCallback)
464
+ {
465
+ try
466
+ {
467
+ let tmpFilePath = sanitizePath(pWorkItem.Settings.FilePath);
468
+ if (!tmpFilePath)
469
+ {
470
+ return fCallback(null, {
471
+ Outputs: { Content: '', StdOut: 'Invalid file path.' },
472
+ Log: ['ReadFile: invalid or unsafe path rejected.']
473
+ });
474
+ }
475
+
476
+ let tmpFullPath = libPath.join(pContentPath, tmpFilePath);
477
+ let tmpRealContent = libFs.realpathSync(pContentPath);
478
+ if (!tmpFullPath.startsWith(tmpRealContent))
479
+ {
480
+ return fCallback(null, {
481
+ Outputs: { Content: '', StdOut: 'Access denied.' },
482
+ Log: ['ReadFile: path outside content directory.']
483
+ });
484
+ }
485
+
486
+ if (!libFs.existsSync(tmpFullPath))
487
+ {
488
+ return fCallback(null, {
489
+ Outputs: { Content: '', StdOut: 'File not found.' },
490
+ Log: [`ReadFile: ${tmpFilePath} not found.`]
491
+ });
492
+ }
493
+
494
+ let tmpContent = libFs.readFileSync(tmpFullPath, 'utf8');
495
+ pFable.log.info(`Beacon ReadFile: ${tmpFilePath} (${tmpContent.length} bytes)`);
496
+ return fCallback(null, {
497
+ Outputs: { Content: tmpContent, StdOut: `Read ${tmpContent.length} bytes from ${tmpFilePath}` },
498
+ Log: [`ReadFile: read ${tmpContent.length} bytes from ${tmpFilePath}`]
499
+ });
500
+ }
501
+ catch (pError)
502
+ {
503
+ return fCallback(pError);
504
+ }
505
+ }
506
+ },
507
+
508
+ 'SaveFile':
509
+ {
510
+ Description: 'Save content to a file',
511
+ SettingsSchema: [
512
+ { Name: 'FilePath', DataType: 'String', Required: true },
513
+ { Name: 'Content', DataType: 'String', Required: true }
514
+ ],
515
+ Handler: function (pWorkItem, pContext, fCallback)
516
+ {
517
+ try
518
+ {
519
+ let tmpFilePath = sanitizePath(pWorkItem.Settings.FilePath);
520
+ if (!tmpFilePath)
521
+ {
522
+ return fCallback(null, {
523
+ Outputs: { FilePath: '', StdOut: 'Invalid file path.' },
524
+ Log: ['SaveFile: invalid or unsafe path rejected.']
525
+ });
526
+ }
527
+
528
+ let tmpFullPath = libPath.join(pContentPath, tmpFilePath);
529
+ let tmpRealContent = libFs.realpathSync(pContentPath);
530
+ let tmpTargetDir = libPath.dirname(tmpFullPath);
531
+ if (!tmpTargetDir.startsWith(tmpRealContent))
532
+ {
533
+ return fCallback(null, {
534
+ Outputs: { FilePath: '', StdOut: 'Access denied.' },
535
+ Log: ['SaveFile: path outside content directory.']
536
+ });
537
+ }
538
+
539
+ // Ensure directory exists
540
+ if (!libFs.existsSync(tmpTargetDir))
541
+ {
542
+ libFs.mkdirSync(tmpTargetDir, { recursive: true });
543
+ }
544
+
545
+ let tmpContent = pWorkItem.Settings.Content || '';
546
+ libFs.writeFileSync(tmpFullPath, tmpContent, 'utf8');
547
+ pFable.log.info(`Beacon SaveFile: ${tmpFilePath} (${tmpContent.length} bytes)`);
548
+ return fCallback(null, {
549
+ Outputs: { FilePath: tmpFilePath, StdOut: `Saved ${tmpContent.length} bytes to ${tmpFilePath}` },
550
+ Log: [`SaveFile: wrote ${tmpContent.length} bytes to ${tmpFilePath}`]
551
+ });
552
+ }
553
+ catch (pError)
554
+ {
555
+ return fCallback(pError);
556
+ }
557
+ }
558
+ },
559
+
560
+ 'ListFiles':
561
+ {
562
+ Description: 'List files in a content directory',
563
+ SettingsSchema: [
564
+ { Name: 'Path', DataType: 'String', Required: false }
565
+ ],
566
+ Handler: function (pWorkItem, pContext, fCallback)
567
+ {
568
+ try
569
+ {
570
+ let tmpRelPath = pWorkItem.Settings.Path || '';
571
+ let tmpSafePath = tmpRelPath ? sanitizePath(tmpRelPath) : '';
572
+ let tmpTargetPath = tmpSafePath
573
+ ? libPath.join(pContentPath, tmpSafePath)
574
+ : pContentPath;
575
+
576
+ let tmpRealContent = libFs.realpathSync(pContentPath);
577
+ if (!tmpTargetPath.startsWith(tmpRealContent))
578
+ {
579
+ return fCallback(null, {
580
+ Outputs: { Files: '[]', StdOut: 'Access denied.' },
581
+ Log: ['ListFiles: path outside content directory.']
582
+ });
583
+ }
584
+
585
+ if (!libFs.existsSync(tmpTargetPath))
586
+ {
587
+ return fCallback(null, {
588
+ Outputs: { Files: '[]', StdOut: 'Directory not found.' },
589
+ Log: [`ListFiles: ${tmpSafePath || '/'} not found.`]
590
+ });
591
+ }
592
+
593
+ let tmpEntries = libFs.readdirSync(tmpTargetPath, { withFileTypes: true });
594
+ let tmpFiles = tmpEntries.map(function (pEntry)
595
+ {
596
+ return {
597
+ Name: pEntry.name,
598
+ IsDirectory: pEntry.isDirectory()
599
+ };
600
+ });
601
+
602
+ pFable.log.info(`Beacon ListFiles: ${tmpSafePath || '/'} (${tmpFiles.length} entries)`);
603
+ return fCallback(null, {
604
+ Outputs: { Files: JSON.stringify(tmpFiles), StdOut: `Found ${tmpFiles.length} entries in ${tmpSafePath || '/'}` },
605
+ Log: [`ListFiles: ${tmpFiles.length} entries in ${tmpSafePath || '/'}`]
606
+ });
607
+ }
608
+ catch (pError)
609
+ {
610
+ return fCallback(pError);
611
+ }
612
+ }
613
+ },
614
+
615
+ 'CreateFolder':
616
+ {
617
+ Description: 'Create a folder in the content directory',
618
+ SettingsSchema: [
619
+ { Name: 'Path', DataType: 'String', Required: true }
620
+ ],
621
+ Handler: function (pWorkItem, pContext, fCallback)
622
+ {
623
+ try
624
+ {
625
+ let tmpSafePath = sanitizePath(pWorkItem.Settings.Path);
626
+ if (!tmpSafePath)
627
+ {
628
+ return fCallback(null, {
629
+ Outputs: { StdOut: 'Invalid folder path.' },
630
+ Log: ['CreateFolder: invalid or unsafe path rejected.']
631
+ });
632
+ }
633
+
634
+ let tmpFullPath = libPath.join(pContentPath, tmpSafePath);
635
+ let tmpRealContent = libFs.realpathSync(pContentPath);
636
+ let tmpTargetParent = libPath.dirname(tmpFullPath);
637
+ if (libFs.existsSync(tmpTargetParent))
638
+ {
639
+ tmpTargetParent = libFs.realpathSync(tmpTargetParent);
640
+ }
641
+ if (!tmpTargetParent.startsWith(tmpRealContent))
642
+ {
643
+ return fCallback(null, {
644
+ Outputs: { StdOut: 'Access denied.' },
645
+ Log: ['CreateFolder: path outside content directory.']
646
+ });
647
+ }
648
+
649
+ if (libFs.existsSync(tmpFullPath))
650
+ {
651
+ return fCallback(null, {
652
+ Outputs: { StdOut: 'Folder already exists.' },
653
+ Log: [`CreateFolder: ${tmpSafePath} already exists.`]
654
+ });
655
+ }
656
+
657
+ libFs.mkdirSync(tmpFullPath, { recursive: true });
658
+ pFable.log.info(`Beacon CreateFolder: ${tmpSafePath}`);
659
+ return fCallback(null, {
660
+ Outputs: { StdOut: `Created folder ${tmpSafePath}` },
661
+ Log: [`CreateFolder: created ${tmpSafePath}`]
662
+ });
663
+ }
664
+ catch (pError)
665
+ {
666
+ return fCallback(pError);
667
+ }
668
+ }
669
+ }
670
+ }
671
+ });
672
+
673
+ tmpBeacon.enable(function (pError)
674
+ {
675
+ if (pError)
676
+ {
677
+ pFable.log.warn(`Content System Beacon: enable failed: ${pError.message}`);
678
+ }
679
+ else
680
+ {
681
+ pFable.log.info('Content System Beacon: enabled and connected to Ultravisor.');
682
+ }
683
+ });
684
+ }
685
+
415
686
  module.exports = setupContentSystemServer;
@@ -18,6 +18,15 @@ class ContentSystemCommandServe extends libCommandLineCommand
18
18
  this.options.CommandOptions.push(
19
19
  { Name: '-p, --port [port]', Description: 'Port to serve on (defaults to random 7000-7999).', Default: '0' });
20
20
 
21
+ this.options.CommandOptions.push(
22
+ { Name: '-b, --beacon [url]', Description: 'Enable beacon mode and connect to the Ultravisor server at [url] (e.g. http://localhost:54321).' });
23
+
24
+ this.options.CommandOptions.push(
25
+ { Name: '--beacon-name [name]', Description: 'Beacon identity name (defaults to "content-system-1").', Default: 'content-system-1' });
26
+
27
+ this.options.CommandOptions.push(
28
+ { Name: '--beacon-password [password]', Description: 'Beacon authentication password.', Default: '' });
29
+
21
30
  this.addCommand();
22
31
  }
23
32
 
@@ -55,6 +64,17 @@ class ContentSystemCommandServe extends libCommandLineCommand
55
64
  this.log.info(`Created content directory: ${tmpContentPath}`);
56
65
  }
57
66
 
67
+ let tmpBeaconConfig = {};
68
+ if (this.CommandOptions.beacon)
69
+ {
70
+ tmpBeaconConfig = {
71
+ Enabled: true,
72
+ ServerURL: (typeof this.CommandOptions.beacon === 'string') ? this.CommandOptions.beacon : 'http://localhost:54321',
73
+ Name: this.CommandOptions.beaconName || 'content-system-1',
74
+ Password: this.CommandOptions.beaconPassword || ''
75
+ };
76
+ }
77
+
58
78
  let tmpSelf = this;
59
79
  let tmpSetupServer = require('../ContentSystem-Server-Setup.js');
60
80
 
@@ -62,7 +82,8 @@ class ContentSystemCommandServe extends libCommandLineCommand
62
82
  {
63
83
  ContentPath: tmpContentPath,
64
84
  DistPath: tmpDistPath,
65
- Port: tmpPort
85
+ Port: tmpPort,
86
+ Beacon: tmpBeaconConfig
66
87
  },
67
88
  function (pError, pServerInfo)
68
89
  {