retold-remote 0.0.22 → 0.0.25

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 (47) hide show
  1. package/css/retold-remote.css +87 -20
  2. package/docs/README.md +59 -11
  3. package/docs/_sidebar.md +1 -0
  4. package/docs/collections.md +30 -0
  5. package/docs/ebook-reader.md +75 -1
  6. package/docs/image-explorer.md +27 -1
  7. package/docs/server-setup.md +28 -18
  8. package/docs/stack-launcher.md +218 -0
  9. package/docs/ultravisor-integration.md +2 -0
  10. package/package.json +10 -7
  11. package/source/Pict-Application-RetoldRemote.js +2 -0
  12. package/source/RetoldRemote-ExtensionMaps.js +1 -1
  13. package/source/cli/RetoldRemote-Server-Setup.js +240 -2
  14. package/source/cli/RetoldRemote-Stack-Launcher.js +387 -0
  15. package/source/cli/RetoldRemote-Stack-Run.js +41 -0
  16. package/source/cli/commands/RetoldRemote-Command-Serve.js +129 -54
  17. package/source/providers/CollectionManager-AddItems.js +166 -0
  18. package/source/providers/Pict-Provider-GalleryNavigation.js +46 -0
  19. package/source/providers/keyboard-handlers/KeyHandler-ImageExplorer.js +5 -0
  20. package/source/providers/keyboard-handlers/KeyHandler-Viewer.js +23 -0
  21. package/source/server/RetoldRemote-CollectionExportService.js +696 -0
  22. package/source/server/RetoldRemote-CollectionService.js +5 -0
  23. package/source/server/RetoldRemote-EbookService.js +194 -3
  24. package/source/server/RetoldRemote-SubimageService.js +530 -0
  25. package/source/server/RetoldRemote-ToolDetector.js +50 -0
  26. package/source/server/RetoldRemote-UltravisorOperations.js +6 -6
  27. package/source/views/MediaViewer-EbookViewer.js +419 -1
  28. package/source/views/MediaViewer-PdfViewer.js +963 -0
  29. package/source/views/PictView-Remote-CollectionsPanel.js +166 -0
  30. package/source/views/PictView-Remote-ImageExplorer.js +606 -1
  31. package/source/views/PictView-Remote-ImageViewer.js +2 -2
  32. package/source/views/PictView-Remote-Layout.js +12 -0
  33. package/source/views/PictView-Remote-MediaViewer.js +83 -25
  34. package/source/views/PictView-Remote-SubimagesPanel.js +353 -0
  35. package/web-application/css/retold-remote.css +87 -20
  36. package/web-application/docs/README.md +59 -11
  37. package/web-application/docs/_sidebar.md +1 -0
  38. package/web-application/docs/collections.md +30 -0
  39. package/web-application/docs/ebook-reader.md +75 -1
  40. package/web-application/docs/image-explorer.md +27 -1
  41. package/web-application/docs/server-setup.md +28 -18
  42. package/web-application/docs/stack-launcher.md +218 -0
  43. package/web-application/docs/ultravisor-integration.md +2 -0
  44. package/web-application/retold-remote.js +399 -45
  45. package/web-application/retold-remote.js.map +1 -1
  46. package/web-application/retold-remote.min.js +13 -12
  47. package/web-application/retold-remote.min.js.map +1 -1
