@owdproject/core 3.3.1 → 4.0.0-alpha.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/README.md +91 -59
  2. package/dist/dom/index.d.mts +65 -0
  3. package/dist/dom/index.d.ts +65 -0
  4. package/dist/dom/index.mjs +224 -0
  5. package/dist/events/index.d.mts +70 -0
  6. package/dist/events/index.d.ts +70 -0
  7. package/dist/events/index.mjs +3 -0
  8. package/dist/index.d.mts +73 -0
  9. package/dist/index.d.ts +73 -0
  10. package/dist/index.mjs +115 -0
  11. package/dist/layout/index.d.mts +41 -0
  12. package/dist/layout/index.d.ts +41 -0
  13. package/dist/layout/index.mjs +64 -0
  14. package/dist/machines/index.d.mts +31 -0
  15. package/dist/machines/index.d.ts +31 -0
  16. package/dist/machines/index.mjs +2 -0
  17. package/dist/managers/index.d.mts +117 -0
  18. package/dist/managers/index.d.ts +117 -0
  19. package/dist/managers/index.mjs +6 -0
  20. package/dist/session/index.d.mts +57 -0
  21. package/dist/session/index.d.ts +57 -0
  22. package/dist/session/index.mjs +65 -0
  23. package/dist/shared/core.B0B0KfVK.mjs +865 -0
  24. package/dist/shared/core.B1XxB289.mjs +239 -0
  25. package/dist/shared/core.B3mp7ztE.mjs +237 -0
  26. package/dist/shared/core.BOvFdEVQ.d.ts +114 -0
  27. package/dist/shared/core.CsDFgGnX.mjs +40 -0
  28. package/dist/shared/core.Di1eFcr_.d.mts +114 -0
  29. package/dist/shared/core.Pd_Ogt4g.mjs +101 -0
  30. package/dist/shared/core.b3Xldzyu.mjs +29 -0
  31. package/dist/types/index.d.mts +266 -0
  32. package/dist/types/index.d.ts +266 -0
  33. package/dist/types/index.mjs +1 -0
  34. package/package.json +60 -87
  35. package/bin/cli.js +0 -395
  36. package/bin/desktop.js +0 -5
  37. package/bin/lib/catalog.js +0 -293
  38. package/bin/lib/config.js +0 -109
  39. package/bin/lib/desktopConfig.js +0 -56
  40. package/bin/lib/generateTemplate.js +0 -389
  41. package/bin/lib/install.js +0 -408
  42. package/bin/lib/migrateModulePlayground.config.mjs +0 -164
  43. package/bin/lib/migrateModulePlayground.mjs +0 -313
  44. package/bin/lib/npmVersions.js +0 -124
  45. package/bin/lib/playgroundContext.js +0 -188
  46. package/bin/lib/scaffold.js +0 -197
  47. package/bin/lib/status.js +0 -289
  48. package/bin/lib/validateModule.js +0 -697
  49. package/bin/lib/workspace.js +0 -182
  50. package/bin/owd.js +0 -5
  51. package/bin/tui.js +0 -1379
  52. package/dist/module.d.mts +0 -13
  53. package/dist/module.json +0 -9
  54. package/dist/module.mjs +0 -268
  55. package/dist/types.d.mts +0 -7
  56. package/index.ts +0 -5
  57. package/module.ts +0 -223
  58. package/runtime/components/Desktop/Application/DesktopApplicationRender.vue +0 -20
  59. package/runtime/components/Desktop/Application/DesktopApplicationWindowsRender.vue +0 -38
  60. package/runtime/components/Desktop/Background/DesktopBackground.vue +0 -16
  61. package/runtime/components/Desktop/Core/DesktopCore.vue +0 -90
  62. package/runtime/components/Desktop/Time/DesktopTime.vue +0 -56
  63. package/runtime/components/Desktop/Window/DesktopWindow.vue +0 -204
  64. package/runtime/components/Desktop/Window/DesktopWindowContent.vue +0 -28
  65. package/runtime/components/Desktop/Window/DesktopWindowNav.vue +0 -66
  66. package/runtime/composables/useApplicationEntries.ts +0 -51
  67. package/runtime/composables/useApplicationManager.ts +0 -285
  68. package/runtime/composables/useDesktopManager.ts +0 -49
  69. package/runtime/composables/useTerminalManager.ts +0 -66
  70. package/runtime/composables/useWorkspaceManager.ts +0 -41
  71. package/runtime/internal/controllers/ApplicationController.ts +0 -345
  72. package/runtime/internal/controllers/WindowController.ts +0 -462
  73. package/runtime/plugins/02.desktop-register-desktop-apps.client.ts +0 -17
  74. package/runtime/plugins/resize.client.ts +0 -6
  75. package/runtime/stores/storeApplicationMeta.ts +0 -59
  76. package/runtime/stores/storeApplicationWindows.ts +0 -20
  77. package/runtime/stores/storeDesktop.ts +0 -111
  78. package/runtime/stores/storeDesktopDefaultApps.ts +0 -53
  79. package/runtime/stores/storeDesktopVolume.ts +0 -18
  80. package/runtime/stores/storeDesktopWindow.ts +0 -20
  81. package/runtime/stores/storeDesktopWorkspace.ts +0 -75
  82. package/runtime/utils/captureElementToCanvas.ts +0 -21
  83. package/runtime/utils/explorerEntryPath.ts +0 -10
  84. package/runtime/utils/resolveDesktopConfigPath.ts +0 -52
  85. package/runtime/utils/splitDesktopUserConfig.ts +0 -85
  86. package/runtime/utils/utilApp.ts +0 -37
  87. package/runtime/utils/utilCommon.ts +0 -26
  88. package/runtime/utils/utilDebug.ts +0 -30
  89. package/runtime/utils/utilDesktop.ts +0 -35
  90. package/runtime/utils/utilTerminal.ts +0 -6
  91. package/runtime/utils/utilWindow.ts +0 -15
  92. package/runtime/utils/validateDesktopUserConfig.ts +0 -44
  93. package/types/index.d.ts +0 -420
