@owdproject/cli 0.0.3 → 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 -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,365 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process'
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
3
|
+
import { join } from 'node:path'
|
|
4
|
+
import { fetchLatestVersion } from './npmVersions.js'
|
|
5
|
+
import { desktopMetaDir, fullName, SCOPE, KINDS, inferKind } from './workspace.js'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @param {string} githubUser
|
|
9
|
+
* @param {string} pkgShortName
|
|
10
|
+
*/
|
|
11
|
+
export async function resolveForkUser(githubUser, pkgShortName) {
|
|
12
|
+
if (!githubUser || githubUser === 'owdproject') return { exists: false, isFork: false }
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
const res = await fetch(`https://api.github.com/repos/${githubUser}/${pkgShortName}`, {
|
|
16
|
+
headers: { Accept: 'application/vnd.github+json', 'User-Agent': 'owd-desktop-cli' },
|
|
17
|
+
})
|
|
18
|
+
if (res.ok) {
|
|
19
|
+
const data = await res.json()
|
|
20
|
+
// data.fork === true only if this repo was forked FROM another repo
|
|
21
|
+
return { exists: true, isFork: data.fork === true }
|
|
22
|
+
}
|
|
23
|
+
} catch {
|
|
24
|
+
/* ignore */
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return { exists: false, isFork: false }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @param {string} workspaceRoot
|
|
32
|
+
* @param {string} pkgName
|
|
33
|
+
*/
|
|
34
|
+
function hasLocalWorkspaceSource(workspaceRoot, pkgName) {
|
|
35
|
+
const pkgShort = pkgName.startsWith(SCOPE) ? pkgName.slice(SCOPE.length) : pkgName
|
|
36
|
+
const kind = inferKind(pkgShort)
|
|
37
|
+
return existsSync(join(workspaceRoot, KINDS[kind].workspaceDir, pkgShort, 'package.json'))
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const CACHE_TTL_MS = 60 * 60 * 1000
|
|
41
|
+
const SSH_CACHE_TTL_MS = 15 * 60 * 1000
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @typedef {{ type: 'npm' } | { type: 'git', protocol: 'https' | 'ssh', owner: string }} InstallSourceChoice
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @typedef {{
|
|
49
|
+
* id: string
|
|
50
|
+
* kind: 'npm' | 'git'
|
|
51
|
+
* protocol?: 'https' | 'ssh'
|
|
52
|
+
* owner?: string
|
|
53
|
+
* label: string
|
|
54
|
+
* url: string
|
|
55
|
+
* choice: InstallSourceChoice
|
|
56
|
+
* }} SourceOption
|
|
57
|
+
*/
|
|
58
|
+
|
|
59
|
+
function cachePath(workspaceRoot) {
|
|
60
|
+
return join(desktopMetaDir(workspaceRoot), 'package-sources.json')
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function readCacheFile(workspaceRoot) {
|
|
64
|
+
const path = cachePath(workspaceRoot)
|
|
65
|
+
if (!existsSync(path)) return null
|
|
66
|
+
try {
|
|
67
|
+
return JSON.parse(readFileSync(path, 'utf8'))
|
|
68
|
+
} catch {
|
|
69
|
+
return null
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function writeCacheFile(workspaceRoot, data) {
|
|
74
|
+
mkdirSync(desktopMetaDir(workspaceRoot), { recursive: true })
|
|
75
|
+
writeFileSync(cachePath(workspaceRoot), JSON.stringify(data, null, 2) + '\n')
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* @param {string[] | undefined} githubOrgs
|
|
80
|
+
* @param {string | null | undefined} githubUser
|
|
81
|
+
*/
|
|
82
|
+
export function trustedPublishers(githubOrgs, githubUser) {
|
|
83
|
+
return [...new Set([...(githubOrgs ?? ['owdproject']), githubUser].filter(Boolean))]
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* @param {{ org?: string }} entry
|
|
88
|
+
* @param {{ githubOrgs?: string[], githubUser?: string | null }} settings
|
|
89
|
+
*/
|
|
90
|
+
export function isTrustedPublisher(entry, settings) {
|
|
91
|
+
if (!entry.org || entry.org === 'workspace') return true
|
|
92
|
+
return trustedPublishers(settings.githubOrgs, settings.githubUser).includes(entry.org)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* @param {string} owner
|
|
97
|
+
* @param {string} shortName
|
|
98
|
+
* @param {'https' | 'ssh'} protocol
|
|
99
|
+
*/
|
|
100
|
+
export function githubCloneUrl(owner, shortName, protocol) {
|
|
101
|
+
return protocol === 'ssh'
|
|
102
|
+
? `git@github.com:${owner}/${shortName}.git`
|
|
103
|
+
: `https://github.com/${owner}/${shortName}.git`
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function githubHtmlUrl(owner, shortName) {
|
|
107
|
+
return `https://github.com/${owner}/${shortName}`
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function npmPackageUrl(pkgName) {
|
|
111
|
+
const short = pkgName.startsWith(SCOPE) ? pkgName.slice(SCOPE.length) : pkgName
|
|
112
|
+
return `https://www.npmjs.com/package/${SCOPE}${short}`
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* @param {string} workspaceRoot
|
|
117
|
+
* @param {{ force?: boolean }} [options]
|
|
118
|
+
*/
|
|
119
|
+
export async function detectGithubSshAuth(workspaceRoot, options = {}) {
|
|
120
|
+
const cache = readCacheFile(workspaceRoot)
|
|
121
|
+
const cached = cache?.sshAuth
|
|
122
|
+
if (!options.force && cached?.checkedAt && Date.now() - cached.checkedAt < SSH_CACHE_TTL_MS) {
|
|
123
|
+
return cached
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** @type {{ checkedAt: number, available: boolean, githubLogin: string | null, message: string }} */
|
|
127
|
+
let result = {
|
|
128
|
+
checkedAt: Date.now(),
|
|
129
|
+
available: false,
|
|
130
|
+
githubLogin: null,
|
|
131
|
+
message: 'SSH not configured',
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
const out = execSync(
|
|
136
|
+
'ssh -T -o BatchMode=yes -o StrictHostKeyChecking=accept-new git@github.com 2>&1',
|
|
137
|
+
{ encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 8000 },
|
|
138
|
+
)
|
|
139
|
+
const text = out || ''
|
|
140
|
+
const login = text.match(/Hi ([^!]+)!/)?.[1]?.trim() ?? null
|
|
141
|
+
if (login) {
|
|
142
|
+
result = {
|
|
143
|
+
checkedAt: Date.now(),
|
|
144
|
+
available: true,
|
|
145
|
+
githubLogin: login,
|
|
146
|
+
message: `authenticated as ${login}`,
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
} catch (error) {
|
|
150
|
+
const text = `${error.stdout ?? ''}${error.stderr ?? ''}${error.message ?? ''}`
|
|
151
|
+
const login = text.match(/Hi ([^!]+)!/)?.[1]?.trim() ?? null
|
|
152
|
+
if (login) {
|
|
153
|
+
result = {
|
|
154
|
+
checkedAt: Date.now(),
|
|
155
|
+
available: true,
|
|
156
|
+
githubLogin: login,
|
|
157
|
+
message: `authenticated as ${login}`,
|
|
158
|
+
}
|
|
159
|
+
} else if (/Permission denied|publickey/i.test(text)) {
|
|
160
|
+
result = {
|
|
161
|
+
checkedAt: Date.now(),
|
|
162
|
+
available: false,
|
|
163
|
+
githubLogin: null,
|
|
164
|
+
message: 'SSH keys not accepted by GitHub',
|
|
165
|
+
}
|
|
166
|
+
} else {
|
|
167
|
+
result = {
|
|
168
|
+
checkedAt: Date.now(),
|
|
169
|
+
available: false,
|
|
170
|
+
githubLogin: null,
|
|
171
|
+
message: 'SSH probe failed',
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const next = { ...(cache ?? { fetchedAt: 0, packages: {} }), sshAuth: result }
|
|
177
|
+
writeCacheFile(workspaceRoot, next)
|
|
178
|
+
return result
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* @param {string} workspaceRoot
|
|
183
|
+
*/
|
|
184
|
+
export function getCachedSshAuth(workspaceRoot) {
|
|
185
|
+
const cache = readCacheFile(workspaceRoot)
|
|
186
|
+
return cache?.sshAuth ?? null
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* @param {string} shortName
|
|
191
|
+
* @param {{ htmlUrl?: string | null, org?: string }} [catalogEntry]
|
|
192
|
+
* @param {{ githubUser?: string | null }} settings
|
|
193
|
+
*/
|
|
194
|
+
export async function resolvePackageSources(shortName, settings, workspaceRoot, catalogEntry = {}) {
|
|
195
|
+
const pkgName = fullName(shortName)
|
|
196
|
+
const officialOwner = catalogEntry.org && catalogEntry.org !== 'workspace' ? catalogEntry.org : 'owdproject'
|
|
197
|
+
const officialHtml =
|
|
198
|
+
catalogEntry.htmlUrl ?? githubHtmlUrl(officialOwner, shortName)
|
|
199
|
+
|
|
200
|
+
const cache = readCacheFile(workspaceRoot)
|
|
201
|
+
const cachedPkg = cache?.packages?.[shortName]
|
|
202
|
+
const cacheFresh = cachedPkg && cache?.fetchedAt && Date.now() - cache.fetchedAt < CACHE_TTL_MS
|
|
203
|
+
|
|
204
|
+
let npm = cachedPkg?.npm ?? null
|
|
205
|
+
if (!cacheFresh || npm === undefined) {
|
|
206
|
+
const version = fetchLatestVersion(pkgName, { optional: true })
|
|
207
|
+
npm = version ? { version } : null
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
let forkOwner = settings.githubUser && settings.githubUser !== 'owdproject' ? settings.githubUser : null
|
|
211
|
+
let forkExists = false
|
|
212
|
+
let forkIsActualFork = false
|
|
213
|
+
if (forkOwner) {
|
|
214
|
+
const result = await resolveForkUser(forkOwner, shortName)
|
|
215
|
+
forkExists = result.exists
|
|
216
|
+
forkIsActualFork = result.isFork
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const local = workspaceRoot && hasLocalWorkspaceSource(workspaceRoot, pkgName)
|
|
220
|
+
|
|
221
|
+
const metadata = {
|
|
222
|
+
shortName,
|
|
223
|
+
pkgName,
|
|
224
|
+
npm,
|
|
225
|
+
github: {
|
|
226
|
+
official: {
|
|
227
|
+
owner: officialOwner,
|
|
228
|
+
htmlUrl: officialHtml,
|
|
229
|
+
exists: Boolean(catalogEntry.htmlUrl || officialOwner),
|
|
230
|
+
},
|
|
231
|
+
fork: forkOwner && forkExists
|
|
232
|
+
? { owner: forkOwner, exists: forkExists, isFork: forkIsActualFork, htmlUrl: githubHtmlUrl(forkOwner, shortName) }
|
|
233
|
+
: null,
|
|
234
|
+
},
|
|
235
|
+
local: local ? true : false,
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const packages = { ...(cache?.packages ?? {}), [shortName]: { npm, github: metadata.github } }
|
|
239
|
+
writeCacheFile(workspaceRoot, {
|
|
240
|
+
fetchedAt: Date.now(),
|
|
241
|
+
sshAuth: cache?.sshAuth ?? null,
|
|
242
|
+
packages,
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
return metadata
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* @param {Awaited<ReturnType<typeof resolvePackageSources>>} metadata
|
|
250
|
+
* @param {{ githubUser?: string | null }} settings
|
|
251
|
+
* @param {{ available?: boolean }} sshAuth
|
|
252
|
+
* @returns {SourceOption[]}
|
|
253
|
+
*/
|
|
254
|
+
export function buildSourceOptions(metadata, settings, sshAuth = {}) {
|
|
255
|
+
/** @type {SourceOption[]} */
|
|
256
|
+
const options = []
|
|
257
|
+
|
|
258
|
+
const official = metadata.github.official
|
|
259
|
+
if (official?.owner) {
|
|
260
|
+
options.push({
|
|
261
|
+
id: 'git-https-official',
|
|
262
|
+
kind: 'git',
|
|
263
|
+
protocol: 'https',
|
|
264
|
+
owner: official.owner,
|
|
265
|
+
label: `GitHub HTTPS — ${official.owner}/${metadata.shortName}`,
|
|
266
|
+
url: githubCloneUrl(official.owner, metadata.shortName, 'https'),
|
|
267
|
+
choice: { type: 'git', protocol: 'https', owner: official.owner },
|
|
268
|
+
})
|
|
269
|
+
if (sshAuth.available) {
|
|
270
|
+
options.push({
|
|
271
|
+
id: 'git-ssh-official',
|
|
272
|
+
kind: 'git',
|
|
273
|
+
protocol: 'ssh',
|
|
274
|
+
owner: official.owner,
|
|
275
|
+
label: `GitHub SSH — ${official.owner}/${metadata.shortName}`,
|
|
276
|
+
url: githubCloneUrl(official.owner, metadata.shortName, 'ssh'),
|
|
277
|
+
choice: { type: 'git', protocol: 'ssh', owner: official.owner },
|
|
278
|
+
})
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const fork = metadata.github.fork
|
|
283
|
+
const officialOwnerLower = official?.owner?.toLowerCase()
|
|
284
|
+
const forkOwnerLower = fork?.owner?.toLowerCase()
|
|
285
|
+
const userLower = settings.githubUser?.toLowerCase()
|
|
286
|
+
// Skip fork options when fork owner is the same as the official repo owner
|
|
287
|
+
// (the official options already cover that user)
|
|
288
|
+
const forkIsSameAsOfficial = forkOwnerLower && forkOwnerLower === officialOwnerLower
|
|
289
|
+
const forkMissingIsSameAsOfficial = userLower && userLower === officialOwnerLower
|
|
290
|
+
|
|
291
|
+
if (fork?.exists && fork.owner && !forkIsSameAsOfficial) {
|
|
292
|
+
options.push({
|
|
293
|
+
id: 'git-https-fork',
|
|
294
|
+
kind: 'git',
|
|
295
|
+
protocol: 'https',
|
|
296
|
+
owner: fork.owner,
|
|
297
|
+
label: `GitHub HTTPS — ${fork.owner}/${metadata.shortName} (your fork)`,
|
|
298
|
+
url: githubCloneUrl(fork.owner, metadata.shortName, 'https'),
|
|
299
|
+
choice: { type: 'git', protocol: 'https', owner: fork.owner },
|
|
300
|
+
})
|
|
301
|
+
if (sshAuth.available) {
|
|
302
|
+
options.push({
|
|
303
|
+
id: 'git-ssh-fork',
|
|
304
|
+
kind: 'git',
|
|
305
|
+
protocol: 'ssh',
|
|
306
|
+
owner: fork.owner,
|
|
307
|
+
label: `GitHub SSH — ${fork.owner}/${metadata.shortName} (your fork)`,
|
|
308
|
+
url: githubCloneUrl(fork.owner, metadata.shortName, 'ssh'),
|
|
309
|
+
choice: { type: 'git', protocol: 'ssh', owner: fork.owner },
|
|
310
|
+
})
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (metadata.npm?.version) {
|
|
315
|
+
options.push({
|
|
316
|
+
id: 'npm',
|
|
317
|
+
kind: 'npm',
|
|
318
|
+
label: `npm — ${metadata.pkgName}@${metadata.npm.version}`,
|
|
319
|
+
url: npmPackageUrl(metadata.pkgName),
|
|
320
|
+
choice: { type: 'npm' },
|
|
321
|
+
})
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return options
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* @param {object[]} entries
|
|
329
|
+
* @param {string} workspaceRoot
|
|
330
|
+
* @param {{ githubOrgs?: string[], githubUser?: string | null }} settings
|
|
331
|
+
*/
|
|
332
|
+
export async function enrichCatalogEntries(entries, workspaceRoot, settings) {
|
|
333
|
+
const cache = readCacheFile(workspaceRoot)
|
|
334
|
+
const sshAuth = getCachedSshAuth(workspaceRoot) ?? { available: false }
|
|
335
|
+
|
|
336
|
+
return entries.map((entry) => {
|
|
337
|
+
const pkgMeta = cache?.packages?.[entry.shortName]
|
|
338
|
+
return {
|
|
339
|
+
...entry,
|
|
340
|
+
trusted: isTrustedPublisher(entry, settings),
|
|
341
|
+
sourcesMeta: {
|
|
342
|
+
npm: pkgMeta?.npm ?? null,
|
|
343
|
+
github: {
|
|
344
|
+
official: {
|
|
345
|
+
owner: entry.org && entry.org !== 'workspace' ? entry.org : 'owdproject',
|
|
346
|
+
htmlUrl: entry.htmlUrl ?? githubHtmlUrl(entry.org ?? 'owdproject', entry.shortName),
|
|
347
|
+
},
|
|
348
|
+
fork: pkgMeta?.github?.fork ?? null,
|
|
349
|
+
},
|
|
350
|
+
},
|
|
351
|
+
}
|
|
352
|
+
})
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* @param {{ sourcesMeta?: { npm?: { version?: string } | null }, localSource?: boolean, htmlUrl?: string, trusted?: boolean }} item
|
|
357
|
+
*/
|
|
358
|
+
export function formatSourceBadges(item) {
|
|
359
|
+
const parts = []
|
|
360
|
+
if (item.localSource) parts.push(`{cyan-fg}l{/}`)
|
|
361
|
+
if (item.sourcesMeta?.npm?.version) parts.push(`{green-fg}n{/}`)
|
|
362
|
+
if (item.htmlUrl || item.sourcesMeta?.github) parts.push(`{blue-fg}g{/}`)
|
|
363
|
+
if (item.trusted === false) parts.push(`{yellow-fg}!{/}`)
|
|
364
|
+
return parts.length ? ` ${parts.join('')}` : ''
|
|
365
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
2
|
+
import { dirname, join, resolve, sep } from 'node:path'
|
|
3
|
+
import { SCOPE } from './workspace.js'
|
|
4
|
+
|
|
5
|
+
const PLAYGROUND_CONFIG = join('playground', 'desktop.config.ts')
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @param {string} dir
|
|
9
|
+
* @returns {boolean}
|
|
10
|
+
*/
|
|
11
|
+
export function hasPlaygroundDesktopConfig(dir) {
|
|
12
|
+
return (
|
|
13
|
+
existsSync(join(dir, 'playground')) &&
|
|
14
|
+
existsSync(join(dir, PLAYGROUND_CONFIG))
|
|
15
|
+
)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @param {string} pkgPath
|
|
20
|
+
* @returns {{ name: string } | null}
|
|
21
|
+
*/
|
|
22
|
+
function readOwdPackageJson(pkgPath) {
|
|
23
|
+
if (!existsSync(pkgPath)) return null
|
|
24
|
+
try {
|
|
25
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'))
|
|
26
|
+
if (typeof pkg.name === 'string' && pkg.name.startsWith(SCOPE)) {
|
|
27
|
+
return { name: pkg.name }
|
|
28
|
+
}
|
|
29
|
+
} catch {
|
|
30
|
+
/* ignore */
|
|
31
|
+
}
|
|
32
|
+
return null
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Walk up from cwd until a package.json with @owdproject/* name and a playground
|
|
37
|
+
* with desktop.config.ts is found.
|
|
38
|
+
*
|
|
39
|
+
* @param {string} [startDir]
|
|
40
|
+
* @returns {string | null} Absolute package directory
|
|
41
|
+
*/
|
|
42
|
+
export function findOwdModulePackageRoot(startDir = process.cwd()) {
|
|
43
|
+
let dir = resolve(startDir)
|
|
44
|
+
for (;;) {
|
|
45
|
+
const meta = readOwdPackageJson(join(dir, 'package.json'))
|
|
46
|
+
if (meta && hasPlaygroundDesktopConfig(dir)) {
|
|
47
|
+
return dir
|
|
48
|
+
}
|
|
49
|
+
const parent = dirname(dir)
|
|
50
|
+
if (parent === dir) return null
|
|
51
|
+
dir = parent
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* @param {string} path
|
|
57
|
+
* @param {string} root
|
|
58
|
+
*/
|
|
59
|
+
function isPathInside(path, root) {
|
|
60
|
+
const normalized = resolve(path)
|
|
61
|
+
const base = resolve(root)
|
|
62
|
+
return normalized === base || normalized.startsWith(base + sep)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* @typedef {'workspace' | 'playground'} DevTargetMode
|
|
67
|
+
*/
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* @typedef {object} DevTarget
|
|
71
|
+
* @property {DevTargetMode} mode
|
|
72
|
+
* @property {string} workspaceRoot
|
|
73
|
+
* @property {string} packageDir Directory where `pnpm run dev` runs
|
|
74
|
+
* @property {string | null} packageName Set when mode is playground
|
|
75
|
+
* @property {string | null} playgroundDir Absolute path to playground/
|
|
76
|
+
*/
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* @param {string} packageDir
|
|
80
|
+
* @param {string} workspaceRoot
|
|
81
|
+
* @param {string} packageName
|
|
82
|
+
* @returns {DevTarget}
|
|
83
|
+
*/
|
|
84
|
+
function playgroundTarget(packageDir, workspaceRoot, packageName) {
|
|
85
|
+
return {
|
|
86
|
+
mode: 'playground',
|
|
87
|
+
workspaceRoot,
|
|
88
|
+
packageDir: resolve(packageDir),
|
|
89
|
+
packageName,
|
|
90
|
+
playgroundDir: join(resolve(packageDir), 'playground'),
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* @param {string} workspaceRoot
|
|
96
|
+
* @returns {DevTarget}
|
|
97
|
+
*/
|
|
98
|
+
function workspaceTarget(workspaceRoot) {
|
|
99
|
+
return {
|
|
100
|
+
mode: 'workspace',
|
|
101
|
+
workspaceRoot: resolve(workspaceRoot),
|
|
102
|
+
packageDir: resolve(workspaceRoot),
|
|
103
|
+
packageName: null,
|
|
104
|
+
playgroundDir: null,
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Resolve whether the CLI/TUI should start the monorepo desktop shell or a
|
|
110
|
+
* module package playground dev server.
|
|
111
|
+
*
|
|
112
|
+
* @param {string} [cwd]
|
|
113
|
+
* @param {string | null} workspaceRoot
|
|
114
|
+
* @param {{ forcePlayground?: boolean }} [options]
|
|
115
|
+
* @returns {DevTarget | null} null when workspaceRoot is missing
|
|
116
|
+
*/
|
|
117
|
+
export function resolveDevTarget(cwd, workspaceRoot, options = {}) {
|
|
118
|
+
if (!workspaceRoot) return null
|
|
119
|
+
|
|
120
|
+
const normalizedCwd = resolve(cwd ?? process.cwd())
|
|
121
|
+
const root = resolve(workspaceRoot)
|
|
122
|
+
const desktopDir = join(root, 'desktop')
|
|
123
|
+
const packageRoot = findOwdModulePackageRoot(normalizedCwd)
|
|
124
|
+
const forcePlayground = options.forcePlayground === true
|
|
125
|
+
|
|
126
|
+
if (packageRoot) {
|
|
127
|
+
const meta = readOwdPackageJson(join(packageRoot, 'package.json'))
|
|
128
|
+
const packageName = meta?.name ?? null
|
|
129
|
+
|
|
130
|
+
if (forcePlayground) {
|
|
131
|
+
return playgroundTarget(packageRoot, root, packageName)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const atWorkspaceRoot = normalizedCwd === root
|
|
135
|
+
const underDesktopOnly =
|
|
136
|
+
isPathInside(normalizedCwd, desktopDir) && !isPathInside(normalizedCwd, packageRoot)
|
|
137
|
+
|
|
138
|
+
if (!atWorkspaceRoot && !underDesktopOnly && isPathInside(normalizedCwd, packageRoot)) {
|
|
139
|
+
return playgroundTarget(packageRoot, root, packageName)
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (forcePlayground) {
|
|
144
|
+
return null
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return workspaceTarget(root)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* @param {DevTarget | string | null | undefined} target
|
|
152
|
+
* @param {string} [workspaceRoot]
|
|
153
|
+
* @returns {DevTarget | null}
|
|
154
|
+
*/
|
|
155
|
+
export function normalizeDevTarget(target, workspaceRoot) {
|
|
156
|
+
if (!target) return workspaceRoot ? workspaceTarget(workspaceRoot) : null
|
|
157
|
+
if (typeof target === 'string') return workspaceTarget(target)
|
|
158
|
+
if (target.mode && target.workspaceRoot) return target
|
|
159
|
+
if (workspaceRoot) {
|
|
160
|
+
return {
|
|
161
|
+
...workspaceTarget(workspaceRoot),
|
|
162
|
+
...target,
|
|
163
|
+
workspaceRoot: resolve(workspaceRoot),
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return null
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* @param {DevTarget | string} target
|
|
171
|
+
* @returns {string}
|
|
172
|
+
*/
|
|
173
|
+
export function devSpawnCwd(target) {
|
|
174
|
+
const resolved = typeof target === 'string' ? workspaceTarget(target) : target
|
|
175
|
+
return resolved.packageDir
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* @param {DevTarget | string} target
|
|
180
|
+
* @returns {string}
|
|
181
|
+
*/
|
|
182
|
+
export function devTargetLogLabel(target) {
|
|
183
|
+
const resolved = typeof target === 'string' ? workspaceTarget(target) : target
|
|
184
|
+
if (resolved.mode === 'playground' && resolved.packageName) {
|
|
185
|
+
return `playground ${resolved.packageName}`
|
|
186
|
+
}
|
|
187
|
+
return 'workspace desktop'
|
|
188
|
+
}
|