@@ -0,0 +1,387 @@
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
+ * Start the full stack: spawn ultravisor as a child process, wait for
262
+ * it to be ready, then return so the caller can start retold-remote.
263
+ *
264
+ * @param {object} pOptions
265
+ * @param {object} pOptions.Logger - A fable-style logger ({ info, warn, error })
266
+ * @param {number} [pOptions.UltravisorPort=54321] - Port for ultravisor
267
+ * @param {string} [pOptions.DataPath] - Override ultravisor data path
268
+ * @param {string} [pOptions.StagingPath] - Override ultravisor staging path
269
+ * @param {string} [pOptions.ConfigDir] - Override ultravisor config dir
270
+ * @param {Function} fCallback - Callback(pError, pStackInfo)
271
+ * pStackInfo: { UltravisorURL, UltravisorChild, UltravisorPort, Paths }
272
+ */
273
+ function start(pOptions, fCallback)
274
+ {
275
+ let tmpLog = pOptions.Logger || console;
276
+ let tmpPaths = resolveStackPaths();
277
+
278
+ let tmpPort = pOptions.UltravisorPort || 54321;
279
+ let tmpDataPath = pOptions.DataPath || tmpPaths.UltravisorData;
280
+ let tmpStagingPath = pOptions.StagingPath || tmpPaths.UltravisorStaging;
281
+ let tmpConfigDir = pOptions.ConfigDir || tmpPaths.ConfigDir;
282
+
283
+ tmpLog.info('==========================================================');
284
+ tmpLog.info(' Retold Stack Launcher');
285
+ tmpLog.info('==========================================================');
286
+
287
+ // Check if ultravisor is already running on the target port
288
+ checkPortOpen(tmpPort, '127.0.0.1', (pAlreadyRunning) =>
289
+ {
290
+ if (pAlreadyRunning)
291
+ {
292
+ tmpLog.info(`[stack] ultravisor already running on port ${tmpPort}, reusing`);
293
+ return fCallback(null,
294
+ {
295
+ UltravisorURL: 'http://localhost:' + tmpPort,
296
+ UltravisorChild: null,
297
+ UltravisorPort: tmpPort,
298
+ Paths: tmpPaths,
299
+ AlreadyRunning: true
300
+ });
301
+ }
302
+
303
+ // Spawn ultravisor as a child process
304
+ spawnUltravisor(
305
+ {
306
+ Port: tmpPort,
307
+ DataPath: tmpDataPath,
308
+ StagingPath: tmpStagingPath,
309
+ ConfigDir: tmpConfigDir,
310
+ Logger: tmpLog
311
+ },
312
+ (pSpawnError, pChild) =>
313
+ {
314
+ if (pSpawnError)
315
+ {
316
+ return fCallback(pSpawnError);
317
+ }
318
+
319
+ // Wait for ultravisor to start accepting connections
320
+ waitForUltravisor(tmpPort, 30000, (pWaitError, pAttempts) =>
321
+ {
322
+ if (pWaitError)
323
+ {
324
+ try { pChild.kill(); } catch (e) { /* ignore */ }
325
+ return fCallback(pWaitError);
326
+ }
327
+
328
+ tmpLog.info(`[stack] ultravisor ready (after ${pAttempts} attempts)`);
329
+
330
+ return fCallback(null,
331
+ {
332
+ UltravisorURL: 'http://localhost:' + tmpPort,
333
+ UltravisorChild: pChild,
334
+ UltravisorPort: tmpPort,
335
+ Paths: tmpPaths,
336
+ AlreadyRunning: false
337
+ });
338
+ });
339
+ });
340
+ });
341
+ }
342
+
343
+ /**
344
+ * Stop the stack — kill the ultravisor child process if we spawned it.
345
+ *
346
+ * @param {object} pStackInfo - The object returned from start()
347
+ * @param {Function} fCallback - Callback() called after shutdown
348
+ */
349
+ function stop(pStackInfo, fCallback)
350
+ {
351
+ if (pStackInfo && pStackInfo.UltravisorChild && !pStackInfo.AlreadyRunning)
352
+ {
353
+ try
354
+ {
355
+ pStackInfo.UltravisorChild.kill('SIGTERM');
356
+ }
357
+ catch (pError)
358
+ {
359
+ // ignore
360
+ }
361
+ // Give it a moment to exit gracefully
362
+ setTimeout(() =>
363
+ {
364
+ try
365
+ {
366
+ pStackInfo.UltravisorChild.kill('SIGKILL');
367
+ }
368
+ catch (pError)
369
+ {
370
+ // ignore — already gone
371
+ }
372
+ if (fCallback) fCallback();
373
+ }, 1000);
374
+ }
375
+ else if (fCallback)
376
+ {
377
+ fCallback();
378
+ }
379
+ }
380
+
381
+ module.exports =
382
+ {
383
+ resolveStackPaths: resolveStackPaths,
384
+ resolveUltravisorBin: resolveUltravisorBin,
385
+ start: start,
386
+ stop: stop
387
+ };
@@ -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();
@@ -29,6 +29,9 @@ class RetoldRemoteCommandServe extends libCommandLineCommand
29
29
  this.options.CommandOptions.push(
30
30
  { Name: '-u, --ultravisor [url]', Description: 'Connect to Ultravisor mesh. URL defaults to http://localhost:54321 if omitted.', Default: '' });
31
31
 
32
+ this.options.CommandOptions.push(
33
+ { Name: '--stack', Description: 'Run the full stack: spawn Ultravisor as a child process and connect to it. Uses XDG-style data paths under ~/.local/share and ~/.cache.', Default: false });
34
+
32
35
  this.options.CommandOptions.push(
33
36
  { Name: '-l, --logfile [path]', Description: 'Write logs to a file (auto-generates timestamped name if path omitted).', Default: '' });