package/bin/tui.js DELETED
@@ -1,1379 +0,0 @@
1
- import { execSync } from 'node:child_process'
2
- import { dirname, join } from 'node:path'
3
- import { fileURLToPath } from 'node:url'
4
- import blessed from 'neo-blessed'
5
- import {
6
- findWorkspaceRoot,
7
- loadSettings,
8
- saveSettings,
9
- desktopPaths,
10
- KINDS,
11
- shortName,
12
- isWorkspaceInstallMode,
13
- isInstallableDesktopModule,
14
- normalizeInstallMode,
15
- hasDocsModuleInstalled,
16
- docsBasePathFromConfig,
17
- } from './lib/workspace.js'
18
- import {
19
- readDesktopConfig,
20
- writeDesktopConfig,
21
- readDesktopDependencies,
22
- resolveConfigPathForWrite,
23
- } from './lib/config.js'
24
- import { warnLegacyDesktopConfig } from './lib/desktopConfig.js'
25
- import {
26
- loadCatalog,
27
- filterCatalog,
28
- mergeInstalled,
29
- readThemeMeta,
30
- } from './lib/catalog.js'
31
- import {
32
- installPackage,
33
- materializeToWorkspace,
34
- hasLocalWorkspaceSource,
35
- runPrepareModules,
36
- resolveInstallPlan,
37
- installTag,
38
- } from './lib/install.js'
39
- import {
40
- getClientStatus,
41
- startDev,
42
- stopDev,
43
- waitForDev,
44
- devLogPath,
45
- formatSparkline,
46
- formatBar,
47
- } from './lib/status.js'
48
- import { resolveDevTarget } from './lib/playgroundContext.js'
49
-
50
- const __dirname = dirname(fileURLToPath(import.meta.url))
51
-
52
- const C = {
53
- border: 'cyan',
54
- borderDim: '#3a3a4a',
55
- focus: 'green',
56
- accent: '#7ee787',
57
- warn: 'yellow',
58
- err: 'red',
59
- muted: '#6c7086',
60
- title: 'white',
61
- userMode: '#7ee787',
62
- devMode: 'cyan',
63
- }
64
-
65
- const SPINNER = ['|', '/', '-', '\\']
66
- const MEM_HISTORY_LEN = 30
67
-
68
- /** @param {string} commandName */
69
- export async function runTui(commandName = 'desktop') {
70
- const workspaceRoot = findWorkspaceRoot()
71
- if (!workspaceRoot) {
72
- console.error('Not inside an OWD workspace.')
73
- process.exit(1)
74
- }
75
-
76
- const paths = desktopPaths(workspaceRoot)
77
- const devTarget = resolveDevTarget(process.cwd(), workspaceRoot)
78
- const playgroundActive = devTarget?.mode === 'playground'
79
- const playgroundLabel = devTarget?.packageName ?? null
80
- warnLegacyDesktopConfig(
81
- paths.configLegacy ? { legacy: true, file: 'owd.config.ts' } : null,
82
- )
83
- let settings = loadSettings(workspaceRoot)
84
- let config = readDesktopConfig(paths.config, workspaceRoot)
85
- let deps = readDesktopDependencies(paths.packageJson)
86
-
87
- let catalog = []
88
- let catalogCacheAge = ''
89
- /** @type {'catalog' | 'theme'} */
90
- let focusPanel = 'catalog'
91
- let activeTab = 'module'
92
- let pendingTheme = config.theme
93
- /** @type {Map<string, boolean>} */
94
- const pendingPackages = new Map()
95
- let statusLine = ''
96
- let devPhase = 'idle'
97
- let clientStatus = await getClientStatus(workspaceRoot, settings.devPort)
98
- /** @type {number[]} */
99
- const memHistory = []
100
- let spinnerFrame = 0
101
- let spinnerTimer = null
102
- let pulseOn = true
103
- let modeFlash = 0
104
- let saveProgress = null
105
- let settingsOpen = false
106
- let menuOpen = false
107
-
108
- /** @type {{ id: string, label: string, key: string }[]} */
109
- const MENU_ITEMS = [
110
- { id: 'start', label: 'Start dev server', key: 's' },
111
- { id: 'stop', label: 'Stop dev server', key: 'x' },
112
- { id: 'reboot', label: 'Reboot dev server', key: 'R' },
113
- { id: 'save', label: 'Save catalog / theme', key: 'w' },
114
- { id: 'refresh', label: 'Refresh package list', key: 'r' },
115
- { id: 'docs', label: 'Open documentation', key: 'i' },
116
- { id: 'settings', label: 'Settings', key: 'g' },
117
- { id: 'mode', label: 'Toggle install mode (USER/DEV)', key: 'd' },
118
- { id: 'build', label: 'Build (pnpm generate)', key: 'b' },
119
- { id: 'quit', label: 'Quit control panel', key: 'q' },
120
- ]
121
-
122
- const screen = blessed.screen({
123
- smartCSR: true,
124
- fullUnicode: true,
125
- title: 'Open Web Desktop',
126
- dockCorners: false,
127
- })
128
-
129
- const HEADER_ROWS = playgroundActive ? 4 : 3
130
- const MAIN_TOP = HEADER_ROWS
131
- const CLIENT_PANEL_H = 10
132
- const PANELS_TOP = MAIN_TOP + CLIENT_PANEL_H
133
-
134
- const titleBar = blessed.box({
135
- parent: screen,
136
- top: 0,
137
- left: 0,
138
- width: '100%',
139
- height: HEADER_ROWS,
140
- tags: true,
141
- content: '',
142
- style: { fg: C.title, bg: 'black' },
143
- })
144
-
145
- const clientBox = blessed.box({
146
- parent: screen,
147
- top: MAIN_TOP,
148
- left: 0,
149
- width: '58%',
150
- height: CLIENT_PANEL_H,
151
- tags: true,
152
- border: { type: 'line' },
153
- style: {
154
- fg: 'white',
155
- bg: 'black',
156
- border: { fg: C.border },
157
- },
158
- label: ' Dev server ',
159
- })
160
-
161
- const metricsBox = blessed.box({
162
- parent: screen,
163
- top: MAIN_TOP,
164
- left: '58%',
165
- width: '42%',
166
- height: CLIENT_PANEL_H,
167
- tags: true,
168
- border: { type: 'line' },
169
- style: {
170
- fg: 'white',
171
- bg: 'black',
172
- border: { fg: C.borderDim },
173
- },
174
- label: ' Metrics ',
175
- })
176
-
177
- const themeBox = blessed.box({
178
- parent: screen,
179
- top: PANELS_TOP,
180
- left: 0,
181
- width: '32%',
182
- height: '100%-15',
183
- tags: true,
184
- border: { type: 'line' },
185
- style: {
186
- fg: 'white',
187
- bg: 'black',
188
- border: { fg: C.borderDim },
189
- },
190
- label: ' Theme ',
191
- })
192
-
193
- const themeList = blessed.list({
194
- parent: themeBox,
195
- top: 1,
196
- left: 1,
197
- width: '100%-2',
198
- bottom: 9,
199
- tags: true,
200
- keys: true,
201
- vi: true,
202
- mouse: true,
203
- scrollbar: true,
204
- alwaysScroll: true,
205
- style: {
206
- fg: 'white',
207
- bg: 'black',
208
- selected: { bg: '#313244', fg: C.accent, bold: true },
209
- },
210
- })
211
-
212
- const themeInfo = blessed.box({
213
- parent: themeBox,
214
- bottom: 1,
215
- left: 1,
216
- width: '100%-2',
217
- height: 7,
218
- tags: true,
219
- content: '',
220
- style: { fg: C.muted, bg: 'black' },
221
- })
222
-
223
- let themeListSyncing = false
224
-
225
- const catalogBox = blessed.box({
226
- parent: screen,
227
- top: PANELS_TOP,
228
- left: '32%',
229
- width: '68%',
230
- height: '100%-15',
231
- tags: true,
232
- border: { type: 'line' },
233
- style: {
234
- fg: 'white',
235
- bg: 'black',
236
- border: { fg: C.focus },
237
- },
238
- label: ' Apps & modules ',
239
- })
240
-
241
- const tabBar = blessed.box({
242
- parent: catalogBox,
243
- top: 1,
244
- left: 1,
245
- width: '100%-2',
246
- height: 1,
247
- tags: true,
248
- content: '',
249
- })
250
-
251
- const catalogList = blessed.list({
252
- parent: catalogBox,
253
- top: 2,
254
- left: 1,
255
- width: '100%-2',
256
- bottom: 9,
257
- tags: true,
258
- keys: true,
259
- vi: true,
260
- mouse: true,
261
- scrollbar: true,
262
- alwaysScroll: true,
263
- style: {
264
- fg: 'white',
265
- bg: 'black',
266
- selected: { bg: '#45475a', fg: 'white', bold: true },
267
- },
268
- })
269
-
270
- const detailBox = blessed.box({
271
- parent: catalogBox,
272
- bottom: 1,
273
- left: 1,
274
- width: '100%-2',
275
- height: 7,
276
- tags: true,
277
- content: '',
278
- style: { fg: C.muted, bg: 'black' },
279
- })
280
-
281
- /** Keep list above detail; shrink detail on short terminals. */
282
- function layoutCatalogPanel() {
283
- const rows = screen.height ?? 24
284
- const detailH = rows < 26 ? 4 : rows < 34 ? 5 : 7
285
-
286
- Object.assign(detailBox.position, {
287
- bottom: 1,
288
- left: 1,
289
- width: '100%-2',
290
- height: detailH,
291
- top: undefined,
292
- right: undefined,
293
- })
294
-
295
- Object.assign(catalogList.position, {
296
- top: 2,
297
- left: 1,
298
- width: '100%-2',
299
- bottom: detailH + 2,
300
- height: undefined,
301
- right: undefined,
302
- })
303
-
304
- detailBox.emit('resize')
305
- catalogList.emit('resize')
306
- }
307
-
308
- layoutCatalogPanel()
309
- screen.on('resize', () => {
310
- layoutCatalogPanel()
311
- screen.render()
312
- })
313
-
314
- const helpBar = blessed.box({
315
- parent: screen,
316
- bottom: 0,
317
- left: 0,
318
- width: '100%',
319
- height: 5,
320
- tags: true,
321
- border: { type: 'line' },
322
- label: ' Keys ',
323
- style: { fg: C.muted, bg: '#11111b', border: { fg: C.borderDim } },
324
- })
325
-
326
- const settingsOverlay = blessed.box({
327
- parent: screen,
328
- top: 'center',
329
- left: 'center',
330
- width: 54,
331
- height: 14,
332
- hidden: true,
333
- tags: true,
334
- border: { type: 'line' },
335
- label: ' Settings ',
336
- style: { fg: 'white', bg: '#1e1e2e', border: { fg: C.accent } },
337
- content: '',
338
- })
339
-
340
- const settingsInfo = blessed.box({
341
- parent: settingsOverlay,
342
- top: 1,
343
- left: 2,
344
- width: '100%-4',
345
- height: 3,
346
- tags: true,
347
- style: { fg: C.muted, bg: '#1e1e2e' },
348
- })
349
-
350
- const githubInput = blessed.textbox({
351
- parent: settingsOverlay,
352
- top: 5,
353
- left: 2,
354
- width: '100%-4',
355
- height: 3,
356
- inputOnFocus: true,
357
- keys: true,
358
- mouse: true,
359
- style: { fg: 'white', bg: '#313244', focus: { border: { fg: C.accent } } },
360
- border: { type: 'line' },
361
- label: ' GitHub username ',
362
- })
363
-
364
- const menuOverlay = blessed.box({
365
- parent: screen,
366
- top: 'center',
367
- left: 'center',
368
- width: 52,
369
- height: MENU_ITEMS.length + 4,
370
- hidden: true,
371
- tags: true,
372
- border: { type: 'line' },
373
- label: ' Menu ',
374
- style: { fg: 'white', bg: '#1e1e2e', border: { fg: C.accent } },
375
- })
376
-
377
- const menuList = blessed.list({
378
- parent: menuOverlay,
379
- top: 1,
380
- left: 1,
381
- width: '100%-2',
382
- height: MENU_ITEMS.length + 1,
383
- tags: true,
384
- keys: true,
385
- vi: true,
386
- mouse: true,
387
- scrollbar: true,
388
- style: {
389
- fg: 'white',
390
- bg: '#1e1e2e',
391
- selected: { bg: '#313244', fg: C.accent, bold: true },
392
- },
393
- items: MENU_ITEMS.map(({ label, key }) => `[${key}] ${label}`),
394
- })
395
-
396
- function overlayBlocksKeys() {
397
- return settingsOpen || menuOpen
398
- }
399
-
400
- function installModeLabel() {
401
- return isWorkspaceInstallMode(settings) ? 'DEV' : 'USER'
402
- }
403
-
404
- function installModeColor() {
405
- return isWorkspaceInstallMode(settings) ? C.devMode : C.userMode
406
- }
407
-
408
- function renderModeHeader() {
409
- const isDev = isWorkspaceInstallMode(settings)
410
- const mode = installModeLabel()
411
- const color = installModeColor()
412
-
413
- const installLine = isDev
414
- ? settings.githubUser
415
- ? `save → git clone {bold}github.com/${settings.githubUser}/…{/}`
416
- : 'save → git clone {bold}github.com/owdproject/…{/}'
417
- : 'save → {bold}npm install{/} (registry)'
418
-
419
- const listCount = catalog.length
420
- ? `{bold}${catalog.length}{/} packages`
421
- : 'loading…'
422
- const listCache = catalogCacheAge ? ` · ${catalogCacheAge}` : ''
423
-
424
- const borderFlash = modeFlash > 0 ? 'white' : C.title
425
- titleBar.style.fg = borderFlash
426
-
427
- const playgroundLine = playgroundActive && playgroundLabel
428
- ? ` {${C.accent}-fg}Playground:{/} {bold}${playgroundLabel}{/} {${C.muted}-fg}[s] starts playground · catalog [w] → monorepo desktop{/}`
429
- : null
430
-
431
- titleBar.setContent(
432
- [
433
- ` {bold}{white-fg}Open Web Desktop{/}{/} {${C.muted}-fg}control panel{/} {${C.muted}-fg}[m] menu{/}`,
434
- playgroundLine,
435
- ` {${C.muted}-fg}Install mode{/} {bold}{${color}-fg}${mode}{/}{/} ${installLine} {${C.muted}-fg}[d] switch{/}`,
436
- ` {${C.muted}-fg}Package list{/} ${listCount} from GitHub${listCache} {${C.muted}-fg}[r] refresh{/}`,
437
- ]
438
- .filter(Boolean)
439
- .join('\n'),
440
- )
441
- }
442
-
443
- function themeEntries() {
444
- return mergeInstalled(filterCatalog(catalog, 'theme'), config, deps, workspaceRoot)
445
- }
446
-
447
- function catalogEntries() {
448
- return mergeInstalled(filterCatalog(catalog, activeTab), config, deps, workspaceRoot)
449
- }
450
-
451
- function applyFocusStyles() {
452
- const themeFocused = focusPanel === 'theme'
453
- themeBox.style.border.fg = themeFocused ? C.focus : C.borderDim
454
- catalogBox.style.border.fg = themeFocused ? C.borderDim : C.focus
455
- themeBox.setLabel(themeFocused ? ' {bold}Theme ◀{/} ' : ' Theme ')
456
- const tab = KINDS[activeTab].label
457
- catalogBox.setLabel(
458
- themeFocused ? ' Apps & modules ' : ` {bold}Apps & modules · ${tab} ◀{/} `,
459
- )
460
- }
461
-
462
- function panelLabel() {
463
- if (focusPanel === 'theme') return 'THEME'
464
- return `APPS & MODULES · ${KINDS[activeTab].label.toUpperCase()}`
465
- }
466
-
467
- function isDevServerUp() {
468
- return (
469
- devPhase === 'running' ||
470
- devPhase === 'starting' ||
471
- clientStatus.running ||
472
- clientStatus.http.up
473
- )
474
- }
475
-
476
- function serverKeyHints() {
477
- return `{${C.muted}-fg}[s]{/} start/stop {${C.muted}-fg}[m]{/} menu`
478
- }
479
-
480
- function defaultStatusHint() {
481
- return 'Press [s] start/stop · [m] menu · [w] save catalog changes'
482
- }
483
-
484
- function renderHelp(tone = 'info') {
485
- const statusColor = tone === 'error' ? C.err : tone === 'ok' ? C.accent : C.warn
486
- const msg = statusLine || defaultStatusHint()
487
- const progress =
488
- saveProgress !== null
489
- ? ` ${formatBar(saveProgress.step, saveProgress.total, 16)} ${saveProgress.label}`
490
- : ''
491
-
492
- const navLine =
493
- focusPanel === 'theme'
494
- ? `{${C.muted}-fg}Navigate{/} {bold}↑↓{/} {bold}PgUp/PgDn{/} scroll {bold}Enter{/}/{bold}Space{/} apply {bold}Tab{/} → apps & modules`
495
- : `{${C.muted}-fg}Navigate{/} {bold}↑↓{/} {bold}j/k{/} {bold}PgUp/PgDn{/} scroll {bold}Space{/} toggle {bold}Tab{/} → theme {bold}1{/} apps {bold}2{/} modules {bold}3{/} theme panel`
496
-
497
- helpBar.setContent(
498
- [
499
- `{bold}{${statusColor}-fg}${msg}{/}{/}${progress}`,
500
- `{${C.focus}-fg}▸{/} {bold}${panelLabel()}{/}`,
501
- navLine,
502
- `{${C.muted}-fg}Server{/} {bold}s{/} start/stop {bold}m{/} menu {${C.muted}-fg}│{/} {bold}w{/} save {bold}r{/} refresh {bold}i{/} docs {bold}g{/} settings {bold}q{/} quit`,
503
- ].join('\n'),
504
- )
505
- }
506
-
507
- function setStatus(msg, tone = 'info') {
508
- statusLine = msg
509
- renderHelp(tone)
510
- screen.render()
511
- }
512
-
513
- /** Clear stray CLI output and redraw the full TUI (after save, build, etc.). */
514
- function refreshTerminalUi() {
515
- try {
516
- screen.program.clear()
517
- screen.program.home()
518
- } catch {
519
- process.stdout.write('\x1b[2J\x1b[H')
520
- }
521
- layoutCatalogPanel()
522
- if (focusPanel === 'theme') themeList.focus()
523
- else catalogList.focus()
524
- renderAll()
525
- }
526
-
527
- function startSpinner(message) {
528
- stopSpinner()
529
- spinnerFrame = 0
530
- setStatus(message)
531
- spinnerTimer = setInterval(() => {
532
- spinnerFrame = (spinnerFrame + 1) % SPINNER.length
533
- statusLine = `${SPINNER[spinnerFrame]} ${message}`
534
- renderHelp('info')
535
- screen.render()
536
- }, 120)
537
- }
538
-
539
- function stopSpinner() {
540
- if (spinnerTimer) {
541
- clearInterval(spinnerTimer)
542
- spinnerTimer = null
543
- }
544
- }
545
-
546
- async function flashModeBorder() {
547
- for (let i = 3; i > 0; i--) {
548
- modeFlash = i
549
- renderModeHeader()
550
- screen.render()
551
- await new Promise((r) => setTimeout(r, 80))
552
- }
553
- modeFlash = 0
554
- renderModeHeader()
555
- }
556
-
557
- function toggleInstallMode() {
558
- const next = isWorkspaceInstallMode(settings) ? 'npm' : 'workspace'
559
- settings = { ...settings, installMode: normalizeInstallMode(next) }
560
- saveSettings(workspaceRoot, settings)
561
- renderModeHeader()
562
- renderCatalogList()
563
- renderDetail()
564
- flashModeBorder()
565
- setStatus(
566
- next === 'workspace'
567
- ? 'Dev mode — saves will git clone into apps/packages/themes'
568
- : 'User mode — saves will install from npm',
569
- 'ok',
570
- )
571
- }
572
-
573
- function renderSettingsPanel() {
574
- const mode = installModeLabel()
575
- const color = installModeColor()
576
- const saveBehavior = isWorkspaceInstallMode(settings)
577
- ? settings.githubUser
578
- ? `git clone from github.com/${settings.githubUser}/…`
579
- : 'git clone from github.com/owdproject/…'
580
- : 'install from npm registry'
581
-
582
- settingsInfo.setContent(
583
- [
584
- `{bold}Install mode{/} {${color}-fg}${mode}{/}`,
585
- `{${C.muted}-fg}When you press {bold}w{/} (save): ${saveBehavior}{/}`,
586
- `{${C.muted}-fg}[d] toggle USER/DEV · GitHub username below = your fork org (optional){/}`,
587
- ].join('\n'),
588
- )
589
- }
590
-
591
- function openSettings() {
592
- settingsOpen = true
593
- settingsOverlay.show()
594
- renderSettingsPanel()
595
- githubInput.setValue(settings.githubUser ?? '')
596
- githubInput.focus()
597
- setStatus('Settings — Enter save · Esc cancel · d toggles USER/DEV')
598
- screen.render()
599
- }
600
-
601
- function closeSettings() {
602
- settingsOpen = false
603
- settingsOverlay.hide()
604
- focusCatalog()
605
- screen.render()
606
- }
607
-
608
- function openMenu() {
609
- if (settingsOpen) return
610
- menuOpen = true
611
- menuOverlay.show()
612
- menuList.focus()
613
- setStatus('Menu — ↑↓ select · Enter run · Esc close')
614
- screen.render()
615
- }
616
-
617
- function closeMenu() {
618
- menuOpen = false
619
- menuOverlay.hide()
620
- focusCatalog()
621
- screen.render()
622
- }
623
-
624
- async function runMenuAction(id) {
625
- closeMenu()
626
- switch (id) {
627
- case 'start':
628
- await startDevServer()
629
- break
630
- case 'stop':
631
- await stopDevServer()
632
- break
633
- case 'reboot':
634
- await rebootDevServer()
635
- break
636
- case 'save':
637
- await applyChanges()
638
- break
639
- case 'refresh':
640
- await refreshCatalog()
641
- break
642
- case 'docs':
643
- openDocsInBrowser()
644
- break
645
- case 'settings':
646
- openSettings()
647
- break
648
- case 'mode':
649
- toggleInstallMode()
650
- break
651
- case 'build':
652
- stopSpinner()
653
- setStatus('Running pnpm run generate…')
654
- screen.render()
655
- try {
656
- execSync('pnpm run generate', { cwd: workspaceRoot, stdio: 'pipe' })
657
- setStatus('Generate finished', 'ok')
658
- } catch (err) {
659
- setStatus(`Generate failed: ${err.message}`, 'error')
660
- } finally {
661
- refreshTerminalUi()
662
- }
663
- break
664
- case 'quit':
665
- process.exit(0)
666
- break
667
- default:
668
- break
669
- }
670
- }
671
-
672
- function saveSettingsFromOverlay() {
673
- const user = githubInput.getValue().trim() || null
674
- settings = { ...settings, githubUser: user }
675
- saveSettings(workspaceRoot, settings)
676
- closeSettings()
677
- renderModeHeader()
678
- renderCatalogList()
679
- renderDetail()
680
- setStatus(user ? `GitHub user → ${user}` : 'GitHub user cleared', 'ok')
681
- }
682
-
683
- function formatThemeInfo(meta) {
684
- const name = meta.name?.replace('@owdproject/', '') ?? '—'
685
- const authorShort = String(meta.author ?? '—')
686
- .replace(/\s*·\s*https?:\/\/\S+/g, '')
687
- .slice(0, 48)
688
- const gitShort = meta.remote
689
- ? meta.remote
690
- .replace(/^git\+/, '')
691
- .replace(/^https?:\/\/github\.com\//, '')
692
- .replace(/\.git$/, '')
693
- : null
694
- const desc = meta.description
695
- ? meta.description.length > 72
696
- ? `${meta.description.slice(0, 72)}…`
697
- : meta.description
698
- : null
699
-
700
- return [
701
- `{${C.borderDim}-fg}{underline} ── selected ── {/}`,
702
- `{bold}{${C.accent}-fg}${name}{/}{/} {${C.muted}-fg}v${meta.version} · ${meta.license}{/}`,
703
- desc ? `{${C.muted}-fg}${desc}{/}` : null,
704
- gitShort
705
- ? `{${C.muted}-fg}${authorShort}{/} · {${C.accent}-fg}${gitShort}{/}`
706
- : `{${C.muted}-fg}${authorShort}{/}`,
707
- ]
708
- .filter(Boolean)
709
- .join('\n')
710
- }
711
-
712
- function renderTabs() {
713
- const tabs = ['app', 'module'].map((key, i) => {
714
- const active = activeTab === key
715
- const label = KINDS[key].label
716
- return active
717
- ? `{bold}{${C.accent}-fg} ${i + 1}:${label} {/}{/}`
718
- : `{${C.muted}-fg} ${i + 1}:${label}{/}`
719
- })
720
- tabBar.setContent(` ${tabs.join(' ')} {${C.muted}-fg}│ {bold}3{/} or {bold}Tab{/} → theme (left){/}`)
721
- }
722
-
723
- function renderClient() {
724
- const http = clientStatus.http
725
- let stateLabel
726
- let dotChar
727
- let dotColor
728
-
729
- if (devPhase === 'starting') {
730
- stateLabel = 'STARTING…'
731
- dotChar = SPINNER[spinnerFrame % SPINNER.length]
732
- dotColor = C.warn
733
- } else if (clientStatus.running || http.up) {
734
- stateLabel = 'RUNNING'
735
- dotChar = pulseOn ? '●' : '◉'
736
- dotColor = C.accent
737
- devPhase = 'running'
738
- } else {
739
- stateLabel = 'STOPPED'
740
- dotChar = '○'
741
- dotColor = C.err
742
- devPhase = 'stopped'
743
- }
744
-
745
- const serverRunning = clientStatus.running || http.up
746
- const pidLine = clientStatus.pid
747
- ? `PID ${clientStatus.pid} ${clientStatus.stats.memMb} MiB ${clientStatus.stats.threads} thr`
748
- : serverRunning
749
- ? 'HTTP responding (process not matched)'
750
- : 'dev server stopped'
751
-
752
- const warnDev =
753
- isWorkspaceInstallMode(settings) && !settings.githubUser
754
- ? `\n {${C.warn}-fg}⚠ DEV mode: open settings [g] to set your GitHub username for fork clones{/}`
755
- : ''
756
-
757
- const docsInstalled = hasDocsModuleInstalled(config, deps)
758
- const docsLine = docsInstalled
759
- ? clientStatus.running || http.up
760
- ? ` {${C.muted}-fg}Documentation installed{/} {${C.accent}-fg}[i]{/} open ${docsBasePathFromConfig(config)}`
761
- : ` {${C.muted}-fg}Documentation installed{/} {${C.muted}-fg}[s] start dev server, then [i] open docs{/}`
762
- : ''
763
-
764
- const devTargetLine = playgroundActive && playgroundLabel
765
- ? ` {${C.accent}-fg}Dev target:{/} {bold}${playgroundLabel}{/} playground`
766
- : null
767
-
768
- clientBox.setContent(
769
- [
770
- ` {${dotColor}-fg}${dotChar}{/} {bold}${stateLabel}{/} http://127.0.0.1:${settings.devPort} HTTP ${http.status || '—'}`,
771
- ` ${pidLine}`,
772
- devTargetLine,
773
- ` ${serverKeyHints()}`,
774
- ` {${C.muted}-fg}Theme:{/} {bold}${(pendingTheme ?? config.theme ?? '—').replace('@owdproject/', '')}{/} {${C.muted}-fg}│ [w] save{/}`,
775
- docsLine,
776
- warnDev,
777
- ]
778
- .filter(Boolean)
779
- .join('\n'),
780
- )
781
- }
782
-
783
- function getDesiredPackages(kind) {
784
- if (kind === 'theme') {
785
- return pendingTheme ?? config.theme ? [pendingTheme ?? config.theme] : []
786
- }
787
- return filterCatalog(catalog, kind)
788
- .filter((item) => {
789
- const pending = pendingPackages.get(item.name)
790
- if (pending !== undefined) return pending
791
- return kind === 'app'
792
- ? (config.apps ?? []).includes(item.name)
793
- : (config.modules ?? []).includes(item.name)
794
- })
795
- .map((item) => item.name)
796
- }
797
-
798
- function enabledCountForTab(kind) {
799
- return getDesiredPackages(kind).length
800
- }
801
-
802
- function renderMetrics() {
803
- const tabLabel = KINDS[activeTab].label
804
- const onDesktop = enabledCountForTab(activeTab)
805
-
806
- const spark = memHistory.length ? formatSparkline(memHistory) : '—'
807
- const memLabel =
808
- memHistory.length > 0
809
- ? `${memHistory[memHistory.length - 1]} MiB`
810
- : clientStatus.stats.memMb
811
- ? `${clientStatus.stats.memMb} MiB`
812
- : '—'
813
-
814
- const apps = enabledCountForTab('app')
815
- const mods = enabledCountForTab('module')
816
- const themeShort = (pendingTheme ?? config.theme ?? '—').replace('@owdproject/', '')
817
-
818
- const unsaved = pendingPackages.size
819
- const unsavedLine =
820
- unsaved > 0
821
- ? ` {${C.warn}-fg}${unsaved}{/} {${C.muted}-fg}unsaved — [w] save{/}`
822
- : null
823
-
824
- metricsBox.setContent(
825
- [
826
- ` {${C.muted}-fg}Memory{/} {bold}${memLabel}{/}`,
827
- ` {${C.accent}-fg}${spark}{/}`,
828
- ` {${C.muted}-fg}${tabLabel}{/} {bold}${onDesktop}{/} {${C.muted}-fg}on desktop{/}`,
829
- ` {${C.muted}-fg}Stack{/} ${themeShort} · ${apps} apps · ${mods} modules`,
830
- unsavedLine,
831
- ]
832
- .filter(Boolean)
833
- .join('\n'),
834
- )
835
- }
836
-
837
- function restoreThemeListSelection(entries, themeName) {
838
- if (!themeName || !entries.length) return
839
- const idx = entries.findIndex((e) => e.name === themeName)
840
- if (idx < 0) return
841
- themeList._listInitialized = false
842
- themeList.select(idx)
843
- }
844
-
845
- function renderThemeList() {
846
- const entries = themeEntries()
847
- const activeTheme = pendingTheme ?? config.theme
848
- const items = entries.map((item) => {
849
- const isActive = activeTheme === item.name
850
- const bullet = isActive ? `{${C.accent}-fg}●{/}` : '○'
851
- const tag = item.org === 'workspace' ? `{${C.muted}-fg}[local]{/}` : `{${C.muted}-fg}[${installTag(settings, item)}]{/}`
852
- return `${bullet} ${item.shortName} ${tag}`
853
- })
854
-
855
- themeListSyncing = true
856
- themeList.setItems(items.length ? items : ['{gray-fg}(no themes in catalog){/}'])
857
- restoreThemeListSelection(entries, activeTheme)
858
- themeListSyncing = false
859
-
860
- const meta = readThemeMeta(workspaceRoot, activeTheme ?? '')
861
- themeInfo.setContent(formatThemeInfo(meta))
862
- }
863
-
864
- function previewThemeAt(index, announce = false) {
865
- const entries = themeEntries()
866
- const item = entries[index]
867
- if (!item) return
868
- pendingTheme = item.name
869
- renderThemeList()
870
- renderClient()
871
- renderMetrics()
872
- if (announce) {
873
- setStatus(`Theme → ${item.shortName} (press [w] to save)`, 'ok')
874
- }
875
- }
876
-
877
- function restoreCatalogListSelection(entries, pkgName) {
878
- if (!pkgName || !entries.length) return
879
- const idx = entries.findIndex((e) => e.name === pkgName)
880
- if (idx < 0) return
881
- catalogList._listInitialized = false
882
- catalogList.select(idx)
883
- }
884
-
885
- function listPageStep(list) {
886
- return Math.max(1, list.height - list.iheight - 1)
887
- }
888
-
889
- function renderCatalogList() {
890
- const entries = catalogEntries()
891
- const prevPkg = entries[catalogList.selected]?.name
892
- const items = entries.map((item) => {
893
- const pending = pendingPackages.get(item.name)
894
- const on = pending !== undefined ? pending : item.installed
895
- const box = on ? `{${C.accent}-fg}[✓]{/}` : '[ ]'
896
- const tag = `{${C.muted}-fg}[${installTag(settings, item)}]{/}`
897
- const desc = (item.description ?? '').slice(0, 36)
898
- return `${box} {bold}${item.shortName}{/} ${tag} ${desc}`
899
- })
900
- catalogList.setItems(items.length ? items : ['{gray-fg}Loading…{/}'])
901
- restoreCatalogListSelection(entries, prevPkg)
902
- renderTabs()
903
- }
904
-
905
- async function renderDetail() {
906
- const entries = catalogEntries()
907
- const idx = catalogList.selected
908
- const item = entries[idx]
909
- if (!item) {
910
- detailBox.setContent(`{${C.muted}-fg}Select a package for install preview{/}`)
911
- return
912
- }
913
-
914
- const plan = await resolveInstallPlan(item.name, settings, workspaceRoot)
915
- if (plan.error) {
916
- detailBox.setContent(`{${C.err}-fg}${plan.error}{/}`)
917
- return
918
- }
919
-
920
- const kind = KINDS[activeTab]
921
- const target = plan.targetDir ?? `${kind.workspaceDir}/${item.shortName}`
922
- const npmOnly =
923
- isWorkspaceInstallMode(settings) && item.installed && !item.localSource
924
- detailBox.setContent(
925
- [
926
- `{bold}{${C.accent}-fg}${item.shortName}{/}{/}`,
927
- `{${C.muted}-fg}Source:{/} ${plan.label ?? plan.mode}`,
928
- `{${C.muted}-fg}Target:{/} ${target}/`,
929
- npmOnly
930
- ? `{${C.warn}-fg}[npm] in config — save [w] to clone into workspace{/}`
931
- : null,
932
- item.description ? `{${C.muted}-fg}${item.description.slice(0, 120)}{/}` : null,
933
- ]
934
- .filter(Boolean)
935
- .join('\n'),
936
- )
937
- }
938
-
939
- function renderAll() {
940
- renderModeHeader()
941
- applyFocusStyles()
942
- renderClient()
943
- renderMetrics()
944
- renderThemeList()
945
- renderCatalogList()
946
- renderDetail()
947
- renderHelp('info')
948
- screen.render()
949
- }
950
-
951
- function focusTheme() {
952
- focusPanel = 'theme'
953
- themeList.focus()
954
- applyFocusStyles()
955
- setStatus('Theme panel — ↑↓ to pick, Enter to apply, Tab for packages')
956
- }
957
-
958
- function focusCatalog() {
959
- focusPanel = 'catalog'
960
- catalogList.focus()
961
- applyFocusStyles()
962
- setStatus(`${KINDS[activeTab].label} — Space toggles, [w] saves when done`)
963
- }
964
-
965
- function selectThemeAt(index) {
966
- previewThemeAt(index, true)
967
- }
968
-
969
- function toggleCatalogAt(index) {
970
- const entries = catalogEntries()
971
- const item = entries[index]
972
- if (!item) return
973
- const pending = pendingPackages.get(item.name)
974
- const next = pending !== undefined ? !pending : !item.installed
975
- pendingPackages.set(item.name, next)
976
- renderCatalogList()
977
- renderMetrics()
978
- renderDetail()
979
- setStatus(`${next ? 'Will install' : 'Will remove'} ${item.shortName}`)
980
- }
981
-
982
- async function applyChanges() {
983
- stopSpinner()
984
- const packagesToInstall = []
985
- const packagesToMaterialize = []
986
- const nextApps = getDesiredPackages('app')
987
- const nextModules = getDesiredPackages('module').filter(isInstallableDesktopModule)
988
- const nextTheme = pendingTheme ?? config.theme
989
- const prevApps = new Set(config.apps ?? [])
990
- const prevModules = new Set(config.modules ?? [])
991
-
992
- for (const pkg of nextApps) {
993
- if (!prevApps.has(pkg)) packagesToInstall.push(pkg)
994
- }
995
- for (const pkg of nextModules) {
996
- if (!prevModules.has(pkg)) packagesToInstall.push(pkg)
997
- }
998
- if (nextTheme && nextTheme !== config.theme) packagesToInstall.push(nextTheme)
999
-
1000
- if (isWorkspaceInstallMode(settings)) {
1001
- const enabled = [...nextApps, ...nextModules, nextTheme].filter(Boolean)
1002
- for (const pkg of enabled) {
1003
- if (packagesToInstall.includes(pkg)) continue
1004
- if (!hasLocalWorkspaceSource(workspaceRoot, pkg)) {
1005
- packagesToMaterialize.push(pkg)
1006
- }
1007
- }
1008
- }
1009
-
1010
- const totalSteps = packagesToInstall.length + packagesToMaterialize.length + 2
1011
- let step = 0
1012
- let didClone = false
1013
-
1014
- const bump = (label) => {
1015
- step++
1016
- saveProgress = { step, total: totalSteps, label }
1017
- setStatus(label)
1018
- screen.render()
1019
- }
1020
-
1021
- try {
1022
- bump('Preparing…')
1023
-
1024
- for (const pkg of packagesToInstall) {
1025
- bump(`Installing ${shortName(pkg)}…`)
1026
- await installPackage(pkg, settings, workspaceRoot, { stdio: 'pipe' })
1027
- if (isWorkspaceInstallMode(settings)) didClone = true
1028
- }
1029
-
1030
- for (const pkg of packagesToMaterialize) {
1031
- const plan = await resolveInstallPlan(pkg, settings, workspaceRoot)
1032
- if (plan.error) throw new Error(plan.error)
1033
- const from = plan.label ?? plan.source?.label ?? shortName(pkg)
1034
- bump(`Cloning ${shortName(pkg)} from ${from}…`)
1035
- await materializeToWorkspace(pkg, settings, workspaceRoot, { stdio: 'pipe' })
1036
- didClone = true
1037
- }
1038
-
1039
- if (isWorkspaceInstallMode(settings) && didClone) {
1040
- bump('prepare:modules…')
1041
- runPrepareModules(workspaceRoot, 'pipe')
1042
- }
1043
-
1044
- bump('Writing config…')
1045
- writeDesktopConfig(resolveConfigPathForWrite(paths), workspaceRoot, {
1046
- theme: nextTheme,
1047
- apps: nextApps,
1048
- modules: nextModules,
1049
- })
1050
-
1051
- for (const pkg of config.apps ?? []) {
1052
- if (!nextApps.includes(pkg)) {
1053
- execSync(`pnpm remove ${pkg}`, { cwd: paths.desktop, stdio: 'pipe' })
1054
- }
1055
- }
1056
- for (const pkg of config.modules ?? []) {
1057
- if (!nextModules.includes(pkg)) {
1058
- execSync(`pnpm remove ${pkg}`, { cwd: paths.desktop, stdio: 'pipe' })
1059
- }
1060
- }
1061
-
1062
- pendingPackages.clear()
1063
- paths.config = resolveConfigPathForWrite(paths)
1064
- config = readDesktopConfig(paths.config, workspaceRoot)
1065
- deps = readDesktopDependencies(paths.packageJson)
1066
- pendingTheme = config.theme
1067
- saveProgress = null
1068
- renderAll()
1069
- setStatus('Saved successfully', 'ok')
1070
- } catch (err) {
1071
- saveProgress = null
1072
- setStatus(`Save failed: ${err.message}`, 'error')
1073
- }
1074
- }
1075
-
1076
- async function startDevServer() {
1077
- if (devPhase === 'starting') {
1078
- setStatus('Dev server is already starting…')
1079
- return
1080
- }
1081
- if (isDevServerUp()) {
1082
- setStatus('Dev server already running — [m] menu to stop or reboot', 'info')
1083
- return
1084
- }
1085
-
1086
- devPhase = 'starting'
1087
- startSpinner('Starting dev server…')
1088
- renderAll()
1089
- startDev(devTarget)
1090
- clientStatus = await waitForDev(workspaceRoot, settings.devPort)
1091
- stopSpinner()
1092
- devPhase = clientStatus.http.up ? 'running' : 'stopped'
1093
- renderAll()
1094
- const targetHint = playgroundActive && playgroundLabel ? ` (${playgroundLabel})` : ''
1095
- setStatus(
1096
- clientStatus.http.up
1097
- ? `Dev server up on port ${settings.devPort}${targetHint} — [m] menu to stop or reboot`
1098
- : `Dev not responding — check ${devLogPath(workspaceRoot)}`,
1099
- clientStatus.http.up ? 'ok' : 'error',
1100
- )
1101
- }
1102
-
1103
- async function stopDevServer() {
1104
- if (devPhase === 'starting') {
1105
- setStatus('Dev server is still starting…')
1106
- return
1107
- }
1108
- if (!isDevServerUp()) {
1109
- setStatus('Dev server not running — press [s] to start', 'info')
1110
- return
1111
- }
1112
-
1113
- stopDev(workspaceRoot, clientStatus.pid)
1114
- devPhase = 'stopped'
1115
- memHistory.length = 0
1116
- clientStatus = await getClientStatus(workspaceRoot, settings.devPort)
1117
- renderAll()
1118
- setStatus('Dev server stopped — press [s] to start', 'ok')
1119
- }
1120
-
1121
- async function rebootDevServer() {
1122
- if (devPhase === 'starting') {
1123
- setStatus('Cannot reboot while dev server is starting…')
1124
- return
1125
- }
1126
- if (!isDevServerUp()) {
1127
- setStatus('Dev server not running — press [s] to start', 'info')
1128
- return
1129
- }
1130
-
1131
- devPhase = 'starting'
1132
- startSpinner('Rebooting dev server…')
1133
- renderAll()
1134
- stopDev(workspaceRoot, clientStatus.pid)
1135
- memHistory.length = 0
1136
- await new Promise((r) => setTimeout(r, 800))
1137
- startDev(devTarget)
1138
- clientStatus = await waitForDev(workspaceRoot, settings.devPort)
1139
- stopSpinner()
1140
- devPhase = clientStatus.http.up ? 'running' : 'stopped'
1141
- renderAll()
1142
- const targetHint = playgroundActive && playgroundLabel ? ` (${playgroundLabel})` : ''
1143
- setStatus(
1144
- clientStatus.http.up
1145
- ? `Dev server rebooted on port ${settings.devPort}${targetHint}`
1146
- : `Reboot failed — check ${devLogPath(workspaceRoot)}`,
1147
- clientStatus.http.up ? 'ok' : 'error',
1148
- )
1149
- }
1150
-
1151
- async function refreshCatalog() {
1152
- startSpinner('Fetching GitHub catalog…')
1153
- try {
1154
- const result = await loadCatalog(workspaceRoot, settings)
1155
- catalog = result.entries
1156
- catalogCacheAge = result.cacheAge ?? ''
1157
- config = readDesktopConfig(paths.config, workspaceRoot)
1158
- deps = readDesktopDependencies(paths.packageJson)
1159
- pendingTheme = pendingTheme ?? config.theme
1160
- stopSpinner()
1161
- renderAll()
1162
- setStatus(`Package list: ${catalog.length} from GitHub`, 'ok')
1163
- } catch (err) {
1164
- stopSpinner()
1165
- setStatus(`Package list error: ${err.message}`, 'error')
1166
- }
1167
- }
1168
-
1169
- screen.key(['escape'], () => {
1170
- if (settingsOpen) {
1171
- closeSettings()
1172
- return
1173
- }
1174
- if (menuOpen) {
1175
- closeMenu()
1176
- }
1177
- })
1178
-
1179
- screen.key(['q', 'C-c'], () => {
1180
- if (settingsOpen) {
1181
- closeSettings()
1182
- return
1183
- }
1184
- if (menuOpen) {
1185
- closeMenu()
1186
- return
1187
- }
1188
- process.exit(0)
1189
- })
1190
-
1191
- screen.key(['tab'], () => {
1192
- if (overlayBlocksKeys()) return
1193
- if (focusPanel === 'catalog') focusTheme()
1194
- else focusCatalog()
1195
- })
1196
- screen.key(['S-tab'], () => {
1197
- if (!overlayBlocksKeys()) focusCatalog()
1198
- })
1199
-
1200
- screen.key(['m', 'M'], () => {
1201
- if (settingsOpen) return
1202
- if (menuOpen) closeMenu()
1203
- else openMenu()
1204
- })
1205
-
1206
- screen.key(['d', 'D'], () => {
1207
- if (!overlayBlocksKeys()) toggleInstallMode()
1208
- })
1209
- screen.key(['s', 'S'], () => {
1210
- if (!overlayBlocksKeys()) {
1211
- if (isDevServerUp()) stopDevServer()
1212
- else startDevServer()
1213
- }
1214
- })
1215
- screen.key(['x', 'X'], () => {
1216
- if (!overlayBlocksKeys()) stopDevServer()
1217
- })
1218
- screen.key(['R'], () => {
1219
- if (!overlayBlocksKeys()) rebootDevServer()
1220
- })
1221
- screen.key(['g'], () => {
1222
- if (!overlayBlocksKeys()) openSettings()
1223
- })
1224
-
1225
- function openDocsInBrowser() {
1226
- if (!hasDocsModuleInstalled(config, deps)) {
1227
- setStatus('Install @owdproject/module-docs first', 'error')
1228
- return
1229
- }
1230
- const http = clientStatus.http
1231
- if (!clientStatus.running && !http.up) {
1232
- setStatus('Start dev server [s] before opening docs', 'error')
1233
- return
1234
- }
1235
- const base = docsBasePathFromConfig(config)
1236
- const url = `http://127.0.0.1:${settings.devPort}${base}`
1237
- const platform = process.platform
1238
- try {
1239
- if (platform === 'darwin') {
1240
- execSync('open', [url])
1241
- } else if (platform === 'win32') {
1242
- execSync('cmd', ['/c', 'start', '', url])
1243
- } else {
1244
- execSync('xdg-open', [url])
1245
- }
1246
- setStatus(`Opened ${url}`, 'ok')
1247
- } catch (err) {
1248
- setStatus(`Could not open browser: ${err.message}`, 'error')
1249
- }
1250
- }
1251
-
1252
- screen.key(['i', 'I'], () => {
1253
- if (!overlayBlocksKeys()) openDocsInBrowser()
1254
- })
1255
- screen.key(['b'], () => {
1256
- if (!overlayBlocksKeys()) {
1257
- stopSpinner()
1258
- setStatus('Running pnpm run generate…')
1259
- screen.render()
1260
- try {
1261
- execSync('pnpm run generate', { cwd: workspaceRoot, stdio: 'pipe' })
1262
- setStatus('Generate finished', 'ok')
1263
- } catch (err) {
1264
- setStatus(`Generate failed: ${err.message}`, 'error')
1265
- } finally {
1266
- refreshTerminalUi()
1267
- }
1268
- }
1269
- })
1270
- screen.key(['w', 'W'], () => {
1271
- if (!overlayBlocksKeys()) applyChanges()
1272
- })
1273
- screen.key(['r'], () => {
1274
- if (!overlayBlocksKeys()) refreshCatalog()
1275
- })
1276
-
1277
- screen.key(['1'], () => {
1278
- if (overlayBlocksKeys()) return
1279
- activeTab = 'app'
1280
- focusCatalog()
1281
- renderAll()
1282
- })
1283
- screen.key(['2'], () => {
1284
- if (overlayBlocksKeys()) return
1285
- activeTab = 'module'
1286
- focusCatalog()
1287
- renderAll()
1288
- })
1289
- screen.key(['3', 't'], () => {
1290
- if (overlayBlocksKeys()) return
1291
- focusTheme()
1292
- renderAll()
1293
- })
1294
-
1295
- githubInput.key(['enter'], () => saveSettingsFromOverlay())
1296
- githubInput.key(['escape'], () => closeSettings())
1297
-
1298
- menuList.on('select', (_item, index) => {
1299
- const entry = MENU_ITEMS[index]
1300
- if (entry) runMenuAction(entry.id)
1301
- })
1302
- menuList.key(['escape'], () => closeMenu())
1303
-
1304
- themeList.on('select item', (_item, index) => {
1305
- if (themeListSyncing) return
1306
- previewThemeAt(index)
1307
- })
1308
- themeList.on('select', (_item, index) => {
1309
- if (themeListSyncing) return
1310
- selectThemeAt(index)
1311
- })
1312
- themeList.key(['space'], function () {
1313
- selectThemeAt(this.selected)
1314
- })
1315
- themeList.key(['pageup'], function () {
1316
- this.up(listPageStep(this))
1317
- this.screen.render()
1318
- })
1319
- themeList.key(['pagedown'], function () {
1320
- this.down(listPageStep(this))
1321
- this.screen.render()
1322
- })
1323
-
1324
- catalogList.on('select item', () => {
1325
- renderDetail()
1326
- })
1327
- catalogList.on('select', (_item, index) => toggleCatalogAt(index))
1328
- catalogList.key(['space'], function () {
1329
- toggleCatalogAt(this.selected)
1330
- })
1331
- catalogList.key(['pageup'], function () {
1332
- this.up(listPageStep(this))
1333
- renderDetail()
1334
- this.screen.render()
1335
- })
1336
- catalogList.key(['pagedown'], function () {
1337
- this.down(listPageStep(this))
1338
- renderDetail()
1339
- this.screen.render()
1340
- })
1341
-
1342
- await refreshCatalog()
1343
- clientStatus = await getClientStatus(workspaceRoot, settings.devPort)
1344
- devPhase = clientStatus.running ? 'running' : 'stopped'
1345
- focusCatalog()
1346
- renderAll()
1347
-
1348
- const serverHint = ' Press [s] start/stop · [m] menu.'
1349
-
1350
- if (!isWorkspaceInstallMode(settings)) {
1351
- setStatus(
1352
- `USER mode: save installs from npm. Press [d] to switch to DEV (git clone).${serverHint}`,
1353
- 'info',
1354
- )
1355
- } else if (!settings.githubUser) {
1356
- setStatus(
1357
- `DEV mode: save clones from github.com/owdproject. Press [g] for your GitHub username.${serverHint}`,
1358
- 'ok',
1359
- )
1360
- } else if (serverHint) {
1361
- setStatus(serverHint.trim(), 'info')
1362
- }
1363
-
1364
- setInterval(async () => {
1365
- pulseOn = !pulseOn
1366
- if (devPhase !== 'starting' && !settingsOpen) {
1367
- clientStatus = await getClientStatus(workspaceRoot, settings.devPort)
1368
- if (clientStatus.stats.memMb > 0) {
1369
- memHistory.push(clientStatus.stats.memMb)
1370
- if (memHistory.length > MEM_HISTORY_LEN) memHistory.shift()
1371
- }
1372
- if (clientStatus.http.up) devPhase = 'running'
1373
- else if (devPhase === 'running') devPhase = 'stopped'
1374
- }
1375
- renderClient()
1376
- renderMetrics()
1377
- screen.render()
1378
- }, 2000)
1379
- }