@owdproject/cli 0.0.3 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +20 -0
- package/README.md +50 -16
- package/bin/bridge.js +129 -0
- package/bin/cli.js +491 -0
- package/bin/cp.js +3542 -0
- package/bin/desktop.js +5 -0
- package/bin/lib/catalog.js +504 -0
- package/bin/lib/config.js +173 -0
- package/bin/lib/cpAscii.js +39 -0
- package/bin/lib/cpFormat.js +343 -0
- package/bin/lib/create.js +246 -0
- package/bin/lib/desktopConfig.js +48 -0
- package/bin/lib/generateTemplate.js +427 -0
- package/bin/lib/install.js +521 -0
- package/bin/lib/installWizardFlow.js +792 -0
- package/bin/lib/migrateModulePlayground.config.mjs +187 -0
- package/bin/lib/migrateModulePlayground.mjs +318 -0
- package/bin/lib/npmVersions.js +124 -0
- package/bin/lib/packageSources.js +375 -0
- package/bin/lib/playgroundContext.js +188 -0
- package/bin/lib/scaffold.js +205 -0
- package/bin/lib/status.js +319 -0
- package/bin/lib/validateModule.js +978 -0
- package/bin/lib/workspace.js +289 -0
- package/bin/owd.js +2 -118
- package/go/bridge/bridge.go +220 -0
- package/go/desktop-go +0 -0
- package/go/go.mod +33 -0
- package/go/go.sum +52 -0
- package/go/main.go +34 -0
- package/go/owd-cli +0 -0
- package/go/tui/engine.go +472 -0
- package/go/tui/engine_test.go +40 -0
- package/go/tui/handlers.go +604 -0
- package/go/tui/helpers.go +916 -0
- package/go/tui/helpers_test.go +97 -0
- package/go/tui/modals.go +423 -0
- package/go/tui/resolver.go +377 -0
- package/go/tui/resolver_test.go +86 -0
- package/go/tui/styles.go +153 -0
- package/go/tui/tasks.go +850 -0
- package/go/tui/tui.go +869 -0
- package/go/tui/tui_test.go +69 -0
- package/go/tui/types.go +230 -0
- package/go/tui/view.go +812 -0
- package/go/tui/workqueue.go +179 -0
- package/go/tui/workqueue_test.go +90 -0
- package/package.json +33 -22
- package/src/commands/create.js +0 -11
- package/src/commands/generate.js +0 -12
- package/src/commands/installApp.js +0 -9
- package/src/commands/installTheme.js +0 -9
- package/src/commands/start.js +0 -13
- package/src/utils/cloneRepo.js +0 -25
- package/src/utils/installPackage.js +0 -15
- package/src/utils/logging.js +0 -15
package/bin/cp.js
ADDED
|
@@ -0,0 +1,3542 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process'
|
|
2
|
+
import { watch, existsSync, statSync, openSync, readSync, closeSync, writeFileSync, readFileSync, readdirSync } from 'node:fs'
|
|
3
|
+
import { dirname, join } from 'node:path'
|
|
4
|
+
import { fileURLToPath } from 'node:url'
|
|
5
|
+
import blessed from 'neo-blessed'
|
|
6
|
+
import {
|
|
7
|
+
findWorkspaceRoot,
|
|
8
|
+
loadSettings,
|
|
9
|
+
saveSettings,
|
|
10
|
+
desktopPaths,
|
|
11
|
+
KINDS,
|
|
12
|
+
shortName,
|
|
13
|
+
isInstallableDesktopModule,
|
|
14
|
+
isDesktopKitPackage,
|
|
15
|
+
inferKind,
|
|
16
|
+
normalizeCatalogSort,
|
|
17
|
+
hasDocsModuleInstalled,
|
|
18
|
+
docsBasePathFromConfig,
|
|
19
|
+
getPackagesRequiredByTheme as getPackagesRequiredByThemeImport,
|
|
20
|
+
} from './lib/workspace.js'
|
|
21
|
+
import {
|
|
22
|
+
executeInstallPlan as extExecuteInstallPlan,
|
|
23
|
+
runStartupInstallFlow as extRunStartupInstallFlow,
|
|
24
|
+
} from './lib/installWizardFlow.js'
|
|
25
|
+
import {
|
|
26
|
+
readDesktopConfig,
|
|
27
|
+
writeDesktopConfig,
|
|
28
|
+
readDesktopDependencies,
|
|
29
|
+
resolveConfigPathForWrite,
|
|
30
|
+
} from './lib/config.js'
|
|
31
|
+
import { warnLegacyDesktopConfig } from './lib/desktopConfig.js'
|
|
32
|
+
import {
|
|
33
|
+
loadCatalog,
|
|
34
|
+
filterCatalog,
|
|
35
|
+
mergeInstalled,
|
|
36
|
+
effectiveInstalledSets,
|
|
37
|
+
CATALOG_SORT_MODES,
|
|
38
|
+
CATALOG_SORT_LABELS,
|
|
39
|
+
CATALOG_SORT_DESCRIPTIONS,
|
|
40
|
+
} from './lib/catalog.js'
|
|
41
|
+
import {
|
|
42
|
+
installPackage,
|
|
43
|
+
materializeToWorkspace,
|
|
44
|
+
hasLocalWorkspaceSource,
|
|
45
|
+
runPrepareModules,
|
|
46
|
+
resolveInstallPlan,
|
|
47
|
+
cloneRepo,
|
|
48
|
+
spawnAsync,
|
|
49
|
+
} from './lib/install.js'
|
|
50
|
+
import {
|
|
51
|
+
resolvePackageSources,
|
|
52
|
+
buildSourceOptions,
|
|
53
|
+
detectGithubSshAuth,
|
|
54
|
+
getCachedSshAuth,
|
|
55
|
+
trustedPublishers,
|
|
56
|
+
githubCloneUrl,
|
|
57
|
+
} from './lib/packageSources.js'
|
|
58
|
+
import {
|
|
59
|
+
formatCatalogRowPlain,
|
|
60
|
+
formatLegendLine,
|
|
61
|
+
formatDetailPanel,
|
|
62
|
+
formatHeaderLine,
|
|
63
|
+
getColumnWidths,
|
|
64
|
+
} from './lib/cpFormat.js'
|
|
65
|
+
import {
|
|
66
|
+
radarSpinner,
|
|
67
|
+
progressTrack,
|
|
68
|
+
statusPrefix,
|
|
69
|
+
spinnerFrameCount,
|
|
70
|
+
} from './lib/cpAscii.js'
|
|
71
|
+
import {
|
|
72
|
+
getClientStatus,
|
|
73
|
+
startDev,
|
|
74
|
+
stopDev,
|
|
75
|
+
waitForDev,
|
|
76
|
+
waitForDevStop,
|
|
77
|
+
devLogPath,
|
|
78
|
+
formatSparkline,
|
|
79
|
+
} from './lib/status.js'
|
|
80
|
+
import { resolveDevTarget } from './lib/playgroundContext.js'
|
|
81
|
+
|
|
82
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
83
|
+
|
|
84
|
+
const C = {
|
|
85
|
+
border: 'cyan',
|
|
86
|
+
borderDim: '#3a3a4a',
|
|
87
|
+
focus: 'green',
|
|
88
|
+
accent: '#7ee787',
|
|
89
|
+
warn: 'yellow',
|
|
90
|
+
err: 'red',
|
|
91
|
+
muted: '#6c7086',
|
|
92
|
+
title: 'white',
|
|
93
|
+
npm: '#7ee787',
|
|
94
|
+
git: 'cyan',
|
|
95
|
+
local: '#ff79c6',
|
|
96
|
+
add: '#7ee787',
|
|
97
|
+
remove: 'red',
|
|
98
|
+
label: '#a6adc8',
|
|
99
|
+
borderActive: 'white',
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const FORMAT_COLORS = {
|
|
103
|
+
npm: C.npm,
|
|
104
|
+
git: C.git,
|
|
105
|
+
local: C.local,
|
|
106
|
+
warn: C.warn,
|
|
107
|
+
add: C.add,
|
|
108
|
+
remove: C.remove,
|
|
109
|
+
accent: C.accent,
|
|
110
|
+
muted: C.muted,
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Text color only — no bg fill so the terminal theme shows through. */
|
|
114
|
+
function flatStyle(fg = 'white') {
|
|
115
|
+
return { fg }
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Colored border lines + label (no panel fill). */
|
|
119
|
+
function panelStyle(borderFg, fg = 'white') {
|
|
120
|
+
return {
|
|
121
|
+
fg,
|
|
122
|
+
border: { fg: borderFg },
|
|
123
|
+
label: { fg },
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const LIST_SCROLLBAR = {
|
|
128
|
+
ch: '│',
|
|
129
|
+
track: { ch: ' ' },
|
|
130
|
+
style: { fg: C.borderDim },
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const LIST_STYLE = {
|
|
134
|
+
fg: 'white',
|
|
135
|
+
item: { fg: 'white' },
|
|
136
|
+
scrollbar: { fg: C.borderDim },
|
|
137
|
+
selected: { fg: C.accent, bold: true },
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Unicode superscript hints (HTML <sup> equivalent in TUI). */
|
|
141
|
+
const KEY_SUP = {
|
|
142
|
+
'0': '⁰',
|
|
143
|
+
'1': '¹',
|
|
144
|
+
'2': '²',
|
|
145
|
+
'3': '³',
|
|
146
|
+
'4': '⁴',
|
|
147
|
+
'5': '⁵',
|
|
148
|
+
'6': '⁶',
|
|
149
|
+
'7': '⁷',
|
|
150
|
+
'8': '⁸',
|
|
151
|
+
'9': '⁹',
|
|
152
|
+
a: 'ᵃ',
|
|
153
|
+
b: 'ᵇ',
|
|
154
|
+
d: 'ᵈ',
|
|
155
|
+
g: 'ᵍ',
|
|
156
|
+
i: 'ⁱ',
|
|
157
|
+
m: 'ᵐ',
|
|
158
|
+
o: 'ᵒ',
|
|
159
|
+
q: 'ᵠ',
|
|
160
|
+
r: 'ʳ',
|
|
161
|
+
s: 'ˢ',
|
|
162
|
+
t: 'ᵗ',
|
|
163
|
+
w: 'ʷ',
|
|
164
|
+
x: 'ˣ',
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** @param {string} key */
|
|
168
|
+
function keySup(key) {
|
|
169
|
+
const k = String(key).toLowerCase()
|
|
170
|
+
return KEY_SUP[k] ?? KEY_SUP[key] ?? key
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** @param {string} key */
|
|
174
|
+
function keyHint(key) {
|
|
175
|
+
return `{${C.accent}-fg}${keySup(key)}{/}`
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** First letter primary, optional superscript key. */
|
|
179
|
+
function cardTitle(title, key = null) {
|
|
180
|
+
const first = title.charAt(0)
|
|
181
|
+
const rest = title.slice(1)
|
|
182
|
+
const isKeyFirstLetter = key !== null && key.toLowerCase() === first.toLowerCase()
|
|
183
|
+
const hint = (key !== null && !isKeyFirstLetter) ? keyHint(key) : ''
|
|
184
|
+
return `{${C.accent}-fg}${first}{/}{white-fg}${rest}{/}${hint}`
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** @param {string} title @param {string | null} [key] */
|
|
188
|
+
function cardLabel(title, key = null) {
|
|
189
|
+
return ` ${cardTitle(title, key)} `
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const MEM_HISTORY_LEN = 30
|
|
193
|
+
const LEGEND_ROWS = 1
|
|
194
|
+
const TAB_ROWS = 1
|
|
195
|
+
|
|
196
|
+
function areConfigsEqual(c1, c2) {
|
|
197
|
+
if (!c1 || !c2) return false
|
|
198
|
+
if (shortName(c1.theme ?? '') !== shortName(c2.theme ?? '')) return false
|
|
199
|
+
if ((c1.apps ?? []).length !== (c2.apps ?? []).length) return false
|
|
200
|
+
if ((c1.modules ?? []).length !== (c2.modules ?? []).length) return false
|
|
201
|
+
|
|
202
|
+
const apps1 = [...(c1.apps ?? [])].sort()
|
|
203
|
+
const apps2 = [...(c2.apps ?? [])].sort()
|
|
204
|
+
for (let i = 0; i < apps1.length; i++) {
|
|
205
|
+
if (apps1[i] !== apps2[i]) return false
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const mods1 = [...(c1.modules ?? [])].sort()
|
|
209
|
+
const mods2 = [...(c2.modules ?? [])].sort()
|
|
210
|
+
for (let i = 0; i < mods1.length; i++) {
|
|
211
|
+
if (mods1[i] !== mods2[i]) return false
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return true
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const ansiRegex = /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g;
|
|
218
|
+
function stripAnsi(str) {
|
|
219
|
+
return str.replace(ansiRegex, '')
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function truncateFormatted(str, maxLen) {
|
|
223
|
+
let result = ''
|
|
224
|
+
let visibleCount = 0
|
|
225
|
+
let i = 0
|
|
226
|
+
let truncated = false
|
|
227
|
+
while (i < str.length) {
|
|
228
|
+
if (str[i] === '{') {
|
|
229
|
+
const closeIdx = str.indexOf('}', i)
|
|
230
|
+
if (closeIdx !== -1) {
|
|
231
|
+
result += str.slice(i, closeIdx + 1)
|
|
232
|
+
i = closeIdx + 1
|
|
233
|
+
continue
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
if (visibleCount >= maxLen) {
|
|
237
|
+
truncated = true
|
|
238
|
+
break
|
|
239
|
+
}
|
|
240
|
+
result += str[i]
|
|
241
|
+
visibleCount++
|
|
242
|
+
i++
|
|
243
|
+
}
|
|
244
|
+
if (truncated) {
|
|
245
|
+
result += '...{/}'
|
|
246
|
+
}
|
|
247
|
+
return result
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** @param {string} commandName */
|
|
251
|
+
export async function runCp(commandName = 'desktop') {
|
|
252
|
+
process.title = 'Desktop'
|
|
253
|
+
const workspaceRoot = findWorkspaceRoot()
|
|
254
|
+
if (!workspaceRoot) {
|
|
255
|
+
console.error('Not inside an OWD workspace.')
|
|
256
|
+
process.exit(1)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
let paths = desktopPaths(workspaceRoot)
|
|
260
|
+
const devTarget = resolveDevTarget(process.cwd(), workspaceRoot)
|
|
261
|
+
const playgroundActive = devTarget?.mode === 'playground'
|
|
262
|
+
const playgroundLabel = devTarget?.packageName ?? null
|
|
263
|
+
warnLegacyDesktopConfig(
|
|
264
|
+
paths.configLegacy ? { legacy: true, file: 'owd.config.ts' } : null,
|
|
265
|
+
)
|
|
266
|
+
let settings = loadSettings(workspaceRoot)
|
|
267
|
+
let configError = null
|
|
268
|
+
let config = null
|
|
269
|
+
try {
|
|
270
|
+
config = readDesktopConfig(paths.config, workspaceRoot)
|
|
271
|
+
} catch (err) {
|
|
272
|
+
configError = err.message
|
|
273
|
+
config = { apps: [], modules: [], theme: null }
|
|
274
|
+
}
|
|
275
|
+
let deps = readDesktopDependencies(paths.packageJson)
|
|
276
|
+
|
|
277
|
+
function getGitStatus(root) {
|
|
278
|
+
try {
|
|
279
|
+
const branch = execSync('git rev-parse --abbrev-ref HEAD', {
|
|
280
|
+
cwd: root,
|
|
281
|
+
encoding: 'utf8',
|
|
282
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
283
|
+
}).trim()
|
|
284
|
+
|
|
285
|
+
const porcelain = execSync('git status --porcelain', {
|
|
286
|
+
cwd: root,
|
|
287
|
+
encoding: 'utf8',
|
|
288
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
289
|
+
}).trim()
|
|
290
|
+
|
|
291
|
+
if (!porcelain) {
|
|
292
|
+
return { branch, status: 'clean' }
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const lines = porcelain.split('\n').filter(Boolean)
|
|
296
|
+
const modifiedCount = lines.filter(l => !l.startsWith('??')).length
|
|
297
|
+
const untrackedCount = lines.filter(l => l.startsWith('??')).length
|
|
298
|
+
|
|
299
|
+
const parts = []
|
|
300
|
+
if (modifiedCount > 0) parts.push(`${modifiedCount} mod`)
|
|
301
|
+
if (untrackedCount > 0) parts.push(`${untrackedCount} untracked`)
|
|
302
|
+
return { branch, status: parts.join(', ') || 'clean' }
|
|
303
|
+
} catch {
|
|
304
|
+
return { branch: '—', status: 'unknown' }
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function getMissingGitWarnings(root) {
|
|
309
|
+
const warnings = []
|
|
310
|
+
const checkDirs = ['apps', 'themes', 'packages']
|
|
311
|
+
|
|
312
|
+
for (const dirName of checkDirs) {
|
|
313
|
+
const fullDir = join(root, dirName)
|
|
314
|
+
if (!existsSync(fullDir)) continue
|
|
315
|
+
|
|
316
|
+
try {
|
|
317
|
+
const subdirs = readdirSync(fullDir, { withFileTypes: true })
|
|
318
|
+
for (const subdir of subdirs) {
|
|
319
|
+
if (!subdir.isDirectory()) continue
|
|
320
|
+
const name = subdir.name
|
|
321
|
+
if (name.startsWith('.')) continue
|
|
322
|
+
|
|
323
|
+
// Exclude exclusions
|
|
324
|
+
if (dirName === 'packages') {
|
|
325
|
+
if (name.startsWith('core') || name.startsWith('nx') || name.startsWith('kit-')) {
|
|
326
|
+
continue
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const gitPath = join(fullDir, name, '.git')
|
|
331
|
+
if (!existsSync(gitPath)) {
|
|
332
|
+
warnings.push(subdir.name)
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
} catch {
|
|
336
|
+
// Ignore read errors
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return warnings
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function getWorkspaceGitChanges(root) {
|
|
344
|
+
// Returns { added, modified, deleted } summed across the main repo
|
|
345
|
+
// and all sub-repos in apps/, packages/, themes/ that have a .git folder.
|
|
346
|
+
const sumChanges = (dir) => {
|
|
347
|
+
let added = 0, modified = 0, deleted = 0
|
|
348
|
+
try {
|
|
349
|
+
const porcelain = execSync('git status --porcelain', {
|
|
350
|
+
cwd: dir,
|
|
351
|
+
encoding: 'utf8',
|
|
352
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
353
|
+
}).trim()
|
|
354
|
+
if (!porcelain) return { added: 0, modified: 0, deleted: 0 }
|
|
355
|
+
for (const line of porcelain.split('\n').filter(Boolean)) {
|
|
356
|
+
if (line.startsWith('??')) { added++; continue }
|
|
357
|
+
const xy = line.slice(0, 2)
|
|
358
|
+
if (xy.includes('A')) added++
|
|
359
|
+
else if (xy.includes('D')) deleted++
|
|
360
|
+
else modified++
|
|
361
|
+
}
|
|
362
|
+
} catch { /* no git or not a repo */ }
|
|
363
|
+
return { added, modified, deleted }
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
let totalAdded = 0, totalModified = 0, totalDeleted = 0
|
|
367
|
+
let appsRepos = 0
|
|
368
|
+
let modulesRepos = 0
|
|
369
|
+
let themesRepos = 0
|
|
370
|
+
|
|
371
|
+
// Main workspace repo
|
|
372
|
+
const main = sumChanges(root)
|
|
373
|
+
totalAdded += main.added
|
|
374
|
+
totalModified += main.modified
|
|
375
|
+
totalDeleted += main.deleted
|
|
376
|
+
const mainRepo = (main.added > 0 || main.modified > 0 || main.deleted > 0) ? 1 : 0
|
|
377
|
+
|
|
378
|
+
// Sub-repos
|
|
379
|
+
const checkDirs = ['apps', 'themes', 'packages']
|
|
380
|
+
for (const dirName of checkDirs) {
|
|
381
|
+
const fullDir = join(root, dirName)
|
|
382
|
+
if (!existsSync(fullDir)) continue
|
|
383
|
+
try {
|
|
384
|
+
const subdirs = readdirSync(fullDir, { withFileTypes: true })
|
|
385
|
+
for (const subdir of subdirs) {
|
|
386
|
+
if (!subdir.isDirectory()) continue
|
|
387
|
+
const name = subdir.name
|
|
388
|
+
if (name.startsWith('.')) continue
|
|
389
|
+
if (dirName === 'packages') {
|
|
390
|
+
if (name.startsWith('core') || name.startsWith('nx') || name.startsWith('kit-')) continue
|
|
391
|
+
}
|
|
392
|
+
const subGit = join(fullDir, name, '.git')
|
|
393
|
+
if (!existsSync(subGit)) continue
|
|
394
|
+
const sub = sumChanges(join(fullDir, name))
|
|
395
|
+
if (sub.added > 0 || sub.modified > 0 || sub.deleted > 0) {
|
|
396
|
+
if (dirName === 'apps') appsRepos++
|
|
397
|
+
else if (dirName === 'packages') modulesRepos++
|
|
398
|
+
else if (dirName === 'themes') themesRepos++
|
|
399
|
+
}
|
|
400
|
+
totalAdded += sub.added
|
|
401
|
+
totalModified += sub.modified
|
|
402
|
+
totalDeleted += sub.deleted
|
|
403
|
+
}
|
|
404
|
+
} catch { /* ignore */ }
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
return {
|
|
408
|
+
added: totalAdded,
|
|
409
|
+
modified: totalModified,
|
|
410
|
+
deleted: totalDeleted,
|
|
411
|
+
mainRepo,
|
|
412
|
+
appsRepos,
|
|
413
|
+
modulesRepos,
|
|
414
|
+
themesRepos,
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
let nuxtVersion = '—'
|
|
419
|
+
let pnpmVersion = '—'
|
|
420
|
+
try {
|
|
421
|
+
const pkg = JSON.parse(readFileSync(join(workspaceRoot, 'package.json'), 'utf8'))
|
|
422
|
+
nuxtVersion = pkg.devDependencies?.nuxt || pkg.dependencies?.nuxt || '—'
|
|
423
|
+
pnpmVersion = (pkg.packageManager || '').replace('pnpm@', '') || '—'
|
|
424
|
+
} catch {
|
|
425
|
+
// Ignore
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
let isWritingConfig = false
|
|
429
|
+
let ignoreNextConfigWatch = false
|
|
430
|
+
let isInstalling = false
|
|
431
|
+
let hasInstallError = false
|
|
432
|
+
let isStartingServer = false
|
|
433
|
+
let configWatcher = null
|
|
434
|
+
let configWatchTimeout = null
|
|
435
|
+
let logWatcher = null
|
|
436
|
+
|
|
437
|
+
const packageUpdates = new Map()
|
|
438
|
+
const localGitChanges = new Map()
|
|
439
|
+
let checkingUpdates = false
|
|
440
|
+
|
|
441
|
+
let catalog = []
|
|
442
|
+
let catalogCacheAge = ''
|
|
443
|
+
let activeTab = 'app'
|
|
444
|
+
let pendingTheme = config.theme
|
|
445
|
+
/** @type {Map<string, boolean>} */
|
|
446
|
+
const pendingPackages = new Map()
|
|
447
|
+
/** @type {Map<string, import('./lib/packageSources.js').InstallSourceChoice>} */
|
|
448
|
+
const installChoices = new Map()
|
|
449
|
+
let statusLine = ''
|
|
450
|
+
let lastTone = 'info'
|
|
451
|
+
let confirmDialogOpen = false
|
|
452
|
+
let activeTaskMessage = null
|
|
453
|
+
let devPhase = 'idle'
|
|
454
|
+
let clientStatus = await getClientStatus(workspaceRoot, settings.devPort)
|
|
455
|
+
/** @type {number[]} */
|
|
456
|
+
const memHistory = []
|
|
457
|
+
let spinnerFrame = 0
|
|
458
|
+
let showcaseIndex = 0
|
|
459
|
+
let spinnerTimer = null
|
|
460
|
+
let saveProgress = null
|
|
461
|
+
let currentColumns = getColumnWidths(76)
|
|
462
|
+
let settingsOpen = false
|
|
463
|
+
let menuOpen = false
|
|
464
|
+
let installWizardOpen = false
|
|
465
|
+
let installWizardResolve = null
|
|
466
|
+
let installWizardIsStartup = false
|
|
467
|
+
/** @type {string[]} */
|
|
468
|
+
let installWizardQueue = []
|
|
469
|
+
let installWizardIndex = 0
|
|
470
|
+
/** @type {import('./lib/packageSources.js').SourceOption[]} */
|
|
471
|
+
let installWizardOptions = []
|
|
472
|
+
let installWizardUntrusted = false
|
|
473
|
+
let installWizardConfirmUntrusted = false
|
|
474
|
+
let installWizardAutoExecute = true
|
|
475
|
+
let sortPickerOpen = false
|
|
476
|
+
let sshAuthStatus = getCachedSshAuth(workspaceRoot)
|
|
477
|
+
/** @type {ReturnType<typeof setTimeout> | null} */
|
|
478
|
+
let configRestartHintTimer = null
|
|
479
|
+
let configRestartHintUntil = 0
|
|
480
|
+
|
|
481
|
+
/** @type {{ id: string, label: string, key: string }[]} */
|
|
482
|
+
const MENU_ITEMS = [
|
|
483
|
+
{ id: 'start', label: 'Start dev server', key: 's' },
|
|
484
|
+
{ id: 'stop', label: 'Stop dev server', key: 'x' },
|
|
485
|
+
{ id: 'reboot', label: 'Reboot dev server', key: 'R' },
|
|
486
|
+
{ id: 'save', label: 'Save catalog / theme', key: 'w' },
|
|
487
|
+
{ id: 'refresh', label: 'Refresh package list', key: 'r' },
|
|
488
|
+
{ id: 'check-updates', label: 'Check GitHub/NPM updates', key: 'u' },
|
|
489
|
+
{ id: 'docs', label: 'Open documentation', key: 'i' },
|
|
490
|
+
{ id: 'settings', label: 'Settings', key: 'g' },
|
|
491
|
+
{ id: 'build', label: 'Build (pnpm generate)', key: 'b' },
|
|
492
|
+
{ id: 'create', label: 'Create new app or module', key: 'n' },
|
|
493
|
+
{ id: 'quit', label: 'Quit control panel', key: 'q' },
|
|
494
|
+
]
|
|
495
|
+
|
|
496
|
+
const screen = blessed.screen({
|
|
497
|
+
smartCSR: true,
|
|
498
|
+
fullUnicode: true,
|
|
499
|
+
title: 'Desktop',
|
|
500
|
+
dockCorners: false,
|
|
501
|
+
style: flatStyle(),
|
|
502
|
+
})
|
|
503
|
+
|
|
504
|
+
const HEADER_ROWS = 0
|
|
505
|
+
const MAIN_TOP = 0
|
|
506
|
+
const CLIENT_PANEL_H = 9
|
|
507
|
+
const PANELS_TOP = MAIN_TOP + CLIENT_PANEL_H
|
|
508
|
+
|
|
509
|
+
const clientBox = blessed.box({
|
|
510
|
+
parent: screen,
|
|
511
|
+
top: MAIN_TOP,
|
|
512
|
+
left: 0,
|
|
513
|
+
width: '58%-1',
|
|
514
|
+
height: CLIENT_PANEL_H,
|
|
515
|
+
tags: true,
|
|
516
|
+
border: { type: 'line' },
|
|
517
|
+
style: panelStyle(C.border),
|
|
518
|
+
label: ` {bold}{${C.accent}-fg}Desktop{/}{/} control panel · ${keyHint('m')} menu · ${keyHint('g')} settings `,
|
|
519
|
+
})
|
|
520
|
+
|
|
521
|
+
const metricsBox = blessed.box({
|
|
522
|
+
parent: screen,
|
|
523
|
+
top: MAIN_TOP,
|
|
524
|
+
left: '58%',
|
|
525
|
+
width: '42%',
|
|
526
|
+
height: CLIENT_PANEL_H,
|
|
527
|
+
tags: true,
|
|
528
|
+
border: { type: 'line' },
|
|
529
|
+
style: panelStyle(C.border),
|
|
530
|
+
label: ' Metrics ',
|
|
531
|
+
})
|
|
532
|
+
|
|
533
|
+
const catalogBox = blessed.box({
|
|
534
|
+
parent: screen,
|
|
535
|
+
top: PANELS_TOP,
|
|
536
|
+
left: 0,
|
|
537
|
+
width: '100%',
|
|
538
|
+
bottom: 5,
|
|
539
|
+
tags: true,
|
|
540
|
+
border: { type: 'line' },
|
|
541
|
+
style: panelStyle(C.borderDim),
|
|
542
|
+
label: cardLabel('Catalog'),
|
|
543
|
+
})
|
|
544
|
+
|
|
545
|
+
const logsBox = blessed.box({
|
|
546
|
+
parent: screen,
|
|
547
|
+
border: { type: 'line' },
|
|
548
|
+
style: panelStyle(C.borderDim),
|
|
549
|
+
label: cardLabel('Logs', 'l'),
|
|
550
|
+
tags: true,
|
|
551
|
+
parseAnsi: true,
|
|
552
|
+
scrollable: true,
|
|
553
|
+
alwaysScroll: true,
|
|
554
|
+
scrollbar: LIST_SCROLLBAR,
|
|
555
|
+
mouse: true,
|
|
556
|
+
keys: true,
|
|
557
|
+
top: PANELS_TOP,
|
|
558
|
+
left: '58%',
|
|
559
|
+
width: '42%',
|
|
560
|
+
bottom: 5,
|
|
561
|
+
})
|
|
562
|
+
|
|
563
|
+
const tabBar = blessed.box({
|
|
564
|
+
parent: catalogBox,
|
|
565
|
+
top: 0,
|
|
566
|
+
left: 1,
|
|
567
|
+
width: '100%-2',
|
|
568
|
+
height: TAB_ROWS,
|
|
569
|
+
tags: true,
|
|
570
|
+
content: '',
|
|
571
|
+
style: flatStyle(),
|
|
572
|
+
mouse: true,
|
|
573
|
+
})
|
|
574
|
+
|
|
575
|
+
const headerBar = blessed.box({
|
|
576
|
+
parent: catalogBox,
|
|
577
|
+
top: TAB_ROWS + 1,
|
|
578
|
+
left: 1,
|
|
579
|
+
width: '100%-2',
|
|
580
|
+
height: 1,
|
|
581
|
+
tags: true,
|
|
582
|
+
content: '',
|
|
583
|
+
style: flatStyle(C.muted),
|
|
584
|
+
})
|
|
585
|
+
|
|
586
|
+
const catalogList = blessed.list({
|
|
587
|
+
parent: catalogBox,
|
|
588
|
+
top: TAB_ROWS + 2,
|
|
589
|
+
left: 1,
|
|
590
|
+
width: '100%-2',
|
|
591
|
+
bottom: 9,
|
|
592
|
+
tags: true,
|
|
593
|
+
keys: true,
|
|
594
|
+
vi: true,
|
|
595
|
+
mouse: true,
|
|
596
|
+
scrollbar: LIST_SCROLLBAR,
|
|
597
|
+
alwaysScroll: true,
|
|
598
|
+
style: LIST_STYLE,
|
|
599
|
+
})
|
|
600
|
+
|
|
601
|
+
const detailDivider = blessed.box({
|
|
602
|
+
parent: catalogBox,
|
|
603
|
+
bottom: 8,
|
|
604
|
+
left: 1,
|
|
605
|
+
width: '100%-2',
|
|
606
|
+
height: 1,
|
|
607
|
+
tags: true,
|
|
608
|
+
content: '',
|
|
609
|
+
style: flatStyle(C.borderDim),
|
|
610
|
+
})
|
|
611
|
+
|
|
612
|
+
const detailBox = blessed.box({
|
|
613
|
+
parent: catalogBox,
|
|
614
|
+
bottom: 1,
|
|
615
|
+
left: 1,
|
|
616
|
+
width: '100%-2',
|
|
617
|
+
height: 7,
|
|
618
|
+
tags: true,
|
|
619
|
+
content: '',
|
|
620
|
+
style: flatStyle(C.muted),
|
|
621
|
+
})
|
|
622
|
+
|
|
623
|
+
function layoutCatalogPanel() {
|
|
624
|
+
const cols = screen.width ?? 80
|
|
625
|
+
const rows = screen.height ?? 24
|
|
626
|
+
|
|
627
|
+
const showLogs = (isDevServerUp() || isInstalling || hasInstallError) && cols >= 120
|
|
628
|
+
|
|
629
|
+
if (showLogs) {
|
|
630
|
+
logsBox.show()
|
|
631
|
+
|
|
632
|
+
clientBox.position.width = '40%-1'
|
|
633
|
+
|
|
634
|
+
metricsBox.position.left = '40%'
|
|
635
|
+
metricsBox.position.width = '28%-1'
|
|
636
|
+
|
|
637
|
+
catalogBox.position.width = '68%-1'
|
|
638
|
+
|
|
639
|
+
logsBox.position.left = '68%'
|
|
640
|
+
logsBox.position.width = '32%'
|
|
641
|
+
logsBox.position.top = MAIN_TOP
|
|
642
|
+
logsBox.position.height = rows - MAIN_TOP - 5
|
|
643
|
+
} else {
|
|
644
|
+
logsBox.hide()
|
|
645
|
+
if (screen.focused === logsBox) {
|
|
646
|
+
focusCatalog()
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
clientBox.position.width = '58%-1'
|
|
650
|
+
|
|
651
|
+
metricsBox.position.left = '58%'
|
|
652
|
+
metricsBox.position.width = '42%'
|
|
653
|
+
|
|
654
|
+
catalogBox.position.width = '100%'
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
const detailH = rows < 26 ? 4 : rows < 34 ? 5 : 7
|
|
658
|
+
catalogBox.position.height = rows - PANELS_TOP - 5
|
|
659
|
+
|
|
660
|
+
Object.assign(detailBox.position, {
|
|
661
|
+
bottom: 1,
|
|
662
|
+
left: 1,
|
|
663
|
+
width: '100%-2',
|
|
664
|
+
height: detailH,
|
|
665
|
+
top: undefined,
|
|
666
|
+
right: undefined,
|
|
667
|
+
})
|
|
668
|
+
|
|
669
|
+
Object.assign(detailDivider.position, {
|
|
670
|
+
bottom: detailH + 1,
|
|
671
|
+
left: 1,
|
|
672
|
+
width: '100%-2',
|
|
673
|
+
height: 1,
|
|
674
|
+
top: undefined,
|
|
675
|
+
right: undefined,
|
|
676
|
+
})
|
|
677
|
+
|
|
678
|
+
Object.assign(headerBar.position, {
|
|
679
|
+
top: TAB_ROWS + 1,
|
|
680
|
+
left: 1,
|
|
681
|
+
width: '100%-2',
|
|
682
|
+
height: 1,
|
|
683
|
+
bottom: undefined,
|
|
684
|
+
right: undefined,
|
|
685
|
+
})
|
|
686
|
+
|
|
687
|
+
Object.assign(catalogList.position, {
|
|
688
|
+
top: TAB_ROWS + 2,
|
|
689
|
+
left: 1,
|
|
690
|
+
width: '100%-2',
|
|
691
|
+
bottom: detailH + 2,
|
|
692
|
+
height: undefined,
|
|
693
|
+
right: undefined,
|
|
694
|
+
})
|
|
695
|
+
|
|
696
|
+
const catalogBoxWidth = catalogBox.width || (showLogs ? Math.floor(cols * 0.68) : cols)
|
|
697
|
+
const targetWidth = Math.max(40, catalogBoxWidth - 4)
|
|
698
|
+
currentColumns = getColumnWidths(targetWidth)
|
|
699
|
+
detailDivider.setContent(`{${C.borderDim}-fg}${ '─'.repeat(targetWidth) }{/}`)
|
|
700
|
+
|
|
701
|
+
clientBox.emit('resize')
|
|
702
|
+
metricsBox.emit('resize')
|
|
703
|
+
catalogBox.emit('resize')
|
|
704
|
+
detailBox.emit('resize')
|
|
705
|
+
detailDivider.emit('resize')
|
|
706
|
+
catalogList.emit('resize')
|
|
707
|
+
logsBox.emit('resize')
|
|
708
|
+
headerBar.emit('resize')
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
layoutCatalogPanel()
|
|
712
|
+
screen.on('resize', () => {
|
|
713
|
+
renderAll()
|
|
714
|
+
})
|
|
715
|
+
|
|
716
|
+
const helpBar = blessed.box({
|
|
717
|
+
parent: screen,
|
|
718
|
+
bottom: 0,
|
|
719
|
+
left: 0,
|
|
720
|
+
width: '100%',
|
|
721
|
+
height: 5,
|
|
722
|
+
tags: true,
|
|
723
|
+
border: { type: 'line' },
|
|
724
|
+
label: ' Status & Shortcuts ',
|
|
725
|
+
style: panelStyle(C.borderDim, C.muted),
|
|
726
|
+
})
|
|
727
|
+
|
|
728
|
+
const settingsOverlay = blessed.box({
|
|
729
|
+
parent: screen,
|
|
730
|
+
top: 'center',
|
|
731
|
+
left: 'center',
|
|
732
|
+
width: 70,
|
|
733
|
+
height: 13,
|
|
734
|
+
hidden: true,
|
|
735
|
+
tags: true,
|
|
736
|
+
border: { type: 'line' },
|
|
737
|
+
label: cardLabel('Settings', 'g'),
|
|
738
|
+
style: panelStyle(C.accent),
|
|
739
|
+
content: '',
|
|
740
|
+
})
|
|
741
|
+
|
|
742
|
+
const settingsInfo = blessed.box({
|
|
743
|
+
parent: settingsOverlay,
|
|
744
|
+
top: 1,
|
|
745
|
+
left: 2,
|
|
746
|
+
width: '100%-4',
|
|
747
|
+
height: 4,
|
|
748
|
+
tags: true,
|
|
749
|
+
style: flatStyle(C.muted),
|
|
750
|
+
})
|
|
751
|
+
|
|
752
|
+
const trustedOrgsInput = blessed.textbox({
|
|
753
|
+
parent: settingsOverlay,
|
|
754
|
+
top: 6,
|
|
755
|
+
left: 2,
|
|
756
|
+
width: '100%-4',
|
|
757
|
+
height: 3,
|
|
758
|
+
inputOnFocus: true,
|
|
759
|
+
keys: true,
|
|
760
|
+
mouse: true,
|
|
761
|
+
style: {
|
|
762
|
+
fg: 'white',
|
|
763
|
+
focus: { border: { fg: C.accent } },
|
|
764
|
+
border: { fg: C.borderDim },
|
|
765
|
+
label: { fg: 'white' },
|
|
766
|
+
},
|
|
767
|
+
border: { type: 'line' },
|
|
768
|
+
label: ' Trusted orgs (comma-separated) ',
|
|
769
|
+
})
|
|
770
|
+
|
|
771
|
+
const menuOverlay = blessed.box({
|
|
772
|
+
parent: screen,
|
|
773
|
+
top: 'center',
|
|
774
|
+
left: 'center',
|
|
775
|
+
width: 52,
|
|
776
|
+
height: MENU_ITEMS.length + 4,
|
|
777
|
+
hidden: true,
|
|
778
|
+
tags: true,
|
|
779
|
+
border: { type: 'line' },
|
|
780
|
+
label: cardLabel('Menu', 'm'),
|
|
781
|
+
style: panelStyle(C.accent),
|
|
782
|
+
})
|
|
783
|
+
|
|
784
|
+
const menuList = blessed.list({
|
|
785
|
+
parent: menuOverlay,
|
|
786
|
+
top: 1,
|
|
787
|
+
left: 1,
|
|
788
|
+
width: '100%-2',
|
|
789
|
+
height: MENU_ITEMS.length + 1,
|
|
790
|
+
tags: true,
|
|
791
|
+
keys: true,
|
|
792
|
+
vi: true,
|
|
793
|
+
mouse: true,
|
|
794
|
+
scrollbar: LIST_SCROLLBAR,
|
|
795
|
+
style: LIST_STYLE,
|
|
796
|
+
items: MENU_ITEMS.map(({ label, key }) => `${keyHint(key)} ${label}`),
|
|
797
|
+
})
|
|
798
|
+
|
|
799
|
+
const installWizardOverlay = blessed.box({
|
|
800
|
+
parent: screen,
|
|
801
|
+
top: 'center',
|
|
802
|
+
left: 'center',
|
|
803
|
+
width: 68,
|
|
804
|
+
height: 16,
|
|
805
|
+
hidden: true,
|
|
806
|
+
tags: true,
|
|
807
|
+
border: { type: 'line' },
|
|
808
|
+
label: ' Install source ',
|
|
809
|
+
style: panelStyle(C.accent),
|
|
810
|
+
})
|
|
811
|
+
|
|
812
|
+
const installWizardHeader = blessed.box({
|
|
813
|
+
parent: installWizardOverlay,
|
|
814
|
+
top: 1,
|
|
815
|
+
left: 2,
|
|
816
|
+
width: '100%-4',
|
|
817
|
+
height: 3,
|
|
818
|
+
tags: true,
|
|
819
|
+
style: flatStyle(C.muted),
|
|
820
|
+
})
|
|
821
|
+
|
|
822
|
+
const installWizardWarning = blessed.box({
|
|
823
|
+
parent: installWizardOverlay,
|
|
824
|
+
top: 4,
|
|
825
|
+
left: 2,
|
|
826
|
+
width: '100%-4',
|
|
827
|
+
height: 2,
|
|
828
|
+
hidden: true,
|
|
829
|
+
tags: true,
|
|
830
|
+
style: flatStyle(C.warn),
|
|
831
|
+
})
|
|
832
|
+
|
|
833
|
+
const installWizardList = blessed.list({
|
|
834
|
+
parent: installWizardOverlay,
|
|
835
|
+
top: 6,
|
|
836
|
+
left: 2,
|
|
837
|
+
width: '100%-4',
|
|
838
|
+
height: 6,
|
|
839
|
+
tags: true,
|
|
840
|
+
keys: true,
|
|
841
|
+
vi: true,
|
|
842
|
+
mouse: true,
|
|
843
|
+
scrollbar: LIST_SCROLLBAR,
|
|
844
|
+
style: LIST_STYLE,
|
|
845
|
+
items: [],
|
|
846
|
+
})
|
|
847
|
+
|
|
848
|
+
const installWizardFooter = blessed.box({
|
|
849
|
+
parent: installWizardOverlay,
|
|
850
|
+
bottom: 1,
|
|
851
|
+
left: 2,
|
|
852
|
+
width: '100%-4',
|
|
853
|
+
height: 2,
|
|
854
|
+
tags: true,
|
|
855
|
+
style: flatStyle(C.muted),
|
|
856
|
+
})
|
|
857
|
+
|
|
858
|
+
const sortPickerOverlay = blessed.box({
|
|
859
|
+
parent: screen,
|
|
860
|
+
top: 'center',
|
|
861
|
+
left: 'center',
|
|
862
|
+
width: 52,
|
|
863
|
+
height: 12,
|
|
864
|
+
hidden: true,
|
|
865
|
+
tags: true,
|
|
866
|
+
border: { type: 'line' },
|
|
867
|
+
label: cardLabel('Sort', 'O'),
|
|
868
|
+
style: panelStyle(C.accent),
|
|
869
|
+
})
|
|
870
|
+
|
|
871
|
+
const sortPickerList = blessed.list({
|
|
872
|
+
parent: sortPickerOverlay,
|
|
873
|
+
top: 1,
|
|
874
|
+
left: 1,
|
|
875
|
+
width: '100%-2',
|
|
876
|
+
height: 8,
|
|
877
|
+
tags: true,
|
|
878
|
+
keys: true,
|
|
879
|
+
vi: true,
|
|
880
|
+
mouse: true,
|
|
881
|
+
scrollbar: LIST_SCROLLBAR,
|
|
882
|
+
style: LIST_STYLE,
|
|
883
|
+
})
|
|
884
|
+
|
|
885
|
+
const installProgressOverlay = blessed.box({
|
|
886
|
+
parent: screen,
|
|
887
|
+
top: 'center',
|
|
888
|
+
left: 'center',
|
|
889
|
+
width: 60,
|
|
890
|
+
height: 8,
|
|
891
|
+
hidden: true,
|
|
892
|
+
tags: true,
|
|
893
|
+
border: { type: 'line' },
|
|
894
|
+
label: cardLabel(' Syncing Workspace '),
|
|
895
|
+
style: panelStyle(C.accent),
|
|
896
|
+
})
|
|
897
|
+
|
|
898
|
+
const installProgressContent = blessed.box({
|
|
899
|
+
parent: installProgressOverlay,
|
|
900
|
+
top: 1,
|
|
901
|
+
left: 2,
|
|
902
|
+
width: '100%-4',
|
|
903
|
+
height: 4,
|
|
904
|
+
tags: true,
|
|
905
|
+
})
|
|
906
|
+
|
|
907
|
+
const ctx = {
|
|
908
|
+
getConfig: () => config,
|
|
909
|
+
setConfig: (val) => { config = val },
|
|
910
|
+
getDeps: () => deps,
|
|
911
|
+
setDeps: (val) => { deps = val },
|
|
912
|
+
getPaths: () => paths,
|
|
913
|
+
setPaths: (val) => { paths = val },
|
|
914
|
+
getSettings: () => settings,
|
|
915
|
+
setSettings: (val) => { settings = val },
|
|
916
|
+
getWorkspaceRoot: () => workspaceRoot,
|
|
917
|
+
getCatalog: () => catalog,
|
|
918
|
+
getPendingTheme: () => pendingTheme,
|
|
919
|
+
setPendingTheme: (val) => { pendingTheme = val },
|
|
920
|
+
getPendingPackages: () => pendingPackages,
|
|
921
|
+
getInstallChoices: () => installChoices,
|
|
922
|
+
isInstalling: () => isInstalling,
|
|
923
|
+
setInstalling: (val) => {
|
|
924
|
+
isInstalling = val
|
|
925
|
+
if (val) {
|
|
926
|
+
hasInstallError = false
|
|
927
|
+
startLogWatcher()
|
|
928
|
+
} else if (!isDevServerUp() && !hasInstallError) {
|
|
929
|
+
stopLogWatcher()
|
|
930
|
+
}
|
|
931
|
+
layoutCatalogPanel()
|
|
932
|
+
},
|
|
933
|
+
isWritingConfig: () => isWritingConfig,
|
|
934
|
+
setWritingConfig: (val) => { isWritingConfig = val },
|
|
935
|
+
getIgnoreNextConfigWatch: () => ignoreNextConfigWatch,
|
|
936
|
+
setIgnoreNextConfigWatch: (val) => { ignoreNextConfigWatch = val },
|
|
937
|
+
isDevServerUp: () => isDevServerUp(),
|
|
938
|
+
rebootDevServer: () => rebootDevServer(),
|
|
939
|
+
clearLogs: () => clearLogsBox(),
|
|
940
|
+
getConfigError: () => configError,
|
|
941
|
+
setConfigError: (val) => { configError = val },
|
|
942
|
+
getConfigRestartHintUntil: () => configRestartHintUntil,
|
|
943
|
+
setConfigRestartHintUntil: (val) => { configRestartHintUntil = val },
|
|
944
|
+
getConfigRestartHintTimer: () => configRestartHintTimer,
|
|
945
|
+
setConfigRestartHintTimer: (val) => { configRestartHintTimer = val },
|
|
946
|
+
getSaveProgress: () => saveProgress,
|
|
947
|
+
setSaveProgress: (val) => { saveProgress = val },
|
|
948
|
+
getSpinnerFrame: () => spinnerFrame,
|
|
949
|
+
setSpinnerFrame: (val) => { spinnerFrame = val },
|
|
950
|
+
collectInstallPlan: () => collectInstallPlan(),
|
|
951
|
+
startInstallWizard: (packagesToInstall, isStartup = false, opts = {}) => startInstallWizard(packagesToInstall, isStartup, opts),
|
|
952
|
+
showCustomConfirm: (opts) => showCustomConfirm(opts),
|
|
953
|
+
setStatus: (msg, tone) => setStatus(msg, tone),
|
|
954
|
+
startSpinner: (msg) => startSpinner(msg),
|
|
955
|
+
stopSpinner: () => stopSpinner(),
|
|
956
|
+
renderAll: () => renderAll(),
|
|
957
|
+
renderClient: () => renderClient(),
|
|
958
|
+
renderHelp: () => renderHelp(),
|
|
959
|
+
updateCatalogKeysState: () => updateCatalogKeysState(),
|
|
960
|
+
installProgressOverlay,
|
|
961
|
+
installProgressContent,
|
|
962
|
+
screen,
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
|
|
966
|
+
// Custom update selection dialog in the style of the install wizard
|
|
967
|
+
function showUpdateSelectionDialog({ title, updates }) {
|
|
968
|
+
return new Promise((resolve) => {
|
|
969
|
+
confirmDialogOpen = true
|
|
970
|
+
updateCatalogKeysState()
|
|
971
|
+
|
|
972
|
+
const innerHeight = Math.max(12, 8 + updates.length)
|
|
973
|
+
|
|
974
|
+
const dialogBox = blessed.box({
|
|
975
|
+
parent: screen,
|
|
976
|
+
border: 'line',
|
|
977
|
+
height: innerHeight,
|
|
978
|
+
width: 68,
|
|
979
|
+
top: 'center',
|
|
980
|
+
left: 'center',
|
|
981
|
+
label: cardLabel(` ${title} `),
|
|
982
|
+
style: panelStyle(C.accent),
|
|
983
|
+
tags: true,
|
|
984
|
+
keys: true,
|
|
985
|
+
})
|
|
986
|
+
|
|
987
|
+
const headerBox = blessed.box({
|
|
988
|
+
parent: dialogBox,
|
|
989
|
+
top: 1,
|
|
990
|
+
left: 2,
|
|
991
|
+
width: '100%-4',
|
|
992
|
+
height: updates.length + 2,
|
|
993
|
+
tags: true,
|
|
994
|
+
style: flatStyle(C.muted),
|
|
995
|
+
})
|
|
996
|
+
|
|
997
|
+
const updatesText = updates.map(([name, info]) => {
|
|
998
|
+
if (info.latestVersion) {
|
|
999
|
+
return ` • {bold}${name}{/} (${info.localVersion} -> {green-fg}${info.latestVersion}{/}) {${C.npm}-fg}[NPM]{/}`
|
|
1000
|
+
} else {
|
|
1001
|
+
const suffix = info.behindCount > 0 ? ` (${info.behindCount} commit${info.behindCount > 1 ? 's' : ''} behind)` : ''
|
|
1002
|
+
return ` • {bold}${name}{/}${suffix} {${C.git}-fg}[GIT]{/}`
|
|
1003
|
+
}
|
|
1004
|
+
}).join('\n')
|
|
1005
|
+
|
|
1006
|
+
headerBox.setContent(
|
|
1007
|
+
`{bold}Updates are available for packages:{/}\n` +
|
|
1008
|
+
updatesText
|
|
1009
|
+
)
|
|
1010
|
+
|
|
1011
|
+
const choiceList = blessed.list({
|
|
1012
|
+
parent: dialogBox,
|
|
1013
|
+
top: updates.length + 3,
|
|
1014
|
+
left: 2,
|
|
1015
|
+
width: '100%-4',
|
|
1016
|
+
height: 3,
|
|
1017
|
+
tags: true,
|
|
1018
|
+
keys: true,
|
|
1019
|
+
vi: true,
|
|
1020
|
+
mouse: true,
|
|
1021
|
+
scrollbar: LIST_SCROLLBAR,
|
|
1022
|
+
style: LIST_STYLE,
|
|
1023
|
+
items: [
|
|
1024
|
+
` {${C.accent}-fg}▶{/} Run Update Wizard `,
|
|
1025
|
+
` {${C.muted}-fg}▷{/} Skip updates `,
|
|
1026
|
+
]
|
|
1027
|
+
})
|
|
1028
|
+
|
|
1029
|
+
blessed.box({
|
|
1030
|
+
parent: dialogBox,
|
|
1031
|
+
bottom: 1,
|
|
1032
|
+
left: 2,
|
|
1033
|
+
width: '100%-4',
|
|
1034
|
+
height: 1,
|
|
1035
|
+
tags: true,
|
|
1036
|
+
style: flatStyle(C.muted),
|
|
1037
|
+
content: `{${C.muted}-fg}Enter select · Esc cancel{/}`
|
|
1038
|
+
})
|
|
1039
|
+
|
|
1040
|
+
function close(value) {
|
|
1041
|
+
confirmDialogOpen = false
|
|
1042
|
+
updateCatalogKeysState()
|
|
1043
|
+
dialogBox.destroy()
|
|
1044
|
+
focusCatalog()
|
|
1045
|
+
screen.render()
|
|
1046
|
+
resolve(value)
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
choiceList.on('select', () => {
|
|
1050
|
+
close(choiceList.selected === 0)
|
|
1051
|
+
})
|
|
1052
|
+
|
|
1053
|
+
dialogBox.key(['escape', 'q'], () => {
|
|
1054
|
+
close(false)
|
|
1055
|
+
})
|
|
1056
|
+
|
|
1057
|
+
choiceList.key(['escape', 'q'], () => {
|
|
1058
|
+
close(false)
|
|
1059
|
+
})
|
|
1060
|
+
|
|
1061
|
+
choiceList.select(0)
|
|
1062
|
+
dialogBox.setFront()
|
|
1063
|
+
choiceList.focus()
|
|
1064
|
+
screen.render()
|
|
1065
|
+
})
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
|
|
1069
|
+
// Custom reusable navigable confirm dialog (with arrow key navigation, mouse support, and vertical stacking)
|
|
1070
|
+
function showCustomConfirm({ title, message, yesText, noText, cancelText, height = 11, width = 'half' }) {
|
|
1071
|
+
return new Promise((resolve) => {
|
|
1072
|
+
confirmDialogOpen = true
|
|
1073
|
+
updateCatalogKeysState()
|
|
1074
|
+
|
|
1075
|
+
const dialogBox = blessed.box({
|
|
1076
|
+
parent: screen,
|
|
1077
|
+
border: 'line',
|
|
1078
|
+
height: height,
|
|
1079
|
+
width: width,
|
|
1080
|
+
top: 'center',
|
|
1081
|
+
left: 'center',
|
|
1082
|
+
label: cardLabel(` ${title} `),
|
|
1083
|
+
style: panelStyle(C.accent),
|
|
1084
|
+
tags: true,
|
|
1085
|
+
keys: true,
|
|
1086
|
+
})
|
|
1087
|
+
|
|
1088
|
+
const hasCancel = !!cancelText
|
|
1089
|
+
const bottomOffset = hasCancel ? 7 : 5
|
|
1090
|
+
|
|
1091
|
+
const msgBox = blessed.box({
|
|
1092
|
+
parent: dialogBox,
|
|
1093
|
+
top: 1,
|
|
1094
|
+
left: 2,
|
|
1095
|
+
right: 2,
|
|
1096
|
+
bottom: bottomOffset,
|
|
1097
|
+
content: message,
|
|
1098
|
+
tags: true,
|
|
1099
|
+
})
|
|
1100
|
+
|
|
1101
|
+
const yesBtn = blessed.box({
|
|
1102
|
+
parent: dialogBox,
|
|
1103
|
+
bottom: hasCancel ? 5 : 3,
|
|
1104
|
+
left: 2,
|
|
1105
|
+
right: 2,
|
|
1106
|
+
height: 1,
|
|
1107
|
+
content: yesText,
|
|
1108
|
+
align: 'center',
|
|
1109
|
+
valign: 'middle',
|
|
1110
|
+
clickable: true,
|
|
1111
|
+
style: {
|
|
1112
|
+
bg: '#21262d',
|
|
1113
|
+
fg: '#2ea44f',
|
|
1114
|
+
bold: true,
|
|
1115
|
+
}
|
|
1116
|
+
})
|
|
1117
|
+
|
|
1118
|
+
const noBtn = blessed.box({
|
|
1119
|
+
parent: dialogBox,
|
|
1120
|
+
bottom: hasCancel ? 3 : 1,
|
|
1121
|
+
left: 2,
|
|
1122
|
+
right: 2,
|
|
1123
|
+
height: 1,
|
|
1124
|
+
content: noText,
|
|
1125
|
+
align: 'center',
|
|
1126
|
+
valign: 'middle',
|
|
1127
|
+
clickable: true,
|
|
1128
|
+
style: {
|
|
1129
|
+
bg: '#21262d',
|
|
1130
|
+
fg: '#cb2431',
|
|
1131
|
+
bold: true,
|
|
1132
|
+
}
|
|
1133
|
+
})
|
|
1134
|
+
|
|
1135
|
+
let cancelBtn = null
|
|
1136
|
+
if (hasCancel) {
|
|
1137
|
+
cancelBtn = blessed.box({
|
|
1138
|
+
parent: dialogBox,
|
|
1139
|
+
bottom: 1,
|
|
1140
|
+
left: 2,
|
|
1141
|
+
right: 2,
|
|
1142
|
+
height: 1,
|
|
1143
|
+
content: cancelText,
|
|
1144
|
+
align: 'center',
|
|
1145
|
+
valign: 'middle',
|
|
1146
|
+
clickable: true,
|
|
1147
|
+
style: {
|
|
1148
|
+
bg: '#21262d',
|
|
1149
|
+
fg: '#8b949e',
|
|
1150
|
+
bold: true,
|
|
1151
|
+
}
|
|
1152
|
+
})
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
let selectedIndex = 0
|
|
1156
|
+
const maxIndex = hasCancel ? 2 : 1
|
|
1157
|
+
|
|
1158
|
+
function updateSelection() {
|
|
1159
|
+
yesBtn.style.bg = '#21262d'
|
|
1160
|
+
yesBtn.style.fg = '#2ea44f'
|
|
1161
|
+
noBtn.style.bg = '#21262d'
|
|
1162
|
+
noBtn.style.fg = '#cb2431'
|
|
1163
|
+
if (cancelBtn) {
|
|
1164
|
+
cancelBtn.style.bg = '#21262d'
|
|
1165
|
+
cancelBtn.style.fg = '#8b949e'
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
if (selectedIndex === 0) {
|
|
1169
|
+
yesBtn.style.bg = '#2ea44f'
|
|
1170
|
+
yesBtn.style.fg = 'white'
|
|
1171
|
+
} else if (selectedIndex === 1) {
|
|
1172
|
+
noBtn.style.bg = '#cb2431'
|
|
1173
|
+
noBtn.style.fg = 'white'
|
|
1174
|
+
} else if (selectedIndex === 2 && cancelBtn) {
|
|
1175
|
+
cancelBtn.style.bg = '#8b949e'
|
|
1176
|
+
cancelBtn.style.fg = 'black'
|
|
1177
|
+
}
|
|
1178
|
+
screen.render()
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
function cleanup() {
|
|
1182
|
+
screen.removeListener('keypress', keyHandler)
|
|
1183
|
+
dialogBox.destroy()
|
|
1184
|
+
confirmDialogOpen = false
|
|
1185
|
+
updateCatalogKeysState()
|
|
1186
|
+
screen.render()
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
const keyHandler = (ch, key) => {
|
|
1190
|
+
if (!key) return
|
|
1191
|
+
if (key.name === 'up' || key.name === 'left') {
|
|
1192
|
+
selectedIndex = (selectedIndex - 1 + (maxIndex + 1)) % (maxIndex + 1)
|
|
1193
|
+
updateSelection()
|
|
1194
|
+
} else if (key.name === 'down' || key.name === 'right' || key.name === 'tab') {
|
|
1195
|
+
selectedIndex = (selectedIndex + 1) % (maxIndex + 1)
|
|
1196
|
+
updateSelection()
|
|
1197
|
+
} else if (key.name === 'enter') {
|
|
1198
|
+
cleanup()
|
|
1199
|
+
if (selectedIndex === 0) resolve(true)
|
|
1200
|
+
else if (selectedIndex === 1) resolve(false)
|
|
1201
|
+
else resolve(null)
|
|
1202
|
+
} else if (key.name === 'escape') {
|
|
1203
|
+
cleanup()
|
|
1204
|
+
resolve(null)
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
screen.on('keypress', keyHandler)
|
|
1209
|
+
|
|
1210
|
+
yesBtn.on('click', () => {
|
|
1211
|
+
cleanup()
|
|
1212
|
+
resolve(true)
|
|
1213
|
+
})
|
|
1214
|
+
|
|
1215
|
+
noBtn.on('click', () => {
|
|
1216
|
+
cleanup()
|
|
1217
|
+
resolve(false)
|
|
1218
|
+
})
|
|
1219
|
+
|
|
1220
|
+
if (cancelBtn) {
|
|
1221
|
+
cancelBtn.on('click', () => {
|
|
1222
|
+
cleanup()
|
|
1223
|
+
resolve(null)
|
|
1224
|
+
})
|
|
1225
|
+
cancelBtn.on('mouseover', () => {
|
|
1226
|
+
selectedIndex = 2
|
|
1227
|
+
updateSelection()
|
|
1228
|
+
})
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
yesBtn.on('mouseover', () => {
|
|
1232
|
+
selectedIndex = 0
|
|
1233
|
+
updateSelection()
|
|
1234
|
+
})
|
|
1235
|
+
|
|
1236
|
+
noBtn.on('mouseover', () => {
|
|
1237
|
+
selectedIndex = 1
|
|
1238
|
+
updateSelection()
|
|
1239
|
+
})
|
|
1240
|
+
|
|
1241
|
+
dialogBox.setFront()
|
|
1242
|
+
dialogBox.focus()
|
|
1243
|
+
updateSelection()
|
|
1244
|
+
})
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
function overlayBlocksKeys() {
|
|
1248
|
+
return settingsOpen || menuOpen || installWizardOpen || sortPickerOpen || confirmDialogOpen || isInstalling || isStartingServer
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
function updateCatalogKeysState() {
|
|
1252
|
+
const blocked = overlayBlocksKeys()
|
|
1253
|
+
catalogList.keys = !blocked
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
function pendingQueueSummary() {
|
|
1257
|
+
const installs = []
|
|
1258
|
+
const removals = []
|
|
1259
|
+
for (const [name, on] of pendingPackages) {
|
|
1260
|
+
if (on) installs.push(shortName(name))
|
|
1261
|
+
else removals.push(shortName(name))
|
|
1262
|
+
}
|
|
1263
|
+
const themeChange =
|
|
1264
|
+
pendingTheme && pendingTheme !== config.theme ? shortName(pendingTheme) : null
|
|
1265
|
+
const parts = []
|
|
1266
|
+
if (installs.length) parts.push(`+${installs.join(', +')}`)
|
|
1267
|
+
if (themeChange) parts.push(`+theme:${themeChange}`)
|
|
1268
|
+
if (removals.length) parts.push(`−${removals.join(', −')}`)
|
|
1269
|
+
return parts.length ? parts.join(' ') : null
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
function catalogFilterOptions() {
|
|
1273
|
+
return {
|
|
1274
|
+
sortMode: settings.catalogSort,
|
|
1275
|
+
config: effectiveInstalledSets(config, pendingPackages, pendingTheme),
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
|
|
1280
|
+
|
|
1281
|
+
function renderLegendBar() {
|
|
1282
|
+
renderHelp()
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
function catalogEntries() {
|
|
1286
|
+
return mergeInstalled(
|
|
1287
|
+
filterCatalog(catalog, activeTab, catalogFilterOptions()),
|
|
1288
|
+
config,
|
|
1289
|
+
deps,
|
|
1290
|
+
workspaceRoot,
|
|
1291
|
+
)
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
function applyFocusStyles() {
|
|
1295
|
+
const tab = KINDS[activeTab].label
|
|
1296
|
+
catalogBox.setLabel(` ${cardTitle('Catalog')} · ${tab} ◀ `)
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
function sortModeLabel() {
|
|
1300
|
+
return CATALOG_SORT_LABELS[normalizeCatalogSort(settings.catalogSort)]
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
function applyCatalogSort(mode) {
|
|
1304
|
+
settings = { ...settings, catalogSort: mode }
|
|
1305
|
+
saveSettings(workspaceRoot, settings)
|
|
1306
|
+
renderCatalogList()
|
|
1307
|
+
renderLegendBar()
|
|
1308
|
+
renderTabs()
|
|
1309
|
+
renderDetail()
|
|
1310
|
+
setStatus(`Sort → ${sortModeLabel()}`, 'ok')
|
|
1311
|
+
screen.render()
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
function cycleCatalogSort() {
|
|
1315
|
+
const idx = CATALOG_SORT_MODES.indexOf(normalizeCatalogSort(settings.catalogSort))
|
|
1316
|
+
const next = CATALOG_SORT_MODES[(idx + 1) % CATALOG_SORT_MODES.length]
|
|
1317
|
+
applyCatalogSort(next)
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
function formatSortPickerItems() {
|
|
1321
|
+
const current = normalizeCatalogSort(settings.catalogSort)
|
|
1322
|
+
return CATALOG_SORT_MODES.map((mode) => {
|
|
1323
|
+
const bullet = mode === current ? `{${C.accent}-fg}>{/}` : ' '
|
|
1324
|
+
const label = CATALOG_SORT_LABELS[mode]
|
|
1325
|
+
const desc = CATALOG_SORT_DESCRIPTIONS[mode]
|
|
1326
|
+
return `${bullet} {bold}${label.padEnd(10)}{/} {${C.muted}-fg}${desc}{/}`
|
|
1327
|
+
})
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
function openSortPicker() {
|
|
1331
|
+
if (settingsOpen || menuOpen || installWizardOpen || sortPickerOpen) return
|
|
1332
|
+
sortPickerOpen = true
|
|
1333
|
+
updateCatalogKeysState()
|
|
1334
|
+
sortPickerOverlay.show()
|
|
1335
|
+
sortPickerList.setItems(formatSortPickerItems())
|
|
1336
|
+
const idx = CATALOG_SORT_MODES.indexOf(normalizeCatalogSort(settings.catalogSort))
|
|
1337
|
+
sortPickerList.select(idx >= 0 ? idx : 0)
|
|
1338
|
+
sortPickerList.focus()
|
|
1339
|
+
setStatus(`Sort — Enter apply · ${keyHint('o')} quick cycle · Esc close`)
|
|
1340
|
+
screen.render()
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
function closeSortPicker() {
|
|
1344
|
+
sortPickerOpen = false
|
|
1345
|
+
updateCatalogKeysState()
|
|
1346
|
+
sortPickerOverlay.hide()
|
|
1347
|
+
focusCatalog()
|
|
1348
|
+
screen.render()
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
function selectSortAt(index) {
|
|
1352
|
+
const mode = CATALOG_SORT_MODES[index]
|
|
1353
|
+
if (!mode) return
|
|
1354
|
+
closeSortPicker()
|
|
1355
|
+
applyCatalogSort(mode)
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
function isDevServerUp() {
|
|
1359
|
+
return (
|
|
1360
|
+
devPhase === 'running' ||
|
|
1361
|
+
devPhase === 'starting' ||
|
|
1362
|
+
clientStatus.running ||
|
|
1363
|
+
clientStatus.http.up
|
|
1364
|
+
)
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
function serverKeyHints() {
|
|
1368
|
+
return `${keyHint('d')} start/stop ${keyHint('m')} menu`
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
function defaultStatusHint() {
|
|
1372
|
+
return 'Ready'
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
function renderHelp(tone = null) {
|
|
1376
|
+
if (tone !== null) {
|
|
1377
|
+
lastTone = tone
|
|
1378
|
+
}
|
|
1379
|
+
const statusColor = lastTone === 'error' ? C.err : lastTone === 'ok' ? C.accent : C.warn
|
|
1380
|
+
const msg = statusLine || defaultStatusHint()
|
|
1381
|
+
|
|
1382
|
+
const msgLine = ` {bold}{${statusColor}-fg}${msg}{/}{/}`
|
|
1383
|
+
|
|
1384
|
+
const queue = pendingQueueSummary()
|
|
1385
|
+
const saveShortcut = queue
|
|
1386
|
+
? `{${C.accent}-fg}${keyHint('s')} save (${queue}){/}`
|
|
1387
|
+
: `${keyHint('s')} save`
|
|
1388
|
+
|
|
1389
|
+
const serverRunning = isDevServerUp()
|
|
1390
|
+
const serverShortcut = serverRunning
|
|
1391
|
+
? `{${C.accent}-fg}${keyHint('x')} stop server{/}`
|
|
1392
|
+
: `${keyHint('d')} start server`
|
|
1393
|
+
|
|
1394
|
+
helpBar.setContent(
|
|
1395
|
+
[
|
|
1396
|
+
msgLine,
|
|
1397
|
+
` Legend: {${C.add}-fg}[+]{/} add {${C.remove}-fg}[-]{/} remove {${C.accent}-fg}[*]{/} on desktop | {${C.npm}-fg}NPM{/} registry {${C.git}-fg}GIT{/} repo {${C.local}-fg}LOC{/} workspace | {${C.warn}-fg}WRN{/} untrusted`,
|
|
1398
|
+
` Shortcuts: ↑↓ Space toggle ${keyHint('1')}${keyHint('2')}${keyHint('3')} tabs ${saveShortcut} ${serverShortcut} ${keyHint('g')} settings ${keyHint('O')} sort ${keyHint('m')} menu ${keyHint('u')} updates ${keyHint('r')} refresh ${keyHint('n')} new ${keyHint('q')} quit`,
|
|
1399
|
+
].join('\n'),
|
|
1400
|
+
)
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
function setStatus(msg, tone = 'info') {
|
|
1404
|
+
statusLine = `${statusPrefix(tone)}${msg}`
|
|
1405
|
+
renderHelp(tone)
|
|
1406
|
+
screen.render()
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
function handleExternalConfigChange() {
|
|
1410
|
+
if (isWritingConfig) return
|
|
1411
|
+
if (configWatchTimeout) {
|
|
1412
|
+
clearTimeout(configWatchTimeout)
|
|
1413
|
+
}
|
|
1414
|
+
configWatchTimeout = setTimeout(async () => {
|
|
1415
|
+
try {
|
|
1416
|
+
const newConfig = readDesktopConfig(paths.config, workspaceRoot)
|
|
1417
|
+
configError = null
|
|
1418
|
+
if (!areConfigsEqual(config, newConfig)) {
|
|
1419
|
+
const oldTheme = config ? config.theme : null
|
|
1420
|
+
config = newConfig
|
|
1421
|
+
deps = readDesktopDependencies(paths.packageJson)
|
|
1422
|
+
|
|
1423
|
+
for (const [name, on] of pendingPackages.entries()) {
|
|
1424
|
+
const isApp = name.startsWith('app-') || inferKind(shortName(name)) === 'app'
|
|
1425
|
+
const list = isApp ? (config.apps ?? []) : (config.modules ?? [])
|
|
1426
|
+
if (on === list.includes(name)) {
|
|
1427
|
+
pendingPackages.delete(name)
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
if (pendingTheme === oldTheme || !pendingTheme) {
|
|
1431
|
+
pendingTheme = config.theme
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
renderAll()
|
|
1435
|
+
setStatus('Configuration reloaded from disk', 'ok')
|
|
1436
|
+
await syncDependenciesWithConfig()
|
|
1437
|
+
} else {
|
|
1438
|
+
renderAll()
|
|
1439
|
+
}
|
|
1440
|
+
} catch (err) {
|
|
1441
|
+
configError = err.message
|
|
1442
|
+
renderAll()
|
|
1443
|
+
setStatus(`Error reading config: ${err.message}`, 'error')
|
|
1444
|
+
}
|
|
1445
|
+
}, 200)
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
function startConfigWatcher() {
|
|
1449
|
+
if (configWatcher) {
|
|
1450
|
+
configWatcher.close()
|
|
1451
|
+
}
|
|
1452
|
+
try {
|
|
1453
|
+
configWatcher = watch(paths.config, (eventType) => {
|
|
1454
|
+
if (eventType === 'rename') {
|
|
1455
|
+
// Re-bind watcher to the new inode (e.g. VS Code atomic save)
|
|
1456
|
+
setTimeout(startConfigWatcher, 100)
|
|
1457
|
+
}
|
|
1458
|
+
if (eventType === 'change' || eventType === 'rename') {
|
|
1459
|
+
if (ignoreNextConfigWatch) {
|
|
1460
|
+
ignoreNextConfigWatch = false
|
|
1461
|
+
return
|
|
1462
|
+
}
|
|
1463
|
+
handleExternalConfigChange()
|
|
1464
|
+
}
|
|
1465
|
+
})
|
|
1466
|
+
configWatcher.on('error', () => {
|
|
1467
|
+
setTimeout(startConfigWatcher, 500)
|
|
1468
|
+
})
|
|
1469
|
+
} catch (err) {
|
|
1470
|
+
setTimeout(startConfigWatcher, 1000)
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
function readLogs() {
|
|
1475
|
+
const logFile = devLogPath(workspaceRoot)
|
|
1476
|
+
if (!existsSync(logFile)) {
|
|
1477
|
+
logsBox.setContent('')
|
|
1478
|
+
screen.render()
|
|
1479
|
+
return
|
|
1480
|
+
}
|
|
1481
|
+
try {
|
|
1482
|
+
const stats = statSync(logFile)
|
|
1483
|
+
const streamSize = Math.min(stats.size, 10000)
|
|
1484
|
+
if (streamSize === 0) {
|
|
1485
|
+
logsBox.setContent('')
|
|
1486
|
+
screen.render()
|
|
1487
|
+
return
|
|
1488
|
+
}
|
|
1489
|
+
const fd = openSync(logFile, 'r')
|
|
1490
|
+
const buffer = Buffer.alloc(streamSize)
|
|
1491
|
+
readSync(fd, buffer, 0, streamSize, stats.size - streamSize)
|
|
1492
|
+
closeSync(fd)
|
|
1493
|
+
|
|
1494
|
+
const rawText = buffer.toString('utf8')
|
|
1495
|
+
const escapedText = rawText.replace(/{/g, '\\{').replace(/}/g, '\\}')
|
|
1496
|
+
|
|
1497
|
+
const lines = escapedText.split('\n')
|
|
1498
|
+
const lastLines = lines.slice(-100).join('\n')
|
|
1499
|
+
|
|
1500
|
+
logsBox.setContent(lastLines)
|
|
1501
|
+
logsBox.setScrollPerc(100)
|
|
1502
|
+
screen.render()
|
|
1503
|
+
} catch (err) {
|
|
1504
|
+
logsBox.setContent(`{red-fg}Error reading logs: ${err.message}{/}`)
|
|
1505
|
+
screen.render()
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
function clearLogsBox() {
|
|
1510
|
+
const logFile = devLogPath(workspaceRoot)
|
|
1511
|
+
try {
|
|
1512
|
+
writeFileSync(logFile, '')
|
|
1513
|
+
} catch {
|
|
1514
|
+
/* ignore */
|
|
1515
|
+
}
|
|
1516
|
+
logsBox.setContent('{gray-fg}Waiting for logs...{/}')
|
|
1517
|
+
logsBox.setScrollPerc(0)
|
|
1518
|
+
screen.render()
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
function startLogWatcher() {
|
|
1522
|
+
if (logWatcher) logWatcher.close()
|
|
1523
|
+
const logFile = devLogPath(workspaceRoot)
|
|
1524
|
+
readLogs()
|
|
1525
|
+
try {
|
|
1526
|
+
logWatcher = watch(logFile, (eventType) => {
|
|
1527
|
+
if (eventType === 'change') {
|
|
1528
|
+
readLogs()
|
|
1529
|
+
}
|
|
1530
|
+
})
|
|
1531
|
+
logWatcher.on('error', () => {
|
|
1532
|
+
setTimeout(startLogWatcher, 1000)
|
|
1533
|
+
})
|
|
1534
|
+
} catch {
|
|
1535
|
+
setTimeout(startLogWatcher, 1000)
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
function stopLogWatcher() {
|
|
1540
|
+
if (logWatcher) {
|
|
1541
|
+
logWatcher.close()
|
|
1542
|
+
logWatcher = null
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
/** Clear stray CLI output and redraw the full TUI (after save, build, etc.). */
|
|
1547
|
+
function refreshTerminalUi() {
|
|
1548
|
+
try {
|
|
1549
|
+
screen.program.clear()
|
|
1550
|
+
screen.program.home()
|
|
1551
|
+
} catch {
|
|
1552
|
+
process.stdout.write('\x1b[2J\x1b[H')
|
|
1553
|
+
}
|
|
1554
|
+
layoutCatalogPanel()
|
|
1555
|
+
catalogList.focus()
|
|
1556
|
+
renderAll()
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
function startSpinner(message, showProgressOverlay = false) {
|
|
1560
|
+
stopSpinner()
|
|
1561
|
+
activeTaskMessage = message
|
|
1562
|
+
spinnerFrame = 0
|
|
1563
|
+
setStatus(message)
|
|
1564
|
+
spinnerTimer = setInterval(() => {
|
|
1565
|
+
spinnerFrame = (spinnerFrame + 1) % spinnerFrameCount
|
|
1566
|
+
statusLine = `${radarSpinner(spinnerFrame)} ${message}`
|
|
1567
|
+
renderHelp('info')
|
|
1568
|
+
|
|
1569
|
+
if (showProgressOverlay && saveProgress) {
|
|
1570
|
+
const bar = `[${progressTrack(saveProgress.step, saveProgress.total, 0, 36)}]`
|
|
1571
|
+
const spin = radarSpinner(spinnerFrame)
|
|
1572
|
+
installProgressContent.setContent(
|
|
1573
|
+
[
|
|
1574
|
+
` {bold}${spin} ${saveProgress.label}{/}`,
|
|
1575
|
+
'',
|
|
1576
|
+
` {cyan-fg}${bar}{/}`,
|
|
1577
|
+
` Step ${saveProgress.step} of ${saveProgress.total}`,
|
|
1578
|
+
].join('\n')
|
|
1579
|
+
)
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
renderAll()
|
|
1583
|
+
}, 150)
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
function stopSpinner() {
|
|
1587
|
+
activeTaskMessage = null
|
|
1588
|
+
if (spinnerTimer) {
|
|
1589
|
+
clearInterval(spinnerTimer)
|
|
1590
|
+
spinnerTimer = null
|
|
1591
|
+
}
|
|
1592
|
+
renderAll()
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
function renderSettingsPanel() {
|
|
1596
|
+
const ssh = sshAuthStatus?.available
|
|
1597
|
+
? `{${C.accent}-fg}SSH authenticated{/} as ${sshAuthStatus.githubLogin ?? '?'}`
|
|
1598
|
+
: `{${C.warn}-fg}SSH not available{/} — use HTTPS git clones`
|
|
1599
|
+
settingsInfo.setContent(
|
|
1600
|
+
[
|
|
1601
|
+
ssh,
|
|
1602
|
+
sshAuthStatus?.githubLogin
|
|
1603
|
+
? `{${C.muted}-fg}GitHub username '${sshAuthStatus.githubLogin}' detected via SSH and used for fork clones.{/}`
|
|
1604
|
+
: `{${C.muted}-fg}Configure SSH or environment variable OWD_GITHUB_USER to enable fork clones.{/}`,
|
|
1605
|
+
`{${C.muted}-fg}Enter save · t test SSH · Esc cancel{/}`,
|
|
1606
|
+
].join('\n'),
|
|
1607
|
+
)
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
async function testSshFromSettings() {
|
|
1611
|
+
setStatus('Testing SSH to GitHub…')
|
|
1612
|
+
screen.render()
|
|
1613
|
+
sshAuthStatus = await detectGithubSshAuth(workspaceRoot, { force: true })
|
|
1614
|
+
if (sshAuthStatus.available && sshAuthStatus.githubLogin) {
|
|
1615
|
+
settings.githubUser = sshAuthStatus.githubLogin
|
|
1616
|
+
}
|
|
1617
|
+
renderSettingsPanel()
|
|
1618
|
+
setStatus(sshAuthStatus.message ?? 'SSH test complete', sshAuthStatus.available ? 'ok' : 'error')
|
|
1619
|
+
screen.render()
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
function openSettings() {
|
|
1623
|
+
settingsOpen = true
|
|
1624
|
+
updateCatalogKeysState()
|
|
1625
|
+
settingsOverlay.show()
|
|
1626
|
+
renderSettingsPanel()
|
|
1627
|
+
trustedOrgsInput.setValue((settings.githubOrgs ?? ['owdproject']).join(', '))
|
|
1628
|
+
trustedOrgsInput.focus()
|
|
1629
|
+
setStatus('Settings — Enter save · t test SSH · Esc cancel')
|
|
1630
|
+
screen.render()
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
function closeSettings() {
|
|
1634
|
+
settingsOpen = false
|
|
1635
|
+
updateCatalogKeysState()
|
|
1636
|
+
settingsOverlay.hide()
|
|
1637
|
+
focusCatalog()
|
|
1638
|
+
screen.render()
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
function openMenu() {
|
|
1642
|
+
if (settingsOpen || installWizardOpen || sortPickerOpen) return
|
|
1643
|
+
|
|
1644
|
+
menuOpen = true
|
|
1645
|
+
updateCatalogKeysState()
|
|
1646
|
+
menuOverlay.show()
|
|
1647
|
+
menuList.focus()
|
|
1648
|
+
setStatus('Menu — ↑↓ select · Enter run · Esc close')
|
|
1649
|
+
screen.render()
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
function closeMenu() {
|
|
1653
|
+
menuOpen = false
|
|
1654
|
+
updateCatalogKeysState()
|
|
1655
|
+
menuOverlay.hide()
|
|
1656
|
+
focusCatalog()
|
|
1657
|
+
screen.render()
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
function getLocalGitChanges(dir) {
|
|
1661
|
+
try {
|
|
1662
|
+
const porcelain = execSync('git status --porcelain', {
|
|
1663
|
+
cwd: dir,
|
|
1664
|
+
encoding: 'utf8',
|
|
1665
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
1666
|
+
}).trim()
|
|
1667
|
+
if (!porcelain) return null
|
|
1668
|
+
|
|
1669
|
+
let added = 0, modified = 0, deleted = 0
|
|
1670
|
+
for (const line of porcelain.split('\n').filter(Boolean)) {
|
|
1671
|
+
if (line.startsWith('??')) { added++; continue }
|
|
1672
|
+
const xy = line.slice(0, 2)
|
|
1673
|
+
if (xy.includes('A')) added++
|
|
1674
|
+
else if (xy.includes('D')) deleted++
|
|
1675
|
+
else modified++
|
|
1676
|
+
}
|
|
1677
|
+
return { added, modified, deleted }
|
|
1678
|
+
} catch {
|
|
1679
|
+
return null
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
function updateLocalChanges() {
|
|
1684
|
+
const checkDirs = ['apps', 'themes', 'packages']
|
|
1685
|
+
for (const dirName of checkDirs) {
|
|
1686
|
+
const fullDir = join(workspaceRoot, dirName)
|
|
1687
|
+
if (!existsSync(fullDir)) continue
|
|
1688
|
+
try {
|
|
1689
|
+
const subdirs = readdirSync(fullDir, { withFileTypes: true })
|
|
1690
|
+
for (const subdir of subdirs) {
|
|
1691
|
+
if (!subdir.isDirectory()) continue
|
|
1692
|
+
const name = subdir.name
|
|
1693
|
+
if (name.startsWith('.')) continue
|
|
1694
|
+
const subGit = join(fullDir, name, '.git')
|
|
1695
|
+
if (existsSync(subGit)) {
|
|
1696
|
+
const changes = getLocalGitChanges(join(fullDir, name))
|
|
1697
|
+
if (changes) {
|
|
1698
|
+
localGitChanges.set(name, changes)
|
|
1699
|
+
} else {
|
|
1700
|
+
localGitChanges.delete(name)
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
} catch {}
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1708
|
+
function getLocalVersion(entry) {
|
|
1709
|
+
const short = shortName(entry.name)
|
|
1710
|
+
const kind = entry.kind
|
|
1711
|
+
if (entry.localSource) {
|
|
1712
|
+
const pkgJsonPath = join(workspaceRoot, KINDS[kind].workspaceDir, short, 'package.json')
|
|
1713
|
+
if (existsSync(pkgJsonPath)) {
|
|
1714
|
+
try {
|
|
1715
|
+
return JSON.parse(readFileSync(pkgJsonPath, 'utf8')).version
|
|
1716
|
+
} catch {}
|
|
1717
|
+
}
|
|
1718
|
+
} else {
|
|
1719
|
+
const nodeModulesPath = join(workspaceRoot, 'desktop', 'node_modules', entry.name, 'package.json')
|
|
1720
|
+
if (existsSync(nodeModulesPath)) {
|
|
1721
|
+
try {
|
|
1722
|
+
return JSON.parse(readFileSync(nodeModulesPath, 'utf8')).version
|
|
1723
|
+
} catch {}
|
|
1724
|
+
}
|
|
1725
|
+
try {
|
|
1726
|
+
const desktopPkg = JSON.parse(readFileSync(join(workspaceRoot, 'desktop', 'package.json'), 'utf8'))
|
|
1727
|
+
const versionSpec = desktopPkg.dependencies?.[entry.name] || desktopPkg.devDependencies?.[entry.name]
|
|
1728
|
+
if (versionSpec) {
|
|
1729
|
+
return versionSpec.replace(/[\^~]/g, '')
|
|
1730
|
+
}
|
|
1731
|
+
} catch {}
|
|
1732
|
+
}
|
|
1733
|
+
return null
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
function semverCompare(v1, v2) {
|
|
1737
|
+
const parse = (v) => String(v).replace(/^[^\d]+/, '').split('.').map(Number)
|
|
1738
|
+
const a = parse(v1)
|
|
1739
|
+
const b = parse(v2)
|
|
1740
|
+
for (let i = 0; i < Math.max(a.length, b.length); i++) {
|
|
1741
|
+
const na = a[i] || 0
|
|
1742
|
+
const nb = b[i] || 0
|
|
1743
|
+
if (na < nb) return -1
|
|
1744
|
+
if (na > nb) return 1
|
|
1745
|
+
}
|
|
1746
|
+
return 0
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1749
|
+
async function checkRepoUpdate(dir) {
|
|
1750
|
+
try {
|
|
1751
|
+
await spawnAsync('git', ['fetch'], {
|
|
1752
|
+
cwd: dir,
|
|
1753
|
+
env: { ...process.env, GIT_TERMINAL_PROMPT: '0', SSH_ASKPASS: '', DISPLAY: '' }
|
|
1754
|
+
})
|
|
1755
|
+
|
|
1756
|
+
const behind = execSync('git rev-list --count HEAD..@{u}', {
|
|
1757
|
+
cwd: dir,
|
|
1758
|
+
encoding: 'utf8',
|
|
1759
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
1760
|
+
}).trim()
|
|
1761
|
+
|
|
1762
|
+
const count = parseInt(behind, 10)
|
|
1763
|
+
return {
|
|
1764
|
+
hasUpdate: !isNaN(count) && count > 0,
|
|
1765
|
+
behindCount: isNaN(count) ? 0 : count,
|
|
1766
|
+
}
|
|
1767
|
+
} catch {
|
|
1768
|
+
try {
|
|
1769
|
+
const branch = execSync('git rev-parse --abbrev-ref HEAD', {
|
|
1770
|
+
cwd: dir,
|
|
1771
|
+
encoding: 'utf8',
|
|
1772
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
1773
|
+
}).trim()
|
|
1774
|
+
if (!branch || branch === 'HEAD') return { hasUpdate: false, behindCount: 0 }
|
|
1775
|
+
|
|
1776
|
+
const behind = execSync(`git rev-list --count HEAD..origin/${branch}`, {
|
|
1777
|
+
cwd: dir,
|
|
1778
|
+
encoding: 'utf8',
|
|
1779
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
1780
|
+
}).trim()
|
|
1781
|
+
|
|
1782
|
+
const count = parseInt(behind, 10)
|
|
1783
|
+
return {
|
|
1784
|
+
hasUpdate: !isNaN(count) && count > 0,
|
|
1785
|
+
behindCount: isNaN(count) ? 0 : count,
|
|
1786
|
+
}
|
|
1787
|
+
} catch {
|
|
1788
|
+
return { hasUpdate: false, behindCount: 0 }
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1793
|
+
async function runUpdateWizard(updates) {
|
|
1794
|
+
if (isInstalling) {
|
|
1795
|
+
setStatus('Install/update process already running.', 'error')
|
|
1796
|
+
return
|
|
1797
|
+
}
|
|
1798
|
+
ctx.setInstalling(true)
|
|
1799
|
+
|
|
1800
|
+
// Determine steps to execute and compute totalSteps
|
|
1801
|
+
const stepsToExecute = []
|
|
1802
|
+
let needInstall = false
|
|
1803
|
+
|
|
1804
|
+
for (const [name, updateInfo] of updates) {
|
|
1805
|
+
if (name === 'client') {
|
|
1806
|
+
stepsToExecute.push({
|
|
1807
|
+
label: 'Updating client (Git)…',
|
|
1808
|
+
action: async () => {
|
|
1809
|
+
await spawnAsync('git', ['pull'], { cwd: workspaceRoot })
|
|
1810
|
+
}
|
|
1811
|
+
})
|
|
1812
|
+
needInstall = true
|
|
1813
|
+
continue
|
|
1814
|
+
}
|
|
1815
|
+
|
|
1816
|
+
const entry = catalog.find(e => e.shortName === name || e.name === name)
|
|
1817
|
+
const isGitUpdate = updateInfo.localGit || (entry && entry.localSource)
|
|
1818
|
+
|
|
1819
|
+
if (isGitUpdate) {
|
|
1820
|
+
let localDir = null
|
|
1821
|
+
if (entry && entry.kind) {
|
|
1822
|
+
localDir = join(workspaceRoot, KINDS[entry.kind].workspaceDir, name)
|
|
1823
|
+
} else {
|
|
1824
|
+
if (existsSync(join(workspaceRoot, 'apps', name))) {
|
|
1825
|
+
localDir = join(workspaceRoot, 'apps', name)
|
|
1826
|
+
} else if (existsSync(join(workspaceRoot, 'themes', name))) {
|
|
1827
|
+
localDir = join(workspaceRoot, 'themes', name)
|
|
1828
|
+
} else if (existsSync(join(workspaceRoot, 'packages', name))) {
|
|
1829
|
+
localDir = join(workspaceRoot, 'packages', name)
|
|
1830
|
+
}
|
|
1831
|
+
}
|
|
1832
|
+
|
|
1833
|
+
if (localDir) {
|
|
1834
|
+
stepsToExecute.push({
|
|
1835
|
+
label: `Updating ${name} (Git)…`,
|
|
1836
|
+
action: async () => {
|
|
1837
|
+
await spawnAsync('git', ['pull'], { cwd: localDir })
|
|
1838
|
+
}
|
|
1839
|
+
})
|
|
1840
|
+
needInstall = true
|
|
1841
|
+
}
|
|
1842
|
+
} else if (entry) {
|
|
1843
|
+
const latestVer = updateInfo.latestVersion
|
|
1844
|
+
stepsToExecute.push({
|
|
1845
|
+
label: `Updating ${shortName(entry.name)} to ${latestVer} (NPM)…`,
|
|
1846
|
+
action: async () => {
|
|
1847
|
+
await spawnAsync('pnpm', ['--filter', 'desktop', 'add', `${entry.name}@${latestVer}`], { cwd: workspaceRoot })
|
|
1848
|
+
}
|
|
1849
|
+
})
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1853
|
+
if (needInstall) {
|
|
1854
|
+
stepsToExecute.push({
|
|
1855
|
+
label: 'Updating dependencies (pnpm)…',
|
|
1856
|
+
action: async () => {
|
|
1857
|
+
await spawnAsync('pnpm', ['install'], { cwd: workspaceRoot })
|
|
1858
|
+
}
|
|
1859
|
+
})
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1862
|
+
const totalSteps = stepsToExecute.length
|
|
1863
|
+
let currentStepIndex = 0
|
|
1864
|
+
|
|
1865
|
+
// Configure and show progress overlay
|
|
1866
|
+
installProgressOverlay.setLabel(cardLabel(' Syncing Workspace '))
|
|
1867
|
+
installProgressOverlay.show()
|
|
1868
|
+
installProgressOverlay.setFront()
|
|
1869
|
+
installProgressOverlay.focus()
|
|
1870
|
+
|
|
1871
|
+
const updateProgress = (label) => {
|
|
1872
|
+
saveProgress = {
|
|
1873
|
+
step: Math.min(currentStepIndex + 1, totalSteps),
|
|
1874
|
+
total: totalSteps,
|
|
1875
|
+
label
|
|
1876
|
+
}
|
|
1877
|
+
setStatus(label)
|
|
1878
|
+
}
|
|
1879
|
+
|
|
1880
|
+
startSpinner('Running Update Wizard…', true)
|
|
1881
|
+
renderAll()
|
|
1882
|
+
|
|
1883
|
+
try {
|
|
1884
|
+
for (const step of stepsToExecute) {
|
|
1885
|
+
updateProgress(step.label)
|
|
1886
|
+
await step.action()
|
|
1887
|
+
currentStepIndex++
|
|
1888
|
+
}
|
|
1889
|
+
|
|
1890
|
+
setStatus('All updates applied successfully!', 'ok')
|
|
1891
|
+
packageUpdates.clear()
|
|
1892
|
+
updateLocalChanges()
|
|
1893
|
+
await checkForUpdates({ silent: true })
|
|
1894
|
+
} catch (err) {
|
|
1895
|
+
hasInstallError = true
|
|
1896
|
+
setStatus(`Update Wizard failed: ${err.message}`, 'error')
|
|
1897
|
+
} finally {
|
|
1898
|
+
saveProgress = null
|
|
1899
|
+
installProgressOverlay.hide()
|
|
1900
|
+
ctx.setInstalling(false)
|
|
1901
|
+
stopSpinner()
|
|
1902
|
+
focusCatalog()
|
|
1903
|
+
renderAll()
|
|
1904
|
+
}
|
|
1905
|
+
}
|
|
1906
|
+
|
|
1907
|
+
async function checkForUpdates({ silent = false } = {}) {
|
|
1908
|
+
if (checkingUpdates) return
|
|
1909
|
+
checkingUpdates = true
|
|
1910
|
+
if (!silent) {
|
|
1911
|
+
startSpinner('Checking for GitHub & NPM updates…')
|
|
1912
|
+
renderAll()
|
|
1913
|
+
}
|
|
1914
|
+
|
|
1915
|
+
try {
|
|
1916
|
+
const repos = []
|
|
1917
|
+
// 1. Client itself
|
|
1918
|
+
repos.push({ name: 'client', dir: workspaceRoot })
|
|
1919
|
+
|
|
1920
|
+
// 2. Sub-repositories
|
|
1921
|
+
const checkDirs = ['apps', 'themes', 'packages']
|
|
1922
|
+
for (const dirName of checkDirs) {
|
|
1923
|
+
const fullDir = join(workspaceRoot, dirName)
|
|
1924
|
+
if (!existsSync(fullDir)) continue
|
|
1925
|
+
try {
|
|
1926
|
+
const subdirs = readdirSync(fullDir, { withFileTypes: true })
|
|
1927
|
+
for (const subdir of subdirs) {
|
|
1928
|
+
if (!subdir.isDirectory()) continue
|
|
1929
|
+
const name = subdir.name
|
|
1930
|
+
if (name.startsWith('.')) continue
|
|
1931
|
+
const subGit = join(fullDir, name, '.git')
|
|
1932
|
+
if (existsSync(subGit)) {
|
|
1933
|
+
repos.push({ name, dir: join(fullDir, name) })
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
} catch {}
|
|
1937
|
+
}
|
|
1938
|
+
|
|
1939
|
+
// 3. NPM packages from catalog
|
|
1940
|
+
const npmPackages = catalog.filter(e => e.installed && !e.localSource)
|
|
1941
|
+
|
|
1942
|
+
// Run parallel checks
|
|
1943
|
+
await Promise.all([
|
|
1944
|
+
...repos.map(async (repo) => {
|
|
1945
|
+
const res = await checkRepoUpdate(repo.dir)
|
|
1946
|
+
if (res.hasUpdate) {
|
|
1947
|
+
packageUpdates.set(repo.name, {
|
|
1948
|
+
...res,
|
|
1949
|
+
localGit: true
|
|
1950
|
+
})
|
|
1951
|
+
} else {
|
|
1952
|
+
packageUpdates.delete(repo.name)
|
|
1953
|
+
}
|
|
1954
|
+
}),
|
|
1955
|
+
...npmPackages.map(async (item) => {
|
|
1956
|
+
const localVer = getLocalVersion(item)
|
|
1957
|
+
if (!localVer) return
|
|
1958
|
+
try {
|
|
1959
|
+
const res = await fetch(`https://registry.npmjs.org/${encodeURIComponent(item.name)}/latest`)
|
|
1960
|
+
if (res.ok) {
|
|
1961
|
+
const json = await res.json()
|
|
1962
|
+
const latestVer = json.version
|
|
1963
|
+
if (latestVer && semverCompare(localVer, latestVer) < 0) {
|
|
1964
|
+
packageUpdates.set(item.shortName, {
|
|
1965
|
+
hasUpdate: true,
|
|
1966
|
+
behindCount: 0,
|
|
1967
|
+
localVersion: localVer,
|
|
1968
|
+
latestVersion: latestVer
|
|
1969
|
+
})
|
|
1970
|
+
return
|
|
1971
|
+
}
|
|
1972
|
+
}
|
|
1973
|
+
} catch {}
|
|
1974
|
+
packageUpdates.delete(item.shortName)
|
|
1975
|
+
})
|
|
1976
|
+
])
|
|
1977
|
+
|
|
1978
|
+
const updateCount = packageUpdates.size
|
|
1979
|
+
if (!silent) stopSpinner()
|
|
1980
|
+
|
|
1981
|
+
if (updateCount > 0) {
|
|
1982
|
+
showUpdateSelectionDialog({
|
|
1983
|
+
title: 'Updates Available',
|
|
1984
|
+
updates: [...packageUpdates.entries()]
|
|
1985
|
+
}).then((runWizard) => {
|
|
1986
|
+
if (runWizard) {
|
|
1987
|
+
runUpdateWizard([...packageUpdates.entries()])
|
|
1988
|
+
}
|
|
1989
|
+
})
|
|
1990
|
+
} else {
|
|
1991
|
+
if (!silent) {
|
|
1992
|
+
setStatus('All repositories and packages are up-to-date.', 'ok')
|
|
1993
|
+
}
|
|
1994
|
+
}
|
|
1995
|
+
} catch (err) {
|
|
1996
|
+
if (!silent) {
|
|
1997
|
+
stopSpinner()
|
|
1998
|
+
setStatus(`Failed to check updates: ${err.message}`, 'error')
|
|
1999
|
+
}
|
|
2000
|
+
} finally {
|
|
2001
|
+
checkingUpdates = false
|
|
2002
|
+
renderAll()
|
|
2003
|
+
}
|
|
2004
|
+
}
|
|
2005
|
+
|
|
2006
|
+
async function runMenuAction(id) {
|
|
2007
|
+
closeMenu()
|
|
2008
|
+
switch (id) {
|
|
2009
|
+
case 'start':
|
|
2010
|
+
await startDevServer()
|
|
2011
|
+
break
|
|
2012
|
+
case 'stop':
|
|
2013
|
+
await stopDevServer()
|
|
2014
|
+
break
|
|
2015
|
+
case 'reboot':
|
|
2016
|
+
await rebootDevServer()
|
|
2017
|
+
break
|
|
2018
|
+
case 'save':
|
|
2019
|
+
await applyChanges()
|
|
2020
|
+
break
|
|
2021
|
+
case 'refresh':
|
|
2022
|
+
await refreshCatalog()
|
|
2023
|
+
break
|
|
2024
|
+
case 'check-updates':
|
|
2025
|
+
await checkForUpdates()
|
|
2026
|
+
break
|
|
2027
|
+
case 'docs':
|
|
2028
|
+
openDocsInBrowser()
|
|
2029
|
+
break
|
|
2030
|
+
case 'settings':
|
|
2031
|
+
openSettings()
|
|
2032
|
+
break
|
|
2033
|
+
case 'build':
|
|
2034
|
+
stopSpinner()
|
|
2035
|
+
setStatus('Running pnpm run generate…')
|
|
2036
|
+
screen.render()
|
|
2037
|
+
try {
|
|
2038
|
+
execSync('pnpm run generate', { cwd: workspaceRoot, stdio: 'pipe' })
|
|
2039
|
+
setStatus('Generate finished', 'ok')
|
|
2040
|
+
} catch (err) {
|
|
2041
|
+
setStatus(`Generate failed: ${err.message}`, 'error')
|
|
2042
|
+
} finally {
|
|
2043
|
+
refreshTerminalUi()
|
|
2044
|
+
}
|
|
2045
|
+
break
|
|
2046
|
+
case 'create':
|
|
2047
|
+
process.exit(10)
|
|
2048
|
+
break
|
|
2049
|
+
case 'quit':
|
|
2050
|
+
process.exit(0)
|
|
2051
|
+
break
|
|
2052
|
+
default:
|
|
2053
|
+
break
|
|
2054
|
+
}
|
|
2055
|
+
}
|
|
2056
|
+
|
|
2057
|
+
function saveSettingsFromOverlay() {
|
|
2058
|
+
const orgsRaw = trustedOrgsInput.getValue().trim()
|
|
2059
|
+
const githubOrgs = orgsRaw
|
|
2060
|
+
? orgsRaw.split(',').map((s) => s.trim()).filter(Boolean)
|
|
2061
|
+
: ['owdproject']
|
|
2062
|
+
const user = settings.githubUser || (sshAuthStatus?.available ? sshAuthStatus.githubLogin : null)
|
|
2063
|
+
settings = { ...settings, githubUser: user, githubOrgs }
|
|
2064
|
+
saveSettings(workspaceRoot, settings)
|
|
2065
|
+
closeSettings()
|
|
2066
|
+
renderClient()
|
|
2067
|
+
renderCatalogList()
|
|
2068
|
+
renderDetail()
|
|
2069
|
+
setStatus(user ? `Settings saved — GitHub user ${user}` : 'Settings saved', 'ok')
|
|
2070
|
+
}
|
|
2071
|
+
|
|
2072
|
+
function renderTabs() {
|
|
2073
|
+
const tab = (id, name, numKey) => {
|
|
2074
|
+
const active = activeTab === id
|
|
2075
|
+
if (active) {
|
|
2076
|
+
return `{bold}${cardTitle(name, numKey)}{/}`
|
|
2077
|
+
}
|
|
2078
|
+
return `{${C.muted}-fg}${name}{/}${keyHint(numKey)}`
|
|
2079
|
+
}
|
|
2080
|
+
tabBar.setContent(
|
|
2081
|
+
` ${tab('app', 'Apps', '1')} ${tab('module', 'Modules', '2')} ${tab('theme', 'Themes', '3')} | {${C.muted}-fg}Sort:{/} {bold}${sortModeLabel()}{/} ${keyHint('o')}${keyHint('O')}`,
|
|
2082
|
+
)
|
|
2083
|
+
}
|
|
2084
|
+
|
|
2085
|
+
|
|
2086
|
+
|
|
2087
|
+
const npmDownloads = {
|
|
2088
|
+
'@owdproject/core': 410,
|
|
2089
|
+
'@owdproject/app-about': 8,
|
|
2090
|
+
'@owdproject/app-todo': 12,
|
|
2091
|
+
'@owdproject/app-terminal': 24,
|
|
2092
|
+
'@owdproject/module-fs': 52,
|
|
2093
|
+
'@owdproject/module-persistence': 38,
|
|
2094
|
+
'@owdproject/theme-gnome': 14,
|
|
2095
|
+
'@owdproject/theme-win95': 36,
|
|
2096
|
+
}
|
|
2097
|
+
|
|
2098
|
+
async function loadNpmDownloads() {
|
|
2099
|
+
try {
|
|
2100
|
+
const pkgs = ['@owdproject/core']
|
|
2101
|
+
for (const item of catalog) {
|
|
2102
|
+
const name = item.name
|
|
2103
|
+
pkgs.push(name)
|
|
2104
|
+
}
|
|
2105
|
+
|
|
2106
|
+
await Promise.allSettled(
|
|
2107
|
+
pkgs.map(async (name) => {
|
|
2108
|
+
try {
|
|
2109
|
+
const res = await fetch(`https://api.npmjs.org/downloads/point/last-week/${name}`)
|
|
2110
|
+
const data = await res.json()
|
|
2111
|
+
if (data && typeof data.downloads === 'number') {
|
|
2112
|
+
npmDownloads[name] = data.downloads
|
|
2113
|
+
}
|
|
2114
|
+
} catch {
|
|
2115
|
+
// Ignore single fetch failure
|
|
2116
|
+
}
|
|
2117
|
+
})
|
|
2118
|
+
)
|
|
2119
|
+
|
|
2120
|
+
renderClient()
|
|
2121
|
+
screen.render()
|
|
2122
|
+
} catch (err) {
|
|
2123
|
+
// Ignore
|
|
2124
|
+
}
|
|
2125
|
+
}
|
|
2126
|
+
|
|
2127
|
+
function getShowcaseItems() {
|
|
2128
|
+
const items = [
|
|
2129
|
+
`{bold}Discord Server{/} · Join the community: https://discord.gg/owdproject`,
|
|
2130
|
+
`{bold}Tip{/} · Press {${C.accent}-fg}Space{/} on any app or module to toggle installation/removal`,
|
|
2131
|
+
`{bold}Tip{/} · Press {${C.accent}-fg}1{/}, {${C.accent}-fg}2{/}, or {${C.accent}-fg}3{/} keys to switch catalog tabs`,
|
|
2132
|
+
`{bold}Tip{/} · Press {${C.accent}-fg}w{/} to review changes and launch the package install wizard`,
|
|
2133
|
+
`{bold}Tip{/} · Press {${C.accent}-fg}g{/} to configure your GitHub user for forks/clones`,
|
|
2134
|
+
`{bold}Tip{/} · Press {${C.accent}-fg}o{/} to cycle catalog sorting, or {${C.accent}-fg}O{/} for sort menu`,
|
|
2135
|
+
`{bold}Tip{/} · Press {${C.accent}-fg}m{/} to open the command action menu`,
|
|
2136
|
+
`{bold}Tip{/} · Click anywhere on the bottom status bar to force-refresh catalog`,
|
|
2137
|
+
`{bold}Tip{/} · Press {${C.accent}-fg}s{/} to start dev server, {${C.accent}-fg}x{/} to stop, and {${C.accent}-fg}R{/} to reboot`,
|
|
2138
|
+
`{bold}Tip{/} · Press {${C.accent}-fg}l{/} to focus Logs; press {${C.accent}-fg}Esc{/} to return to Catalog`,
|
|
2139
|
+
]
|
|
2140
|
+
|
|
2141
|
+
const apps = catalog.filter((e) => e.kind === 'app')
|
|
2142
|
+
const mods = catalog.filter((e) => e.kind === 'module')
|
|
2143
|
+
const themes = catalog.filter((e) => e.kind === 'theme')
|
|
2144
|
+
|
|
2145
|
+
const totalStars = catalog.reduce((acc, item) => acc + (item.stars ?? 0), 0)
|
|
2146
|
+
if (totalStars > 0) {
|
|
2147
|
+
items.push(`{bold}GitHub Stats{/} · ${totalStars} total stars across all catalog modules`)
|
|
2148
|
+
}
|
|
2149
|
+
|
|
2150
|
+
if (catalog.length > 0) {
|
|
2151
|
+
const sortedByStars = [...catalog].sort((a, b) => (b.stars ?? 0) - (a.stars ?? 0))
|
|
2152
|
+
const topStar = sortedByStars[0]
|
|
2153
|
+
if (topStar && topStar.stars > 0) {
|
|
2154
|
+
items.push(`{bold}Trending{/} · {bold}{${C.accent}-fg}${topStar.shortName}{/}{/} is most popular with ★${topStar.stars} stars`)
|
|
2155
|
+
}
|
|
2156
|
+
}
|
|
2157
|
+
|
|
2158
|
+
const coreDownloads = npmDownloads['@owdproject/core']
|
|
2159
|
+
if (coreDownloads !== undefined) {
|
|
2160
|
+
items.push(`{bold}NPM Downloads{/} · core package {bold}@owdproject/core{/} got ${coreDownloads} downloads last week`)
|
|
2161
|
+
}
|
|
2162
|
+
|
|
2163
|
+
let totalCatalogDownloads = 0
|
|
2164
|
+
let topDownloadItem = null
|
|
2165
|
+
let topDownloadCount = 0
|
|
2166
|
+
|
|
2167
|
+
for (const item of catalog) {
|
|
2168
|
+
const name = item.name
|
|
2169
|
+
const dls = npmDownloads[name]
|
|
2170
|
+
if (dls !== undefined) {
|
|
2171
|
+
totalCatalogDownloads += dls
|
|
2172
|
+
if (dls > topDownloadCount) {
|
|
2173
|
+
topDownloadCount = dls
|
|
2174
|
+
topDownloadItem = item
|
|
2175
|
+
}
|
|
2176
|
+
}
|
|
2177
|
+
}
|
|
2178
|
+
|
|
2179
|
+
if (totalCatalogDownloads > 0) {
|
|
2180
|
+
items.push(`{bold}NPM Stats{/} · ${totalCatalogDownloads} total weekly downloads for OWD catalog modules`)
|
|
2181
|
+
}
|
|
2182
|
+
if (topDownloadItem && topDownloadCount > 0) {
|
|
2183
|
+
items.push(`{bold}NPM Stats{/} · {bold}{${C.accent}-fg}${topDownloadItem.shortName}{/}{/} is top with ${topDownloadCount} weekly downloads`)
|
|
2184
|
+
}
|
|
2185
|
+
|
|
2186
|
+
const maxDescLen = 25
|
|
2187
|
+
|
|
2188
|
+
if (apps.length > 0) {
|
|
2189
|
+
const app = apps[showcaseIndex % apps.length]
|
|
2190
|
+
const name = app.name
|
|
2191
|
+
const dls = npmDownloads[name]
|
|
2192
|
+
const dlsStr = dls !== undefined ? ` · ${dls} downloads/wk` : ''
|
|
2193
|
+
const starsStr = app.stars > 0 ? ` · ★${app.stars}` : ''
|
|
2194
|
+
const descStr = app.description
|
|
2195
|
+
? ` · {${C.muted}-fg}${app.description.length > maxDescLen ? app.description.slice(0, maxDescLen) + '...' : app.description}{/}`
|
|
2196
|
+
: ''
|
|
2197
|
+
items.push(`{bold}App Spotlight{/} · {bold}{${C.accent}-fg}${app.shortName}{/}{/}${starsStr}${dlsStr}${descStr}`)
|
|
2198
|
+
}
|
|
2199
|
+
|
|
2200
|
+
if (mods.length > 0) {
|
|
2201
|
+
const mod = mods[showcaseIndex % mods.length]
|
|
2202
|
+
const name = mod.name
|
|
2203
|
+
const dls = npmDownloads[name]
|
|
2204
|
+
const dlsStr = dls !== undefined ? ` · ${dls} downloads/wk` : ''
|
|
2205
|
+
const starsStr = mod.stars > 0 ? ` · ★${mod.stars}` : ''
|
|
2206
|
+
const descStr = mod.description
|
|
2207
|
+
? ` · {${C.muted}-fg}${mod.description.length > maxDescLen ? mod.description.slice(0, maxDescLen) + '...' : mod.description}{/}`
|
|
2208
|
+
: ''
|
|
2209
|
+
items.push(`{bold}Module Spotlight{/} · {bold}{${C.accent}-fg}${mod.shortName}{/}{/}${starsStr}${dlsStr}${descStr}`)
|
|
2210
|
+
}
|
|
2211
|
+
|
|
2212
|
+
if (themes.length > 0) {
|
|
2213
|
+
const theme = themes[showcaseIndex % themes.length]
|
|
2214
|
+
const name = theme.name
|
|
2215
|
+
const dls = npmDownloads[name]
|
|
2216
|
+
const dlsStr = dls !== undefined ? ` · ${dls} downloads/wk` : ''
|
|
2217
|
+
const starsStr = theme.stars > 0 ? ` · ★${theme.stars}` : ''
|
|
2218
|
+
const descStr = theme.description
|
|
2219
|
+
? ` · {${C.muted}-fg}${theme.description.length > maxDescLen ? theme.description.slice(0, maxDescLen) + '...' : theme.description}{/}`
|
|
2220
|
+
: ''
|
|
2221
|
+
items.push(`{bold}Theme Spotlight{/} · {bold}{${C.accent}-fg}${theme.shortName}{/}{/}${starsStr}${dlsStr}${descStr}`)
|
|
2222
|
+
}
|
|
2223
|
+
|
|
2224
|
+
return items
|
|
2225
|
+
}
|
|
2226
|
+
|
|
2227
|
+
function renderClient() {
|
|
2228
|
+
const http = clientStatus.http
|
|
2229
|
+
const serverRunning = clientStatus.running || http.up
|
|
2230
|
+
if (devPhase === 'starting') {
|
|
2231
|
+
// Keep starting state
|
|
2232
|
+
} else if (serverRunning) {
|
|
2233
|
+
devPhase = 'running'
|
|
2234
|
+
} else {
|
|
2235
|
+
devPhase = 'stopped'
|
|
2236
|
+
}
|
|
2237
|
+
|
|
2238
|
+
const stateUrl = `http://127.0.0.1:${settings.devPort}`
|
|
2239
|
+
const hostPort = `localhost:${settings.devPort}`
|
|
2240
|
+
|
|
2241
|
+
if (isInstalling) {
|
|
2242
|
+
screen.title = 'Installing'
|
|
2243
|
+
} else if (checkingUpdates) {
|
|
2244
|
+
screen.title = 'Checking'
|
|
2245
|
+
} else if (activeTaskMessage?.toLowerCase().includes('catalog')) {
|
|
2246
|
+
screen.title = 'Loading'
|
|
2247
|
+
} else if (activeTaskMessage) {
|
|
2248
|
+
screen.title = 'Working'
|
|
2249
|
+
} else if (isStartingServer || devPhase === 'starting') {
|
|
2250
|
+
screen.title = `[~] ${hostPort}`
|
|
2251
|
+
} else if (serverRunning) {
|
|
2252
|
+
screen.title = `[#] ${hostPort}`
|
|
2253
|
+
} else {
|
|
2254
|
+
screen.title = '[ ] Desktop'
|
|
2255
|
+
}
|
|
2256
|
+
|
|
2257
|
+
clientBox.style.border.fg = serverRunning ? C.focus : C.border
|
|
2258
|
+
metricsBox.style.border.fg = serverRunning ? C.focus : C.border
|
|
2259
|
+
|
|
2260
|
+
const boxWidth = clientBox.width || 50
|
|
2261
|
+
const maxValLen = Math.max(10, boxWidth - 16)
|
|
2262
|
+
|
|
2263
|
+
let stateLine
|
|
2264
|
+
if (devPhase === 'starting') {
|
|
2265
|
+
stateLine = ` {${C.warn}-fg}…{/} {bold}STARTING{/} ${stateUrl}`
|
|
2266
|
+
} else if (serverRunning) {
|
|
2267
|
+
stateLine = ` {${C.accent}-fg}●{/} {bold}RUNNING{/} {cyan-fg}${stateUrl}{/} {${C.muted}-fg}(PID ${clientStatus.pid}){/}`
|
|
2268
|
+
} else {
|
|
2269
|
+
stateLine = ` {${C.err}-fg}○{/} {bold}STOPPED{/} ${stateUrl}`
|
|
2270
|
+
}
|
|
2271
|
+
|
|
2272
|
+
const docsInstalled = hasDocsModuleInstalled(config, deps)
|
|
2273
|
+
let extraLine = null
|
|
2274
|
+
if (playgroundActive && playgroundLabel) {
|
|
2275
|
+
extraLine = ` {${C.muted}-fg}Playground{/} {bold}${playgroundLabel}{/} active`
|
|
2276
|
+
if (docsInstalled && serverRunning) {
|
|
2277
|
+
extraLine += ` · {${C.muted}-fg}Docs{/} ${keyHint('i')} open`
|
|
2278
|
+
}
|
|
2279
|
+
} else if (docsInstalled) {
|
|
2280
|
+
extraLine = serverRunning
|
|
2281
|
+
? ` {${C.muted}-fg}Docs{/} ${keyHint('i')} open · ${docsBasePathFromConfig(config)}`
|
|
2282
|
+
: ` {${C.muted}-fg}Docs{/} ${keyHint('d')} start server to view docs`
|
|
2283
|
+
} else {
|
|
2284
|
+
extraLine = ` {${C.muted}-fg}System{/} ready`
|
|
2285
|
+
}
|
|
2286
|
+
|
|
2287
|
+
const configRestartText = 'desktop.config.ts updated — Nuxt is restarting (see dev server log)'
|
|
2288
|
+
const maxRestartLen = Math.max(10, boxWidth - 8)
|
|
2289
|
+
const configRestartDisplay = configRestartText.length > maxRestartLen
|
|
2290
|
+
? configRestartText.slice(0, maxRestartLen - 3) + '...'
|
|
2291
|
+
: configRestartText
|
|
2292
|
+
const configRestartLine =
|
|
2293
|
+
configRestartHintUntil > Date.now() && serverRunning
|
|
2294
|
+
? ` {${C.devMode}-fg}ℹ{/} {${C.muted}-fg}${configRestartDisplay}{/}`
|
|
2295
|
+
: null
|
|
2296
|
+
|
|
2297
|
+
const workspaceShortPath = workspaceRoot.replace(process.env.HOME || '', '~')
|
|
2298
|
+
const workspaceDisplayPath = workspaceShortPath.length > maxValLen
|
|
2299
|
+
? '...' + workspaceShortPath.slice(-(maxValLen - 3))
|
|
2300
|
+
: workspaceShortPath
|
|
2301
|
+
|
|
2302
|
+
// Theme details
|
|
2303
|
+
const activeThemeName = (pendingTheme ?? config.theme ?? '—').replace('@owdproject/', '')
|
|
2304
|
+
const themePendingAsterisk = (pendingTheme && pendingTheme !== config.theme) ? ` {${C.warn}-fg}*{/}` : ''
|
|
2305
|
+
const themeLine = ` {${C.muted}-fg}Theme{/} ${activeThemeName}${themePendingAsterisk}`
|
|
2306
|
+
|
|
2307
|
+
// Git info & workspace changes/warnings integration
|
|
2308
|
+
const gitInfo = getGitStatus(workspaceRoot)
|
|
2309
|
+
const gitWarnings = getMissingGitWarnings(workspaceRoot)
|
|
2310
|
+
const gitWarningBadge = gitWarnings.length > 0 ? ' {yellow-fg}⚠️{/}' : ''
|
|
2311
|
+
const gitChanges = getWorkspaceGitChanges(workspaceRoot)
|
|
2312
|
+
const hasChanges = gitChanges.added > 0 || gitChanges.modified > 0 || gitChanges.deleted > 0
|
|
2313
|
+
|
|
2314
|
+
// Check if client repo has an update
|
|
2315
|
+
const clientUpdate = packageUpdates.get('client')
|
|
2316
|
+
const updateStr = (clientUpdate && clientUpdate.hasUpdate)
|
|
2317
|
+
? ` · {yellow-fg}update available (↑${clientUpdate.behindCount}){/}`
|
|
2318
|
+
: ''
|
|
2319
|
+
|
|
2320
|
+
let gitDisplayText
|
|
2321
|
+
if (hasChanges) {
|
|
2322
|
+
const parts = []
|
|
2323
|
+
if (gitChanges.added > 0) parts.push(`{${C.add}-fg}+${gitChanges.added}{/}`)
|
|
2324
|
+
if (gitChanges.modified > 0) parts.push(`{${C.warn}-fg}~${gitChanges.modified}{/}`)
|
|
2325
|
+
if (gitChanges.deleted > 0) parts.push(`{${C.remove}-fg}-${gitChanges.deleted}{/}`)
|
|
2326
|
+
|
|
2327
|
+
const repoParts = []
|
|
2328
|
+
if (gitChanges.mainRepo > 0) repoParts.push('root')
|
|
2329
|
+
if (gitChanges.appsRepos > 0) repoParts.push(`${gitChanges.appsRepos} app`)
|
|
2330
|
+
if (gitChanges.modulesRepos > 0) repoParts.push(`${gitChanges.modulesRepos} mod`)
|
|
2331
|
+
if (gitChanges.themesRepos > 0) repoParts.push(`${gitChanges.themesRepos} tem`)
|
|
2332
|
+
|
|
2333
|
+
const repoStr = repoParts.length > 0 ? ` (in ${repoParts.join(', ')})` : ''
|
|
2334
|
+
gitDisplayText = `branch: ${gitInfo.branch}${gitWarningBadge} · changes: ${parts.join(' ')}${repoStr}${updateStr}`
|
|
2335
|
+
} else {
|
|
2336
|
+
gitDisplayText = `branch: ${gitInfo.branch}${gitWarningBadge} · status: clean${updateStr}`
|
|
2337
|
+
}
|
|
2338
|
+
|
|
2339
|
+
const gitDisplay = gitDisplayText.length > maxValLen
|
|
2340
|
+
? gitDisplayText.slice(0, maxValLen - 3) + '...'
|
|
2341
|
+
: gitDisplayText
|
|
2342
|
+
|
|
2343
|
+
// Versions
|
|
2344
|
+
const versionsText = `Node ${process.version} · Nuxt ${nuxtVersion} · PNPM ${pnpmVersion}`
|
|
2345
|
+
const versionsDisplay = versionsText.length > maxValLen
|
|
2346
|
+
? versionsText.slice(0, maxValLen - 3) + '...'
|
|
2347
|
+
: versionsText
|
|
2348
|
+
|
|
2349
|
+
const configErrorMsg = configError ? configError.split('\n')[0] : ''
|
|
2350
|
+
const configErrorDisplay = configErrorMsg.length > maxValLen
|
|
2351
|
+
? configErrorMsg.slice(0, maxValLen - 3) + '...'
|
|
2352
|
+
: configErrorMsg
|
|
2353
|
+
|
|
2354
|
+
const workspaceLine = configError
|
|
2355
|
+
? ` {${C.err}-fg}⚠️ Config Error:{/} {${C.warn}-fg}${configErrorDisplay}{/}`
|
|
2356
|
+
: configRestartLine
|
|
2357
|
+
? configRestartLine
|
|
2358
|
+
: ` {${C.muted}-fg}Workspace{/} ${workspaceDisplayPath}`
|
|
2359
|
+
|
|
2360
|
+
const contentLines = [
|
|
2361
|
+
stateLine,
|
|
2362
|
+
truncateFormatted(extraLine, boxWidth - 4),
|
|
2363
|
+
themeLine,
|
|
2364
|
+
'',
|
|
2365
|
+
workspaceLine,
|
|
2366
|
+
` {${C.muted}-fg}Git{/} ${gitDisplay}`,
|
|
2367
|
+
` {${C.muted}-fg}Versions{/} ${versionsDisplay}`,
|
|
2368
|
+
]
|
|
2369
|
+
|
|
2370
|
+
clientBox.setContent(contentLines.join('\n'))
|
|
2371
|
+
|
|
2372
|
+
renderHelp()
|
|
2373
|
+
}
|
|
2374
|
+
|
|
2375
|
+
function getDesiredPackages(kind) {
|
|
2376
|
+
if (kind === 'theme') {
|
|
2377
|
+
return pendingTheme ?? config.theme ? [pendingTheme ?? config.theme] : []
|
|
2378
|
+
}
|
|
2379
|
+
return filterCatalog(catalog, kind, catalogFilterOptions())
|
|
2380
|
+
.filter((item) => {
|
|
2381
|
+
const pending = pendingPackages.get(item.name)
|
|
2382
|
+
if (pending !== undefined) return pending
|
|
2383
|
+
return kind === 'app'
|
|
2384
|
+
? (config.apps ?? []).includes(item.name)
|
|
2385
|
+
: (config.modules ?? []).includes(item.name)
|
|
2386
|
+
})
|
|
2387
|
+
.map((item) => item.name)
|
|
2388
|
+
}
|
|
2389
|
+
|
|
2390
|
+
function enabledCountForTab(kind) {
|
|
2391
|
+
return getDesiredPackages(kind).length
|
|
2392
|
+
}
|
|
2393
|
+
|
|
2394
|
+
function renderMetrics() {
|
|
2395
|
+
const tabLabel = KINDS[activeTab].label
|
|
2396
|
+
const onDesktop = enabledCountForTab(activeTab)
|
|
2397
|
+
|
|
2398
|
+
const spark = memHistory.length ? formatSparkline(memHistory) : '—'
|
|
2399
|
+
const memLabel =
|
|
2400
|
+
memHistory.length > 0
|
|
2401
|
+
? `${memHistory[memHistory.length - 1]} MiB`
|
|
2402
|
+
: clientStatus.stats.memMb
|
|
2403
|
+
? `${clientStatus.stats.memMb} MiB`
|
|
2404
|
+
: '—'
|
|
2405
|
+
|
|
2406
|
+
// Compute local stats
|
|
2407
|
+
let localAppsCount = 0
|
|
2408
|
+
let localModulesCount = 0
|
|
2409
|
+
let localThemesCount = 0
|
|
2410
|
+
try {
|
|
2411
|
+
localAppsCount = readdirSync(join(workspaceRoot, 'apps')).filter(d => !d.startsWith('.')).length
|
|
2412
|
+
} catch {}
|
|
2413
|
+
try {
|
|
2414
|
+
localModulesCount = readdirSync(join(workspaceRoot, 'packages')).filter(d => !d.startsWith('.') && !d.startsWith('core') && !d.startsWith('cli') && !d.startsWith('nx') && !d.startsWith('kit-')).length
|
|
2415
|
+
} catch {}
|
|
2416
|
+
try {
|
|
2417
|
+
localThemesCount = readdirSync(join(workspaceRoot, 'themes')).filter(d => !d.startsWith('.')).length
|
|
2418
|
+
} catch {}
|
|
2419
|
+
|
|
2420
|
+
// Compute registry stats
|
|
2421
|
+
const totalStars = catalog.reduce((acc, item) => acc + (item.stars ?? 0), 0)
|
|
2422
|
+
let totalCatalogDownloads = 0
|
|
2423
|
+
for (const item of catalog) {
|
|
2424
|
+
const name = item.name
|
|
2425
|
+
const dls = npmDownloads[name]
|
|
2426
|
+
if (dls !== undefined) {
|
|
2427
|
+
totalCatalogDownloads += dls
|
|
2428
|
+
}
|
|
2429
|
+
}
|
|
2430
|
+
|
|
2431
|
+
const ghUserText = settings.githubUser ? `${settings.githubUser}` : '(not set)'
|
|
2432
|
+
const sshText = sshAuthStatus?.available
|
|
2433
|
+
? `{${C.accent}-fg}SSH ok{/}`
|
|
2434
|
+
: `{${C.warn}-fg}HTTPS{/}`
|
|
2435
|
+
const trustedText = trustedPublishers(settings.githubOrgs, settings.githubUser).join(', ')
|
|
2436
|
+
const catalogCountText = catalog.length
|
|
2437
|
+
? `${catalog.length} pkgs${catalogCacheAge ? ` (${catalogCacheAge})` : ''}`
|
|
2438
|
+
: 'loading…'
|
|
2439
|
+
|
|
2440
|
+
const maxInnerLines = 7
|
|
2441
|
+
const metricsLines = [
|
|
2442
|
+
` {${C.muted}-fg}Memory{/} {bold}${memLabel}{/} {${C.accent}-fg}${spark}{/}`,
|
|
2443
|
+
` {${C.muted}-fg}Local{/} {bold}${localAppsCount}{/} apps · {bold}${localModulesCount}{/} modules · {bold}${localThemesCount}{/} themes`,
|
|
2444
|
+
` {${C.muted}-fg}Registry{/} {bold}${totalStars}{/} stars · {bold}${totalCatalogDownloads.toLocaleString('en-US')}{/} dls/wk`,
|
|
2445
|
+
` {${C.muted}-fg}GitHub{/} {bold}${ghUserText}{/} · ${sshText}`,
|
|
2446
|
+
` {${C.muted}-fg}Catalog{/} {bold}${catalogCountText}{/} · {${C.muted}-fg}Trust:{/} ${trustedText}`,
|
|
2447
|
+
]
|
|
2448
|
+
|
|
2449
|
+
const showcaseItems = getShowcaseItems()
|
|
2450
|
+
if (showcaseItems.length > 0) {
|
|
2451
|
+
const showcaseItem = showcaseItems[showcaseIndex % showcaseItems.length]
|
|
2452
|
+
const boxWidth = metricsBox.width || 40
|
|
2453
|
+
const borderLine = ` {${C.borderDim}-fg}${ '─'.repeat(Math.max(10, boxWidth - 4)) }{/}`
|
|
2454
|
+
|
|
2455
|
+
const padCount = maxInnerLines - metricsLines.length - 2
|
|
2456
|
+
for (let i = 0; i < padCount; i++) {
|
|
2457
|
+
metricsLines.push('')
|
|
2458
|
+
}
|
|
2459
|
+
|
|
2460
|
+
metricsLines.push(borderLine)
|
|
2461
|
+
metricsLines.push(` ${truncateFormatted(showcaseItem, boxWidth - 4)}`)
|
|
2462
|
+
}
|
|
2463
|
+
|
|
2464
|
+
metricsBox.setContent(metricsLines.join('\n'))
|
|
2465
|
+
}
|
|
2466
|
+
|
|
2467
|
+
function formatCatalogRow(item) {
|
|
2468
|
+
if (activeTab === 'theme') {
|
|
2469
|
+
const selected = shortName(pendingTheme ?? config.theme ?? '') === shortName(item.name)
|
|
2470
|
+
const pendingChange =
|
|
2471
|
+
pendingTheme &&
|
|
2472
|
+
shortName(pendingTheme) !== shortName(config.theme ?? '') &&
|
|
2473
|
+
shortName(item.name) === shortName(pendingTheme)
|
|
2474
|
+
return formatCatalogRowPlain(
|
|
2475
|
+
{
|
|
2476
|
+
...item,
|
|
2477
|
+
installed: selected && !pendingChange,
|
|
2478
|
+
pending: pendingChange ? true : undefined,
|
|
2479
|
+
},
|
|
2480
|
+
{ colors: FORMAT_COLORS, columns: currentColumns, packageUpdates, localGitChanges },
|
|
2481
|
+
)
|
|
2482
|
+
}
|
|
2483
|
+
|
|
2484
|
+
const pending = pendingPackages.get(item.name)
|
|
2485
|
+
return formatCatalogRowPlain(
|
|
2486
|
+
{
|
|
2487
|
+
...item,
|
|
2488
|
+
pending,
|
|
2489
|
+
isNew: activeTab === 'module' && item.isNew,
|
|
2490
|
+
isRecent: activeTab === 'module' && item.isRecent,
|
|
2491
|
+
},
|
|
2492
|
+
{ colors: FORMAT_COLORS, columns: currentColumns, packageUpdates, localGitChanges },
|
|
2493
|
+
)
|
|
2494
|
+
}
|
|
2495
|
+
|
|
2496
|
+
function restoreCatalogListSelection(entries, pkgName) {
|
|
2497
|
+
if (!pkgName || !entries.length) return
|
|
2498
|
+
const idx = entries.findIndex((e) => e.name === pkgName)
|
|
2499
|
+
if (idx < 0) return
|
|
2500
|
+
catalogList._listInitialized = false
|
|
2501
|
+
catalogList.select(idx)
|
|
2502
|
+
}
|
|
2503
|
+
|
|
2504
|
+
function listPageStep(list) {
|
|
2505
|
+
return Math.max(1, list.height - list.iheight - 1)
|
|
2506
|
+
}
|
|
2507
|
+
|
|
2508
|
+
function renderCatalogList() {
|
|
2509
|
+
const entries = catalogEntries()
|
|
2510
|
+
const prevPkg = entries[catalogList.selected]?.name
|
|
2511
|
+
const items = entries.map((item) => formatCatalogRow(item))
|
|
2512
|
+
catalogList.setItems(items.length ? items : ['{gray-fg}Loading…{/}'])
|
|
2513
|
+
restoreCatalogListSelection(entries, prevPkg)
|
|
2514
|
+
renderTabs()
|
|
2515
|
+
}
|
|
2516
|
+
|
|
2517
|
+
async function renderDetail() {
|
|
2518
|
+
const entries = catalogEntries()
|
|
2519
|
+
const idx = catalogList.selected
|
|
2520
|
+
const item = entries[idx]
|
|
2521
|
+
if (!item) {
|
|
2522
|
+
detailBox.setContent(`{${C.muted}-fg}Select a package for install preview{/}`)
|
|
2523
|
+
return
|
|
2524
|
+
}
|
|
2525
|
+
|
|
2526
|
+
const kind = KINDS[activeTab]
|
|
2527
|
+
const target = `${kind.workspaceDir}/${item.shortName}`
|
|
2528
|
+
const detailWidth = currentColumns.sel + currentColumns.name + currentColumns.sources + currentColumns.publisher + currentColumns.meta + 4
|
|
2529
|
+
detailBox.setContent(formatDetailPanel(item, target, FORMAT_COLORS, detailWidth))
|
|
2530
|
+
}
|
|
2531
|
+
|
|
2532
|
+
function renderAll() {
|
|
2533
|
+
layoutCatalogPanel()
|
|
2534
|
+
renderLegendBar()
|
|
2535
|
+
headerBar.setContent(formatHeaderLine(FORMAT_COLORS, currentColumns))
|
|
2536
|
+
applyFocusStyles()
|
|
2537
|
+
renderClient()
|
|
2538
|
+
renderMetrics()
|
|
2539
|
+
renderCatalogList()
|
|
2540
|
+
renderDetail()
|
|
2541
|
+
renderHelp('info')
|
|
2542
|
+
screen.render()
|
|
2543
|
+
}
|
|
2544
|
+
|
|
2545
|
+
function focusCatalog() {
|
|
2546
|
+
catalogList.focus()
|
|
2547
|
+
applyFocusStyles()
|
|
2548
|
+
const hint =
|
|
2549
|
+
activeTab === 'theme'
|
|
2550
|
+
? `${KINDS[activeTab].label} — Space select theme · ${keyHint('s')} save`
|
|
2551
|
+
: `${KINDS[activeTab].label} — Space toggle · ${keyHint('s')} save`
|
|
2552
|
+
setStatus(hint)
|
|
2553
|
+
}
|
|
2554
|
+
|
|
2555
|
+
function selectThemeAt(index) {
|
|
2556
|
+
const entries = catalogEntries()
|
|
2557
|
+
const item = entries[index]
|
|
2558
|
+
if (!item) return
|
|
2559
|
+
if (pendingTheme === item.name) {
|
|
2560
|
+
pendingTheme = config.theme
|
|
2561
|
+
setStatus(`Restored theme to default`)
|
|
2562
|
+
} else {
|
|
2563
|
+
pendingTheme = item.name
|
|
2564
|
+
setStatus(`Theme → ${item.shortName} — ${keyHint('s')} save`, 'ok')
|
|
2565
|
+
}
|
|
2566
|
+
renderCatalogList()
|
|
2567
|
+
renderMetrics()
|
|
2568
|
+
renderClient()
|
|
2569
|
+
renderDetail()
|
|
2570
|
+
screen.render()
|
|
2571
|
+
}
|
|
2572
|
+
|
|
2573
|
+
function toggleCatalogAt(index) {
|
|
2574
|
+
if (activeTab === 'theme') {
|
|
2575
|
+
selectThemeAt(index)
|
|
2576
|
+
return
|
|
2577
|
+
}
|
|
2578
|
+
|
|
2579
|
+
const entries = catalogEntries()
|
|
2580
|
+
const item = entries[index]
|
|
2581
|
+
if (!item) return
|
|
2582
|
+
|
|
2583
|
+
if (pendingPackages.has(item.name)) {
|
|
2584
|
+
pendingPackages.delete(item.name)
|
|
2585
|
+
installChoices.delete(item.name)
|
|
2586
|
+
setStatus(`Restored ${item.shortName}`)
|
|
2587
|
+
} else {
|
|
2588
|
+
const next = !item.installed
|
|
2589
|
+
pendingPackages.set(item.name, next)
|
|
2590
|
+
setStatus(
|
|
2591
|
+
`${next ? 'Queued install' : 'Queued remove'} ${item.shortName} — ${keyHint('s')} review`,
|
|
2592
|
+
)
|
|
2593
|
+
}
|
|
2594
|
+
|
|
2595
|
+
renderCatalogList()
|
|
2596
|
+
renderMetrics()
|
|
2597
|
+
renderClient()
|
|
2598
|
+
renderDetail()
|
|
2599
|
+
screen.render()
|
|
2600
|
+
}
|
|
2601
|
+
|
|
2602
|
+
function setCatalogStateAt(index, forceState) {
|
|
2603
|
+
if (activeTab === 'theme') {
|
|
2604
|
+
if (forceState === true) {
|
|
2605
|
+
selectThemeAt(index)
|
|
2606
|
+
}
|
|
2607
|
+
return
|
|
2608
|
+
}
|
|
2609
|
+
|
|
2610
|
+
const entries = catalogEntries()
|
|
2611
|
+
const item = entries[index]
|
|
2612
|
+
if (!item) return
|
|
2613
|
+
|
|
2614
|
+
const isCurrentDesired = pendingPackages.has(item.name)
|
|
2615
|
+
? pendingPackages.get(item.name)
|
|
2616
|
+
: item.installed
|
|
2617
|
+
|
|
2618
|
+
if (isCurrentDesired === forceState) {
|
|
2619
|
+
if (pendingPackages.has(item.name)) {
|
|
2620
|
+
pendingPackages.delete(item.name)
|
|
2621
|
+
installChoices.delete(item.name)
|
|
2622
|
+
setStatus(`Restored ${item.shortName}`)
|
|
2623
|
+
}
|
|
2624
|
+
} else {
|
|
2625
|
+
pendingPackages.set(item.name, forceState)
|
|
2626
|
+
setStatus(
|
|
2627
|
+
`${forceState ? 'Queued install' : 'Queued remove'} ${item.shortName} — ${keyHint('s')} review`,
|
|
2628
|
+
)
|
|
2629
|
+
}
|
|
2630
|
+
|
|
2631
|
+
renderCatalogList()
|
|
2632
|
+
renderMetrics()
|
|
2633
|
+
renderClient()
|
|
2634
|
+
renderDetail()
|
|
2635
|
+
screen.render()
|
|
2636
|
+
}
|
|
2637
|
+
|
|
2638
|
+
|
|
2639
|
+
function collectInstallPlan() {
|
|
2640
|
+
const packagesToInstall = []
|
|
2641
|
+
const packagesToMaterialize = []
|
|
2642
|
+
const nextApps = getDesiredPackages('app')
|
|
2643
|
+
const nextModules = getDesiredPackages('module').filter(isInstallableDesktopModule)
|
|
2644
|
+
const nextTheme = pendingTheme ?? config.theme
|
|
2645
|
+
const prevApps = new Set(config.apps ?? [])
|
|
2646
|
+
const prevModules = new Set(config.modules ?? [])
|
|
2647
|
+
|
|
2648
|
+
for (const pkg of nextApps) {
|
|
2649
|
+
if (!prevApps.has(pkg)) packagesToInstall.push(pkg)
|
|
2650
|
+
}
|
|
2651
|
+
for (const pkg of nextModules) {
|
|
2652
|
+
if (!prevModules.has(pkg)) packagesToInstall.push(pkg)
|
|
2653
|
+
}
|
|
2654
|
+
if (nextTheme && shortName(nextTheme) !== shortName(config.theme ?? '')) packagesToInstall.push(nextTheme)
|
|
2655
|
+
|
|
2656
|
+
const enabled = [...nextApps, ...nextModules, nextTheme].filter(Boolean)
|
|
2657
|
+
for (const pkg of enabled) {
|
|
2658
|
+
if (packagesToInstall.includes(pkg)) continue
|
|
2659
|
+
if (!hasLocalWorkspaceSource(workspaceRoot, pkg)) {
|
|
2660
|
+
packagesToMaterialize.push(pkg)
|
|
2661
|
+
}
|
|
2662
|
+
}
|
|
2663
|
+
|
|
2664
|
+
for (const pkg of installChoices.keys()) {
|
|
2665
|
+
const choice = installChoices.get(pkg)
|
|
2666
|
+
if (choice?.type === 'npm') {
|
|
2667
|
+
if (!packagesToInstall.includes(pkg)) {
|
|
2668
|
+
packagesToInstall.push(pkg)
|
|
2669
|
+
}
|
|
2670
|
+
} else {
|
|
2671
|
+
if (
|
|
2672
|
+
!packagesToInstall.includes(pkg) &&
|
|
2673
|
+
!packagesToMaterialize.includes(pkg) &&
|
|
2674
|
+
!hasLocalWorkspaceSource(workspaceRoot, pkg)
|
|
2675
|
+
) {
|
|
2676
|
+
packagesToMaterialize.push(pkg)
|
|
2677
|
+
}
|
|
2678
|
+
}
|
|
2679
|
+
}
|
|
2680
|
+
|
|
2681
|
+
return { packagesToInstall, packagesToMaterialize, nextApps, nextModules, nextTheme }
|
|
2682
|
+
}
|
|
2683
|
+
|
|
2684
|
+
function renderInstallWizardPanel() {
|
|
2685
|
+
const pkg = installWizardQueue[installWizardIndex]
|
|
2686
|
+
if (!pkg) return
|
|
2687
|
+
const entry = catalog.find((e) => e.name === pkg)
|
|
2688
|
+
const idx = installWizardIndex + 1
|
|
2689
|
+
const total = installWizardQueue.length
|
|
2690
|
+
installWizardOverlay.setLabel(` Install ${shortName(pkg)} (${idx}/${total}) `)
|
|
2691
|
+
|
|
2692
|
+
const kind = entry?.kind || 'module'
|
|
2693
|
+
const folder = kind === 'app' ? 'apps' : kind === 'theme' ? 'themes' : 'packages'
|
|
2694
|
+
const targetDir = `${folder}/${shortName(pkg)}`
|
|
2695
|
+
|
|
2696
|
+
if (installWizardIsStartup) {
|
|
2697
|
+
installWizardOverlay.height = 19
|
|
2698
|
+
installWizardHeader.height = 6
|
|
2699
|
+
installWizardList.top = 8
|
|
2700
|
+
installWizardWarning.top = 6
|
|
2701
|
+
} else {
|
|
2702
|
+
installWizardOverlay.height = 16
|
|
2703
|
+
installWizardHeader.height = 3
|
|
2704
|
+
installWizardList.top = 6
|
|
2705
|
+
installWizardWarning.top = 4
|
|
2706
|
+
}
|
|
2707
|
+
|
|
2708
|
+
const lines = [`{bold}${pkg}{/}`]
|
|
2709
|
+
if (entry?.description) {
|
|
2710
|
+
lines.push(`{${C.muted}-fg}${entry.description.slice(0, 90)}{/}`)
|
|
2711
|
+
lines.push('')
|
|
2712
|
+
}
|
|
2713
|
+
if (installWizardIsStartup) {
|
|
2714
|
+
lines.push(`{yellow-fg}Active package (workspace:*) but local folder is missing.{/}`)
|
|
2715
|
+
}
|
|
2716
|
+
lines.push(`{${C.muted}-fg}Destination path: {bold}${targetDir}/{/}{/}`)
|
|
2717
|
+
|
|
2718
|
+
installWizardHeader.setContent(lines.join('\n'))
|
|
2719
|
+
|
|
2720
|
+
if (installWizardUntrusted && !installWizardConfirmUntrusted) {
|
|
2721
|
+
installWizardWarning.show()
|
|
2722
|
+
const trusted = trustedPublishers(settings.githubOrgs, settings.githubUser).join(', ')
|
|
2723
|
+
installWizardWarning.setContent(
|
|
2724
|
+
`{${C.warn}-fg}⚠ Publisher "${entry?.org ?? 'unknown'}" is not trusted (${trusted}). Enter to confirm anyway.{/}`,
|
|
2725
|
+
)
|
|
2726
|
+
installWizardList.hide()
|
|
2727
|
+
installWizardFooter.setContent(
|
|
2728
|
+
`{${C.muted}-fg}Enter confirm · s skip · Esc cancel all{/}`,
|
|
2729
|
+
)
|
|
2730
|
+
} else {
|
|
2731
|
+
installWizardWarning.hide()
|
|
2732
|
+
installWizardList.show()
|
|
2733
|
+
installWizardList.setItems(
|
|
2734
|
+
installWizardOptions.map((opt) => {
|
|
2735
|
+
const prefix =
|
|
2736
|
+
opt.kind === 'npm' ? `{${C.npm}-fg}NPM{/}` : opt.kind === 'local' ? `{${C.local}-fg}LOC{/}` : `{${C.git}-fg}GIT{/}`
|
|
2737
|
+
return `${prefix} ${opt.label}`
|
|
2738
|
+
}),
|
|
2739
|
+
)
|
|
2740
|
+
let defaultIdx = 0
|
|
2741
|
+
const isLocalAvailable = installWizardOptions.some((opt) => opt.id === 'local')
|
|
2742
|
+
if (isLocalAvailable) {
|
|
2743
|
+
defaultIdx = installWizardOptions.findIndex((opt) => opt.id === 'local')
|
|
2744
|
+
} else {
|
|
2745
|
+
const lastId = settings.lastInstallChoiceId
|
|
2746
|
+
if (lastId) {
|
|
2747
|
+
let found = installWizardOptions.findIndex((opt) => opt.id === lastId)
|
|
2748
|
+
if (found >= 0) {
|
|
2749
|
+
defaultIdx = found
|
|
2750
|
+
} else {
|
|
2751
|
+
const isSsh = lastId.includes('ssh')
|
|
2752
|
+
const isHttps = lastId.includes('https')
|
|
2753
|
+
const isNpm = lastId === 'npm'
|
|
2754
|
+
if (isNpm) {
|
|
2755
|
+
found = installWizardOptions.findIndex((opt) => opt.id === 'npm')
|
|
2756
|
+
if (found >= 0) defaultIdx = found
|
|
2757
|
+
} else {
|
|
2758
|
+
found = installWizardOptions.findIndex((opt) => {
|
|
2759
|
+
if (isSsh && opt.protocol === 'ssh') return true
|
|
2760
|
+
if (isHttps && opt.protocol === 'https') return true
|
|
2761
|
+
return false
|
|
2762
|
+
})
|
|
2763
|
+
if (found >= 0) defaultIdx = found
|
|
2764
|
+
}
|
|
2765
|
+
}
|
|
2766
|
+
}
|
|
2767
|
+
}
|
|
2768
|
+
installWizardList.select(defaultIdx)
|
|
2769
|
+
installWizardFooter.setContent(
|
|
2770
|
+
`{${C.muted}-fg}↑↓ select source · Enter install · s skip · Esc cancel all{/}`,
|
|
2771
|
+
)
|
|
2772
|
+
}
|
|
2773
|
+
}
|
|
2774
|
+
|
|
2775
|
+
async function openInstallWizardStep() {
|
|
2776
|
+
const pkg = installWizardQueue[installWizardIndex]
|
|
2777
|
+
if (!pkg) {
|
|
2778
|
+
if (installWizardAutoExecute) {
|
|
2779
|
+
closeInstallWizard()
|
|
2780
|
+
await executeInstallPlan()
|
|
2781
|
+
} else {
|
|
2782
|
+
closeInstallWizard(true)
|
|
2783
|
+
}
|
|
2784
|
+
return
|
|
2785
|
+
}
|
|
2786
|
+
|
|
2787
|
+
const entry = catalog.find((e) => e.name === pkg)
|
|
2788
|
+
installWizardUntrusted = entry?.trusted === false
|
|
2789
|
+
installWizardConfirmUntrusted = false
|
|
2790
|
+
|
|
2791
|
+
startSpinner(`Resolving sources for ${shortName(pkg)}…`)
|
|
2792
|
+
screen.render()
|
|
2793
|
+
|
|
2794
|
+
const metadata = await resolvePackageSources(
|
|
2795
|
+
shortName(pkg),
|
|
2796
|
+
settings,
|
|
2797
|
+
workspaceRoot,
|
|
2798
|
+
entry ?? {},
|
|
2799
|
+
)
|
|
2800
|
+
if (!sshAuthStatus) {
|
|
2801
|
+
sshAuthStatus = await detectGithubSshAuth(workspaceRoot)
|
|
2802
|
+
}
|
|
2803
|
+
installWizardOptions = buildSourceOptions(metadata, settings, sshAuthStatus ?? {})
|
|
2804
|
+
|
|
2805
|
+
stopSpinner()
|
|
2806
|
+
|
|
2807
|
+
if (installWizardOptions.length === 0) {
|
|
2808
|
+
setStatus(`No install source for ${shortName(pkg)} — skipped`, 'error')
|
|
2809
|
+
installWizardIndex++
|
|
2810
|
+
await openInstallWizardStep()
|
|
2811
|
+
return
|
|
2812
|
+
}
|
|
2813
|
+
|
|
2814
|
+
installWizardOpen = true
|
|
2815
|
+
updateCatalogKeysState()
|
|
2816
|
+
installWizardOverlay.setFront()
|
|
2817
|
+
installWizardOverlay.show()
|
|
2818
|
+
renderInstallWizardPanel()
|
|
2819
|
+
screen.render()
|
|
2820
|
+
installWizardList.focus()
|
|
2821
|
+
screen.render()
|
|
2822
|
+
}
|
|
2823
|
+
|
|
2824
|
+
function closeInstallWizard(success = false) {
|
|
2825
|
+
installWizardOpen = false
|
|
2826
|
+
updateCatalogKeysState()
|
|
2827
|
+
installWizardOverlay.hide()
|
|
2828
|
+
installWizardQueue = []
|
|
2829
|
+
installWizardIndex = 0
|
|
2830
|
+
installWizardOptions = []
|
|
2831
|
+
installWizardUntrusted = false
|
|
2832
|
+
installWizardConfirmUntrusted = false
|
|
2833
|
+
installWizardIsStartup = false
|
|
2834
|
+
installWizardAutoExecute = true
|
|
2835
|
+
focusCatalog()
|
|
2836
|
+
screen.render()
|
|
2837
|
+
if (installWizardResolve) {
|
|
2838
|
+
const resolve = installWizardResolve
|
|
2839
|
+
installWizardResolve = null
|
|
2840
|
+
resolve(success)
|
|
2841
|
+
}
|
|
2842
|
+
}
|
|
2843
|
+
|
|
2844
|
+
let installWizardProcessing = false
|
|
2845
|
+
|
|
2846
|
+
async function skipInstallWizardStep() {
|
|
2847
|
+
if (installWizardProcessing) return
|
|
2848
|
+
installWizardProcessing = true
|
|
2849
|
+
try {
|
|
2850
|
+
installWizardIndex++
|
|
2851
|
+
if (installWizardIndex >= installWizardQueue.length) {
|
|
2852
|
+
closeInstallWizard(true)
|
|
2853
|
+
return
|
|
2854
|
+
}
|
|
2855
|
+
await openInstallWizardStep()
|
|
2856
|
+
} finally {
|
|
2857
|
+
installWizardProcessing = false
|
|
2858
|
+
}
|
|
2859
|
+
}
|
|
2860
|
+
|
|
2861
|
+
async function confirmInstallWizardStep(choice) {
|
|
2862
|
+
const pkg = installWizardQueue[installWizardIndex]
|
|
2863
|
+
if (!pkg) return
|
|
2864
|
+
installChoices.set(pkg, choice)
|
|
2865
|
+
|
|
2866
|
+
// Save choice to settings
|
|
2867
|
+
const option = installWizardOptions[installWizardList.selected]
|
|
2868
|
+
if (option) {
|
|
2869
|
+
settings.lastInstallChoiceId = option.id
|
|
2870
|
+
saveSettings(workspaceRoot, settings)
|
|
2871
|
+
}
|
|
2872
|
+
|
|
2873
|
+
installWizardIndex++
|
|
2874
|
+
installWizardConfirmUntrusted = false
|
|
2875
|
+
if (installWizardIndex >= installWizardQueue.length) {
|
|
2876
|
+
closeInstallWizard(true)
|
|
2877
|
+
return
|
|
2878
|
+
}
|
|
2879
|
+
await openInstallWizardStep()
|
|
2880
|
+
}
|
|
2881
|
+
|
|
2882
|
+
async function handleInstallWizardSelect() {
|
|
2883
|
+
if (installWizardProcessing) return
|
|
2884
|
+
installWizardProcessing = true
|
|
2885
|
+
try {
|
|
2886
|
+
const pkg = installWizardQueue[installWizardIndex]
|
|
2887
|
+
if (!pkg) return
|
|
2888
|
+
|
|
2889
|
+
if (installWizardUntrusted && !installWizardConfirmUntrusted) {
|
|
2890
|
+
installWizardConfirmUntrusted = true
|
|
2891
|
+
renderInstallWizardPanel()
|
|
2892
|
+
installWizardList.focus()
|
|
2893
|
+
screen.render()
|
|
2894
|
+
return
|
|
2895
|
+
}
|
|
2896
|
+
|
|
2897
|
+
const idx = installWizardList.selected
|
|
2898
|
+
const option = installWizardOptions[idx]
|
|
2899
|
+
if (!option) return
|
|
2900
|
+
await confirmInstallWizardStep(option.choice)
|
|
2901
|
+
} finally {
|
|
2902
|
+
installWizardProcessing = false
|
|
2903
|
+
}
|
|
2904
|
+
}
|
|
2905
|
+
|
|
2906
|
+
function startInstallWizard(packagesToInstall, isStartup = false, { autoExecute = true, clearChoices: doClear = true } = {}) {
|
|
2907
|
+
return new Promise((resolve) => {
|
|
2908
|
+
if (packagesToInstall.length === 0) {
|
|
2909
|
+
resolve(true)
|
|
2910
|
+
return
|
|
2911
|
+
}
|
|
2912
|
+
installWizardIsStartup = isStartup
|
|
2913
|
+
installWizardAutoExecute = autoExecute
|
|
2914
|
+
installWizardResolve = resolve
|
|
2915
|
+
installWizardQueue = [...packagesToInstall]
|
|
2916
|
+
installWizardIndex = 0
|
|
2917
|
+
if (doClear) installChoices.clear()
|
|
2918
|
+
openInstallWizardStep()
|
|
2919
|
+
})
|
|
2920
|
+
}
|
|
2921
|
+
|
|
2922
|
+
async function executeInstallPlan() {
|
|
2923
|
+
return await extExecuteInstallPlan(ctx)
|
|
2924
|
+
}
|
|
2925
|
+
|
|
2926
|
+
async function applyChanges() {
|
|
2927
|
+
stopSpinner()
|
|
2928
|
+
return await runStartupInstallFlow({ isStartup: false })
|
|
2929
|
+
}
|
|
2930
|
+
|
|
2931
|
+
async function syncDependenciesWithConfig() {
|
|
2932
|
+
const declared = [
|
|
2933
|
+
...(config.apps ?? []),
|
|
2934
|
+
...(config.modules ?? []),
|
|
2935
|
+
config.theme,
|
|
2936
|
+
].filter(Boolean)
|
|
2937
|
+
|
|
2938
|
+
const installedDeps = readDesktopDependencies(paths.packageJson)
|
|
2939
|
+
|
|
2940
|
+
const missing = declared.filter(pkg => !installedDeps.includes(pkg))
|
|
2941
|
+
const extra = installedDeps.filter(pkg => !declared.includes(pkg))
|
|
2942
|
+
|
|
2943
|
+
if (missing.length === 0 && extra.length === 0) {
|
|
2944
|
+
return // Already synced!
|
|
2945
|
+
}
|
|
2946
|
+
|
|
2947
|
+
setStatus('Synchronizing dependencies with config…')
|
|
2948
|
+
startSpinner('Syncing package.json…')
|
|
2949
|
+
ctx.setInstalling(true)
|
|
2950
|
+
screen.render()
|
|
2951
|
+
|
|
2952
|
+
try {
|
|
2953
|
+
// 1. Add missing dependencies
|
|
2954
|
+
for (const pkg of missing) {
|
|
2955
|
+
setStatus(`Installing missing package ${shortName(pkg)}…`)
|
|
2956
|
+
screen.render()
|
|
2957
|
+
await installPackage(pkg, settings, workspaceRoot, { stdio: 'pipe' })
|
|
2958
|
+
}
|
|
2959
|
+
|
|
2960
|
+
// 2. Remove extra dependencies
|
|
2961
|
+
for (const pkg of extra) {
|
|
2962
|
+
setStatus(`Removing extra package ${shortName(pkg)}…`)
|
|
2963
|
+
screen.render()
|
|
2964
|
+
await spawnAsync('pnpm', ['remove', pkg], { cwd: paths.desktop })
|
|
2965
|
+
}
|
|
2966
|
+
|
|
2967
|
+
// Reload dependencies
|
|
2968
|
+
deps = readDesktopDependencies(paths.packageJson)
|
|
2969
|
+
renderAll()
|
|
2970
|
+
setStatus('Dependencies synced successfully', 'ok')
|
|
2971
|
+
} catch (err) {
|
|
2972
|
+
hasInstallError = true
|
|
2973
|
+
setStatus(`Sync failed: ${err.message}`, 'error')
|
|
2974
|
+
} finally {
|
|
2975
|
+
ctx.setInstalling(false)
|
|
2976
|
+
stopSpinner()
|
|
2977
|
+
screen.render()
|
|
2978
|
+
}
|
|
2979
|
+
}
|
|
2980
|
+
|
|
2981
|
+
async function startDevServer() {
|
|
2982
|
+
if (isStartingServer || devPhase === 'starting') {
|
|
2983
|
+
setStatus('Dev server is already starting…')
|
|
2984
|
+
return
|
|
2985
|
+
}
|
|
2986
|
+
if (isDevServerUp()) {
|
|
2987
|
+
setStatus(`Dev server already running — ${keyHint('m')} menu to stop or reboot`, 'info')
|
|
2988
|
+
return
|
|
2989
|
+
}
|
|
2990
|
+
|
|
2991
|
+
isStartingServer = true
|
|
2992
|
+
updateCatalogKeysState()
|
|
2993
|
+
|
|
2994
|
+
try {
|
|
2995
|
+
const queue = pendingQueueSummary()
|
|
2996
|
+
if (queue) {
|
|
2997
|
+
const confirmSave = await showCustomConfirm({
|
|
2998
|
+
title: 'Unsaved Changes',
|
|
2999
|
+
message: `You have unsaved changes: ${queue}\nSave and apply changes before starting the dev server?`,
|
|
3000
|
+
yesText: 'Save & Start',
|
|
3001
|
+
noText: 'Discard & Start',
|
|
3002
|
+
cancelText: 'Cancel',
|
|
3003
|
+
height: 13,
|
|
3004
|
+
})
|
|
3005
|
+
|
|
3006
|
+
if (confirmSave === null) {
|
|
3007
|
+
setStatus('Start aborted', 'info')
|
|
3008
|
+
return
|
|
3009
|
+
}
|
|
3010
|
+
|
|
3011
|
+
if (confirmSave) {
|
|
3012
|
+
const saveOk = await applyChanges()
|
|
3013
|
+
if (!saveOk) {
|
|
3014
|
+
setStatus('Start aborted — changes could not be saved/applied', 'error')
|
|
3015
|
+
return
|
|
3016
|
+
}
|
|
3017
|
+
} else {
|
|
3018
|
+
pendingPackages.clear()
|
|
3019
|
+
pendingTheme = config.theme
|
|
3020
|
+
installChoices.clear()
|
|
3021
|
+
renderAll()
|
|
3022
|
+
}
|
|
3023
|
+
}
|
|
3024
|
+
|
|
3025
|
+
startSpinner('Initializing startup checks…')
|
|
3026
|
+
renderAll()
|
|
3027
|
+
|
|
3028
|
+
const ok = await runStartupInstallFlow({ isStartup: true })
|
|
3029
|
+
|
|
3030
|
+
if (!ok) {
|
|
3031
|
+
devPhase = 'stopped'
|
|
3032
|
+
stopSpinner()
|
|
3033
|
+
renderAll()
|
|
3034
|
+
return
|
|
3035
|
+
}
|
|
3036
|
+
|
|
3037
|
+
await syncDependenciesWithConfig()
|
|
3038
|
+
|
|
3039
|
+
// Compile stubs before dev server boots
|
|
3040
|
+
startSpinner('Preparing workspace modules…')
|
|
3041
|
+
renderAll()
|
|
3042
|
+
try {
|
|
3043
|
+
await runPrepareModules(workspaceRoot, 'pipe')
|
|
3044
|
+
} catch (err) {
|
|
3045
|
+
// Log error but attempt to start server anyway so the user can inspect Nuxt log details
|
|
3046
|
+
setStatus(`Preparing modules failed: ${err.message}`, 'error')
|
|
3047
|
+
}
|
|
3048
|
+
|
|
3049
|
+
devPhase = 'starting'
|
|
3050
|
+
startSpinner('Starting dev server…')
|
|
3051
|
+
clearLogsBox()
|
|
3052
|
+
renderAll()
|
|
3053
|
+
startDev(devTarget)
|
|
3054
|
+
startLogWatcher()
|
|
3055
|
+
clientStatus = await waitForDev(workspaceRoot, settings.devPort)
|
|
3056
|
+
stopSpinner()
|
|
3057
|
+
devPhase = clientStatus.http.up ? 'running' : 'stopped'
|
|
3058
|
+
if (!isDevServerUp()) {
|
|
3059
|
+
stopLogWatcher()
|
|
3060
|
+
}
|
|
3061
|
+
renderAll()
|
|
3062
|
+
const targetHint = playgroundActive && playgroundLabel ? ` (${playgroundLabel})` : ''
|
|
3063
|
+
setStatus(
|
|
3064
|
+
clientStatus.http.up
|
|
3065
|
+
? `Dev server up on port ${settings.devPort}${targetHint} — ${keyHint('m')} menu to stop or reboot`
|
|
3066
|
+
: `Dev not responding — check ${devLogPath(workspaceRoot)}`,
|
|
3067
|
+
clientStatus.http.up ? 'ok' : 'error',
|
|
3068
|
+
)
|
|
3069
|
+
} catch (err) {
|
|
3070
|
+
setStatus(`Server start failed: ${err.message}`, 'error')
|
|
3071
|
+
devPhase = 'stopped'
|
|
3072
|
+
stopSpinner()
|
|
3073
|
+
renderAll()
|
|
3074
|
+
} finally {
|
|
3075
|
+
isStartingServer = false
|
|
3076
|
+
updateCatalogKeysState()
|
|
3077
|
+
screen.render()
|
|
3078
|
+
}
|
|
3079
|
+
}
|
|
3080
|
+
|
|
3081
|
+
async function stopDevServer() {
|
|
3082
|
+
if (devPhase === 'starting') {
|
|
3083
|
+
setStatus('Stopping dev server startup…')
|
|
3084
|
+
stopDev(workspaceRoot, clientStatus.pid)
|
|
3085
|
+
devPhase = 'stopped'
|
|
3086
|
+
stopSpinner()
|
|
3087
|
+
stopLogWatcher()
|
|
3088
|
+
renderAll()
|
|
3089
|
+
setStatus('Startup cancelled', 'info')
|
|
3090
|
+
return
|
|
3091
|
+
}
|
|
3092
|
+
if (!isDevServerUp()) {
|
|
3093
|
+
setStatus(`Dev server not running — press ${keyHint('d')} to start`, 'info')
|
|
3094
|
+
return
|
|
3095
|
+
}
|
|
3096
|
+
|
|
3097
|
+
startSpinner('Stopping dev server…')
|
|
3098
|
+
stopDev(workspaceRoot, clientStatus.pid)
|
|
3099
|
+
devPhase = 'stopped'
|
|
3100
|
+
stopLogWatcher()
|
|
3101
|
+
memHistory.length = 0
|
|
3102
|
+
clientStatus = await waitForDevStop(workspaceRoot, settings.devPort)
|
|
3103
|
+
stopSpinner()
|
|
3104
|
+
renderAll()
|
|
3105
|
+
setStatus(`Dev server stopped — press ${keyHint('d')} to start`, 'ok')
|
|
3106
|
+
}
|
|
3107
|
+
|
|
3108
|
+
async function rebootDevServer() {
|
|
3109
|
+
if (devPhase === 'starting') {
|
|
3110
|
+
setStatus('Cannot reboot while dev server is starting…')
|
|
3111
|
+
return
|
|
3112
|
+
}
|
|
3113
|
+
if (!isDevServerUp()) {
|
|
3114
|
+
setStatus(`Dev server not running — press ${keyHint('d')} to start`, 'info')
|
|
3115
|
+
return
|
|
3116
|
+
}
|
|
3117
|
+
|
|
3118
|
+
devPhase = 'starting'
|
|
3119
|
+
startSpinner('Rebooting dev server…')
|
|
3120
|
+
clearLogsBox()
|
|
3121
|
+
stopLogWatcher()
|
|
3122
|
+
renderAll()
|
|
3123
|
+
stopDev(workspaceRoot, clientStatus.pid)
|
|
3124
|
+
memHistory.length = 0
|
|
3125
|
+
await waitForDevStop(workspaceRoot, settings.devPort)
|
|
3126
|
+
startDev(devTarget)
|
|
3127
|
+
startLogWatcher()
|
|
3128
|
+
clientStatus = await waitForDev(workspaceRoot, settings.devPort)
|
|
3129
|
+
stopSpinner()
|
|
3130
|
+
devPhase = clientStatus.http.up ? 'running' : 'stopped'
|
|
3131
|
+
if (!isDevServerUp()) {
|
|
3132
|
+
stopLogWatcher()
|
|
3133
|
+
}
|
|
3134
|
+
renderAll()
|
|
3135
|
+
const targetHint = playgroundActive && playgroundLabel ? ` (${playgroundLabel})` : ''
|
|
3136
|
+
setStatus(
|
|
3137
|
+
clientStatus.http.up
|
|
3138
|
+
? `Dev server rebooted on port ${settings.devPort}${targetHint}`
|
|
3139
|
+
: `Reboot failed — check ${devLogPath(workspaceRoot)}`,
|
|
3140
|
+
clientStatus.http.up ? 'ok' : 'error',
|
|
3141
|
+
)
|
|
3142
|
+
}
|
|
3143
|
+
|
|
3144
|
+
async function refreshCatalog({ force = true } = {}) {
|
|
3145
|
+
startSpinner(force ? 'Fetching GitHub catalog…' : 'Loading GitHub catalog…')
|
|
3146
|
+
try {
|
|
3147
|
+
sshAuthStatus = await detectGithubSshAuth(workspaceRoot, { force })
|
|
3148
|
+
if (sshAuthStatus.available && sshAuthStatus.githubLogin) {
|
|
3149
|
+
settings.githubUser = sshAuthStatus.githubLogin
|
|
3150
|
+
}
|
|
3151
|
+
const result = await loadCatalog(workspaceRoot, settings, { force })
|
|
3152
|
+
catalog = result.entries
|
|
3153
|
+
catalogCacheAge = result.cacheAge ?? ''
|
|
3154
|
+
try {
|
|
3155
|
+
config = readDesktopConfig(paths.config, workspaceRoot)
|
|
3156
|
+
configError = null
|
|
3157
|
+
} catch (err) {
|
|
3158
|
+
configError = err.message
|
|
3159
|
+
config = { apps: [], modules: [], theme: null }
|
|
3160
|
+
}
|
|
3161
|
+
deps = readDesktopDependencies(paths.packageJson)
|
|
3162
|
+
pendingTheme = pendingTheme ?? config.theme
|
|
3163
|
+
stopSpinner()
|
|
3164
|
+
renderAll()
|
|
3165
|
+
loadNpmDownloads()
|
|
3166
|
+
setStatus(
|
|
3167
|
+
force
|
|
3168
|
+
? `Package list: ${catalog.length} from GitHub`
|
|
3169
|
+
: `Package list loaded: ${catalog.length} packages (cache: ${catalogCacheAge})`,
|
|
3170
|
+
'ok',
|
|
3171
|
+
)
|
|
3172
|
+
} catch (err) {
|
|
3173
|
+
stopSpinner()
|
|
3174
|
+
setStatus(`Package list error: ${err.message}`, 'error')
|
|
3175
|
+
}
|
|
3176
|
+
}
|
|
3177
|
+
|
|
3178
|
+
function getPackagesRequiredByTheme(workspaceRoot, themeName) {
|
|
3179
|
+
return getPackagesRequiredByThemeImport(workspaceRoot, themeName)
|
|
3180
|
+
}
|
|
3181
|
+
|
|
3182
|
+
// Keep legacy alias used in startDevServer
|
|
3183
|
+
const getKitsRequiredByTheme = (wr, name) => getPackagesRequiredByTheme(wr, name)
|
|
3184
|
+
|
|
3185
|
+
async function runStartupInstallFlow(opts = {}) {
|
|
3186
|
+
return await extRunStartupInstallFlow(ctx, opts)
|
|
3187
|
+
}
|
|
3188
|
+
|
|
3189
|
+
screen.key(['escape'], () => {
|
|
3190
|
+
if (installWizardOpen) {
|
|
3191
|
+
closeInstallWizard()
|
|
3192
|
+
setStatus('Install cancelled — no changes applied', 'info')
|
|
3193
|
+
return
|
|
3194
|
+
}
|
|
3195
|
+
if (sortPickerOpen) {
|
|
3196
|
+
closeSortPicker()
|
|
3197
|
+
return
|
|
3198
|
+
}
|
|
3199
|
+
if (settingsOpen) {
|
|
3200
|
+
closeSettings()
|
|
3201
|
+
return
|
|
3202
|
+
}
|
|
3203
|
+
if (menuOpen) {
|
|
3204
|
+
closeMenu()
|
|
3205
|
+
}
|
|
3206
|
+
})
|
|
3207
|
+
|
|
3208
|
+
screen.key(['q', 'C-c'], () => {
|
|
3209
|
+
if (installWizardOpen) {
|
|
3210
|
+
closeInstallWizard()
|
|
3211
|
+
setStatus('Install cancelled', 'info')
|
|
3212
|
+
return
|
|
3213
|
+
}
|
|
3214
|
+
if (sortPickerOpen) {
|
|
3215
|
+
closeSortPicker()
|
|
3216
|
+
return
|
|
3217
|
+
}
|
|
3218
|
+
if (settingsOpen) {
|
|
3219
|
+
closeSettings()
|
|
3220
|
+
return
|
|
3221
|
+
}
|
|
3222
|
+
if (menuOpen) {
|
|
3223
|
+
closeMenu()
|
|
3224
|
+
return
|
|
3225
|
+
}
|
|
3226
|
+
process.exit(0)
|
|
3227
|
+
})
|
|
3228
|
+
|
|
3229
|
+
screen.key(['m', 'M'], () => {
|
|
3230
|
+
if (settingsOpen || installWizardOpen || sortPickerOpen) return
|
|
3231
|
+
if (menuOpen) closeMenu()
|
|
3232
|
+
else openMenu()
|
|
3233
|
+
})
|
|
3234
|
+
screen.key(['n', 'N'], () => {
|
|
3235
|
+
if (settingsOpen || installWizardOpen || sortPickerOpen || menuOpen || confirmDialogOpen) return
|
|
3236
|
+
process.exit(10)
|
|
3237
|
+
})
|
|
3238
|
+
screen.key(['d', 'D'], () => {
|
|
3239
|
+
if (!overlayBlocksKeys()) {
|
|
3240
|
+
if (isDevServerUp()) stopDevServer()
|
|
3241
|
+
else startDevServer()
|
|
3242
|
+
}
|
|
3243
|
+
})
|
|
3244
|
+
screen.key(['x', 'X'], () => {
|
|
3245
|
+
if (!overlayBlocksKeys()) stopDevServer()
|
|
3246
|
+
})
|
|
3247
|
+
screen.key(['R'], () => {
|
|
3248
|
+
if (!overlayBlocksKeys()) rebootDevServer()
|
|
3249
|
+
})
|
|
3250
|
+
screen.key(['g'], () => {
|
|
3251
|
+
if (!overlayBlocksKeys()) openSettings()
|
|
3252
|
+
})
|
|
3253
|
+
|
|
3254
|
+
function openDocsInBrowser() {
|
|
3255
|
+
if (!hasDocsModuleInstalled(config, deps)) {
|
|
3256
|
+
setStatus('Install @owdproject/module-docs first', 'error')
|
|
3257
|
+
return
|
|
3258
|
+
}
|
|
3259
|
+
const http = clientStatus.http
|
|
3260
|
+
if (!clientStatus.running && !http.up) {
|
|
3261
|
+
setStatus(`Start dev server ${keyHint('d')} before opening docs`, 'error')
|
|
3262
|
+
return
|
|
3263
|
+
}
|
|
3264
|
+
const base = docsBasePathFromConfig(config)
|
|
3265
|
+
const url = `http://127.0.0.1:${settings.devPort}${base}`
|
|
3266
|
+
const platform = process.platform
|
|
3267
|
+
try {
|
|
3268
|
+
if (platform === 'darwin') {
|
|
3269
|
+
execSync('open', [url])
|
|
3270
|
+
} else if (platform === 'win32') {
|
|
3271
|
+
execSync('cmd', ['/c', 'start', '', url])
|
|
3272
|
+
} else {
|
|
3273
|
+
execSync('xdg-open', [url])
|
|
3274
|
+
}
|
|
3275
|
+
setStatus(`Opened ${url}`, 'ok')
|
|
3276
|
+
} catch (err) {
|
|
3277
|
+
setStatus(`Could not open browser: ${err.message}`, 'error')
|
|
3278
|
+
}
|
|
3279
|
+
}
|
|
3280
|
+
|
|
3281
|
+
screen.key(['i', 'I'], () => {
|
|
3282
|
+
if (!overlayBlocksKeys()) openDocsInBrowser()
|
|
3283
|
+
})
|
|
3284
|
+
screen.key(['b'], () => {
|
|
3285
|
+
if (!overlayBlocksKeys()) {
|
|
3286
|
+
stopSpinner()
|
|
3287
|
+
setStatus('Running pnpm run generate…')
|
|
3288
|
+
screen.render()
|
|
3289
|
+
try {
|
|
3290
|
+
execSync('pnpm run generate', { cwd: workspaceRoot, stdio: 'pipe' })
|
|
3291
|
+
setStatus('Generate finished', 'ok')
|
|
3292
|
+
} catch (err) {
|
|
3293
|
+
setStatus(`Generate failed: ${err.message}`, 'error')
|
|
3294
|
+
} finally {
|
|
3295
|
+
refreshTerminalUi()
|
|
3296
|
+
}
|
|
3297
|
+
}
|
|
3298
|
+
})
|
|
3299
|
+
screen.key(['s', 'S'], () => {
|
|
3300
|
+
if (installWizardOpen) {
|
|
3301
|
+
skipInstallWizardStep()
|
|
3302
|
+
} else if (!overlayBlocksKeys()) {
|
|
3303
|
+
applyChanges()
|
|
3304
|
+
}
|
|
3305
|
+
})
|
|
3306
|
+
screen.key(['enter'], () => {
|
|
3307
|
+
if (installWizardOpen) {
|
|
3308
|
+
handleInstallWizardSelect()
|
|
3309
|
+
}
|
|
3310
|
+
})
|
|
3311
|
+
screen.key(['r'], () => {
|
|
3312
|
+
if (!overlayBlocksKeys()) refreshCatalog()
|
|
3313
|
+
})
|
|
3314
|
+
screen.key(['u', 'U'], () => {
|
|
3315
|
+
if (!overlayBlocksKeys()) checkForUpdates()
|
|
3316
|
+
})
|
|
3317
|
+
|
|
3318
|
+
screen.key(['o'], () => {
|
|
3319
|
+
if (!overlayBlocksKeys()) cycleCatalogSort()
|
|
3320
|
+
})
|
|
3321
|
+
screen.key(['O'], () => {
|
|
3322
|
+
if (!overlayBlocksKeys()) openSortPicker()
|
|
3323
|
+
})
|
|
3324
|
+
|
|
3325
|
+
function changeTab(tabName) {
|
|
3326
|
+
activeTab = tabName
|
|
3327
|
+
if (tabName === 'theme') {
|
|
3328
|
+
focusCatalog()
|
|
3329
|
+
renderCatalogList()
|
|
3330
|
+
restoreCatalogListSelection(catalogEntries(), pendingTheme ?? config.theme)
|
|
3331
|
+
renderLegendBar()
|
|
3332
|
+
applyFocusStyles()
|
|
3333
|
+
renderMetrics()
|
|
3334
|
+
renderDetail()
|
|
3335
|
+
renderTabs()
|
|
3336
|
+
screen.render()
|
|
3337
|
+
} else {
|
|
3338
|
+
focusCatalog()
|
|
3339
|
+
renderAll()
|
|
3340
|
+
}
|
|
3341
|
+
}
|
|
3342
|
+
|
|
3343
|
+
screen.key(['1'], () => {
|
|
3344
|
+
if (overlayBlocksKeys()) return
|
|
3345
|
+
changeTab('app')
|
|
3346
|
+
})
|
|
3347
|
+
screen.key(['2'], () => {
|
|
3348
|
+
if (overlayBlocksKeys()) return
|
|
3349
|
+
changeTab('module')
|
|
3350
|
+
})
|
|
3351
|
+
screen.key(['3'], () => {
|
|
3352
|
+
if (overlayBlocksKeys()) return
|
|
3353
|
+
changeTab('theme')
|
|
3354
|
+
})
|
|
3355
|
+
|
|
3356
|
+
const TABS = ['app', 'module', 'theme']
|
|
3357
|
+
|
|
3358
|
+
function cycleTab(direction) {
|
|
3359
|
+
const currentIndex = TABS.indexOf(activeTab)
|
|
3360
|
+
if (currentIndex === -1) return
|
|
3361
|
+
let nextIndex = currentIndex + direction
|
|
3362
|
+
if (nextIndex < 0) nextIndex = TABS.length - 1
|
|
3363
|
+
if (nextIndex >= TABS.length) nextIndex = 0
|
|
3364
|
+
changeTab(TABS[nextIndex])
|
|
3365
|
+
}
|
|
3366
|
+
|
|
3367
|
+
screen.key(['left'], () => {
|
|
3368
|
+
if (overlayBlocksKeys()) return
|
|
3369
|
+
if (screen.focused === catalogList) {
|
|
3370
|
+
cycleTab(-1)
|
|
3371
|
+
}
|
|
3372
|
+
})
|
|
3373
|
+
|
|
3374
|
+
screen.key(['right'], () => {
|
|
3375
|
+
if (overlayBlocksKeys()) return
|
|
3376
|
+
if (screen.focused === catalogList) {
|
|
3377
|
+
cycleTab(1)
|
|
3378
|
+
}
|
|
3379
|
+
})
|
|
3380
|
+
|
|
3381
|
+
tabBar.on('click', (data) => {
|
|
3382
|
+
if (overlayBlocksKeys()) return
|
|
3383
|
+
const relativeX = data.x - tabBar.aleft
|
|
3384
|
+
if (relativeX >= 1 && relativeX <= 5) {
|
|
3385
|
+
changeTab('app')
|
|
3386
|
+
} else if (relativeX >= 8 && relativeX <= 15) {
|
|
3387
|
+
changeTab('module')
|
|
3388
|
+
} else if (relativeX >= 18 && relativeX <= 24) {
|
|
3389
|
+
changeTab('theme')
|
|
3390
|
+
}
|
|
3391
|
+
})
|
|
3392
|
+
|
|
3393
|
+
trustedOrgsInput.key(['enter'], () => saveSettingsFromOverlay())
|
|
3394
|
+
trustedOrgsInput.key(['escape'], () => closeSettings())
|
|
3395
|
+
settingsOverlay.key(['t', 'T'], () => {
|
|
3396
|
+
if (settingsOpen) testSshFromSettings()
|
|
3397
|
+
})
|
|
3398
|
+
|
|
3399
|
+
installWizardList.key(['space'], () => {})
|
|
3400
|
+
installWizardList.key(['enter'], () => handleInstallWizardSelect())
|
|
3401
|
+
installWizardList.key(['s', 'S'], () => skipInstallWizardStep())
|
|
3402
|
+
installWizardList.key(['escape'], () => {
|
|
3403
|
+
closeInstallWizard()
|
|
3404
|
+
setStatus('Install cancelled — no changes applied', 'info')
|
|
3405
|
+
})
|
|
3406
|
+
installWizardOverlay.key(['s', 'S'], () => skipInstallWizardStep())
|
|
3407
|
+
installWizardOverlay.key(['enter'], () => handleInstallWizardSelect())
|
|
3408
|
+
installWizardOverlay.key(['escape'], () => {
|
|
3409
|
+
closeInstallWizard()
|
|
3410
|
+
setStatus('Install cancelled — no changes applied', 'info')
|
|
3411
|
+
})
|
|
3412
|
+
|
|
3413
|
+
menuList.on('select', (_item, index) => {
|
|
3414
|
+
const entry = MENU_ITEMS[index]
|
|
3415
|
+
if (entry) runMenuAction(entry.id)
|
|
3416
|
+
})
|
|
3417
|
+
menuList.key(['escape'], () => closeMenu())
|
|
3418
|
+
|
|
3419
|
+
sortPickerList.on('select', (_item, index) => selectSortAt(index))
|
|
3420
|
+
sortPickerList.key(['enter'], function () {
|
|
3421
|
+
selectSortAt(this.selected)
|
|
3422
|
+
})
|
|
3423
|
+
sortPickerList.key(['escape'], () => closeSortPicker())
|
|
3424
|
+
sortPickerOverlay.key(['escape'], () => closeSortPicker())
|
|
3425
|
+
|
|
3426
|
+
catalogList.on('select item', () => {
|
|
3427
|
+
renderDetail()
|
|
3428
|
+
})
|
|
3429
|
+
catalogList.key(['space'], function () {
|
|
3430
|
+
if (overlayBlocksKeys()) return
|
|
3431
|
+
toggleCatalogAt(this.selected)
|
|
3432
|
+
})
|
|
3433
|
+
catalogList.key(['+'], function () {
|
|
3434
|
+
if (overlayBlocksKeys()) return
|
|
3435
|
+
setCatalogStateAt(this.selected, true)
|
|
3436
|
+
})
|
|
3437
|
+
catalogList.key(['-'], function () {
|
|
3438
|
+
if (overlayBlocksKeys()) return
|
|
3439
|
+
setCatalogStateAt(this.selected, false)
|
|
3440
|
+
})
|
|
3441
|
+
catalogList.key(['pageup'], function () {
|
|
3442
|
+
if (overlayBlocksKeys()) return
|
|
3443
|
+
this.up(listPageStep(this))
|
|
3444
|
+
renderDetail()
|
|
3445
|
+
this.screen.render()
|
|
3446
|
+
})
|
|
3447
|
+
catalogList.key(['pagedown'], function () {
|
|
3448
|
+
if (overlayBlocksKeys()) return
|
|
3449
|
+
this.down(listPageStep(this))
|
|
3450
|
+
renderDetail()
|
|
3451
|
+
this.screen.render()
|
|
3452
|
+
})
|
|
3453
|
+
|
|
3454
|
+
screen.key(['l', 'L'], () => {
|
|
3455
|
+
if (!overlayBlocksKeys() && isDevServerUp() && logsBox.visible) {
|
|
3456
|
+
logsBox.focus()
|
|
3457
|
+
setStatus('Logs — Use ↑↓ or PageUp/PageDown to scroll · Esc to return focus to Catalog')
|
|
3458
|
+
}
|
|
3459
|
+
})
|
|
3460
|
+
|
|
3461
|
+
logsBox.key(['escape'], () => {
|
|
3462
|
+
focusCatalog()
|
|
3463
|
+
})
|
|
3464
|
+
|
|
3465
|
+
|
|
3466
|
+
|
|
3467
|
+
helpBar.on('click', () => {
|
|
3468
|
+
if (overlayBlocksKeys()) return
|
|
3469
|
+
showCustomConfirm({
|
|
3470
|
+
title: 'Confirm Refresh',
|
|
3471
|
+
message: '\n Do you want to force-refresh the GitHub package catalog?\n This will fetch package data from upstream.',
|
|
3472
|
+
yesText: ' Yes, force-refresh ',
|
|
3473
|
+
noText: ' No, cancel ',
|
|
3474
|
+
height: 11
|
|
3475
|
+
}).then((value) => {
|
|
3476
|
+
if (value) {
|
|
3477
|
+
refreshCatalog({ force: true })
|
|
3478
|
+
}
|
|
3479
|
+
})
|
|
3480
|
+
})
|
|
3481
|
+
|
|
3482
|
+
await refreshCatalog({ force: false })
|
|
3483
|
+
updateLocalChanges()
|
|
3484
|
+
clientStatus = await getClientStatus(workspaceRoot, settings.devPort)
|
|
3485
|
+
devPhase = clientStatus.running ? 'running' : 'stopped'
|
|
3486
|
+
focusCatalog()
|
|
3487
|
+
if (isDevServerUp()) {
|
|
3488
|
+
startLogWatcher()
|
|
3489
|
+
}
|
|
3490
|
+
renderAll()
|
|
3491
|
+
checkForUpdates({ silent: true })
|
|
3492
|
+
|
|
3493
|
+
screen.on('destroy', () => {
|
|
3494
|
+
if (configWatcher) {
|
|
3495
|
+
configWatcher.close()
|
|
3496
|
+
}
|
|
3497
|
+
stopLogWatcher()
|
|
3498
|
+
})
|
|
3499
|
+
|
|
3500
|
+
startConfigWatcher()
|
|
3501
|
+
|
|
3502
|
+
setStatus(
|
|
3503
|
+
`Select packages · ${keyHint('s')} opens install wizard · ${keyHint('u')} updates · ${keyHint('d')} start server · ${keyHint('g')} settings`,
|
|
3504
|
+
'info',
|
|
3505
|
+
)
|
|
3506
|
+
|
|
3507
|
+
let lastShowcaseTime = Date.now()
|
|
3508
|
+
let gitScanCycle = 0
|
|
3509
|
+
|
|
3510
|
+
setInterval(async () => {
|
|
3511
|
+
const now = Date.now()
|
|
3512
|
+
if (now - lastShowcaseTime >= 15000) {
|
|
3513
|
+
showcaseIndex++
|
|
3514
|
+
lastShowcaseTime = now
|
|
3515
|
+
}
|
|
3516
|
+
|
|
3517
|
+
gitScanCycle++
|
|
3518
|
+
if (gitScanCycle % 3 === 0 && !isInstalling) {
|
|
3519
|
+
updateLocalChanges()
|
|
3520
|
+
renderCatalogList()
|
|
3521
|
+
}
|
|
3522
|
+
|
|
3523
|
+
if (devPhase !== 'starting' && !settingsOpen && !overlayBlocksKeys()) {
|
|
3524
|
+
clientStatus = await getClientStatus(workspaceRoot, settings.devPort)
|
|
3525
|
+
if (clientStatus.stats.memMb > 0) {
|
|
3526
|
+
memHistory.push(clientStatus.stats.memMb)
|
|
3527
|
+
if (memHistory.length > MEM_HISTORY_LEN) memHistory.shift()
|
|
3528
|
+
}
|
|
3529
|
+
if (clientStatus.http.up) devPhase = 'running'
|
|
3530
|
+
else if (devPhase === 'running') devPhase = 'stopped'
|
|
3531
|
+
}
|
|
3532
|
+
const serverRunning = isDevServerUp()
|
|
3533
|
+
if (serverRunning && !logWatcher) {
|
|
3534
|
+
startLogWatcher()
|
|
3535
|
+
} else if (!serverRunning && logWatcher) {
|
|
3536
|
+
stopLogWatcher()
|
|
3537
|
+
}
|
|
3538
|
+
renderClient()
|
|
3539
|
+
renderMetrics()
|
|
3540
|
+
screen.render()
|
|
3541
|
+
}, 2000)
|
|
3542
|
+
}
|