34
37
 
@@ -78,14 +81,26 @@ class RetoldRemoteCommandServe extends libCommandLineCommand
78
81
  }
79
82
 
80
83
  let tmpSelf = this;
81
- let tmpSetupServer = require('../RetoldRemote-Server-Setup.js');
82
-
83
84
  let tmpHashedFilenames = !(this.CommandOptions.noHash);
85
+ let tmpStackMode = !!this.CommandOptions.stack;
86
+
87
+ // Resolve XDG-style stack paths once (used by --stack mode)
88
+ let libStackLauncher = require('../RetoldRemote-Stack-Launcher.js');
89
+ let tmpStackPaths = libStackLauncher.resolveStackPaths();
90
+
91
+ // Cache root: explicit > stack default > package default
92
+ let tmpCacheRoot = null;
93
+ if (this.CommandOptions.cachePath)
94
+ {
95
+ tmpCacheRoot = libPath.resolve(this.CommandOptions.cachePath);
96
+ }
97
+ else if (tmpStackMode)
98
+ {
99
+ tmpCacheRoot = tmpStackPaths.RetoldCache;
100
+ }
84
101
 
85
- let tmpCacheRoot = this.CommandOptions.cachePath
86
- ? libPath.resolve(this.CommandOptions.cachePath)
87
- : null;
88
102
  let tmpCacheServer = this.CommandOptions.cacheServer || null;
103
+
89
104
  // -u with no URL → true (Commander behavior for [optional]), default to localhost
90
105
  let tmpUltravisorOpt = this.CommandOptions.ultravisor;
91
106
  let tmpUltravisorURL = null;
@@ -98,65 +113,125 @@ class RetoldRemoteCommandServe extends libCommandLineCommand
98
113
  tmpUltravisorURL = tmpUltravisorOpt;
99
114
  }
100
115
 
