retold 4.0.3 → 4.0.4

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 (32) hide show
  1. package/.claude/settings.local.json +26 -1
  2. package/README.md +20 -0
  3. package/docs/_sidebar.md +2 -1
  4. package/docs/architecture/architecture.md +2 -2
  5. package/docs/architecture/modules.md +3 -4
  6. package/docs/contributing.md +50 -0
  7. package/docs/modules/orator.md +0 -7
  8. package/docs/retold-catalog.json +110 -26
  9. package/docs/retold-keyword-index.json +15118 -15139
  10. package/docs/testing.md +122 -0
  11. package/modules/Include-Retold-Module-List.sh +1 -1
  12. package/package.json +7 -4
  13. package/source/retold-manager/package.json +23 -0
  14. package/source/retold-manager/retold-manager.js +65 -0
  15. package/source/retold-manager/source/Retold-Manager-App.js +1532 -0
  16. package/source/retold-manager/source/Retold-Manager-ModuleCatalog.js +75 -0
  17. package/source/retold-manager/source/Retold-Manager-ProcessRunner.js +706 -0
  18. package/source/retold-manager/source/views/PictView-TUI-Checkout.js +45 -0
  19. package/source/retold-manager/source/views/PictView-TUI-Header.js +41 -0
  20. package/source/retold-manager/source/views/PictView-TUI-Layout.js +53 -0
  21. package/source/retold-manager/source/views/PictView-TUI-Status.js +45 -0
  22. package/source/retold-manager/source/views/PictView-TUI-StatusBar.js +41 -0
  23. package/source/retold-manager/source/views/PictView-TUI-Update.js +45 -0
  24. package/examples/quickstart/layer1/package-lock.json +0 -344
  25. package/examples/quickstart/layer2/package-lock.json +0 -4468
  26. package/examples/quickstart/layer3/package-lock.json +0 -1936
  27. package/examples/quickstart/layer4/package-lock.json +0 -13206
  28. package/examples/quickstart/layer5/package-lock.json +0 -345
  29. package/examples/todo-list/cli-client/package-lock.json +0 -418
  30. package/examples/todo-list/console-client/package-lock.json +0 -426
  31. package/examples/todo-list/server/package-lock.json +0 -6113
  32. package/examples/todo-list/web-client/package-lock.json +0 -12030
