@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
|
@@ -0,0 +1,521 @@
|
|
|
1
|
+
import { execSync, spawn } from 'node:child_process'
|
|
2
|
+
import { existsSync, appendFileSync } from 'node:fs'
|
|
3
|
+
import { createRequire } from 'node:module'
|
|
4
|
+
import { dirname, join } from 'node:path'
|
|
5
|
+
import { fileURLToPath } from 'node:url'
|
|
6
|
+
import {
|
|
7
|
+
KINDS,
|
|
8
|
+
shortName as toShortName,
|
|
9
|
+
isWorkspaceInstallMode,
|
|
10
|
+
inferKind,
|
|
11
|
+
} from './workspace.js'
|
|
12
|
+
import {
|
|
13
|
+
githubCloneUrl,
|
|
14
|
+
resolveForkUser,
|
|
15
|
+
} from './packageSources.js'
|
|
16
|
+
import {
|
|
17
|
+
resolveDesktopConfigPath,
|
|
18
|
+
desktopConfigWritePath,
|
|
19
|
+
} from './desktopConfig.js'
|
|
20
|
+
import { addToDesktopConfig } from './config.js'
|
|
21
|
+
|
|
22
|
+
const require = createRequire(import.meta.url)
|
|
23
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
24
|
+
const desktopBin = join(__dirname, '..', 'desktop.js')
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Runs a shell command asynchronously without blocking the event loop.
|
|
28
|
+
* Resolves when the process exits with code 0, rejects otherwise.
|
|
29
|
+
* @param {string} cmd
|
|
30
|
+
* @param {string[]} args
|
|
31
|
+
* @param {{ cwd?: string, stdio?: 'pipe' | 'inherit' | 'ignore' }} [options]
|
|
32
|
+
*/
|
|
33
|
+
export function spawnAsync(cmd, args, options = {}) {
|
|
34
|
+
return new Promise((resolve, reject) => {
|
|
35
|
+
const child = spawn(cmd, args, {
|
|
36
|
+
cwd: options.cwd,
|
|
37
|
+
stdio: options.stdio ?? 'pipe',
|
|
38
|
+
shell: process.platform === 'win32',
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
let logFile = null
|
|
42
|
+
if (options.cwd) {
|
|
43
|
+
const candidate1 = join(options.cwd, '.desktop', 'dev.log')
|
|
44
|
+
const candidate2 = join(dirname(options.cwd), '.desktop', 'dev.log')
|
|
45
|
+
if (existsSync(dirname(candidate1))) {
|
|
46
|
+
logFile = candidate1
|
|
47
|
+
} else if (existsSync(dirname(candidate2))) {
|
|
48
|
+
logFile = candidate2
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (logFile) {
|
|
53
|
+
try {
|
|
54
|
+
appendFileSync(logFile, `\n[installer] $ ${cmd} ${args.join(' ')}\n`)
|
|
55
|
+
} catch {
|
|
56
|
+
/* ignore */
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
let out = ''
|
|
61
|
+
let err = ''
|
|
62
|
+
if (child.stdout) {
|
|
63
|
+
child.stdout.on('data', (d) => {
|
|
64
|
+
const text = d.toString()
|
|
65
|
+
out += text
|
|
66
|
+
if (logFile) {
|
|
67
|
+
try {
|
|
68
|
+
appendFileSync(logFile, text)
|
|
69
|
+
} catch {
|
|
70
|
+
/* ignore */
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
if (child.stderr) {
|
|
76
|
+
child.stderr.on('data', (d) => {
|
|
77
|
+
const text = d.toString()
|
|
78
|
+
err += text
|
|
79
|
+
if (logFile) {
|
|
80
|
+
try {
|
|
81
|
+
appendFileSync(logFile, text)
|
|
82
|
+
} catch {
|
|
83
|
+
/* ignore */
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
child.on('error', reject)
|
|
89
|
+
child.on('close', (code) => {
|
|
90
|
+
if (code === 0) resolve()
|
|
91
|
+
else {
|
|
92
|
+
const combined = [err.trim(), out.trim()].filter(Boolean).join('\n')
|
|
93
|
+
reject(new Error(combined || `Process exited with code ${code}`))
|
|
94
|
+
}
|
|
95
|
+
})
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const SCOPE = '@owdproject/'
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* @param {string | undefined} from
|
|
103
|
+
* @param {string} pkgShortName
|
|
104
|
+
* @param {boolean} inMonorepo
|
|
105
|
+
* @param {{ npm?: boolean, dev?: boolean }} [options]
|
|
106
|
+
*/
|
|
107
|
+
export function resolveSource(from, pkgShortName, inMonorepo, options = {}) {
|
|
108
|
+
const { npm = false, dev = false } = options
|
|
109
|
+
|
|
110
|
+
if (npm || from === 'npm' || from === 'registry') {
|
|
111
|
+
return { mode: 'npm', label: 'npm registry' }
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (dev && inMonorepo) {
|
|
115
|
+
return {
|
|
116
|
+
mode: 'workspace',
|
|
117
|
+
gitUrl: `https://github.com/owdproject/${pkgShortName}.git`,
|
|
118
|
+
label: `github.com/owdproject/${pkgShortName}`,
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (!from) {
|
|
123
|
+
return { mode: 'npm', label: 'npm registry' }
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (from === 'owdproject' || from === 'official') {
|
|
127
|
+
if (!inMonorepo) {
|
|
128
|
+
return { mode: 'npm', label: 'npm registry' }
|
|
129
|
+
}
|
|
130
|
+
return {
|
|
131
|
+
mode: 'workspace',
|
|
132
|
+
gitUrl: `https://github.com/owdproject/${pkgShortName}.git`,
|
|
133
|
+
label: `github.com/owdproject/${pkgShortName}`,
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const trimmed = from.trim()
|
|
138
|
+
|
|
139
|
+
if (
|
|
140
|
+
trimmed.startsWith('https://') ||
|
|
141
|
+
trimmed.startsWith('http://') ||
|
|
142
|
+
trimmed.startsWith('git@') ||
|
|
143
|
+
trimmed.startsWith('ssh://')
|
|
144
|
+
) {
|
|
145
|
+
return {
|
|
146
|
+
mode: 'workspace',
|
|
147
|
+
gitUrl: normalizeGitUrl(trimmed),
|
|
148
|
+
label: trimmed,
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const slug = trimmed
|
|
153
|
+
.replace(/^github:/, '')
|
|
154
|
+
.replace(/^fork:/, '')
|
|
155
|
+
.replace(/^https?:\/\/github\.com\//, '')
|
|
156
|
+
.replace(/\.git$/, '')
|
|
157
|
+
.replace(/\/$/, '')
|
|
158
|
+
|
|
159
|
+
if (slug.includes('/')) {
|
|
160
|
+
const gitUrl = normalizeGitUrl(`https://github.com/${slug}`)
|
|
161
|
+
return { mode: 'workspace', gitUrl, label: slug }
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (!inMonorepo) {
|
|
165
|
+
return { mode: 'npm', label: 'npm registry' }
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
mode: 'workspace',
|
|
170
|
+
gitUrl: `https://github.com/${slug}/${pkgShortName}.git`,
|
|
171
|
+
label: `github.com/${slug}/${pkgShortName}`,
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function normalizeGitUrl(input) {
|
|
176
|
+
const trimmed = input.trim().replace(/\/$/, '')
|
|
177
|
+
if (trimmed.startsWith('git@') || trimmed.startsWith('ssh://')) return trimmed
|
|
178
|
+
if (/^https?:\/\//.test(trimmed)) {
|
|
179
|
+
return trimmed.endsWith('.git') ? trimmed : `${trimmed}.git`
|
|
180
|
+
}
|
|
181
|
+
const path = trimmed
|
|
182
|
+
.replace(/^github\.com\//, '')
|
|
183
|
+
.replace(/^https?:\/\/github\.com\//, '')
|
|
184
|
+
if (/^[\w.-]+\/[\w.-]+/.test(path)) {
|
|
185
|
+
return `https://github.com/${path.split('/').slice(0, 2).join('/')}.git`
|
|
186
|
+
}
|
|
187
|
+
throw new Error(`Could not parse git source: "${input}"`)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* @param {string} workspaceRoot
|
|
192
|
+
* @param {string} pkgName full @owdproject/name
|
|
193
|
+
*/
|
|
194
|
+
export function hasLocalWorkspaceSource(workspaceRoot, pkgName) {
|
|
195
|
+
const pkgShort = toShortName(pkgName)
|
|
196
|
+
const kind = inferKind(pkgShort)
|
|
197
|
+
return existsSync(join(workspaceRoot, KINDS[kind].workspaceDir, pkgShort, 'package.json'))
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export { resolveForkUser } from './packageSources.js'
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* @typedef {import('./packageSources.js').InstallSourceChoice} InstallSourceChoice
|
|
204
|
+
*/
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* @param {InstallSourceChoice} sourceChoice
|
|
208
|
+
* @param {string} pkgShort
|
|
209
|
+
*/
|
|
210
|
+
export function sourceChoiceToGitUrl(sourceChoice, pkgShort) {
|
|
211
|
+
if (sourceChoice.type !== 'git') return null
|
|
212
|
+
return githubCloneUrl(sourceChoice.owner, pkgShort, sourceChoice.protocol)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* @param {string} pkgName full @owdproject/name
|
|
217
|
+
* @param {{ installMode?: string, githubUser?: string | null }} settings
|
|
218
|
+
* @param {string | null} workspaceRoot
|
|
219
|
+
* @param {InstallSourceChoice} [sourceChoice]
|
|
220
|
+
*/
|
|
221
|
+
export async function resolveInstallPlan(pkgName, settings, workspaceRoot, sourceChoice) {
|
|
222
|
+
const pkgShort = toShortName(pkgName)
|
|
223
|
+
const kind = inferKind(pkgShort)
|
|
224
|
+
const inMonorepo = Boolean(workspaceRoot)
|
|
225
|
+
|
|
226
|
+
if (sourceChoice) {
|
|
227
|
+
if (sourceChoice.type === 'npm') {
|
|
228
|
+
return {
|
|
229
|
+
mode: 'npm',
|
|
230
|
+
pkgName,
|
|
231
|
+
shortName: pkgShort,
|
|
232
|
+
kind,
|
|
233
|
+
label: 'npm registry',
|
|
234
|
+
sourceChoice,
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (!inMonorepo) {
|
|
239
|
+
return {
|
|
240
|
+
error: 'Git clone requires an OWD monorepo (apps/, packages/, themes/).',
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const gitUrl = sourceChoiceToGitUrl(sourceChoice, pkgShort)
|
|
245
|
+
const owner = sourceChoice.owner
|
|
246
|
+
return {
|
|
247
|
+
mode: 'workspace',
|
|
248
|
+
pkgName,
|
|
249
|
+
shortName: pkgShort,
|
|
250
|
+
kind,
|
|
251
|
+
from: owner,
|
|
252
|
+
source: {
|
|
253
|
+
mode: 'workspace',
|
|
254
|
+
gitUrl,
|
|
255
|
+
label: `${sourceChoice.protocol === 'ssh' ? 'SSH' : 'HTTPS'} github.com/${owner}/${pkgShort}`,
|
|
256
|
+
},
|
|
257
|
+
targetDir: join(KINDS[kind].workspaceDir, pkgShort),
|
|
258
|
+
label: `github.com/${owner}/${pkgShort}`,
|
|
259
|
+
sourceChoice,
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (!isWorkspaceInstallMode(settings)) {
|
|
264
|
+
return {
|
|
265
|
+
mode: 'npm',
|
|
266
|
+
pkgName,
|
|
267
|
+
shortName: pkgShort,
|
|
268
|
+
kind,
|
|
269
|
+
label: 'npm registry',
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (!inMonorepo) {
|
|
274
|
+
return {
|
|
275
|
+
error:
|
|
276
|
+
'Dev install mode requires an OWD monorepo (apps/, packages/, themes/). Switch to User mode or use --npm.',
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const fromUser = settings.githubUser
|
|
281
|
+
? await resolveForkUser(settings.githubUser, pkgShort)
|
|
282
|
+
: 'owdproject'
|
|
283
|
+
|
|
284
|
+
const source = resolveSource(fromUser, pkgShort, true)
|
|
285
|
+
return {
|
|
286
|
+
mode: 'workspace',
|
|
287
|
+
pkgName,
|
|
288
|
+
shortName: pkgShort,
|
|
289
|
+
kind,
|
|
290
|
+
from: fromUser,
|
|
291
|
+
source,
|
|
292
|
+
targetDir: join(KINDS[kind].workspaceDir, pkgShort),
|
|
293
|
+
label: source.label,
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* @param {string} pkgName
|
|
299
|
+
* @param {{ installMode?: string, githubUser?: string | null }} settings
|
|
300
|
+
* @param {string} workspaceRoot
|
|
301
|
+
* @param {{ stdio?: 'inherit' | 'pipe' }} [options]
|
|
302
|
+
*/
|
|
303
|
+
export async function installPackage(pkgName, settings, workspaceRoot, options = {}) {
|
|
304
|
+
const plan = await resolveInstallPlan(
|
|
305
|
+
pkgName,
|
|
306
|
+
settings,
|
|
307
|
+
workspaceRoot,
|
|
308
|
+
options.sourceChoice,
|
|
309
|
+
)
|
|
310
|
+
if (plan.error) throw new Error(plan.error)
|
|
311
|
+
|
|
312
|
+
const args = [desktopBin, 'add', plan.shortName]
|
|
313
|
+
|
|
314
|
+
if (plan.mode === 'npm') {
|
|
315
|
+
args.push('--npm')
|
|
316
|
+
} else if (plan.sourceChoice?.type === 'git') {
|
|
317
|
+
const { owner, protocol } = plan.sourceChoice
|
|
318
|
+
if (protocol === 'ssh') {
|
|
319
|
+
args.push('--from', githubCloneUrl(owner, plan.shortName, 'ssh'))
|
|
320
|
+
} else if (owner !== 'owdproject') {
|
|
321
|
+
args.push('--from', owner)
|
|
322
|
+
} else {
|
|
323
|
+
args.push('--dev')
|
|
324
|
+
}
|
|
325
|
+
} else if (plan.from && plan.from !== 'owdproject') {
|
|
326
|
+
args.push('--from', plan.from)
|
|
327
|
+
} else {
|
|
328
|
+
args.push('--dev')
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
await spawnAsync(process.execPath, args, { cwd: workspaceRoot })
|
|
332
|
+
|
|
333
|
+
return plan
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
export async function runPrepareModules(workspaceRoot, _stdio = 'inherit') {
|
|
337
|
+
try {
|
|
338
|
+
await spawnAsync('pnpm', ['run', 'prepare:modules'], { cwd: workspaceRoot })
|
|
339
|
+
} catch {
|
|
340
|
+
/* optional in minimal workspaces */
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
export function printPlan({ pkgName, kind, targetDir, source, branch, dryRun }) {
|
|
347
|
+
const kindMeta = KINDS[kind]
|
|
348
|
+
console.log(`
|
|
349
|
+
${dryRun ? 'Plan (dry run)' : 'Adding to desktop'}
|
|
350
|
+
────────────────────────────────────────
|
|
351
|
+
Package ${pkgName}
|
|
352
|
+
Type ${kindMeta.label}
|
|
353
|
+
Folder ${targetDir}/
|
|
354
|
+
Source ${source.label}${branch ? ` (branch: ${branch})` : ''}
|
|
355
|
+
Config desktop/desktop.config.ts → ${kindMeta.configKey}
|
|
356
|
+
────────────────────────────────────────
|
|
357
|
+
`)
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
export function printNextSteps({ targetDir, dryRun }) {
|
|
361
|
+
if (dryRun) {
|
|
362
|
+
console.log('Dry run complete. Re-run without --dry-run to apply.\n')
|
|
363
|
+
return
|
|
364
|
+
}
|
|
365
|
+
console.log(`Next steps:
|
|
366
|
+
pnpm run dev Start the desktop
|
|
367
|
+
pnpm run prepare:modules Rebuild module stubs if needed
|
|
368
|
+
|
|
369
|
+
Package lives in ${targetDir}/
|
|
370
|
+
`)
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* @param {string} targetDir
|
|
375
|
+
* @param {string} gitUrl
|
|
376
|
+
* @param {string | undefined} branch
|
|
377
|
+
* @param {string} workspaceRoot
|
|
378
|
+
* @param {{ stdio?: 'inherit' | 'pipe' | 'ignore', quiet?: boolean }} [options]
|
|
379
|
+
*/
|
|
380
|
+
export async function cloneRepo(targetDir, gitUrl, branch, workspaceRoot, options = {}) {
|
|
381
|
+
const quiet = options.quiet ?? true
|
|
382
|
+
const absoluteTarget = join(workspaceRoot, targetDir)
|
|
383
|
+
|
|
384
|
+
if (existsSync(join(absoluteTarget, 'package.json'))) {
|
|
385
|
+
if (!quiet) console.log(`Folder ${targetDir}/ already exists — skipping clone.\n`)
|
|
386
|
+
return
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if (existsSync(absoluteTarget)) {
|
|
390
|
+
throw new Error(
|
|
391
|
+
`${targetDir}/ exists but has no package.json. Remove the folder or clone your fork there manually.`,
|
|
392
|
+
)
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const args = ['clone']
|
|
396
|
+
if (branch) args.push('-b', branch)
|
|
397
|
+
args.push(gitUrl, absoluteTarget)
|
|
398
|
+
|
|
399
|
+
if (!quiet) console.log(`Cloning into ${targetDir}/ …\n`)
|
|
400
|
+
await spawnAsync('git', args, { cwd: workspaceRoot })
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
export async function linkWorkspacePackage(desktopPath, pkgName) {
|
|
404
|
+
await spawnAsync('pnpm', ['add', `${pkgName}@workspace:*`], { cwd: desktopPath })
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
async function runDevPrepare(workspaceRoot, pkgName, options = {}) {
|
|
408
|
+
const quiet = options.quiet ?? true
|
|
409
|
+
try {
|
|
410
|
+
await spawnAsync('pnpm', ['--filter', pkgName, 'run', 'dev:prepare'], { cwd: workspaceRoot })
|
|
411
|
+
} catch {
|
|
412
|
+
if (!quiet) console.log('(no dev:prepare script — ok for some packages)\n')
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
export async function runWorkspaceInstall({
|
|
417
|
+
pkgName,
|
|
418
|
+
shortName: pkgShort,
|
|
419
|
+
kind,
|
|
420
|
+
workspaceRoot,
|
|
421
|
+
source,
|
|
422
|
+
branch,
|
|
423
|
+
dryRun,
|
|
424
|
+
}) {
|
|
425
|
+
const { workspaceDir } = KINDS[kind]
|
|
426
|
+
const targetDir = join(workspaceDir, pkgShort)
|
|
427
|
+
const desktopPath = join(workspaceRoot, 'desktop')
|
|
428
|
+
const resolved = resolveDesktopConfigPath(desktopPath)
|
|
429
|
+
const configPath = resolved?.path ?? desktopConfigWritePath(desktopPath)
|
|
430
|
+
|
|
431
|
+
printPlan({
|
|
432
|
+
pkgName,
|
|
433
|
+
kind,
|
|
434
|
+
targetDir,
|
|
435
|
+
source,
|
|
436
|
+
branch,
|
|
437
|
+
dryRun,
|
|
438
|
+
})
|
|
439
|
+
|
|
440
|
+
if (dryRun) {
|
|
441
|
+
printNextSteps({ targetDir, dryRun: true })
|
|
442
|
+
return
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
await cloneRepo(targetDir, source.gitUrl, branch, workspaceRoot)
|
|
446
|
+
await linkWorkspacePackage(desktopPath, pkgName)
|
|
447
|
+
|
|
448
|
+
addToDesktopConfig(configPath, workspaceRoot, KINDS[kind].configKey, pkgName)
|
|
449
|
+
|
|
450
|
+
await runDevPrepare(workspaceRoot, pkgName)
|
|
451
|
+
|
|
452
|
+
console.log(`✓ ${pkgName} is ready.\n`)
|
|
453
|
+
printNextSteps({ targetDir, dryRun: false })
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
export function runNpmInstall(kind, pkgName, workspaceRoot, stdio = 'inherit') {
|
|
457
|
+
return new Promise((resolve, reject) => {
|
|
458
|
+
const child = spawn('node', ['scripts/install-app.mjs', pkgName, `--kind=${kind}`], {
|
|
459
|
+
stdio,
|
|
460
|
+
shell: true,
|
|
461
|
+
cwd: workspaceRoot,
|
|
462
|
+
})
|
|
463
|
+
child.on('error', reject)
|
|
464
|
+
child.on('exit', (code) => {
|
|
465
|
+
if (code === 0) {
|
|
466
|
+
resolve()
|
|
467
|
+
} else {
|
|
468
|
+
reject(new Error(`Installation failed with exit code ${code}`))
|
|
469
|
+
}
|
|
470
|
+
})
|
|
471
|
+
})
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/** Preview label for TUI list rows */
|
|
475
|
+
/**
|
|
476
|
+
* @param {string} pkgName
|
|
477
|
+
* @param {{ installMode?: string, githubUser?: string | null }} settings
|
|
478
|
+
* @param {string} workspaceRoot
|
|
479
|
+
* @param {{ stdio?: 'inherit' | 'pipe' }} [options]
|
|
480
|
+
*/
|
|
481
|
+
export async function materializeToWorkspace(pkgName, settings, workspaceRoot, options = {}) {
|
|
482
|
+
const plan = await resolveInstallPlan(
|
|
483
|
+
pkgName,
|
|
484
|
+
settings,
|
|
485
|
+
workspaceRoot,
|
|
486
|
+
options.sourceChoice,
|
|
487
|
+
)
|
|
488
|
+
if (plan.error) throw new Error(plan.error)
|
|
489
|
+
if (plan.mode !== 'workspace') {
|
|
490
|
+
throw new Error(`Cannot materialize ${pkgName}: source is not a git workspace clone.`)
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
if (hasLocalWorkspaceSource(workspaceRoot, pkgName)) {
|
|
494
|
+
return { skipped: true, plan }
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
const desktopPath = join(workspaceRoot, 'desktop')
|
|
498
|
+
|
|
499
|
+
await cloneRepo(plan.targetDir, plan.source.gitUrl, undefined, workspaceRoot, { quiet: true })
|
|
500
|
+
await linkWorkspacePackage(desktopPath, pkgName)
|
|
501
|
+
await runDevPrepare(workspaceRoot, pkgName, { quiet: true })
|
|
502
|
+
|
|
503
|
+
return { skipped: false, plan }
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/** @param {import('./catalog.js').CatalogEntry} item */
|
|
507
|
+
export function catalogListTag(item) {
|
|
508
|
+
if (item.localSource) return 'local'
|
|
509
|
+
if (item.installed && !item.localSource) return 'npm'
|
|
510
|
+
if (item.org === 'workspace') return 'local'
|
|
511
|
+
return 'catalog'
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
export function installTag(_settings, item) {
|
|
515
|
+
const tag = catalogListTag(item)
|
|
516
|
+
if (tag === 'catalog') {
|
|
517
|
+
if (item.sourcesMeta?.npm?.version) return 'npm+git'
|
|
518
|
+
return 'git'
|
|
519
|
+
}
|
|
520
|
+
return tag
|
|
521
|
+
}
|