101
- tmpSetupServer(
102
- {
103
- ContentPath: tmpContentPath,
104
- DistPath: tmpDistPath,
105
- Port: tmpPort,
106
- HashedFilenames: tmpHashedFilenames,
107
- CacheRoot: tmpCacheRoot,
108
- CacheServer: tmpCacheServer,
109
- UltravisorURL: tmpUltravisorURL
110
- },
111
- function (pError, pServerInfo)
112
- {
113
- if (pError)
114
- {
115
- tmpSelf.log.error(`Failed to start server: ${pError.message}`);
116
- return fCallback(pError);
117
- }
118
-
119
- tmpSelf.log.info('');
120
- tmpSelf.log.info('==========================================================');
121
- tmpSelf.log.info(` Retold Remote running on http://localhost:${pServerInfo.Port}`);
122
- tmpSelf.log.info('==========================================================');
123
- tmpSelf.log.info(` Content: ${tmpContentPath}`);
124
- tmpSelf.log.info(` Assets: ${tmpDistPath}`);
125
- tmpSelf.log.info(` Browse: http://localhost:${pServerInfo.Port}/`);
126
- if (pServerInfo.UltravisorBeacon && pServerInfo.UltravisorBeacon.isEnabled())
127
- {
128
- tmpSelf.log.info(` Beacon: registered with Ultravisor at ${tmpUltravisorURL}`);
129
- }
130
- else if (tmpUltravisorURL)
116
+ // In stack mode, automatically set the ultravisor URL
117
+ if (tmpStackMode && !tmpUltravisorURL)
118
+ {
119
+ tmpUltravisorURL = 'http://localhost:54321';
120
+ }
121
+
122
+ // Hold the stack info so we can clean up on exit
123
+ let tmpStackInfo = null;
124
+
125
+ // Bind the actual server startup so we can call it after (optionally) launching ultravisor
126
+ let _startRetoldRemote = () =>
127
+ {
128
+ let tmpSetupServer = require('../RetoldRemote-Server-Setup.js');
129
+
130
+ tmpSetupServer(
131
131
  {
132
- tmpSelf.log.info(` Beacon: not connected (Ultravisor may be unreachable)`);
133
- }
134
- tmpSelf.log.info('==========================================================');
135
- tmpSelf.log.info('');
136
- tmpSelf.log.info(' Press Ctrl+C to stop.');
137
- tmpSelf.log.info('');
138
-
139
- // Graceful shutdown: disconnect beacon before exit
140
- process.on('SIGINT', () =>
132
+ ContentPath: tmpContentPath,
133
+ DistPath: tmpDistPath,
134
+ Port: tmpPort,
135
+ HashedFilenames: tmpHashedFilenames,
136
+ CacheRoot: tmpCacheRoot,
137
+ CacheServer: tmpCacheServer,
138
+ UltravisorURL: tmpUltravisorURL
139
+ },
140
+ function (pError, pServerInfo)
141
141
  {
142
+ if (pError)
143
+ {
144
+ tmpSelf.log.error(`Failed to start server: ${pError.message}`);
145
+ if (tmpStackInfo)
146
+ {
147
+ libStackLauncher.stop(tmpStackInfo, () => {});
148
+ }
149
+ return fCallback(pError);
150
+ }
151
+
142
152
  tmpSelf.log.info('');
143
- tmpSelf.log.info('Shutting down...');
153
+ tmpSelf.log.info('==========================================================');
154
+ tmpSelf.log.info(` Retold Remote running on http://localhost:${pServerInfo.Port}`);
155
+ tmpSelf.log.info('==========================================================');
156
+ tmpSelf.log.info(` Content: ${tmpContentPath}`);
157
+ tmpSelf.log.info(` Cache: ${tmpCacheRoot || '(default)'}`);
158
+ tmpSelf.log.info(` Browse: http://localhost:${pServerInfo.Port}/`);
144
159
  if (pServerInfo.UltravisorBeacon && pServerInfo.UltravisorBeacon.isEnabled())
145
160
  {
146
- pServerInfo.UltravisorBeacon.disconnectBeacon(() =>
147
- {
148
- process.exit(0);
149
- });
161
+ tmpSelf.log.info(` Beacon: registered with Ultravisor at ${tmpUltravisorURL}`);
162
+ }
163
+ else if (tmpUltravisorURL)
164
+ {
165
+ tmpSelf.log.info(` Beacon: not connected (Ultravisor may be unreachable)`);
150
166
  }
151
- else
167
+ if (tmpStackMode)
152
168
  {
153
- process.exit(0);
169
+ tmpSelf.log.info(` Stack: ultravisor + retold-remote (orator-conversion embedded)`);
154
170
  }
171
+ tmpSelf.log.info('==========================================================');
172
+ tmpSelf.log.info('');
173
+ tmpSelf.log.info(' Press Ctrl+C to stop.');
174
+ tmpSelf.log.info('');
175
+
176
+ // Graceful shutdown: disconnect beacon and stop child processes before exit
177
+ let _shutdown = () =>
178
+ {
179
+ tmpSelf.log.info('');
180
+ tmpSelf.log.info('Shutting down...');
181
+
182
+ let _finish = () =>
183
+ {
184
+ if (tmpStackInfo)
185
+ {
186
+ libStackLauncher.stop(tmpStackInfo, () => process.exit(0));
187
+ }
188
+ else
189
+ {
190
+ process.exit(0);
191
+ }
192
+ };
193
+
194
+ if (pServerInfo.UltravisorBeacon && pServerInfo.UltravisorBeacon.isEnabled())
195
+ {
196
+ pServerInfo.UltravisorBeacon.disconnectBeacon(_finish);
197
+ }
198
+ else
199
+ {
200
+ _finish();
201
+ }
202
+ };
203
+
204
+ process.on('SIGINT', _shutdown);
205
+ process.on('SIGTERM', _shutdown);
206
+
207
+ // Intentionally do NOT call fCallback() here.
208
+ // The server should keep running.
155
209
  });
210
+ };
156
211
 
157
- // Intentionally do NOT call fCallback() here.
158
- // The server should keep running.
159
- });
212
+ // In stack mode, launch ultravisor first, then start retold-remote
213
+ if (tmpStackMode)
214
+ {
215
+ libStackLauncher.start(
216
+ {
217
+ Logger: tmpSelf.log,
218
+ UltravisorPort: 54321
219
+ },
220
+ (pStackError, pStackInfo) =>
221
+ {
222
+ if (pStackError)
223
+ {
224
+ tmpSelf.log.error(`Stack launch failed: ${pStackError.message}`);
225
+ return fCallback(pStackError);
226
+ }
227
+ tmpStackInfo = pStackInfo;
228
+ _startRetoldRemote();
229
+ });
230
+ }
231
+ else
232
+ {
233
+ _startRetoldRemote();
234
+ }
160
235
  }
161
236
  }
162
237