@owdproject/cli 0.0.2 → 0.1.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 +127 -0
- package/bin/cli.js +433 -0
- package/bin/cp.js +3523 -0
- package/bin/desktop.js +5 -0
- package/bin/lib/catalog.js +501 -0
- package/bin/lib/config.js +130 -0
- package/bin/lib/cpAscii.js +39 -0
- package/bin/lib/cpFormat.js +343 -0
- package/bin/lib/desktopConfig.js +8 -0
- package/bin/lib/generateTemplate.js +426 -0
- package/bin/lib/install.js +525 -0
- package/bin/lib/installWizardFlow.js +726 -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 +365 -0
- package/bin/lib/playgroundContext.js +188 -0
- package/bin/lib/scaffold.js +205 -0
- package/bin/lib/status.js +317 -0
- package/bin/lib/validateModule.js +868 -0
- package/bin/lib/workspace.js +222 -0
- package/bin/owd.js +2 -118
- package/go/bridge/bridge.go +200 -0
- package/go/desktop-go +0 -0
- package/go/go.mod +26 -0
- package/go/go.sum +37 -0
- package/go/main.go +30 -0
- package/go/tui/tui.go +3014 -0
- package/package.json +33 -23
- 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
|
@@ -0,0 +1,726 @@
|
|
|
1
|
+
import { join } from 'node:path'
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
3
|
+
import {
|
|
4
|
+
isInstallableDesktopModule,
|
|
5
|
+
isDesktopKitPackage,
|
|
6
|
+
shortName,
|
|
7
|
+
getPackagesRequiredByTheme
|
|
8
|
+
} from './workspace.js'
|
|
9
|
+
import {
|
|
10
|
+
readDesktopConfig,
|
|
11
|
+
readDesktopDependencies,
|
|
12
|
+
writeDesktopConfig,
|
|
13
|
+
resolveConfigPathForWrite,
|
|
14
|
+
} from './config.js'
|
|
15
|
+
import {
|
|
16
|
+
installPackage,
|
|
17
|
+
materializeToWorkspace,
|
|
18
|
+
runPrepareModules,
|
|
19
|
+
spawnAsync,
|
|
20
|
+
resolveInstallPlan,
|
|
21
|
+
cloneRepo,
|
|
22
|
+
hasLocalWorkspaceSource,
|
|
23
|
+
linkWorkspacePackage,
|
|
24
|
+
} from './install.js'
|
|
25
|
+
import {
|
|
26
|
+
mergeInstalled,
|
|
27
|
+
} from './catalog.js'
|
|
28
|
+
import {
|
|
29
|
+
progressTrack,
|
|
30
|
+
radarSpinner,
|
|
31
|
+
spinnerFrameCount,
|
|
32
|
+
} from './cpAscii.js'
|
|
33
|
+
function keyHint(key) {
|
|
34
|
+
return `{green-fg}[${key}]{/}`
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Executes the save changes install plan.
|
|
39
|
+
* @param {any} ctx
|
|
40
|
+
*/
|
|
41
|
+
export async function executeInstallPlan(ctx) {
|
|
42
|
+
ctx.stopSpinner()
|
|
43
|
+
let installSpinnerTimer = null
|
|
44
|
+
const plan = ctx.collectInstallPlan()
|
|
45
|
+
const { packagesToInstall, packagesToMaterialize } = plan
|
|
46
|
+
let { nextApps, nextModules, nextTheme } = plan
|
|
47
|
+
|
|
48
|
+
const config = ctx.getConfig()
|
|
49
|
+
const settings = ctx.getSettings()
|
|
50
|
+
const workspaceRoot = ctx.getWorkspaceRoot()
|
|
51
|
+
const installChoices = ctx.getInstallChoices()
|
|
52
|
+
const paths = ctx.getPaths()
|
|
53
|
+
|
|
54
|
+
const currentDeps = readDesktopDependencies(paths.packageJson)
|
|
55
|
+
|
|
56
|
+
const wizardSet = new Set([...packagesToInstall, ...packagesToMaterialize])
|
|
57
|
+
const confirmed = (pkg) =>
|
|
58
|
+
!wizardSet.has(pkg) ||
|
|
59
|
+
installChoices.has(pkg) ||
|
|
60
|
+
currentDeps.includes(pkg) ||
|
|
61
|
+
hasLocalWorkspaceSource(workspaceRoot, pkg)
|
|
62
|
+
|
|
63
|
+
nextApps = nextApps.filter(confirmed)
|
|
64
|
+
nextModules = nextModules.filter(confirmed)
|
|
65
|
+
if (
|
|
66
|
+
nextTheme &&
|
|
67
|
+
wizardSet.has(nextTheme) &&
|
|
68
|
+
!installChoices.has(nextTheme) &&
|
|
69
|
+
!currentDeps.includes(nextTheme) &&
|
|
70
|
+
!hasLocalWorkspaceSource(workspaceRoot, nextTheme)
|
|
71
|
+
) {
|
|
72
|
+
nextTheme = config.theme
|
|
73
|
+
ctx.setPendingTheme(config.theme)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const toInstall = packagesToInstall.filter((pkg) => installChoices.has(pkg))
|
|
77
|
+
const toMaterialize = packagesToMaterialize.filter((pkg) => installChoices.has(pkg))
|
|
78
|
+
|
|
79
|
+
const activePackages = [...nextApps, ...nextModules, nextTheme].filter(Boolean)
|
|
80
|
+
const toLink = []
|
|
81
|
+
for (const pkgName of activePackages) {
|
|
82
|
+
if (!currentDeps.includes(pkgName)) {
|
|
83
|
+
if (hasLocalWorkspaceSource(workspaceRoot, pkgName)) {
|
|
84
|
+
toLink.push(pkgName)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const gitPackages = [...toInstall, ...toMaterialize].filter(
|
|
90
|
+
(pkg) => installChoices.get(pkg)?.type === 'git'
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
const actualMaterialize = toMaterialize.filter((pkg) => {
|
|
94
|
+
const choice = installChoices.get(pkg)
|
|
95
|
+
return choice && choice.type !== 'npm'
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
const gitToInstall = toInstall.filter((pkg) => installChoices.get(pkg)?.type === 'git')
|
|
99
|
+
const expectedClonedPackages = [...gitToInstall, ...actualMaterialize, ...toLink]
|
|
100
|
+
const willClone = expectedClonedPackages.length > 0
|
|
101
|
+
|
|
102
|
+
let calculatedTotal = 1 // Initializing
|
|
103
|
+
calculatedTotal += gitPackages.length // Pre-clonings
|
|
104
|
+
calculatedTotal += toLink.length // Linking loop
|
|
105
|
+
calculatedTotal += toInstall.length // Installing loop
|
|
106
|
+
calculatedTotal += actualMaterialize.length // Cloning loop
|
|
107
|
+
if (willClone) {
|
|
108
|
+
calculatedTotal += 1 // Installing dependencies
|
|
109
|
+
calculatedTotal += expectedClonedPackages.length // Preparing loop
|
|
110
|
+
calculatedTotal += 1 // Finalizing workspace
|
|
111
|
+
}
|
|
112
|
+
calculatedTotal += 1 // Updating configuration
|
|
113
|
+
|
|
114
|
+
const totalSteps = calculatedTotal
|
|
115
|
+
let step = 0
|
|
116
|
+
let didClone = false
|
|
117
|
+
/** @type {Array<{pkg: string, error: string}>} */
|
|
118
|
+
const failures = []
|
|
119
|
+
|
|
120
|
+
const bump = (label) => {
|
|
121
|
+
step++
|
|
122
|
+
const currentStep = Math.min(step, totalSteps)
|
|
123
|
+
ctx.setSaveProgress({ step: currentStep, total: totalSteps, label })
|
|
124
|
+
ctx.setStatus(label)
|
|
125
|
+
|
|
126
|
+
// Update the progress modal content
|
|
127
|
+
const bar = `[${progressTrack(currentStep, totalSteps, 0, 36)}]`
|
|
128
|
+
const spin = radarSpinner(ctx.getSpinnerFrame())
|
|
129
|
+
let displayLabel = label
|
|
130
|
+
if (label.startsWith('Cloning ')) {
|
|
131
|
+
displayLabel = `Cloning: ${label.slice(8).replace(/…$/, '')}`
|
|
132
|
+
} else if (label.startsWith('Installing ') && label !== 'Installing dependencies…') {
|
|
133
|
+
displayLabel = `Installing: ${label.slice(11).replace(/…$/, '')}`
|
|
134
|
+
} else if (label.startsWith('Preparing ') && label !== 'Preparing…') {
|
|
135
|
+
displayLabel = `Preparing: ${label.slice(10).replace(/—$/, '')}`
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
ctx.installProgressContent.setContent(
|
|
139
|
+
[
|
|
140
|
+
` {bold}${spin} ${displayLabel}{/}`,
|
|
141
|
+
'',
|
|
142
|
+
` {cyan-fg}${bar}{/}`,
|
|
143
|
+
` Step ${currentStep} of ${totalSteps}`,
|
|
144
|
+
].join('\n')
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
ctx.screen.render()
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
ctx.setInstalling(true)
|
|
151
|
+
ctx.updateCatalogKeysState()
|
|
152
|
+
ctx.installProgressOverlay.show()
|
|
153
|
+
ctx.installProgressOverlay.setFront()
|
|
154
|
+
ctx.installProgressOverlay.focus()
|
|
155
|
+
|
|
156
|
+
installSpinnerTimer = setInterval(() => {
|
|
157
|
+
ctx.setSpinnerFrame((ctx.getSpinnerFrame() + 1) % spinnerFrameCount)
|
|
158
|
+
ctx.renderHelp()
|
|
159
|
+
|
|
160
|
+
// Update spinner in modal
|
|
161
|
+
const saveProgress = ctx.getSaveProgress()
|
|
162
|
+
if (saveProgress !== null) {
|
|
163
|
+
const bar = `[${progressTrack(saveProgress.step, saveProgress.total, 0, 36)}]`
|
|
164
|
+
const spin = radarSpinner(ctx.getSpinnerFrame())
|
|
165
|
+
let displayLabel = saveProgress.label
|
|
166
|
+
if (displayLabel.startsWith('Cloning ')) {
|
|
167
|
+
displayLabel = `Cloning: ${displayLabel.slice(8).replace(/…$/, '')}`
|
|
168
|
+
} else if (displayLabel.startsWith('Installing ') && displayLabel !== 'Installing dependencies…') {
|
|
169
|
+
displayLabel = `Installing: ${displayLabel.slice(11).replace(/…$/, '')}`
|
|
170
|
+
} else if (displayLabel.startsWith('Preparing ') && displayLabel !== 'Preparing…') {
|
|
171
|
+
displayLabel = `Preparing: ${displayLabel.slice(10).replace(/—$/, '')}`
|
|
172
|
+
}
|
|
173
|
+
ctx.installProgressContent.setContent(
|
|
174
|
+
[
|
|
175
|
+
` {bold}${spin} ${displayLabel}{/}`,
|
|
176
|
+
'',
|
|
177
|
+
` {cyan-fg}${bar}{/}`,
|
|
178
|
+
` Step ${saveProgress.step} of ${saveProgress.total}`,
|
|
179
|
+
].join('\n')
|
|
180
|
+
)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
ctx.screen.render()
|
|
184
|
+
}, 150)
|
|
185
|
+
|
|
186
|
+
try {
|
|
187
|
+
bump('Initializing…')
|
|
188
|
+
|
|
189
|
+
// Pre-clone all Git repositories first to prevent pnpm workspace failures on missing packages
|
|
190
|
+
if (gitPackages.length > 0) {
|
|
191
|
+
for (const pkg of gitPackages) {
|
|
192
|
+
bump(`Cloning ${shortName(pkg)}…`)
|
|
193
|
+
ctx.clearLogs()
|
|
194
|
+
const choice = installChoices.get(pkg)
|
|
195
|
+
const plan = await resolveInstallPlan(pkg, settings, workspaceRoot, choice)
|
|
196
|
+
if (!plan.error && plan.targetDir && plan.source?.gitUrl) {
|
|
197
|
+
try {
|
|
198
|
+
await cloneRepo(plan.targetDir, plan.source.gitUrl, undefined, workspaceRoot, { quiet: true })
|
|
199
|
+
} catch (err) {
|
|
200
|
+
// Non-fatal pre-clone failure
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const clonedPackages = []
|
|
207
|
+
|
|
208
|
+
if (toLink.length > 0) {
|
|
209
|
+
for (const pkg of toLink) {
|
|
210
|
+
bump(`Linking ${shortName(pkg)}…`)
|
|
211
|
+
ctx.clearLogs()
|
|
212
|
+
try {
|
|
213
|
+
await linkWorkspacePackage(paths.desktop, pkg)
|
|
214
|
+
clonedPackages.push(pkg)
|
|
215
|
+
didClone = true
|
|
216
|
+
} catch (err) {
|
|
217
|
+
const msg = err.message?.split('\n').find(l => l.trim()) ?? String(err)
|
|
218
|
+
failures.push({ pkg, error: msg })
|
|
219
|
+
nextApps = nextApps.filter((p) => p !== pkg)
|
|
220
|
+
nextModules = nextModules.filter((p) => p !== pkg)
|
|
221
|
+
if (nextTheme === pkg) {
|
|
222
|
+
nextTheme = config.theme
|
|
223
|
+
ctx.setPendingTheme(config.theme)
|
|
224
|
+
}
|
|
225
|
+
ctx.setStatus(`${shortName(pkg)}: ${msg}`, 'error')
|
|
226
|
+
ctx.screen.render()
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
for (const pkg of toInstall) {
|
|
232
|
+
const choice = installChoices.get(pkg)
|
|
233
|
+
bump(`Installing ${shortName(pkg)}…`)
|
|
234
|
+
ctx.clearLogs()
|
|
235
|
+
try {
|
|
236
|
+
await installPackage(pkg, settings, workspaceRoot, {
|
|
237
|
+
stdio: 'pipe',
|
|
238
|
+
sourceChoice: choice,
|
|
239
|
+
})
|
|
240
|
+
if (choice?.type === 'git') {
|
|
241
|
+
didClone = true
|
|
242
|
+
clonedPackages.push(pkg)
|
|
243
|
+
}
|
|
244
|
+
} catch (err) {
|
|
245
|
+
const msg = err.message?.split('\n').find(l => l.trim()) ?? String(err)
|
|
246
|
+
failures.push({ pkg, error: msg })
|
|
247
|
+
nextApps = nextApps.filter((p) => p !== pkg)
|
|
248
|
+
nextModules = nextModules.filter((p) => p !== pkg)
|
|
249
|
+
if (nextTheme === pkg) {
|
|
250
|
+
nextTheme = config.theme
|
|
251
|
+
ctx.setPendingTheme(config.theme)
|
|
252
|
+
}
|
|
253
|
+
ctx.setStatus(`${shortName(pkg)}: ${msg}`, 'error')
|
|
254
|
+
ctx.screen.render()
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
for (const pkg of toMaterialize) {
|
|
259
|
+
const choice = installChoices.get(pkg)
|
|
260
|
+
if (!choice || choice.type === 'npm') continue
|
|
261
|
+
bump(`Cloning ${shortName(pkg)}…`)
|
|
262
|
+
ctx.clearLogs()
|
|
263
|
+
try {
|
|
264
|
+
await materializeToWorkspace(pkg, settings, workspaceRoot, {
|
|
265
|
+
stdio: 'pipe',
|
|
266
|
+
sourceChoice: choice,
|
|
267
|
+
})
|
|
268
|
+
didClone = true
|
|
269
|
+
clonedPackages.push(pkg)
|
|
270
|
+
} catch (err) {
|
|
271
|
+
const msg = err.message?.split('\n').find(l => l.trim()) ?? String(err)
|
|
272
|
+
failures.push({ pkg, error: msg })
|
|
273
|
+
ctx.setStatus(`${shortName(pkg)}: ${msg}`, 'error')
|
|
274
|
+
ctx.screen.render()
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (didClone) {
|
|
279
|
+
bump('Installing dependencies…')
|
|
280
|
+
ctx.clearLogs()
|
|
281
|
+
try {
|
|
282
|
+
await spawnAsync('pnpm', ['install'], { cwd: workspaceRoot })
|
|
283
|
+
} catch {
|
|
284
|
+
// Non-fatal
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
bump('Updating configuration…')
|
|
289
|
+
ctx.setIgnoreNextConfigWatch(true)
|
|
290
|
+
ctx.setWritingConfig(true)
|
|
291
|
+
writeDesktopConfig(resolveConfigPathForWrite(paths), workspaceRoot, {
|
|
292
|
+
theme: nextTheme,
|
|
293
|
+
apps: nextApps,
|
|
294
|
+
modules: nextModules,
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
for (const pkg of config.apps ?? []) {
|
|
298
|
+
if (!nextApps.includes(pkg)) {
|
|
299
|
+
await spawnAsync('pnpm', ['remove', pkg], { cwd: paths.desktop })
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
for (const pkg of config.modules ?? []) {
|
|
303
|
+
if (!nextModules.includes(pkg)) {
|
|
304
|
+
await spawnAsync('pnpm', ['remove', pkg], { cwd: paths.desktop })
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
pendingPackages.clear()
|
|
309
|
+
installChoices.clear()
|
|
310
|
+
paths.config = resolveConfigPathForWrite(paths)
|
|
311
|
+
ctx.setPaths(paths)
|
|
312
|
+
|
|
313
|
+
let configError = null
|
|
314
|
+
let newConfig = config
|
|
315
|
+
try {
|
|
316
|
+
newConfig = readDesktopConfig(paths.config, workspaceRoot)
|
|
317
|
+
} catch (err) {
|
|
318
|
+
configError = err.message
|
|
319
|
+
newConfig = { apps: [], modules: [], theme: null }
|
|
320
|
+
}
|
|
321
|
+
ctx.setConfig(newConfig)
|
|
322
|
+
ctx.setConfigError(configError)
|
|
323
|
+
|
|
324
|
+
const newDeps = readDesktopDependencies(paths.packageJson)
|
|
325
|
+
ctx.setDeps(newDeps)
|
|
326
|
+
ctx.setPendingTheme(newConfig.theme)
|
|
327
|
+
ctx.setSaveProgress(null)
|
|
328
|
+
|
|
329
|
+
if (ctx.getConfigRestartHintTimer()) {
|
|
330
|
+
clearTimeout(ctx.getConfigRestartHintTimer())
|
|
331
|
+
ctx.setConfigRestartHintTimer(null)
|
|
332
|
+
}
|
|
333
|
+
if (ctx.isDevServerUp()) {
|
|
334
|
+
if (nextTheme !== config.theme) {
|
|
335
|
+
ctx.rebootDevServer()
|
|
336
|
+
} else {
|
|
337
|
+
ctx.setConfigRestartHintUntil(Date.now() + 5000)
|
|
338
|
+
ctx.setConfigRestartHintTimer(setTimeout(() => {
|
|
339
|
+
ctx.setConfigRestartHintUntil(0)
|
|
340
|
+
ctx.setConfigRestartHintTimer(null)
|
|
341
|
+
ctx.renderClient()
|
|
342
|
+
ctx.screen.render()
|
|
343
|
+
}, 5000))
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
ctx.renderAll()
|
|
348
|
+
|
|
349
|
+
setTimeout(() => {
|
|
350
|
+
ctx.setWritingConfig(false)
|
|
351
|
+
}, 500)
|
|
352
|
+
|
|
353
|
+
if (failures.length > 0) {
|
|
354
|
+
const names = failures.map((f) => shortName(f.pkg)).join(', ')
|
|
355
|
+
const detail = failures[0].error
|
|
356
|
+
ctx.setStatus(
|
|
357
|
+
`${failures.length} package(s) failed (${names}) — ${detail}`,
|
|
358
|
+
'error',
|
|
359
|
+
)
|
|
360
|
+
return false
|
|
361
|
+
} else {
|
|
362
|
+
ctx.setStatus(
|
|
363
|
+
ctx.isDevServerUp()
|
|
364
|
+
? 'Saved — desktop.config.ts updated; Nuxt restarts automatically (see dev server log)'
|
|
365
|
+
: `Saved successfully — start dev server ${keyHint('d')} to apply`,
|
|
366
|
+
'ok',
|
|
367
|
+
)
|
|
368
|
+
return true
|
|
369
|
+
}
|
|
370
|
+
} catch (err) {
|
|
371
|
+
ctx.setWritingConfig(false)
|
|
372
|
+
ctx.setSaveProgress(null)
|
|
373
|
+
ctx.setStatus(`Save failed: ${err.message}`, 'error')
|
|
374
|
+
return false
|
|
375
|
+
} finally {
|
|
376
|
+
if (installSpinnerTimer) {
|
|
377
|
+
clearInterval(installSpinnerTimer)
|
|
378
|
+
}
|
|
379
|
+
ctx.setInstalling(false)
|
|
380
|
+
ctx.installProgressOverlay.hide()
|
|
381
|
+
ctx.updateCatalogKeysState()
|
|
382
|
+
ctx.screen.render()
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Unified startup install flow blocker.
|
|
388
|
+
* @param {any} ctx
|
|
389
|
+
* @param {{ isStartup?: boolean }} [opts]
|
|
390
|
+
*/
|
|
391
|
+
export async function runStartupInstallFlow(ctx, { isStartup = false } = {}) {
|
|
392
|
+
ctx.stopSpinner()
|
|
393
|
+
|
|
394
|
+
const paths = ctx.getPaths()
|
|
395
|
+
const workspaceRoot = ctx.getWorkspaceRoot()
|
|
396
|
+
const installChoices = ctx.getInstallChoices()
|
|
397
|
+
const pendingPackages = ctx.getPendingPackages()
|
|
398
|
+
const settings = ctx.getSettings()
|
|
399
|
+
const catalog = ctx.getCatalog()
|
|
400
|
+
|
|
401
|
+
// Reload latest config/deps state
|
|
402
|
+
try {
|
|
403
|
+
ctx.setConfig(readDesktopConfig(paths.config, workspaceRoot))
|
|
404
|
+
ctx.setDeps(readDesktopDependencies(paths.packageJson))
|
|
405
|
+
} catch {}
|
|
406
|
+
|
|
407
|
+
const config = ctx.getConfig()
|
|
408
|
+
const deps = ctx.getDeps()
|
|
409
|
+
|
|
410
|
+
const plan = ctx.collectInstallPlan()
|
|
411
|
+
let { nextApps, nextModules, nextTheme } = plan
|
|
412
|
+
|
|
413
|
+
const allCloned = []
|
|
414
|
+
let didClone = false
|
|
415
|
+
const skippedKits = []
|
|
416
|
+
|
|
417
|
+
async function cloneOrInstallPkg(pkg) {
|
|
418
|
+
const choice = installChoices.get(pkg)
|
|
419
|
+
if (!choice) return
|
|
420
|
+
if (choice.type === 'npm') {
|
|
421
|
+
try {
|
|
422
|
+
await installPackage(pkg, settings, workspaceRoot, { stdio: 'pipe', sourceChoice: choice })
|
|
423
|
+
} catch (err) {
|
|
424
|
+
const msg = err.message?.split('\n').find(l => l.trim()) ?? String(err)
|
|
425
|
+
ctx.setStatus(`${shortName(pkg)}: ${msg}`, 'error')
|
|
426
|
+
ctx.screen.render()
|
|
427
|
+
}
|
|
428
|
+
return
|
|
429
|
+
}
|
|
430
|
+
try {
|
|
431
|
+
await materializeToWorkspace(pkg, settings, workspaceRoot, { stdio: 'pipe', sourceChoice: choice })
|
|
432
|
+
didClone = true
|
|
433
|
+
allCloned.push(pkg)
|
|
434
|
+
} catch (err) {
|
|
435
|
+
const msg = err.message?.split('\n').find(l => l.trim()) ?? String(err)
|
|
436
|
+
ctx.setStatus(`${shortName(pkg)}: ${msg}`, 'error')
|
|
437
|
+
ctx.screen.render()
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const merged = mergeInstalled(catalog, config, deps, workspaceRoot)
|
|
442
|
+
const missingAppsModules = merged
|
|
443
|
+
.filter((item) => item.installed && !item.localSource && item.kind !== 'theme')
|
|
444
|
+
.map((item) => item.name)
|
|
445
|
+
|
|
446
|
+
const desiredTheme = ctx.getPendingTheme() ?? config.theme
|
|
447
|
+
const themeMissing = !!(desiredTheme && !hasLocalWorkspaceSource(workspaceRoot, desiredTheme))
|
|
448
|
+
const round1Queue = [...missingAppsModules, ...(themeMissing ? [desiredTheme] : [])]
|
|
449
|
+
|
|
450
|
+
if (round1Queue.length > 0) {
|
|
451
|
+
ctx.stopSpinner()
|
|
452
|
+
ctx.renderAll()
|
|
453
|
+
const success = await ctx.startInstallWizard(round1Queue, isStartup, { autoExecute: false })
|
|
454
|
+
if (!success) {
|
|
455
|
+
ctx.setStatus('Startup aborted', 'error')
|
|
456
|
+
return false
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// Determine effective theme after wizard choices
|
|
461
|
+
let activeTheme = desiredTheme
|
|
462
|
+
if (themeMissing && !installChoices.has(desiredTheme)) {
|
|
463
|
+
activeTheme = config.theme
|
|
464
|
+
nextTheme = config.theme
|
|
465
|
+
ctx.setPendingTheme(config.theme)
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const activePackages = [...nextApps, ...nextModules, activeTheme].filter(Boolean)
|
|
469
|
+
const toLink = []
|
|
470
|
+
for (const pkgName of activePackages) {
|
|
471
|
+
if (!deps.includes(pkgName)) {
|
|
472
|
+
if (hasLocalWorkspaceSource(workspaceRoot, pkgName)) {
|
|
473
|
+
toLink.push(pkgName)
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
const round1Confirmed = round1Queue.filter((pkg) => installChoices.has(pkg))
|
|
479
|
+
const failedLinks = new Set()
|
|
480
|
+
|
|
481
|
+
let dynTotal = round1Confirmed.length + toLink.length
|
|
482
|
+
let step = 0
|
|
483
|
+
const syncProgress = { step: 0, total: Math.max(dynTotal, 1), label: 'Initializing…' }
|
|
484
|
+
|
|
485
|
+
let installSpinnerTimer = null
|
|
486
|
+
|
|
487
|
+
const renderProgressContent = () => {
|
|
488
|
+
let lbl = syncProgress.label
|
|
489
|
+
if (lbl.startsWith('Cloning ')) lbl = `Cloning: ${lbl.slice(8).replace(/…$/, '')}`
|
|
490
|
+
else if (lbl.startsWith('Preparing ')) lbl = `Preparing: ${lbl.slice(10).replace(/…$/, '')}`
|
|
491
|
+
const bar = `[${progressTrack(syncProgress.step, syncProgress.total, 0, 36)}]`
|
|
492
|
+
const spin = radarSpinner(ctx.getSpinnerFrame())
|
|
493
|
+
ctx.installProgressContent.setContent([
|
|
494
|
+
` {bold}${spin} ${lbl}{/}`,
|
|
495
|
+
'',
|
|
496
|
+
` {cyan-fg}${bar}{/}`,
|
|
497
|
+
` Step ${syncProgress.step} of ${syncProgress.total}`,
|
|
498
|
+
].join('\n'))
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
const bump = (label, addSteps = 0) => {
|
|
502
|
+
step++
|
|
503
|
+
dynTotal += addSteps
|
|
504
|
+
syncProgress.step = Math.min(step, dynTotal)
|
|
505
|
+
syncProgress.total = dynTotal
|
|
506
|
+
syncProgress.label = label
|
|
507
|
+
ctx.setStatus(label)
|
|
508
|
+
renderProgressContent()
|
|
509
|
+
ctx.screen.render()
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
const openProgress = () => {
|
|
513
|
+
ctx.setInstalling(true)
|
|
514
|
+
ctx.updateCatalogKeysState()
|
|
515
|
+
ctx.installProgressOverlay.show()
|
|
516
|
+
ctx.installProgressOverlay.setFront()
|
|
517
|
+
ctx.installProgressOverlay.focus()
|
|
518
|
+
renderProgressContent()
|
|
519
|
+
ctx.screen.render()
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
const closeProgress = () => {
|
|
523
|
+
if (installSpinnerTimer) { clearInterval(installSpinnerTimer); installSpinnerTimer = null }
|
|
524
|
+
ctx.setInstalling(false)
|
|
525
|
+
ctx.installProgressOverlay.hide()
|
|
526
|
+
ctx.updateCatalogKeysState()
|
|
527
|
+
ctx.screen.render()
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
const hasWork = round1Confirmed.length > 0 || toLink.length > 0
|
|
531
|
+
if (hasWork) {
|
|
532
|
+
openProgress()
|
|
533
|
+
installSpinnerTimer = setInterval(() => {
|
|
534
|
+
ctx.setSpinnerFrame((ctx.getSpinnerFrame() + 1) % spinnerFrameCount)
|
|
535
|
+
ctx.renderHelp()
|
|
536
|
+
renderProgressContent()
|
|
537
|
+
ctx.screen.render()
|
|
538
|
+
}, 150)
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
try {
|
|
542
|
+
if (toLink.length > 0) {
|
|
543
|
+
for (const pkg of toLink) {
|
|
544
|
+
bump(`Linking ${shortName(pkg)}…`)
|
|
545
|
+
ctx.clearLogs()
|
|
546
|
+
try {
|
|
547
|
+
await linkWorkspacePackage(paths.desktop, pkg)
|
|
548
|
+
allCloned.push(pkg)
|
|
549
|
+
didClone = true
|
|
550
|
+
} catch (err) {
|
|
551
|
+
const msg = err.message?.split('\n').find(l => l.trim()) ?? String(err)
|
|
552
|
+
failedLinks.add(pkg)
|
|
553
|
+
ctx.setStatus(`${shortName(pkg)}: ${msg}`, 'error')
|
|
554
|
+
ctx.screen.render()
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
for (const pkg of round1Confirmed) {
|
|
560
|
+
bump(`Cloning ${shortName(pkg)}…`)
|
|
561
|
+
ctx.clearLogs()
|
|
562
|
+
await cloneOrInstallPkg(pkg)
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
if (activeTheme) {
|
|
566
|
+
const themeKits = getPackagesRequiredByTheme(workspaceRoot, activeTheme)
|
|
567
|
+
const missingKits = themeKits.filter((kit) => !hasLocalWorkspaceSource(workspaceRoot, kit))
|
|
568
|
+
|
|
569
|
+
if (missingKits.length > 0) {
|
|
570
|
+
closeProgress()
|
|
571
|
+
ctx.renderAll()
|
|
572
|
+
|
|
573
|
+
const success = await ctx.startInstallWizard(missingKits, isStartup, {
|
|
574
|
+
autoExecute: false,
|
|
575
|
+
clearChoices: false,
|
|
576
|
+
})
|
|
577
|
+
|
|
578
|
+
const kitConfirmed = success
|
|
579
|
+
? missingKits.filter((kit) => installChoices.has(kit))
|
|
580
|
+
: []
|
|
581
|
+
const kitSkipped = missingKits.filter((kit) => !installChoices.has(kit))
|
|
582
|
+
skippedKits.push(...kitSkipped)
|
|
583
|
+
|
|
584
|
+
dynTotal += kitConfirmed.length
|
|
585
|
+
syncProgress.total = dynTotal
|
|
586
|
+
if (kitConfirmed.length > 0) {
|
|
587
|
+
openProgress()
|
|
588
|
+
installSpinnerTimer = setInterval(() => {
|
|
589
|
+
ctx.setSpinnerFrame((ctx.getSpinnerFrame() + 1) % spinnerFrameCount)
|
|
590
|
+
ctx.renderHelp()
|
|
591
|
+
renderProgressContent()
|
|
592
|
+
ctx.screen.render()
|
|
593
|
+
}, 150)
|
|
594
|
+
|
|
595
|
+
for (const kit of kitConfirmed) {
|
|
596
|
+
bump(`Cloning ${shortName(kit)}…`)
|
|
597
|
+
ctx.clearLogs()
|
|
598
|
+
await cloneOrInstallPkg(kit)
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
if (didClone) {
|
|
605
|
+
if (!ctx.isInstalling()) {
|
|
606
|
+
dynTotal += 1 + allCloned.length + 1
|
|
607
|
+
syncProgress.total = dynTotal
|
|
608
|
+
openProgress()
|
|
609
|
+
installSpinnerTimer = setInterval(() => {
|
|
610
|
+
ctx.setSpinnerFrame((ctx.getSpinnerFrame() + 1) % spinnerFrameCount)
|
|
611
|
+
ctx.renderHelp()
|
|
612
|
+
renderProgressContent()
|
|
613
|
+
ctx.screen.render()
|
|
614
|
+
}, 150)
|
|
615
|
+
} else {
|
|
616
|
+
bump('', 1 + allCloned.length + 1)
|
|
617
|
+
step--
|
|
618
|
+
syncProgress.step = step
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
bump('Installing dependencies…')
|
|
622
|
+
ctx.clearLogs()
|
|
623
|
+
try {
|
|
624
|
+
await spawnAsync('pnpm', ['install'], { cwd: workspaceRoot })
|
|
625
|
+
} catch {
|
|
626
|
+
// Non-fatal
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
} catch (err) {
|
|
630
|
+
ctx.setStatus(`Install failed: ${err.message}`, 'error')
|
|
631
|
+
} finally {
|
|
632
|
+
closeProgress()
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
nextApps = (plan.nextApps ?? []).filter(
|
|
636
|
+
(pkg) => (!installChoices.has(pkg) || installChoices.get(pkg) !== null) && !failedLinks.has(pkg)
|
|
637
|
+
)
|
|
638
|
+
nextModules = (plan.nextModules ?? []).filter(
|
|
639
|
+
(pkg) => (!installChoices.has(pkg) || installChoices.get(pkg) !== null) && !failedLinks.has(pkg)
|
|
640
|
+
)
|
|
641
|
+
if (failedLinks.has(activeTheme)) {
|
|
642
|
+
activeTheme = config.theme
|
|
643
|
+
}
|
|
644
|
+
nextTheme = activeTheme ?? nextTheme
|
|
645
|
+
|
|
646
|
+
ctx.setIgnoreNextConfigWatch(true)
|
|
647
|
+
ctx.setWritingConfig(true)
|
|
648
|
+
try {
|
|
649
|
+
writeDesktopConfig(resolveConfigPathForWrite(paths), workspaceRoot, {
|
|
650
|
+
theme: nextTheme,
|
|
651
|
+
apps: nextApps,
|
|
652
|
+
modules: nextModules,
|
|
653
|
+
})
|
|
654
|
+
|
|
655
|
+
for (const pkg of config.apps ?? []) {
|
|
656
|
+
if (!nextApps.includes(pkg)) {
|
|
657
|
+
await spawnAsync('pnpm', ['remove', pkg], { cwd: paths.desktop })
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
for (const pkg of config.modules ?? []) {
|
|
661
|
+
if (!nextModules.includes(pkg)) {
|
|
662
|
+
await spawnAsync('pnpm', ['remove', pkg], { cwd: paths.desktop })
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
pendingPackages.clear()
|
|
667
|
+
installChoices.clear()
|
|
668
|
+
paths.config = resolveConfigPathForWrite(paths)
|
|
669
|
+
ctx.setPaths(paths)
|
|
670
|
+
|
|
671
|
+
let configError = null
|
|
672
|
+
let newConfig = config
|
|
673
|
+
try {
|
|
674
|
+
newConfig = readDesktopConfig(paths.config, workspaceRoot)
|
|
675
|
+
} catch (err) {
|
|
676
|
+
configError = err.message
|
|
677
|
+
newConfig = { apps: [], modules: [], theme: null }
|
|
678
|
+
}
|
|
679
|
+
ctx.setConfig(newConfig)
|
|
680
|
+
ctx.setConfigError(configError)
|
|
681
|
+
|
|
682
|
+
const newDeps = readDesktopDependencies(paths.packageJson)
|
|
683
|
+
ctx.setDeps(newDeps)
|
|
684
|
+
ctx.setPendingTheme(newConfig.theme)
|
|
685
|
+
ctx.setSaveProgress(null)
|
|
686
|
+
|
|
687
|
+
if (ctx.getConfigRestartHintTimer()) {
|
|
688
|
+
clearTimeout(ctx.getConfigRestartHintTimer())
|
|
689
|
+
ctx.setConfigRestartHintTimer(null)
|
|
690
|
+
}
|
|
691
|
+
if (ctx.isDevServerUp()) {
|
|
692
|
+
if (nextTheme !== config.theme) {
|
|
693
|
+
ctx.rebootDevServer()
|
|
694
|
+
} else {
|
|
695
|
+
ctx.setConfigRestartHintUntil(Date.now() + 5000)
|
|
696
|
+
ctx.setConfigRestartHintTimer(setTimeout(() => {
|
|
697
|
+
ctx.setConfigRestartHintUntil(0)
|
|
698
|
+
ctx.setConfigRestartHintTimer(null)
|
|
699
|
+
ctx.renderClient()
|
|
700
|
+
ctx.screen.render()
|
|
701
|
+
}, 5000))
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
ctx.renderAll()
|
|
706
|
+
setTimeout(() => { ctx.setWritingConfig(false) }, 500)
|
|
707
|
+
|
|
708
|
+
if (skippedKits.length > 0) {
|
|
709
|
+
const names = skippedKits.map(shortName).join(', ')
|
|
710
|
+
ctx.setStatus(`Done — skipped kit(s): ${names} (server may not work correctly)`, 'warn')
|
|
711
|
+
} else if (!isStartup) {
|
|
712
|
+
ctx.setStatus(
|
|
713
|
+
ctx.isDevServerUp()
|
|
714
|
+
? 'Saved — desktop.config.ts updated; Nuxt restarts automatically (see dev server log)'
|
|
715
|
+
: `Saved successfully — start dev server ${keyHint('d')} to apply`,
|
|
716
|
+
'ok',
|
|
717
|
+
)
|
|
718
|
+
}
|
|
719
|
+
} catch (err) {
|
|
720
|
+
ctx.setWritingConfig(false)
|
|
721
|
+
ctx.setStatus(`Save failed: ${err.message}`, 'error')
|
|
722
|
+
return false
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
return true
|
|
726
|
+
}
|