retold-remote 0.0.23 → 0.0.26

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.
Files changed (79) hide show
  1. package/css/retold-remote.css +343 -20
  2. package/docs/.nojekyll +0 -0
  3. package/docs/README.md +64 -12
  4. package/docs/_cover.md +6 -6
  5. package/docs/_sidebar.md +2 -0
  6. package/docs/_topbar.md +1 -1
  7. package/docs/_version.json +7 -0
  8. package/docs/collections.md +30 -0
  9. package/docs/css/docuserve.css +327 -0
  10. package/docs/ebook-reader.md +75 -1
  11. package/docs/image-explorer.md +62 -2
  12. package/docs/index.html +39 -0
  13. package/docs/retold-catalog.json +254 -0
  14. package/docs/retold-keyword-index.json +31216 -0
  15. package/docs/server-setup.md +122 -91
  16. package/docs/stack-launcher.md +218 -0
  17. package/docs/synology.md +585 -0
  18. package/docs/ultravisor-configuration.md +5 -5
  19. package/docs/ultravisor-integration.md +4 -2
  20. package/package.json +20 -14
  21. package/source/Pict-Application-RetoldRemote.js +22 -0
  22. package/source/RetoldRemote-ExtensionMaps.js +1 -1
  23. package/source/cli/RetoldRemote-Server-Setup.js +460 -7
  24. package/source/cli/RetoldRemote-Stack-Launcher.js +563 -0
  25. package/source/cli/RetoldRemote-Stack-Run.js +41 -0
  26. package/source/cli/commands/RetoldRemote-Command-Serve.js +129 -54
  27. package/source/providers/CollectionManager-AddItems.js +166 -0
  28. package/source/providers/Pict-Provider-GalleryNavigation.js +55 -0
  29. package/source/providers/Pict-Provider-OperationStatus.js +597 -0
  30. package/source/providers/keyboard-handlers/KeyHandler-ImageExplorer.js +20 -1
  31. package/source/providers/keyboard-handlers/KeyHandler-Viewer.js +23 -0
  32. package/source/server/RetoldRemote-AudioWaveformService.js +49 -3
  33. package/source/server/RetoldRemote-CollectionExportService.js +763 -0
  34. package/source/server/RetoldRemote-CollectionService.js +5 -0
  35. package/source/server/RetoldRemote-EbookService.js +218 -3
  36. package/source/server/RetoldRemote-ImageService.js +221 -46
  37. package/source/server/RetoldRemote-MediaService.js +63 -4
  38. package/source/server/RetoldRemote-MetadataCache.js +25 -5
  39. package/source/server/RetoldRemote-OperationBroadcaster.js +363 -0
  40. package/source/server/RetoldRemote-SubimageService.js +680 -0
  41. package/source/server/RetoldRemote-ToolDetector.js +50 -0
  42. package/source/server/RetoldRemote-UltravisorBeacon.js +18 -3
  43. package/source/server/RetoldRemote-UltravisorDispatcher.js +65 -491
  44. package/source/server/RetoldRemote-UltravisorOperations.js +133 -20
  45. package/source/server/RetoldRemote-VideoFrameService.js +302 -9
  46. package/source/views/MediaViewer-EbookViewer.js +419 -1
  47. package/source/views/MediaViewer-PdfViewer.js +1050 -0
  48. package/source/views/PictView-Remote-AudioExplorer.js +77 -1
  49. package/source/views/PictView-Remote-CollectionsPanel.js +213 -0
  50. package/source/views/PictView-Remote-Gallery.js +365 -64
  51. package/source/views/PictView-Remote-ImageExplorer.js +1529 -44
  52. package/source/views/PictView-Remote-ImageViewer.js +2 -2
  53. package/source/views/PictView-Remote-Layout.js +58 -0
  54. package/source/views/PictView-Remote-MediaViewer.js +100 -25
  55. package/source/views/PictView-Remote-RegionsBrowser.js +554 -0
  56. package/source/views/PictView-Remote-SubimagesPanel.js +353 -0
  57. package/source/views/PictView-Remote-TopBar.js +1 -0
  58. package/source/views/PictView-Remote-VideoExplorer.js +77 -1
  59. package/web-application/css/docuserve.css +277 -23
  60. package/web-application/css/retold-remote.css +343 -20
  61. package/web-application/docs/README.md +64 -12
  62. package/web-application/docs/_cover.md +6 -6
  63. package/web-application/docs/_sidebar.md +2 -0
  64. package/web-application/docs/_topbar.md +1 -1
  65. package/web-application/docs/collections.md +30 -0
  66. package/web-application/docs/ebook-reader.md +75 -1
  67. package/web-application/docs/image-explorer.md +62 -2
  68. package/web-application/docs/server-setup.md +122 -91
  69. package/web-application/docs/stack-launcher.md +218 -0
  70. package/web-application/docs/synology.md +585 -0
  71. package/web-application/docs/ultravisor-configuration.md +5 -5
  72. package/web-application/docs/ultravisor-integration.md +4 -2
  73. package/web-application/js/pict-docuserve.min.js +12 -12
  74. package/web-application/js/pict.min.js +2 -2
  75. package/web-application/js/pict.min.js.map +1 -1
  76. package/web-application/retold-remote.js +6596 -1784
  77. package/web-application/retold-remote.js.map +1 -1
  78. package/web-application/retold-remote.min.js +75 -23
  79. package/web-application/retold-remote.min.js.map +1 -1