@@ -0,0 +1,1532 @@
1
+ /**
2
+ * Retold Manager -- Main Application
3
+ *
4
+ * A pict-terminalui application for managing the retold module suite.
5
+ * Provides a file browser for navigating module groups and a terminal
6
+ * output area for running npm/git operations.
7
+ */
8
+
9
+ const blessed = require('blessed');
10
+ const libChildProcess = require('child_process');
11
+ const libFs = require('fs');
12
+ const libPath = require('path');
13
+
14
+ const libPictApplication = require('pict-application');
15
+ const libPictTerminalUI = require('pict-terminalui');
16
+
17
+ const libModuleCatalog = require('./Retold-Manager-ModuleCatalog.js');
18
+ const libProcessRunner = require('./Retold-Manager-ProcessRunner.js');
19
+
20
+ // Views
21
+ const libViewLayout = require('./views/PictView-TUI-Layout.js');
22
+ const libViewHeader = require('./views/PictView-TUI-Header.js');
23
+ const libViewStatusBar = require('./views/PictView-TUI-StatusBar.js');
24
+ const libViewStatus = require('./views/PictView-TUI-Status.js');
25
+ const libViewUpdate = require('./views/PictView-TUI-Update.js');
26
+ const libViewCheckout = require('./views/PictView-TUI-Checkout.js');
27
+
28
+ // Maximum lines to display when viewing a file
29
+ const FILE_VIEW_LINE_LIMIT = 500;
30
+
31
+ class RetoldManagerApp extends libPictApplication
32
+ {
33
+ constructor(pFable, pOptions, pServiceHash)
34
+ {
35
+ super(pFable, pOptions, pServiceHash);
36
+
37
+ this.terminalUI = null;
38
+ this.processRunner = null;
39
+
40
+ // Blessed widget references for direct manipulation
41
+ this._fileBrowser = null;
42
+ this._terminalOutput = null;
43
+ this._screen = null;
44
+
45
+ // Register views
46
+ this.pict.addView('TUI-Layout', libViewLayout.default_configuration, libViewLayout);
47
+ this.pict.addView('TUI-Header', libViewHeader.default_configuration, libViewHeader);
48
+ this.pict.addView('TUI-StatusBar', libViewStatusBar.default_configuration, libViewStatusBar);
49
+ this.pict.addView('TUI-Status', libViewStatus.default_configuration, libViewStatus);
50
+ this.pict.addView('TUI-Update', libViewUpdate.default_configuration, libViewUpdate);
51
+ this.pict.addView('TUI-Checkout', libViewCheckout.default_configuration, libViewCheckout);
52
+ }
53
+
54
+ onAfterInitializeAsync(fCallback)
55
+ {
56
+ // Initialize shared application state
57
+ this.pict.AppData.Manager =
58
+ {
59
+ AppName: 'Retold Manager',
60
+ AppVersion: '0.0.1',
61
+ StatusMessage: 'Ready',
62
+
63
+ Browser:
64
+ {
65
+ Level: 'groups',
66
+ GroupIndex: -1,
67
+ GroupName: '',
68
+ GroupLabel: '',
69
+ ModuleName: '',
70
+ ModulePath: '',
71
+ SubPath: '',
72
+ CurrentPath: 'retold/modules/',
73
+ },
74
+ };
75
+
76
+ // Create the terminal UI environment
77
+ this.terminalUI = new libPictTerminalUI(this.pict,
78
+ {
79
+ Title: 'Retold Manager'
80
+ });
81
+
82
+ // Create the blessed screen
83
+ let tmpScreen = this.terminalUI.createScreen();
84
+
85
+ // Build the blessed widget layout
86
+ this._createBlessedLayout(tmpScreen);
87
+
88
+ // Create the process runner (pass pict.log for activity logging)
89
+ this.processRunner = new libProcessRunner(
90
+ this._terminalOutput,
91
+ this._screen,
92
+ (pState, pMessage) =>
93
+ {
94
+ this._updateStatus(pMessage);
95
+ },
96
+ this.pict.log);
97
+
98
+ // File logging state -- the stream reference when active, null when off
99
+ this._fileLogStream = null;
100
+
101
+ // When true, a Y/N confirmation prompt is active -- suppress other key handlers
102
+ this._awaitingConfirmation = false;
103
+
104
+ // Bind navigation keys
105
+ this._bindNavigation(tmpScreen);
106
+
107
+ // Enable file logging by default
108
+ this._toggleFileLogging();
109
+
110
+ // Populate the initial file list (groups)
111
+ this._populateFileList();
112
+
113
+ // Render the layout view (triggers Header + StatusBar)
114
+ this.pict.views['TUI-Layout'].render();
115
+
116
+ // Do the initial blessed screen render
117
+ tmpScreen.render();
118
+
119
+ // Focus the file browser
120
+ this._fileBrowser.focus();
121
+
122
+ return super.onAfterInitializeAsync(fCallback);
123
+ }
124
+
125
+ // ─────────────────────────────────────────────
126
+ // Widget Layout
127
+ // ─────────────────────────────────────────────
128
+
129
+ _createBlessedLayout(pScreen)
130
+ {
131
+ this._screen = pScreen;
132
+
133
+ // Application container
134
+ let tmpAppContainer = blessed.box(
135
+ {
136
+ parent: pScreen,
137
+ top: 0,
138
+ left: 0,
139
+ width: '100%',
140
+ height: '100%',
141
+ });
142
+ this.terminalUI.registerWidget('#TUI-Application-Container', tmpAppContainer);
143
+
144
+ // Header bar -- 2 lines high
145
+ let tmpHeader = blessed.box(
146
+ {
147
+ parent: pScreen,
148
+ top: 0,
149
+ left: 0,
150
+ width: '100%',
151
+ height: 2,
152
+ tags: true,
153
+ style:
154
+ {
155
+ fg: 'white',
156
+ bg: 'blue',
157
+ bold: true,
158
+ },
159
+ });
160
+ this.terminalUI.registerWidget('#TUI-Header', tmpHeader);
161
+
162
+ // File browser list -- left side, max 40 chars wide
163
+ this._fileBrowser = blessed.list(
164
+ {
165
+ parent: pScreen,
166
+ top: 2,
167
+ left: 0,
168
+ width: 40,
169
+ bottom: 1,
170
+ items: [],
171
+ keys: true,
172
+ vi: true,
173
+ mouse: true,
174
+ border: { type: 'line' },
175
+ label: ' Module Groups ',
176
+ scrollbar:
177
+ {
178
+ style: { bg: 'blue' },
179
+ },
180
+ style:
181
+ {
182
+ fg: 'white',
183
+ bg: 'black',
184
+ selected: { fg: 'black', bg: 'cyan', bold: true },
185
+ item: { fg: 'white' },
186
+ border: { fg: 'cyan' },
187
+ label: { fg: 'cyan', bold: true },
188
+ },
189
+ });
190
+ this.terminalUI.registerWidget('#TUI-FileBrowser', this._fileBrowser);
191
+
192
+ // Terminal output log -- right side
193
+ this._terminalOutput = blessed.log(
194
+ {
195
+ parent: pScreen,
196
+ top: 2,
197
+ left: 40,
198
+ right: 0,
199
+ bottom: 1,
200
+ label: ' Terminal Output ',
201
+ border: { type: 'line' },
202
+ tags: true,
203
+ scrollable: true,
204
+ scrollOnInput: true,
205
+ mouse: true,
206
+ scrollback: 2000,
207
+ scrollbar:
208
+ {
209
+ style: { bg: 'green' },
210
+ },
211
+ style:
212
+ {
213
+ fg: 'white',
214
+ bg: 'black',
215
+ border: { fg: 'green' },
216
+ label: { fg: 'green', bold: true },
217
+ },
218
+ });
219
+ this.terminalUI.registerWidget('#TUI-TerminalOutput', this._terminalOutput);
220
+
221
+ // Status bar -- bottom 1 line
222
+ let tmpStatusBar = blessed.box(
223
+ {
224
+ parent: pScreen,
225
+ bottom: 0,
226
+ left: 0,
227
+ width: '100%',
228
+ height: 1,
229
+ tags: true,
230
+ style:
231
+ {
232
+ fg: 'white',
233
+ bg: 'gray',
234
+ },
235
+ });
236
+ this.terminalUI.registerWidget('#TUI-StatusBar', tmpStatusBar);
237
+ }
238
+
239
+ // ─────────────────────────────────────────────
240
+ // File Browser Navigation
241
+ // ─────────────────────────────────────────────
242
+
243
+ _populateFileList()
244
+ {
245
+ let tmpBrowser = this.pict.AppData.Manager.Browser;
246
+ let tmpItems = [];
247
+
248
+ switch (tmpBrowser.Level)
249
+ {
250
+ case 'groups':
251
+ {
252
+ for (let i = 0; i < libModuleCatalog.Groups.length; i++)
253
+ {
254
+ let tmpGroup = libModuleCatalog.Groups[i];
255
+ tmpItems.push(` ${tmpGroup.Label}/ (${tmpGroup.Modules.length} modules) -- ${tmpGroup.Description}`);
256
+ }
257
+ this._fileBrowser.setLabel(' Module Groups ');
258
+ tmpBrowser.CurrentPath = 'retold/modules/';
259
+ break;
260
+ }
261
+
262
+ case 'modules':
263
+ {
264
+ let tmpGroupPath = libPath.join(libModuleCatalog.BasePath, tmpBrowser.GroupName);
265
+ let tmpEntries = [];
266
+
267
+ try
268
+ {
269
+ let tmpRawEntries = libFs.readdirSync(tmpGroupPath);
270
+ for (let i = 0; i < tmpRawEntries.length; i++)
271
+ {
272
+ let tmpEntryPath = libPath.join(tmpGroupPath, tmpRawEntries[i]);
273
+ try
274
+ {
275
+ let tmpStat = libFs.statSync(tmpEntryPath);
276
+ if (tmpStat.isDirectory() && !tmpRawEntries[i].startsWith('.'))
277
+ {
278
+ tmpEntries.push(tmpRawEntries[i]);
279
+ }
280
+ }
281
+ catch (pError)
282
+ {
283
+ // Skip entries we can't stat
284
+ }
285
+ }
286
+ }
287
+ catch (pError)
288
+ {
289
+ this._terminalOutput.log(`{red-fg}Error reading directory: ${pError.message}{/red-fg}`);
290
+ }
291
+
292
+ tmpEntries.sort();
293
+
294
+ tmpItems.push(' ../');
295
+ for (let i = 0; i < tmpEntries.length; i++)
296
+ {
297
+ tmpItems.push(` ${tmpEntries[i]}/`);
298
+ }
299
+
300
+ this._fileBrowser.setLabel(` ${tmpBrowser.GroupName}/ `);
301
+ tmpBrowser.CurrentPath = `retold/modules/${tmpBrowser.GroupName}/`;
302
+ break;
303
+ }
304
+
305
+ case 'files':
306
+ {
307
+ let tmpBrowsePath = tmpBrowser.SubPath
308
+ ? libPath.join(tmpBrowser.ModulePath, tmpBrowser.SubPath)
309
+ : tmpBrowser.ModulePath;
310
+ let tmpEntries = [];
311
+
312
+ try
313
+ {
314
+ let tmpRawEntries = libFs.readdirSync(tmpBrowsePath);
315
+ for (let i = 0; i < tmpRawEntries.length; i++)
316
+ {
317
+ if (tmpRawEntries[i].startsWith('.') && tmpRawEntries[i] !== '.gitignore')
318
+ {
319
+ continue;
320
+ }
321
+ if (tmpRawEntries[i] === 'node_modules')
322
+ {
323
+ continue;
324
+ }
325
+
326
+ let tmpEntryPath = libPath.join(tmpBrowsePath, tmpRawEntries[i]);
327
+ try
328
+ {
329
+ let tmpStat = libFs.statSync(tmpEntryPath);
330
+ if (tmpStat.isDirectory())
331
+ {
332
+ tmpEntries.push(tmpRawEntries[i] + '/');
333
+ }
334
+ else
335
+ {
336
+ tmpEntries.push(tmpRawEntries[i]);
337
+ }
338
+ }
339
+ catch (pError)
340
+ {
341
+ tmpEntries.push(tmpRawEntries[i]);
342
+ }
343
+ }
344
+ }
345
+ catch (pError)
346
+ {
347
+ this._terminalOutput.log(`{red-fg}Error reading directory: ${pError.message}{/red-fg}`);
348
+ }
349
+
350
+ tmpEntries.sort((a, b) =>
351
+ {
352
+ // Directories first, then files
353
+ let tmpAIsDir = a.endsWith('/');
354
+ let tmpBIsDir = b.endsWith('/');
355
+ if (tmpAIsDir && !tmpBIsDir) return -1;
356
+ if (!tmpAIsDir && tmpBIsDir) return 1;
357
+ return a.localeCompare(b);
358
+ });
359
+
360
+ tmpItems.push(' ../');
361
+ for (let i = 0; i < tmpEntries.length; i++)
362
+ {
363
+ tmpItems.push(` ${tmpEntries[i]}`);
364
+ }
365
+
366
+ let tmpLabelSuffix = tmpBrowser.SubPath ? `${tmpBrowser.SubPath}/` : '';
367
+ this._fileBrowser.setLabel(` ${tmpBrowser.ModuleName}/${tmpLabelSuffix} `);
368
+ tmpBrowser.CurrentPath = tmpBrowser.SubPath
369
+ ? `retold/modules/${tmpBrowser.GroupName}/${tmpBrowser.ModuleName}/${tmpBrowser.SubPath}/`
370
+ : `retold/modules/${tmpBrowser.GroupName}/${tmpBrowser.ModuleName}/`;
371
+ break;
372
+ }
373
+ }
374
+
375
+ this._fileBrowser.setItems(tmpItems);
376
+ this._fileBrowser.select(0);
377
+
378
+ this._updateHeader();
379
+ this._updateStatus(this.pict.AppData.Manager.StatusMessage);
380
+ this._screen.render();
381
+ }
382
+
383
+ _drillIn(pIndex)
384
+ {
385
+ let tmpBrowser = this.pict.AppData.Manager.Browser;
386
+
387
+ switch (tmpBrowser.Level)
388
+ {
389
+ case 'groups':
390
+ {
391
+ if (pIndex < 0 || pIndex >= libModuleCatalog.Groups.length)
392
+ {
393
+ return;
394
+ }
395
+ let tmpGroup = libModuleCatalog.Groups[pIndex];
396
+ tmpBrowser.GroupIndex = pIndex;
397
+ tmpBrowser.GroupName = tmpGroup.Name;
398
+ tmpBrowser.GroupLabel = tmpGroup.Label;
399
+ tmpBrowser.Level = 'modules';
400
+ this._populateFileList();
401
+ break;
402
+ }
403
+
404
+ case 'modules':
405
+ {
406
+ // Index 0 is '../'
407
+ if (pIndex === 0)
408
+ {
409
+ this._drillOut();
410
+ return;
411
+ }
412
+
413
+ // Get the module name from the list item text
414
+ let tmpItemText = this._fileBrowser.getItem(pIndex).getText().trim();
415
+ let tmpModuleName = tmpItemText.replace(/\/$/, '');
416
+
417
+ tmpBrowser.ModuleName = tmpModuleName;
418
+ tmpBrowser.ModulePath = libPath.join(libModuleCatalog.BasePath, tmpBrowser.GroupName, tmpModuleName);
419
+ tmpBrowser.SubPath = '';
420
+ tmpBrowser.Level = 'files';
421
+
422
+ this._populateFileList();
423
+ this._showModuleWelcome();
424
+ break;
425
+ }
426
+
427
+ case 'files':
428
+ {
429
+ // Index 0 is '../'
430
+ if (pIndex === 0)
431
+ {
432
+ this._drillOut();
433
+ return;
434
+ }
435
+
436
+ let tmpItemText = this._fileBrowser.getItem(pIndex).getText().trim();
437
+ let tmpBrowsePath = tmpBrowser.SubPath
438
+ ? libPath.join(tmpBrowser.ModulePath, tmpBrowser.SubPath)
439
+ : tmpBrowser.ModulePath;
440
+
441
+ if (tmpItemText.endsWith('/'))
442
+ {
443
+ // It's a subdirectory -- navigate into it
444
+ let tmpSubdir = tmpItemText.replace(/\/$/, '');
445
+ tmpBrowser.SubPath = tmpBrowser.SubPath
446
+ ? libPath.join(tmpBrowser.SubPath, tmpSubdir)
447
+ : tmpSubdir;
448
+ this._populateFileList();
449
+ }
450
+ else
451
+ {
452
+ // It's a file -- show its contents
453
+ let tmpFilePath = libPath.join(tmpBrowsePath, tmpItemText);
454
+ this._showFileContents(tmpFilePath, tmpItemText);
455
+ }
456
+ break;
457
+ }
458
+ }
459
+ }
460
+
461
+ _drillOut()
462
+ {
463
+ let tmpBrowser = this.pict.AppData.Manager.Browser;
464
+
465
+ switch (tmpBrowser.Level)
466
+ {
467
+ case 'files':
468
+ {
469
+ if (tmpBrowser.SubPath)
470
+ {
471
+ // We're in a subfolder -- go up one level within the module
472
+ let tmpParent = libPath.dirname(tmpBrowser.SubPath);
473
+ tmpBrowser.SubPath = (tmpParent === '.') ? '' : tmpParent;
474
+ this._populateFileList();
475
+ }
476
+ else
477
+ {
478
+ // We're at the module root -- go back to module list
479
+ tmpBrowser.ModuleName = '';
480
+ tmpBrowser.ModulePath = '';
481
+ tmpBrowser.SubPath = '';
482
+ tmpBrowser.Level = 'modules';
483
+ this._populateFileList();
484
+ }
485
+ break;
486
+ }
487
+
488
+ case 'modules':
489
+ {
490
+ tmpBrowser.GroupIndex = -1;
491
+ tmpBrowser.GroupName = '';
492
+ tmpBrowser.GroupLabel = '';
493
+ tmpBrowser.Level = 'groups';
494
+ this._populateFileList();
495
+ break;
496
+ }
497
+
498
+ case 'groups':
499
+ {
500
+ // Already at top -- no-op
501
+ break;
502
+ }
503
+ }
504
+ }
505
+
506
+ // ─────────────────────────────────────────────
507
+ // Module Operations
508
+ // ─────────────────────────────────────────────
509
+
510
+ _getModulePath()
511
+ {
512
+ let tmpBrowser = this.pict.AppData.Manager.Browser;
513
+
514
+ // If we're inside a module's files, use its path
515
+ if (tmpBrowser.Level === 'files' && tmpBrowser.ModulePath)
516
+ {
517
+ return tmpBrowser.ModulePath;
518
+ }
519
+
520
+ // If we're at the module list level, use the highlighted module
521
+ if (tmpBrowser.Level === 'modules')
522
+ {
523
+ let tmpSelected = this._fileBrowser.selected;
524
+ if (tmpSelected > 0) // Skip '../' at index 0
525
+ {
526
+ let tmpItemText = this._fileBrowser.getItem(tmpSelected).getText().trim();
527
+ let tmpModuleName = tmpItemText.replace(/\/$/, '');
528
+ return libPath.join(libModuleCatalog.BasePath, tmpBrowser.GroupName, tmpModuleName);
529
+ }
530
+ }
531
+
532
+ return null;
533
+ }
534
+
535
+ _runModuleOperation(pCommand, pArgs, pLineLimit)
536
+ {
537
+ if (this._awaitingConfirmation) { return; }
538
+
539
+ let tmpModulePath = this._getModulePath();
540
+
541
+ if (!tmpModulePath)
542
+ {
543
+ this._terminalOutput.setContent('');
544
+ this._terminalOutput.log('{yellow-fg}{bold}Select a module first.{/bold}{/yellow-fg}');
545
+ this._terminalOutput.log('');
546
+ this._terminalOutput.log('Navigate into a module group, then select or enter a module');
547
+ this._terminalOutput.log('before running operations.');
548
+ this._screen.render();
549
+ return;
550
+ }
551
+
552
+ this.processRunner.run(pCommand, pArgs, tmpModulePath, pLineLimit);
553
+ }
554
+
555
+ _runDiff()
556
+ {
557
+ if (this._awaitingConfirmation) { return; }
558
+
559
+ let tmpModulePath = this._getModulePath();
560
+
561
+ if (!tmpModulePath)
562
+ {
563
+ this._terminalOutput.setContent('');
564
+ this._terminalOutput.log('{yellow-fg}{bold}Select a module first.{/bold}{/yellow-fg}');
565
+ this._terminalOutput.log('');
566
+ this._terminalOutput.log('Navigate into a module group, then select or enter a module');
567
+ this._terminalOutput.log('before running operations.');
568
+ this._screen.render();
569
+ return;
570
+ }
571
+
572
+ this.processRunner.runSequence(
573
+ [
574
+ {
575
+ command: 'git',
576
+ args: ['diff', '--stat'],
577
+ label: 'Changed files overview (including dist/):'
578
+ },
579
+ {
580
+ command: 'git',
581
+ args: ['diff', '--', '.', ':!dist'],
582
+ label: 'Full diff (excluding dist/):'
583
+ }
584
+ ],
585
+ tmpModulePath);
586
+ }
587
+
588
+ _runPublish()
589
+ {
590
+ if (this._awaitingConfirmation) { return; }
591
+
592
+ let tmpModulePath = this._getModulePath();
593
+
594
+ if (!tmpModulePath)
595
+ {
596
+ this._terminalOutput.setContent('');
597
+ this._terminalOutput.log('{yellow-fg}{bold}Select a module first.{/bold}{/yellow-fg}');
598
+ this._terminalOutput.log('');
599
+ this._terminalOutput.log('Navigate into a module group, then select or enter a module');
600
+ this._terminalOutput.log('before running operations.');
601
+ this._screen.render();
602
+ return;
603
+ }
604
+
605
+ // Clear and start the pre-publish validation report
606
+ this._terminalOutput.setContent('');
607
+ this._terminalOutput.log('{bold}{yellow-fg}Pre-publish validation{/yellow-fg}{/bold}');
608
+ this._terminalOutput.log('');
609
+ this._screen.render();
610
+
611
+ if (this.pict.log)
612
+ {
613
+ this.pict.log.info(`PUBLISH Pre-publish validation for ${tmpModulePath}`);
614
+ }
615
+
616
+ // ── Step 1: Read local package.json ──
617
+ let tmpPkgPath = libPath.join(tmpModulePath, 'package.json');
618
+ let tmpPkg;
619
+ try
620
+ {
621
+ tmpPkg = JSON.parse(libFs.readFileSync(tmpPkgPath, 'utf8'));
622
+ }
623
+ catch (pError)
624
+ {
625
+ this._terminalOutput.log(`{red-fg}{bold}Cannot read package.json:{/bold} ${pError.message}{/red-fg}`);
626
+ this._screen.render();
627
+ return;
628
+ }
629
+
630
+ let tmpPackageName = tmpPkg.name || libPath.basename(tmpModulePath);
631
+ let tmpLocalVersion = tmpPkg.version || '0.0.0';
632
+
633
+ this._terminalOutput.log(`{bold}Package:{/bold} ${tmpPackageName}`);
634
+ this._terminalOutput.log(`{bold}Local:{/bold} v${tmpLocalVersion}`);
635
+ this._screen.render();
636
+
637
+ // ── Step 2: Fetch the currently published version from npm ──
638
+ let tmpPublishedVersion = null;
639
+ try
640
+ {
641
+ tmpPublishedVersion = libChildProcess.execSync(
642
+ `npm view ${tmpPackageName} version`,
643
+ { cwd: tmpModulePath, encoding: 'utf8', timeout: 15000 }
644
+ ).trim();
645
+ }
646
+ catch (pError)
647
+ {
648
+ // Package may not be published yet (404)
649
+ tmpPublishedVersion = null;
650
+ }
651
+
652
+ if (tmpPublishedVersion)
653
+ {
654
+ this._terminalOutput.log(`{bold}npm:{/bold} v${tmpPublishedVersion}`);
655
+
656
+ if (tmpPublishedVersion === tmpLocalVersion)
657
+ {
658
+ this._terminalOutput.log('');
659
+ this._terminalOutput.log('{red-fg}{bold}✗ Version mismatch:{/bold} local version matches what is already published on npm.{/red-fg}');
660
+ this._terminalOutput.log('{red-fg} Bump the version with [v] before publishing.{/red-fg}');
661
+ this._terminalOutput.log('');
662
+ this._terminalOutput.log('{bold}────────────────────────────────────────{/bold}');
663
+ this._terminalOutput.log('{red-fg}{bold}✗ Publish aborted{/bold}{/red-fg}');
664
+ if (this.pict.log)
665
+ {
666
+ this.pict.log.info(`PUBLISH Aborted ${tmpPackageName} -- v${tmpLocalVersion} already on npm`);
667
+ }
668
+ this._updateStatus(`Publish aborted -- version ${tmpLocalVersion} already on npm`);
669
+ this._screen.render();
670
+ return;
671
+ }
672
+ else
673
+ {
674
+ this._terminalOutput.log(`{green-fg} ✓ Local version differs from published{/green-fg}`);
675
+ }
676
+ }
677
+ else
678
+ {
679
+ this._terminalOutput.log('{bold}npm:{/bold} {gray-fg}(not yet published){/gray-fg}');
680
+ this._terminalOutput.log(`{green-fg} ✓ First publish{/green-fg}`);
681
+ }
682
+
683
+ this._terminalOutput.log('');
684
+ this._screen.render();
685
+
686
+ // ── Step 3: Build the set of all retold ecosystem package names ──
687
+ let tmpEcosystemNames = {};
688
+ for (let i = 0; i < libModuleCatalog.Groups.length; i++)
689
+ {
690
+ let tmpGroup = libModuleCatalog.Groups[i];
691
+ for (let j = 0; j < tmpGroup.Modules.length; j++)
692
+ {
693
+ tmpEcosystemNames[tmpGroup.Modules[j]] = true;
694
+ }
695
+ }
696
+
697
+ // ── Step 4: Find ecosystem deps and check their versions against npm ──
698
+ let tmpAllDeps = {};
699
+ if (tmpPkg.dependencies)
700
+ {
701
+ let tmpKeys = Object.keys(tmpPkg.dependencies);
702
+ for (let i = 0; i < tmpKeys.length; i++)
703
+ {
704
+ tmpAllDeps[tmpKeys[i]] = { range: tmpPkg.dependencies[tmpKeys[i]], section: 'dependencies' };
705
+ }
706
+ }
707
+ if (tmpPkg.devDependencies)
708
+ {
709
+ let tmpKeys = Object.keys(tmpPkg.devDependencies);
710
+ for (let i = 0; i < tmpKeys.length; i++)
711
+ {
712
+ tmpAllDeps[tmpKeys[i]] = { range: tmpPkg.devDependencies[tmpKeys[i]], section: 'devDependencies' };
713
+ }
714
+ }
715
+
716
+ let tmpEcosystemDeps = [];
717
+ let tmpDepNames = Object.keys(tmpAllDeps);
718
+ for (let i = 0; i < tmpDepNames.length; i++)
719
+ {
720
+ if (tmpEcosystemNames[tmpDepNames[i]])
721
+ {
722
+ tmpEcosystemDeps.push(tmpDepNames[i]);
723
+ }
724
+ }
725
+
726
+ let tmpHasProblems = false;
727
+
728
+ if (tmpEcosystemDeps.length === 0)
729
+ {
730
+ this._terminalOutput.log('{gray-fg}No retold ecosystem dependencies found.{/gray-fg}');
731
+ }
732
+ else
733
+ {
734
+ this._terminalOutput.log(`{bold}Ecosystem dependency check{/bold} (${tmpEcosystemDeps.length} packages)`);
735
+ this._terminalOutput.log('');
736
+
737
+ for (let i = 0; i < tmpEcosystemDeps.length; i++)
738
+ {
739
+ let tmpDepName = tmpEcosystemDeps[i];
740
+ let tmpDepInfo = tmpAllDeps[tmpDepName];
741
+ let tmpLocalRange = tmpDepInfo.range;
742
+
743
+ // Skip file: references (local dev links)
744
+ if (tmpLocalRange.startsWith('file:'))
745
+ {
746
+ this._terminalOutput.log(` {gray-fg}${tmpDepName} ${tmpLocalRange} (local link -- skipped){/gray-fg}`);
747
+ continue;
748
+ }
749
+
750
+ let tmpLatestVersion = null;
751
+ try
752
+ {
753
+ tmpLatestVersion = libChildProcess.execSync(
754
+ `npm view ${tmpDepName} version`,
755
+ { cwd: tmpModulePath, encoding: 'utf8', timeout: 15000 }
756
+ ).trim();
757
+ }
758
+ catch (pError)
759
+ {
760
+ this._terminalOutput.log(` {yellow-fg}${tmpDepName} ${tmpLocalRange} (could not fetch from npm){/yellow-fg}`);
761
+ continue;
762
+ }
763
+
764
+ // Check if the local range covers the latest version
765
+ // Simple check: extract the version digits from the range (strip ^, ~, >= etc.)
766
+ let tmpRangeVersion = tmpLocalRange.replace(/^[\^~>=<]*/, '');
767
+ if (tmpRangeVersion === tmpLatestVersion)
768
+ {
769
+ this._terminalOutput.log(` {green-fg}✓ ${tmpDepName} ${tmpLocalRange} (latest: ${tmpLatestVersion}){/green-fg}`);
770
+ }
771
+ else
772
+ {
773
+ // Check if the range prefix (^ or ~) might cover the latest
774
+ let tmpRangePrefix = tmpLocalRange.match(/^[\^~]/);
775
+ let tmpCoversLatest = false;
776
+
777
+ if (tmpRangePrefix)
778
+ {
779
+ // Parse major.minor.patch for both
780
+ let tmpRangeParts = tmpRangeVersion.split('.').map(Number);
781
+ let tmpLatestParts = tmpLatestVersion.split('.').map(Number);
782
+
783
+ if (tmpRangePrefix[0] === '^')
784
+ {
785
+ // ^ allows changes that don't modify the left-most non-zero digit
786
+ if (tmpRangeParts[0] > 0)
787
+ {
788
+ tmpCoversLatest = (tmpLatestParts[0] === tmpRangeParts[0])
789
+ && (tmpLatestParts[1] > tmpRangeParts[1]
790
+ || (tmpLatestParts[1] === tmpRangeParts[1] && tmpLatestParts[2] >= tmpRangeParts[2]));
791
+ }
792
+ else if (tmpRangeParts[1] > 0)
793
+ {
794
+ tmpCoversLatest = (tmpLatestParts[0] === 0 && tmpLatestParts[1] === tmpRangeParts[1])
795
+ && (tmpLatestParts[2] >= tmpRangeParts[2]);
796
+ }
797
+ }
798
+ else if (tmpRangePrefix[0] === '~')
799
+ {
800
+ // ~ allows patch-level changes
801
+ tmpCoversLatest = (tmpLatestParts[0] === tmpRangeParts[0] && tmpLatestParts[1] === tmpRangeParts[1])
802
+ && (tmpLatestParts[2] >= tmpRangeParts[2]);
803
+ }
804
+ }
805
+
806
+ if (tmpCoversLatest)
807
+ {
808
+ this._terminalOutput.log(` {green-fg}✓ ${tmpDepName} ${tmpLocalRange} (latest: ${tmpLatestVersion} -- covered by range){/green-fg}`);
809
+ }
810
+ else
811
+ {
812
+ tmpHasProblems = true;
813
+ this._terminalOutput.log(` {red-fg}{bold}✗ ${tmpDepName}{/bold} ${tmpLocalRange} → latest: ${tmpLatestVersion}{/red-fg}`);
814
+ }
815
+ }
816
+
817
+ this._screen.render();
818
+ }
819
+ }
820
+
821
+ this._terminalOutput.log('');
822
+ this._screen.render();
823
+
824
+ if (tmpHasProblems)
825
+ {
826
+ this._terminalOutput.log('{red-fg}{bold}✗ Ecosystem dependencies are out of date.{/bold}{/red-fg}');
827
+ this._terminalOutput.log('{red-fg} Update package.json and run [i]nstall before publishing.{/red-fg}');
828
+ this._terminalOutput.log('');
829
+ this._terminalOutput.log('{bold}────────────────────────────────────────{/bold}');
830
+ this._terminalOutput.log('{red-fg}{bold}✗ Publish aborted{/bold}{/red-fg}');
831
+ if (this.pict.log)
832
+ {
833
+ this.pict.log.info(`PUBLISH Aborted ${tmpPackageName} -- ecosystem deps out of date`);
834
+ }
835
+ this._updateStatus('Publish aborted -- ecosystem deps out of date');
836
+ this._screen.render();
837
+ return;
838
+ }
839
+
840
+ // ── Step 5: All checks passed -- show summary and ask for confirmation ──
841
+ this._terminalOutput.log('{green-fg}{bold}✓ All pre-publish checks passed{/bold}{/green-fg}');
842
+ this._terminalOutput.log('');
843
+
844
+ // ── Step 6: Fetch recent commit log ──
845
+ this._terminalOutput.log('{bold}Recent commits:{/bold}');
846
+ this._terminalOutput.log('');
847
+
848
+ let tmpCommitLog = '';
849
+ try
850
+ {
851
+ // Try to get commits since the published version tag first
852
+ let tmpTagPatterns = [`v${tmpPublishedVersion}`, tmpPublishedVersion];
853
+ let tmpFoundTag = false;
854
+
855
+ if (tmpPublishedVersion)
856
+ {
857
+ for (let i = 0; i < tmpTagPatterns.length; i++)
858
+ {
859
+ try
860
+ {
861
+ tmpCommitLog = libChildProcess.execSync(
862
+ `git log ${tmpTagPatterns[i]}..HEAD --oneline`,
863
+ { cwd: tmpModulePath, encoding: 'utf8', timeout: 10000 }
864
+ ).trim();
865
+ if (tmpCommitLog)
866
+ {
867
+ tmpFoundTag = true;
868
+ break;
869
+ }
870
+ }
871
+ catch (pError)
872
+ {
873
+ // Tag doesn't exist, try the next pattern
874
+ }
875
+ }
876
+ }
877
+
878
+ // Fall back to recent commits if no tag was found
879
+ if (!tmpFoundTag || !tmpCommitLog)
880
+ {
881
+ tmpCommitLog = libChildProcess.execSync(
882
+ 'git log --oneline -20',
883
+ { cwd: tmpModulePath, encoding: 'utf8', timeout: 10000 }
884
+ ).trim();
885
+ }
886
+ }
887
+ catch (pError)
888
+ {
889
+ tmpCommitLog = '';
890
+ }
891
+
892
+ if (tmpCommitLog)
893
+ {
894
+ let tmpCommitLines = tmpCommitLog.split('\n');
895
+ for (let i = 0; i < tmpCommitLines.length; i++)
896
+ {
897
+ let tmpLine = tmpCommitLines[i].replace(/\{/g, '\\{').replace(/\}/g, '\\}');
898
+ this._terminalOutput.log(` {gray-fg}${tmpLine}{/gray-fg}`);
899
+ }
900
+ }
901
+ else
902
+ {
903
+ this._terminalOutput.log(' {gray-fg}(no commits found){/gray-fg}');
904
+ }
905
+
906
+ this._terminalOutput.log('');
907
+ this._terminalOutput.log('{bold}{blue-fg}────────────────────────────────────────{/blue-fg}{/bold}');
908
+ this._terminalOutput.log('');
909
+
910
+ // Summary block
911
+ if (tmpPublishedVersion)
912
+ {
913
+ this._terminalOutput.log(` {bold}${tmpPackageName}{/bold} v${tmpPublishedVersion} → v${tmpLocalVersion}`);
914
+ }
915
+ else
916
+ {
917
+ this._terminalOutput.log(` {bold}${tmpPackageName}{/bold} v${tmpLocalVersion} {gray-fg}(first publish){/gray-fg}`);
918
+ }
919
+ this._terminalOutput.log('');
920
+ this._terminalOutput.log('{bold}{yellow-fg}Publish to npm? [Y] yes [N] no{/yellow-fg}{/bold}');
921
+ this._updateStatus('Publish? Y/N');
922
+ this._awaitingConfirmation = true;
923
+ this._screen.render();
924
+
925
+ // ── Step 7: Wait for Y/N confirmation ──
926
+ let tmpSelf = this;
927
+ let tmpConfirmHandler = function (pCh, pKey)
928
+ {
929
+ // Remove this one-shot listener immediately
930
+ tmpSelf._screen.removeListener('keypress', tmpConfirmHandler);
931
+ tmpSelf._awaitingConfirmation = false;
932
+
933
+ if (pCh === 'y' || pCh === 'Y')
934
+ {
935
+ tmpSelf._terminalOutput.log('');
936
+ tmpSelf._terminalOutput.log('{green-fg}{bold}Publishing...{/bold}{/green-fg}');
937
+ tmpSelf._terminalOutput.log('');
938
+ tmpSelf._screen.render();
939
+
940
+ if (tmpSelf.pict.log)
941
+ {
942
+ tmpSelf.pict.log.info(`PUBLISH Confirmed -- publishing ${tmpPackageName} v${tmpLocalVersion}`);
943
+ }
944
+
945
+ tmpSelf.processRunner.run('npm', ['publish'], tmpModulePath, null, { append: true });
946
+ }
947
+ else
948
+ {
949
+ tmpSelf._terminalOutput.log('');
950
+ tmpSelf._terminalOutput.log('{bold}────────────────────────────────────────{/bold}');
951
+ tmpSelf._terminalOutput.log('{yellow-fg}{bold}Publish cancelled by user{/bold}{/yellow-fg}');
952
+ if (tmpSelf.pict.log)
953
+ {
954
+ tmpSelf.pict.log.info(`PUBLISH Cancelled by user -- ${tmpPackageName} v${tmpLocalVersion}`);
955
+ }
956
+ tmpSelf._updateStatus('Publish cancelled');
957
+ tmpSelf._screen.render();
958
+ }
959
+ };
960
+
961
+ this._screen.on('keypress', tmpConfirmHandler);
962
+ }
963
+
964
+ // ─────────────────────────────────────────────
965
+ // Content Display
966
+ // ─────────────────────────────────────────────
967
+
968
+ _showModuleWelcome()
969
+ {
970
+ let tmpBrowser = this.pict.AppData.Manager.Browser;
971
+
972
+ this._terminalOutput.setContent('');
973
+ this._terminalOutput.log(`{bold}Module: ${tmpBrowser.ModuleName}{/bold}`);
974
+ this._terminalOutput.log(`{gray-fg}Path: ${tmpBrowser.ModulePath}{/gray-fg}`);
975
+ this._terminalOutput.log('');
976
+ this._terminalOutput.log('{bold}Operations:{/bold}');
977
+ this._terminalOutput.log(' {cyan-fg}[i]{/cyan-fg} npm install {cyan-fg}[t]{/cyan-fg} npm test {cyan-fg}[y]{/cyan-fg} npm run types');
978
+ this._terminalOutput.log(' {cyan-fg}[b]{/cyan-fg} npm run build {cyan-fg}[v]{/cyan-fg} Bump version {cyan-fg}[d]{/cyan-fg} git diff');
979
+ this._terminalOutput.log(' {cyan-fg}[o]{/cyan-fg} git commit {cyan-fg}[p]{/cyan-fg} git pull {cyan-fg}[u]{/cyan-fg} git push');
980
+ this._terminalOutput.log(' {cyan-fg}[!]{/cyan-fg} npm publish {cyan-fg}[x]{/cyan-fg} Kill process');
981
+ this._terminalOutput.log('');
982
+ this._terminalOutput.log('{bold}All Modules:{/bold}');
983
+ this._terminalOutput.log(' {cyan-fg}[s]{/cyan-fg} Status.sh {cyan-fg}[r]{/cyan-fg} Update.sh {cyan-fg}[c]{/cyan-fg} Checkout.sh');
984
+ this._terminalOutput.log('');
985
+ this._terminalOutput.log('{bold}Output:{/bold}');
986
+ this._terminalOutput.log(' {cyan-fg}[/]{/cyan-fg} Search output {cyan-fg}]{/cyan-fg} Next match {cyan-fg}[{/cyan-fg} Prev match {cyan-fg}[Esc]{/cyan-fg} Clear search');
987
+ this._terminalOutput.log('');
988
+ this._terminalOutput.log(' {cyan-fg}[g]{/cyan-fg} Go to groups');
989
+ this._terminalOutput.log('');
990
+ this._terminalOutput.log('Select a file to view its contents, or press a shortcut key.');
991
+
992
+ // Try to show package.json version info
993
+ let tmpPkgPath = libPath.join(tmpBrowser.ModulePath, 'package.json');
994
+ try
995
+ {
996
+ let tmpPkg = JSON.parse(libFs.readFileSync(tmpPkgPath, 'utf8'));
997
+ this._terminalOutput.log('');
998
+ this._terminalOutput.log(`{bold}${tmpPkg.name || tmpBrowser.ModuleName}{/bold} v${tmpPkg.version || '?'}`);
999
+ if (tmpPkg.description)
1000
+ {
1001
+ this._terminalOutput.log(`{gray-fg}${tmpPkg.description}{/gray-fg}`);
1002
+ }
1003
+ }
1004
+ catch (pError)
1005
+ {
1006
+ // No package.json or not parseable -- skip
1007
+ }
1008
+
1009
+ this._screen.render();
1010
+ }
1011
+
1012
+ _showFileContents(pFilePath, pFileName)
1013
+ {
1014
+ this._terminalOutput.setContent('');
1015
+ this._terminalOutput.log(`{bold}${pFileName}{/bold}`);
1016
+ this._terminalOutput.log(`{gray-fg}${pFilePath}{/gray-fg}`);
1017
+ this._terminalOutput.log('');
1018
+
1019
+ try
1020
+ {
1021
+ let tmpStat = libFs.statSync(pFilePath);
1022
+
1023
+ // Skip very large files
1024
+ if (tmpStat.size > 1024 * 512)
1025
+ {
1026
+ this._terminalOutput.log(`{yellow-fg}File is too large to display (${(tmpStat.size / 1024).toFixed(0)} KB){/yellow-fg}`);
1027
+ this._screen.render();
1028
+ return;
1029
+ }
1030
+
1031
+ let tmpContent = libFs.readFileSync(pFilePath, 'utf8');
1032
+ let tmpLines = tmpContent.split('\n');
1033
+
1034
+ let tmpDisplayLimit = Math.min(tmpLines.length, FILE_VIEW_LINE_LIMIT);
1035
+ for (let i = 0; i < tmpDisplayLimit; i++)
1036
+ {
1037
+ // Escape curly braces so blessed doesn't parse them as markup tags
1038
+ let tmpLine = tmpLines[i].replace(/\{/g, '\\{').replace(/\}/g, '\\}');
1039
+ this._terminalOutput.log(tmpLine);
1040
+ }
1041
+
1042
+ if (tmpLines.length > FILE_VIEW_LINE_LIMIT)
1043
+ {
1044
+ this._terminalOutput.log('');
1045
+ this._terminalOutput.log(`{yellow-fg}... truncated (showing ${FILE_VIEW_LINE_LIMIT} of ${tmpLines.length} lines){/yellow-fg}`);
1046
+ }
1047
+
1048
+ this._updateStatus(`Viewing: ${pFileName} (${tmpLines.length} lines)`);
1049
+ }
1050
+ catch (pError)
1051
+ {
1052
+ this._terminalOutput.log(`{red-fg}Error reading file: ${pError.message}{/red-fg}`);
1053
+ }
1054
+
1055
+ this._screen.render();
1056
+ }
1057
+
1058
+ _showDirectoryContents(pDirPath, pDirName)
1059
+ {
1060
+ this._terminalOutput.setContent('');
1061
+ this._terminalOutput.log(`{bold}Directory: ${pDirName}/{/bold}`);
1062
+ this._terminalOutput.log(`{gray-fg}${pDirPath}{/gray-fg}`);
1063
+ this._terminalOutput.log('');
1064
+
1065
+ try
1066
+ {
1067
+ let tmpEntries = libFs.readdirSync(pDirPath);
1068
+ tmpEntries.sort();
1069
+
1070
+ for (let i = 0; i < tmpEntries.length; i++)
1071
+ {
1072
+ let tmpEntryPath = libPath.join(pDirPath, tmpEntries[i]);
1073
+ try
1074
+ {
1075
+ let tmpStat = libFs.statSync(tmpEntryPath);
1076
+ if (tmpStat.isDirectory())
1077
+ {
1078
+ this._terminalOutput.log(` {cyan-fg}${tmpEntries[i]}/{/cyan-fg}`);
1079
+ }
1080
+ else
1081
+ {
1082
+ let tmpSize = tmpStat.size;
1083
+ let tmpSizeStr = tmpSize < 1024 ? `${tmpSize} B` : `${(tmpSize / 1024).toFixed(1)} KB`;
1084
+ this._terminalOutput.log(` ${tmpEntries[i]} {gray-fg}(${tmpSizeStr}){/gray-fg}`);
1085
+ }
1086
+ }
1087
+ catch (pError)
1088
+ {
1089
+ this._terminalOutput.log(` ${tmpEntries[i]}`);
1090
+ }
1091
+ }
1092
+
1093
+ this._updateStatus(`Directory: ${pDirName}/ (${tmpEntries.length} entries)`);
1094
+ }
1095
+ catch (pError)
1096
+ {
1097
+ this._terminalOutput.log(`{red-fg}Error reading directory: ${pError.message}{/red-fg}`);
1098
+ }
1099
+
1100
+ this._screen.render();
1101
+ }
1102
+
1103
+ // ─────────────────────────────────────────────
1104
+ // File Logging
1105
+ // ─────────────────────────────────────────────
1106
+
1107
+ _getLogFilePath()
1108
+ {
1109
+ let tmpNow = new Date();
1110
+ let tmpYear = tmpNow.getFullYear();
1111
+ let tmpMonth = String(tmpNow.getMonth() + 1).padStart(2, '0');
1112
+ let tmpDay = String(tmpNow.getDate()).padStart(2, '0');
1113
+ let tmpRepoRoot = libPath.resolve(libModuleCatalog.BasePath, '..');
1114
+
1115
+ return libPath.join(tmpRepoRoot, `Retold-Manager-Log-${tmpYear}-${tmpMonth}-${tmpDay}.log`);
1116
+ }
1117
+
1118
+ _toggleFileLogging()
1119
+ {
1120
+ let tmpLog = this.pict.log;
1121
+
1122
+ if (this._fileLogStream)
1123
+ {
1124
+ // Turn off: close the writer and remove from all stream arrays
1125
+ tmpLog.info('--- File logging disabled ---');
1126
+ this._fileLogStream.closeWriter();
1127
+
1128
+ let tmpUUID = this._fileLogStream.loggerUUID;
1129
+ delete tmpLog.activeLogStreams[tmpUUID];
1130
+
1131
+ let tmpFilterOut = (pStream) => pStream.loggerUUID !== tmpUUID;
1132
+ tmpLog.logStreams = tmpLog.logStreams.filter(tmpFilterOut);
1133
+ tmpLog.logStreamsTrace = tmpLog.logStreamsTrace.filter(tmpFilterOut);
1134
+ tmpLog.logStreamsDebug = tmpLog.logStreamsDebug.filter(tmpFilterOut);
1135
+ tmpLog.logStreamsInfo = tmpLog.logStreamsInfo.filter(tmpFilterOut);
1136
+ tmpLog.logStreamsWarn = tmpLog.logStreamsWarn.filter(tmpFilterOut);
1137
+ tmpLog.logStreamsError = tmpLog.logStreamsError.filter(tmpFilterOut);
1138
+ tmpLog.logStreamsFatal = tmpLog.logStreamsFatal.filter(tmpFilterOut);
1139
+
1140
+ this._fileLogStream = null;
1141
+
1142
+ this._terminalOutput.log('{yellow-fg}File logging OFF{/yellow-fg}');
1143
+ this._updateStatus('Logging disabled');
1144
+ }
1145
+ else
1146
+ {
1147
+ // Turn on: create a simpleflatfile logger and add it to fable-log
1148
+ let tmpLogPath = this._getLogFilePath();
1149
+ let tmpProviders = tmpLog._Providers;
1150
+
1151
+ if (!tmpProviders.simpleflatfile)
1152
+ {
1153
+ this._terminalOutput.log('{red-fg}simpleflatfile log provider not available{/red-fg}');
1154
+ this._screen.render();
1155
+ return;
1156
+ }
1157
+
1158
+ let tmpStreamDef =
1159
+ {
1160
+ loggertype: 'simpleflatfile',
1161
+ level: 'info',
1162
+ path: tmpLogPath,
1163
+ outputloglinestoconsole: false,
1164
+ outputobjectstoconsole: false,
1165
+ Context: 'RetoldManager',
1166
+ };
1167
+
1168
+ let tmpLogger = new tmpProviders.simpleflatfile(tmpStreamDef, tmpLog);
1169
+ tmpLogger.initialize();
1170
+ tmpLog.addLogger(tmpLogger, 'info');
1171
+
1172
+ this._fileLogStream = tmpLogger;
1173
+
1174
+ tmpLog.info('--- File logging enabled ---');
1175
+
1176
+ this._terminalOutput.log(`{green-fg}File logging ON{/green-fg} {gray-fg}${tmpLogPath}{/gray-fg}`);
1177
+ this._updateStatus(`Logging to ${libPath.basename(tmpLogPath)}`);
1178
+ }
1179
+
1180
+ this._updateHeader();
1181
+ this._screen.render();
1182
+ }
1183
+
1184
+ // ─────────────────────────────────────────────
1185
+ // Header & Status Updates
1186
+ // ─────────────────────────────────────────────
1187
+
1188
+ _updateHeader()
1189
+ {
1190
+ let tmpBrowser = this.pict.AppData.Manager.Browser;
1191
+ let tmpHeaderWidget = this.terminalUI.getWidget('#TUI-Header');
1192
+
1193
+ if (!tmpHeaderWidget)
1194
+ {
1195
+ return;
1196
+ }
1197
+
1198
+ let tmpHasModule = (tmpBrowser.Level === 'files' && tmpBrowser.ModulePath)
1199
+ || (tmpBrowser.Level === 'modules');
1200
+
1201
+ // Show [l]og shortcut only when logging is OFF so the user knows how to re-enable it
1202
+ let tmpNavKeys = this._fileLogStream
1203
+ ? '[s]tatus [r] update [c]heckout [/] search | [g] groups [Tab] focus [q] quit'
1204
+ : '[s]tatus [r] update [c]heckout [/] search | [l]og [g] groups [Tab] focus [q] quit';
1205
+
1206
+ if (tmpHasModule)
1207
+ {
1208
+ // Determine the target module name to display
1209
+ let tmpTargetModule = '';
1210
+ if (tmpBrowser.Level === 'files')
1211
+ {
1212
+ tmpTargetModule = tmpBrowser.ModuleName;
1213
+ }
1214
+ else if (tmpBrowser.Level === 'modules')
1215
+ {
1216
+ // Show the highlighted module if one is selected (not ../)
1217
+ let tmpSelected = this._fileBrowser.selected;
1218
+ if (tmpSelected > 0)
1219
+ {
1220
+ tmpTargetModule = this._fileBrowser.getItem(tmpSelected).getText().trim().replace(/\/$/, '');
1221
+ }
1222
+ }
1223
+
1224
+ if (tmpTargetModule)
1225
+ {
1226
+ tmpHeaderWidget.setContent(
1227
+ `{bold} Retold Manager{/bold} | {cyan-fg}${tmpTargetModule}{/cyan-fg} [i]nstall [t]est t[y]pes [b]uild [v]ersion [d]iff c[o]mmit [p]ull p[u]sh [!] publish | [x] kill ${tmpNavKeys}`
1228
+ );
1229
+ }
1230
+ else
1231
+ {
1232
+ tmpHeaderWidget.setContent(
1233
+ `{bold} Retold Manager{/bold} | ${tmpNavKeys}`
1234
+ );
1235
+ }
1236
+ }
1237
+ else
1238
+ {
1239
+ tmpHeaderWidget.setContent(
1240
+ `{bold} Retold Manager{/bold} | ${tmpNavKeys}`
1241
+ );
1242
+ }
1243
+
1244
+ if (this._screen)
1245
+ {
1246
+ this._screen.render();
1247
+ }
1248
+ }
1249
+
1250
+ _updateStatus(pMessage)
1251
+ {
1252
+ this.pict.AppData.Manager.StatusMessage = pMessage;
1253
+
1254
+ if (this.pict.views['TUI-StatusBar'])
1255
+ {
1256
+ this.pict.views['TUI-StatusBar'].render();
1257
+ }
1258
+
1259
+ if (this._screen)
1260
+ {
1261
+ this._screen.render();
1262
+ }
1263
+ }
1264
+
1265
+ // ─────────────────────────────────────────────
1266
+ // Key Bindings
1267
+ // ─────────────────────────────────────────────
1268
+
1269
+ _bindNavigation(pScreen)
1270
+ {
1271
+ let tmpList = this._fileBrowser;
1272
+
1273
+ // Enter -- drill into selected item
1274
+ tmpList.on('select', (pItem, pIndex) =>
1275
+ {
1276
+ this._drillIn(pIndex);
1277
+ });
1278
+
1279
+ // Update the header when the cursor moves in the module list
1280
+ // so the targeted module name stays current
1281
+ tmpList.on('select item', () =>
1282
+ {
1283
+ this._updateHeader();
1284
+ });
1285
+
1286
+ // Backspace / Escape -- go up one level
1287
+ tmpList.key(['backspace', 'escape'], () =>
1288
+ {
1289
+ this._drillOut();
1290
+ });
1291
+
1292
+ // Tab -- toggle focus between file browser and terminal output
1293
+ pScreen.key(['tab'], () =>
1294
+ {
1295
+ if (this._fileBrowser === pScreen.focused)
1296
+ {
1297
+ this._terminalOutput.focus();
1298
+ }
1299
+ else
1300
+ {
1301
+ this._fileBrowser.focus();
1302
+ }
1303
+ this._screen.render();
1304
+ });
1305
+
1306
+ // Module operation shortcuts
1307
+ pScreen.key(['i'], () => { this._runModuleOperation('npm', ['install']); });
1308
+ pScreen.key(['t'], () => { this._runModuleOperation('npm', ['test'], 0); });
1309
+ pScreen.key(['y'], () => { this._runModuleOperation('npm', ['run', 'types']); });
1310
+ pScreen.key(['b'], () => { this._runModuleOperation('npm', ['run', 'build']); });
1311
+ pScreen.key(['v'], () => { this._runModuleOperation('npm', ['version', 'patch', '--no-git-tag-version']); });
1312
+ pScreen.key(['d'], () => { this._runDiff(); });
1313
+ pScreen.key(['o'], () =>
1314
+ {
1315
+ if (this._awaitingConfirmation) { return; }
1316
+
1317
+ let tmpModulePath = this._getModulePath();
1318
+ if (!tmpModulePath)
1319
+ {
1320
+ this._terminalOutput.setContent('');
1321
+ this._terminalOutput.log('{yellow-fg}{bold}Select a module first.{/bold}{/yellow-fg}');
1322
+ this._terminalOutput.log('');
1323
+ this._terminalOutput.log('Navigate into a module group, then select or enter a module');
1324
+ this._terminalOutput.log('before running operations.');
1325
+ this._screen.render();
1326
+ return;
1327
+ }
1328
+
1329
+ this._awaitingConfirmation = true;
1330
+
1331
+ let tmpPrompt = blessed.textbox(
1332
+ {
1333
+ parent: this._screen,
1334
+ bottom: 1,
1335
+ left: 40,
1336
+ right: 0,
1337
+ height: 3,
1338
+ border: { type: 'line' },
1339
+ label: ' Commit Message ',
1340
+ inputOnFocus: true,
1341
+ style:
1342
+ {
1343
+ fg: 'white',
1344
+ bg: 'black',
1345
+ border: { fg: 'yellow' },
1346
+ label: { fg: 'yellow', bold: true },
1347
+ },
1348
+ });
1349
+
1350
+ let tmpCleanup = (pValue) =>
1351
+ {
1352
+ tmpPrompt.destroy();
1353
+ this._awaitingConfirmation = false;
1354
+
1355
+ if (pValue && pValue.trim().length > 0)
1356
+ {
1357
+ // Shell-escape the message: wrap in single quotes, escape any internal single quotes
1358
+ let tmpMessage = pValue.trim().replace(/'/g, "'\\''");
1359
+ this.processRunner.run('git', ['commit', '-a', '-m', `'${tmpMessage}'`], tmpModulePath);
1360
+ }
1361
+
1362
+ this._fileBrowser.focus();
1363
+ this._screen.render();
1364
+ };
1365
+
1366
+ tmpPrompt.on('submit', (pValue) =>
1367
+ {
1368
+ tmpCleanup(pValue);
1369
+ });
1370
+
1371
+ tmpPrompt.on('cancel', () =>
1372
+ {
1373
+ tmpCleanup(null);
1374
+ });
1375
+
1376
+ tmpPrompt.focus();
1377
+ tmpPrompt.readInput();
1378
+ this._screen.render();
1379
+ });
1380
+ pScreen.key(['p'], () => { this._runModuleOperation('git', ['pull']); });
1381
+ pScreen.key(['u'], () => { this._runModuleOperation('git', ['push']); });
1382
+ pScreen.key(['!'], () => { this._runPublish(); });
1383
+
1384
+ // Global script operations (run against all modules)
1385
+ pScreen.key(['s'], () =>
1386
+ {
1387
+ if (this._awaitingConfirmation) { return; }
1388
+ this.pict.views['TUI-Status'].runScript(this.processRunner, libModuleCatalog.BasePath);
1389
+ });
1390
+ pScreen.key(['r'], () =>
1391
+ {
1392
+ if (this._awaitingConfirmation) { return; }
1393
+ this.pict.views['TUI-Update'].runScript(this.processRunner, libModuleCatalog.BasePath);
1394
+ });
1395
+ pScreen.key(['c'], () =>
1396
+ {
1397
+ if (this._awaitingConfirmation) { return; }
1398
+ this.pict.views['TUI-Checkout'].runScript(this.processRunner, libModuleCatalog.BasePath);
1399
+ });
1400
+
1401
+ // Kill running process
1402
+ pScreen.key(['x'], () =>
1403
+ {
1404
+ if (this.processRunner && this.processRunner.isRunning())
1405
+ {
1406
+ this.processRunner.kill();
1407
+ this._terminalOutput.log('{yellow-fg}{bold}Process killed by user{/bold}{/yellow-fg}');
1408
+ this._updateStatus('Killed');
1409
+ this._screen.render();
1410
+ }
1411
+ });
1412
+
1413
+ // Toggle file logging
1414
+ pScreen.key(['l'], () =>
1415
+ {
1416
+ this._toggleFileLogging();
1417
+ });
1418
+
1419
+ // Quit
1420
+ pScreen.key(['q'], () =>
1421
+ {
1422
+ this.terminalUI.destroyScreen();
1423
+ });
1424
+
1425
+ // Search output buffer with /
1426
+ pScreen.key(['/'], () =>
1427
+ {
1428
+ if (this._awaitingConfirmation) { return; }
1429
+ if (!this.processRunner.hasBuffer()) { return; }
1430
+ if (this.processRunner.isRunning()) { return; }
1431
+
1432
+ this._awaitingConfirmation = true;
1433
+
1434
+ // Create an inline prompt at the bottom of the terminal output
1435
+ let tmpPrompt = blessed.textbox(
1436
+ {
1437
+ parent: this._screen,
1438
+ bottom: 1,
1439
+ left: 40,
1440
+ right: 0,
1441
+ height: 3,
1442
+ border: { type: 'line' },
1443
+ label: ' Search ',
1444
+ inputOnFocus: true,
1445
+ style:
1446
+ {
1447
+ fg: 'white',
1448
+ bg: 'black',
1449
+ border: { fg: 'yellow' },
1450
+ label: { fg: 'yellow', bold: true },
1451
+ },
1452
+ });
1453
+
1454
+ let tmpCleanup = (pValue) =>
1455
+ {
1456
+ tmpPrompt.destroy();
1457
+ this._awaitingConfirmation = false;
1458
+
1459
+ if (pValue && pValue.trim().length > 0)
1460
+ {
1461
+ this.processRunner.search(pValue.trim());
1462
+ }
1463
+
1464
+ this._fileBrowser.focus();
1465
+ this._screen.render();
1466
+ };
1467
+
1468
+ // Use the submit event directly -- more reliable than readInput callback
1469
+ // for getting the entered value in blessed ^0.1.x
1470
+ tmpPrompt.on('submit', (pValue) =>
1471
+ {
1472
+ tmpCleanup(pValue);
1473
+ });
1474
+
1475
+ tmpPrompt.on('cancel', () =>
1476
+ {
1477
+ tmpCleanup(null);
1478
+ });
1479
+
1480
+ tmpPrompt.focus();
1481
+ tmpPrompt.readInput();
1482
+ this._screen.render();
1483
+ });
1484
+
1485
+ // Search navigation: next match (] to avoid conflict with other keys)
1486
+ pScreen.key([']'], () =>
1487
+ {
1488
+ if (this._awaitingConfirmation) { return; }
1489
+ if (this.processRunner.isSearchActive())
1490
+ {
1491
+ this.processRunner.searchNavigate(1);
1492
+ }
1493
+ });
1494
+
1495
+ // Search navigation: previous match
1496
+ pScreen.key(['['], () =>
1497
+ {
1498
+ if (this._awaitingConfirmation) { return; }
1499
+ if (this.processRunner.isSearchActive())
1500
+ {
1501
+ this.processRunner.searchNavigate(-1);
1502
+ }
1503
+ });
1504
+
1505
+ // Escape clears search mode and restores full output
1506
+ pScreen.key(['escape'], () =>
1507
+ {
1508
+ if (this._awaitingConfirmation) { return; }
1509
+ if (this.processRunner.isSearchActive())
1510
+ {
1511
+ this.processRunner.searchClear();
1512
+ }
1513
+ });
1514
+
1515
+ // Quick navigation: go to top-level groups
1516
+ pScreen.key(['g'], () =>
1517
+ {
1518
+ let tmpBrowser = this.pict.AppData.Manager.Browser;
1519
+ tmpBrowser.Level = 'groups';
1520
+ tmpBrowser.GroupIndex = -1;
1521
+ tmpBrowser.GroupName = '';
1522
+ tmpBrowser.GroupLabel = '';
1523
+ tmpBrowser.ModuleName = '';
1524
+ tmpBrowser.ModulePath = '';
1525
+ tmpBrowser.SubPath = '';
1526
+ this._populateFileList();
1527
+ this._fileBrowser.focus();
1528
+ });
1529
+ }
1530
+ }
1531
+
1532
+ module.exports = RetoldManagerApp;