@@ -0,0 +1,563 @@
1
+ /**
2
+ * Retold Remote -- Stack Launcher
3
+ *
4
+ * Spawns the full Retold stack as a unit:
5
+ * - Ultravisor (mesh coordinator) as a child process
6
+ * - Retold Remote (this process) connecting to it as a beacon
7
+ * - Orator-Conversion (embedded inside Retold Remote)
8
+ *
9
+ * Provides XDG-style default data paths so the stack runs sanely
10
+ * from anywhere without configuration.
11
+ *
12
+ * Usage:
13
+ * const libStackLauncher = require('./RetoldRemote-Stack-Launcher');
14
+ * libStackLauncher.start({ Logger: log }, (pError, pStackInfo) => { ... });
15
+ *
16
+ * @license MIT
17
+ */
18
+ const libFs = require('fs');
19
+ const libPath = require('path');
20
+ const libOs = require('os');
21
+ const libHttp = require('http');
22
+ const libChildProcess = require('child_process');
23
+
24
+ /**
25
+ * Resolve XDG-style data paths for the Retold stack.
26
+ *
27
+ * Uses XDG Base Directory Specification environment variables
28
+ * with sensible defaults under the user's home directory.
29
+ *
30
+ * @returns {object} { ConfigDir, DataDir, CacheDir, UltravisorData, UltravisorStaging, UltravisorCache, RetoldCache }
31
+ */
32
+ function resolveStackPaths()
33
+ {
34
+ let tmpHome = libOs.homedir();
35
+ let tmpConfigBase = process.env.XDG_CONFIG_HOME || libPath.join(tmpHome, '.config');
36
+ let tmpDataBase = process.env.XDG_DATA_HOME || libPath.join(tmpHome, '.local', 'share');
37
+ let tmpCacheBase = process.env.XDG_CACHE_HOME || libPath.join(tmpHome, '.cache');
38
+
39
+ return {
40
+ ConfigDir: libPath.join(tmpConfigBase, 'retold-stack'),
41
+ DataDir: libPath.join(tmpDataBase, 'retold-stack'),
42
+ CacheDir: libPath.join(tmpCacheBase, 'retold-stack'),
43
+ UltravisorData: libPath.join(tmpDataBase, 'ultravisor', 'datastore'),
44
+ UltravisorStaging: libPath.join(tmpDataBase, 'ultravisor', 'staging'),
45
+ UltravisorCache: libPath.join(tmpCacheBase, 'ultravisor'),
46
+ RetoldCache: libPath.join(tmpCacheBase, 'retold-remote')
47
+ };
48
+ }
49
+
50
+ /**
51
+ * Ensure a directory exists, creating it (and parents) if necessary.
52
+ *
53
+ * @param {string} pDir - Directory path
54
+ */
55
+ function ensureDir(pDir)
56
+ {
57
+ if (!libFs.existsSync(pDir))
58
+ {
59
+ libFs.mkdirSync(pDir, { recursive: true });
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Find the absolute path to the ultravisor CLI runner script.
65
+ * Resolves through node's module resolution so it works no matter
66
+ * where retold-remote is installed.
67
+ *
68
+ * @returns {string|null} Absolute path or null if not found
69
+ */
70
+ function resolveUltravisorBin()
71
+ {
72
+ try
73
+ {
74
+ // Resolve the package.json so we can read its bin entry
75
+ let tmpPackageJsonPath = require.resolve('ultravisor/package.json');
76
+ let tmpPackageDir = libPath.dirname(tmpPackageJsonPath);
77
+ let tmpPackage = JSON.parse(libFs.readFileSync(tmpPackageJsonPath, 'utf8'));
78
+
79
+ let tmpBinEntry = null;
80
+ if (typeof tmpPackage.bin === 'string')
81
+ {
82
+ tmpBinEntry = tmpPackage.bin;
83
+ }
84
+ else if (tmpPackage.bin && typeof tmpPackage.bin === 'object')
85
+ {
86
+ tmpBinEntry = tmpPackage.bin.ultravisor || Object.values(tmpPackage.bin)[0];
87
+ }
88
+
89
+ if (!tmpBinEntry)
90
+ {
91
+ return null;
92
+ }
93
+
94
+ return libPath.resolve(tmpPackageDir, tmpBinEntry);
95
+ }
96
+ catch (pError)
97
+ {
98
+ return null;
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Check whether a TCP port is accepting connections.
104
+ *
105
+ * @param {number} pPort - Port to test
106
+ * @param {string} pHost - Host to test (default localhost)
107
+ * @param {Function} fCallback - Callback(pIsOpen)
108
+ */
109
+ function checkPortOpen(pPort, pHost, fCallback)
110
+ {
111
+ let tmpRequest = libHttp.get(
112
+ {
113
+ host: pHost || '127.0.0.1',
114
+ port: pPort,
115
+ path: '/',
116
+ timeout: 1000
117
+ },
118
+ (pResponse) =>
119
+ {
120
+ // Any HTTP response means the port is open
121
+ pResponse.resume();
122
+ fCallback(true);
123
+ });
124
+
125
+ tmpRequest.on('error', () => fCallback(false));
126
+ tmpRequest.on('timeout', () =>
127
+ {
128
+ tmpRequest.destroy();
129
+ fCallback(false);
130
+ });
131
+ }
132
+
133
+ /**
134
+ * Wait for ultravisor to be ready by polling its HTTP port.
135
+ *
136
+ * @param {number} pPort - Port to poll
137
+ * @param {number} pTimeoutMs - Total wait timeout in milliseconds
138
+ * @param {Function} fCallback - Callback(pError) — pError null if ready
139
+ */
140
+ function waitForUltravisor(pPort, pTimeoutMs, fCallback)
141
+ {
142
+ let tmpStart = Date.now();
143
+ let tmpAttempts = 0;
144
+
145
+ let _attempt = () =>
146
+ {
147
+ tmpAttempts++;
148
+ checkPortOpen(pPort, '127.0.0.1', (pIsOpen) =>
149
+ {
150
+ if (pIsOpen)
151
+ {
152
+ return fCallback(null, tmpAttempts);
153
+ }
154
+
155
+ if (Date.now() - tmpStart > pTimeoutMs)
156
+ {
157
+ return fCallback(new Error(`Ultravisor did not become ready within ${pTimeoutMs}ms`));
158
+ }
159
+
160
+ setTimeout(_attempt, 500);
161
+ });
162
+ };
163
+
164
+ // Initial delay so we don't poll before the process has started
165
+ setTimeout(_attempt, 750);
166
+ }
167
+
168
+ /**
169
+ * Spawn ultravisor as a child process with sane defaults.
170
+ *
171
+ * Writes a temporary config file with the user-specified data paths,
172
+ * then launches `node ultravisor-bin start -c <config>`.
173
+ *
174
+ * @param {object} pOptions - { Port, DataPath, StagingPath, ConfigDir, Logger }
175
+ * @param {Function} fCallback - Callback(pError, pChildProcess)
176
+ */
177
+ function spawnUltravisor(pOptions, fCallback)
178
+ {
179
+ let tmpLog = pOptions.Logger || console;
180
+ let tmpUltravisorBin = resolveUltravisorBin();
181
+
182
+ if (!tmpUltravisorBin)
183
+ {
184
+ return fCallback(new Error('Could not locate the ultravisor package. Run `npm install ultravisor` in retold-remote.'));
185
+ }
186
+
187
+ if (!libFs.existsSync(tmpUltravisorBin))
188
+ {
189
+ return fCallback(new Error(`Ultravisor binary not found at ${tmpUltravisorBin}`));
190
+ }
191
+
192
+ ensureDir(pOptions.ConfigDir);
193
+ ensureDir(pOptions.DataPath);
194
+ ensureDir(pOptions.StagingPath);
195
+
196
+ // Write a config file pointing at the user-specified paths
197
+ let tmpConfig =
198
+ {
199
+ UltravisorAPIServerPort: pOptions.Port,
200
+ UltravisorFileStorePath: pOptions.DataPath,
201
+ UltravisorStagingRoot: pOptions.StagingPath
202
+ };
203
+
204
+ let tmpConfigPath = libPath.join(pOptions.ConfigDir, 'ultravisor-stack.json');
205
+ libFs.writeFileSync(tmpConfigPath, JSON.stringify(tmpConfig, null, '\t'));
206
+
207
+ tmpLog.info(`[stack] launching ultravisor (port ${pOptions.Port})`);
208
+ tmpLog.info(`[stack] data: ${pOptions.DataPath}`);
209
+ tmpLog.info(`[stack] staging: ${pOptions.StagingPath}`);
210
+
211
+ let tmpChild = libChildProcess.spawn(
212
+ process.execPath,
213
+ [tmpUltravisorBin, 'start', '-c', tmpConfigPath],
214
+ {
215
+ stdio: ['ignore', 'pipe', 'pipe'],
216
+ detached: false
217
+ });
218
+
219
+ // Stream child output with a prefix for clarity
220
+ tmpChild.stdout.on('data', (pChunk) =>
221
+ {
222
+ let tmpLines = pChunk.toString().split('\n');
223
+ for (let i = 0; i < tmpLines.length; i++)
224
+ {
225
+ if (tmpLines[i].length > 0)
226
+ {
227
+ tmpLog.info('[ultravisor] ' + tmpLines[i]);
228
+ }
229
+ }
230
+ });
231
+
232
+ tmpChild.stderr.on('data', (pChunk) =>
233
+ {
234
+ let tmpLines = pChunk.toString().split('\n');
235
+ for (let i = 0; i < tmpLines.length; i++)
236
+ {
237
+ if (tmpLines[i].length > 0)
238
+ {
239
+ tmpLog.warn('[ultravisor] ' + tmpLines[i]);
240
+ }
241
+ }
242
+ });
243
+
244
+ tmpChild.on('exit', (pCode, pSignal) =>
245
+ {
246
+ if (pCode !== 0 && pCode !== null)
247
+ {
248
+ tmpLog.warn(`[stack] ultravisor exited with code ${pCode}`);
249
+ }
250
+ });
251
+
252
+ tmpChild.on('error', (pError) =>
253
+ {
254
+ tmpLog.error(`[stack] ultravisor failed to launch: ${pError.message}`);
255
+ });
256
+
257
+ return fCallback(null, tmpChild);
258
+ }
259
+
260
+ /**
261
+ * Detect emulation (QEMU user-mode, Rosetta-on-Docker-Desktop, etc.)
262
+ * which kills performance for native code (sharp/libvips, ffmpeg,
263
+ * ImageMagick, LibreOffice). Logs a loud warning if detected so users
264
+ * know to rebuild for the right architecture.
265
+ *
266
+ * Detection signals (any one of these triggers the warning):
267
+ * - /proc/cpuinfo contains "qemu" or "VirtualApple" (Docker Desktop on Mac)
268
+ * - /proc/cpuinfo vendor_id is anything other than the expected native vendor
269
+ * - /proc/version mentions an arch different from process.arch
270
+ * - A binfmt_misc qemu handler is registered AND we have a mismatch signal
271
+ * - A short native CPU loop runs at significantly less than expected speed
272
+ *
273
+ * @param {object} pLog - Logger
274
+ */
275
+ function checkQemuEmulation(pLog)
276
+ {
277
+ try
278
+ {
279
+ let tmpNodeArch = process.arch;
280
+ let tmpEmulated = false;
281
+ let tmpReason = '';
282
+ let tmpCpuModel = null;
283
+ let tmpCpuVendor = null;
284
+
285
+ // 1. /proc/cpuinfo — most reliable signal
286
+ if (libFs.existsSync('/proc/cpuinfo'))
287
+ {
288
+ try
289
+ {
290
+ let tmpCpuInfo = libFs.readFileSync('/proc/cpuinfo', 'utf8');
291
+
292
+ // QEMU user-mode emulation often leaves the string "qemu" in cpuinfo
293
+ if (/qemu/i.test(tmpCpuInfo))
294
+ {
295
+ tmpEmulated = true;
296
+ tmpReason = '/proc/cpuinfo contains "qemu"';
297
+ }
298
+
299
+ // Docker Desktop on Apple Silicon emulating x86_64 reports vendor_id
300
+ // as "VirtualApple" instead of "GenuineIntel" or "AuthenticAMD"
301
+ let tmpVendorMatch = tmpCpuInfo.match(/^vendor_id\s*:\s*(.+)$/m);
302
+ if (tmpVendorMatch)
303
+ {
304
+ tmpCpuVendor = tmpVendorMatch[1].trim();
305
+ if (tmpNodeArch === 'x64' && tmpCpuVendor === 'VirtualApple')
306
+ {
307
+ tmpEmulated = true;
308
+ tmpReason = 'x86_64 binary on Apple Silicon (VirtualApple vendor)';
309
+ }
310
+ }
311
+
312
+ // Extract the model line for diagnostics
313
+ let tmpModelMatch = tmpCpuInfo.match(/^model name\s*:\s*(.+)$/m);
314
+ if (tmpModelMatch)
315
+ {
316
+ tmpCpuModel = tmpModelMatch[1].trim();
317
+ if (/qemu/i.test(tmpCpuModel) || /VirtualApple/i.test(tmpCpuModel))
318
+ {
319
+ tmpEmulated = true;
320
+ if (!tmpReason) tmpReason = 'CPU model name indicates emulation';
321
+ }
322
+ }
323
+ }
324
+ catch (pError)
325
+ {
326
+ // ignore
327
+ }
328
+ }
329
+
330
+ // 2. /proc/version — Linux kernel build banner
331
+ if (!tmpEmulated && libFs.existsSync('/proc/version'))
332
+ {
333
+ try
334
+ {
335
+ let tmpProcVersion = libFs.readFileSync('/proc/version', 'utf8');
336
+ let tmpVersionLower = tmpProcVersion.toLowerCase();
337
+ // If node says arm64 but the kernel banner says x86_64 (or vice versa)
338
+ // we're definitely emulated.
339
+ if (tmpNodeArch === 'arm64' && tmpVersionLower.indexOf('x86_64') >= 0)
340
+ {
341
+ tmpEmulated = true;
342
+ tmpReason = 'arm64 binary, x86_64 kernel';
343
+ }
344
+ if (tmpNodeArch === 'x64' && tmpVersionLower.indexOf('aarch64') >= 0)
345
+ {
346
+ tmpEmulated = true;
347
+ tmpReason = 'x86_64 binary, aarch64 kernel';
348
+ }
349
+ }
350
+ catch (pError)
351
+ {
352
+ // ignore
353
+ }
354
+ }
355
+
356
+ // 3. Performance heuristic — a tight CPU loop. Native arm64/amd64 should
357
+ // finish 10M trivial integer ops in <100ms. Under emulation it's 5-20x slower.
358
+ // We only run this if we haven't already decided we're emulated, and we keep
359
+ // it small enough not to slow startup noticeably.
360
+ if (!tmpEmulated)
361
+ {
362
+ let tmpLoopStart = Date.now();
363
+ let tmpAccumulator = 0;
364
+ for (let i = 0; i < 10000000; i++)
365
+ {
366
+ tmpAccumulator += i;
367
+ }
368
+ let tmpLoopMs = Date.now() - tmpLoopStart;
369
+ // Native: typically 30-80ms. Emulated: typically 250-900ms.
370
+ // Use 250ms as the threshold — generous enough to avoid false positives
371
+ // on slow native NAS CPUs but still catches QEMU.
372
+ if (tmpLoopMs > 250)
373
+ {
374
+ tmpEmulated = true;
375
+ tmpReason = 'native CPU loop took ' + tmpLoopMs + 'ms (expected < 250ms — likely emulated)';
376
+ }
377
+ else
378
+ {
379
+ pLog.info('[stack] CPU loop self-test: ' + tmpLoopMs + 'ms (healthy)');
380
+ }
381
+ }
382
+
383
+ if (tmpEmulated)
384
+ {
385
+ pLog.warn('==========================================================');
386
+ pLog.warn(' WARNING: container is running under emulation!');
387
+ pLog.warn('==========================================================');
388
+ pLog.warn(' Reason: ' + tmpReason);
389
+ pLog.warn(' Node arch: ' + tmpNodeArch);
390
+ if (tmpCpuVendor)
391
+ {
392
+ pLog.warn(' CPU vendor: ' + tmpCpuVendor);
393
+ }
394
+ if (tmpCpuModel)
395
+ {
396
+ pLog.warn(' CPU model: ' + tmpCpuModel);
397
+ }
398
+ pLog.warn('');
399
+ pLog.warn(' Emulation is extremely slow for native code:');
400
+ pLog.warn(' - sharp / libvips (image processing)');
401
+ pLog.warn(' - ffmpeg / ffprobe (video and audio)');
402
+ pLog.warn(' - ImageMagick (image fallback)');
403
+ pLog.warn(' - LibreOffice (document conversion)');
404
+ pLog.warn(' - Calibre (ebook conversion)');
405
+ pLog.warn('');
406
+ pLog.warn(' Symptoms you may see:');
407
+ pLog.warn(' - Image previews/thumbnails take many seconds');
408
+ pLog.warn(' - Video frame extraction times out');
409
+ pLog.warn(' - Document conversion fails or hangs');
410
+ pLog.warn(' - Random crashes in native modules');
411
+ pLog.warn('');
412
+ pLog.warn(' FIX: rebuild the image for the host architecture.');
413
+ pLog.warn(' On the build machine:');
414
+ pLog.warn(' ./docker-build-and-save.sh --amd64 # for Intel/AMD hosts');
415
+ pLog.warn(' ./docker-build-and-save.sh --arm64 # for ARM hosts');
416
+ pLog.warn(' Then transfer and load the new tar.gz.');
417
+ pLog.warn('==========================================================');
418
+ }
419
+ else
420
+ {
421
+ pLog.info('[stack] arch: ' + tmpNodeArch + ' (native, no emulation detected)');
422
+ }
423
+ }
424
+ catch (pError)
425
+ {
426
+ // Detection itself failed — not critical, just skip
427
+ pLog.warn('[stack] arch detection failed: ' + pError.message);
428
+ }
429
+ }
430
+
431
+ /**
432
+ * Start the full stack: spawn ultravisor as a child process, wait for
433
+ * it to be ready, then return so the caller can start retold-remote.
434
+ *
435
+ * @param {object} pOptions
436
+ * @param {object} pOptions.Logger - A fable-style logger ({ info, warn, error })
437
+ * @param {number} [pOptions.UltravisorPort=54321] - Port for ultravisor
438
+ * @param {string} [pOptions.DataPath] - Override ultravisor data path
439
+ * @param {string} [pOptions.StagingPath] - Override ultravisor staging path
440
+ * @param {string} [pOptions.ConfigDir] - Override ultravisor config dir
441
+ * @param {Function} fCallback - Callback(pError, pStackInfo)
442
+ * pStackInfo: { UltravisorURL, UltravisorChild, UltravisorPort, Paths }
443
+ */
444
+ function start(pOptions, fCallback)
445
+ {
446
+ let tmpLog = pOptions.Logger || console;
447
+ let tmpPaths = resolveStackPaths();
448
+
449
+ let tmpPort = pOptions.UltravisorPort || 54321;
450
+ let tmpDataPath = pOptions.DataPath || tmpPaths.UltravisorData;
451
+ let tmpStagingPath = pOptions.StagingPath || tmpPaths.UltravisorStaging;
452
+ let tmpConfigDir = pOptions.ConfigDir || tmpPaths.ConfigDir;
453
+
454
+ tmpLog.info('==========================================================');
455
+ tmpLog.info(' Retold Stack Launcher');
456
+ tmpLog.info('==========================================================');
457
+
458
+ // QEMU emulation detection: if /proc/sys/kernel/osrelease contains "linuxkit"
459
+ // or the binfmt entries indicate qemu, native operations will be slow.
460
+ // Cross-architecture binaries running under QEMU show up here too.
461
+ checkQemuEmulation(tmpLog);
462
+
463
+ // Check if ultravisor is already running on the target port
464
+ checkPortOpen(tmpPort, '127.0.0.1', (pAlreadyRunning) =>
465
+ {
466
+ if (pAlreadyRunning)
467
+ {
468
+ tmpLog.info(`[stack] ultravisor already running on port ${tmpPort}, reusing`);
469
+ return fCallback(null,
470
+ {
471
+ UltravisorURL: 'http://localhost:' + tmpPort,
472
+ UltravisorChild: null,
473
+ UltravisorPort: tmpPort,
474
+ Paths: tmpPaths,
475
+ AlreadyRunning: true
476
+ });
477
+ }
478
+
479
+ // Spawn ultravisor as a child process
480
+ spawnUltravisor(
481
+ {
482
+ Port: tmpPort,
483
+ DataPath: tmpDataPath,
484
+ StagingPath: tmpStagingPath,
485
+ ConfigDir: tmpConfigDir,
486
+ Logger: tmpLog
487
+ },
488
+ (pSpawnError, pChild) =>
489
+ {
490
+ if (pSpawnError)
491
+ {
492
+ return fCallback(pSpawnError);
493
+ }
494
+
495
+ // Wait for ultravisor to start accepting connections
496
+ waitForUltravisor(tmpPort, 30000, (pWaitError, pAttempts) =>
497
+ {
498
+ if (pWaitError)
499
+ {
500
+ try { pChild.kill(); } catch (e) { /* ignore */ }
501
+ return fCallback(pWaitError);
502
+ }
503
+
504
+ tmpLog.info(`[stack] ultravisor ready (after ${pAttempts} attempts)`);
505
+
506
+ return fCallback(null,
507
+ {
508
+ UltravisorURL: 'http://localhost:' + tmpPort,
509
+ UltravisorChild: pChild,
510
+ UltravisorPort: tmpPort,
511
+ Paths: tmpPaths,
512
+ AlreadyRunning: false
513
+ });
514
+ });
515
+ });
516
+ });
517
+ }
518
+
519
+ /**
520
+ * Stop the stack — kill the ultravisor child process if we spawned it.
521
+ *
522
+ * @param {object} pStackInfo - The object returned from start()
523
+ * @param {Function} fCallback - Callback() called after shutdown
524
+ */
525
+ function stop(pStackInfo, fCallback)
526
+ {
527
+ if (pStackInfo && pStackInfo.UltravisorChild && !pStackInfo.AlreadyRunning)
528
+ {
529
+ try
530
+ {
531
+ pStackInfo.UltravisorChild.kill('SIGTERM');
532
+ }
533
+ catch (pError)
534
+ {
535
+ // ignore
536
+ }
537
+ // Give it a moment to exit gracefully
538
+ setTimeout(() =>
539
+ {
540
+ try
541
+ {
542
+ pStackInfo.UltravisorChild.kill('SIGKILL');
543
+ }
544
+ catch (pError)
545
+ {
546
+ // ignore — already gone
547
+ }
548
+ if (fCallback) fCallback();
549
+ }, 1000);
550
+ }
551
+ else if (fCallback)
552
+ {
553
+ fCallback();
554
+ }
555
+ }
556
+
557
+ module.exports =
558
+ {
559
+ resolveStackPaths: resolveStackPaths,
560
+ resolveUltravisorBin: resolveUltravisorBin,
561
+ start: start,
562
+ stop: stop
563
+ };
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Retold Stack — convenience entry point.
4
+ *
5
+ * Equivalent to running: retold-remote serve --stack [args...]
6
+ *
7
+ * Spawns Ultravisor as a child process, embeds Orator-Conversion,
8
+ * and starts the Retold Remote media browser pointed at the supplied
9
+ * directory (or the current working directory by default).
10
+ *
11
+ * Data paths default to XDG-style locations:
12
+ * ~/.local/share/ultravisor/ — Ultravisor datastore + staging
13
+ * ~/.cache/retold-remote/ — Retold Remote cache
14
+ * ~/.config/retold-stack/ — Stack config files
15
+ *
16
+ * @license MIT
17
+ */
18
+
19
+ // Inject 'serve --stack' as the first arguments if the user did not
20
+ // already specify a subcommand. This makes `retold-stack /some/path`
21
+ // equivalent to `retold-remote serve --stack /some/path`.
22
+ let tmpArgs = process.argv.slice(2);
23
+
24
+ // Detect whether the user already passed a known subcommand
25
+ let tmpKnownCommands = { 'serve': true };
26
+ let tmpHasSubcommand = tmpArgs.length > 0 && tmpKnownCommands[tmpArgs[0]];
27
+
28
+ if (!tmpHasSubcommand)
29
+ {
30
+ tmpArgs = ['serve', '--stack'].concat(tmpArgs);
31
+ }
32
+ else if (tmpArgs.indexOf('--stack') === -1)
33
+ {
34
+ // User passed `retold-stack serve <path>` — append --stack
35
+ tmpArgs.splice(1, 0, '--stack');
36
+ }
37
+
38
+ process.argv = [process.argv[0], process.argv[1]].concat(tmpArgs);
39
+
40
+ const libRetoldRemoteProgram = require('./RetoldRemote-CLI-Program.js');
41
+ libRetoldRemoteProgram.run();