onework 0.1.0-alpha.0 → 0.1.0-alpha.1
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/__tests__/adapter-package-cache.spec.ts +109 -0
- package/__tests__/cli.spec.ts +210 -0
- package/__tests__/desktop-app.spec.ts +106 -0
- package/__tests__/npm-package.spec.ts +149 -0
- package/__tests__/package-launcher.spec.ts +23 -0
- package/__tests__/redirect-packages.spec.ts +27 -0
- package/__tests__/runtime-package.spec.ts +211 -0
- package/cli.js +3 -5
- package/package-version-refresh-worker.cjs +324 -0
- package/package.json +15 -9
- package/src/adapter-package-cache.ts +432 -0
- package/src/cli.ts +5 -0
- package/src/desktop-app.ts +44 -0
- package/src/desktop-install.ts +177 -0
- package/src/desktop-mode.ts +92 -0
- package/src/desktop-release.ts +198 -0
- package/src/npm-package-cache.ts +185 -0
- package/src/npm-package-install.ts +139 -0
- package/src/npm-package.ts +151 -0
- package/src/package-config.ts +23 -0
- package/src/package-launcher.ts +85 -0
- package/src/paths.ts +19 -0
- package/src/process-utils.ts +126 -0
- package/src/program.ts +286 -0
- package/src/progress.ts +174 -0
- package/src/runtime-package.ts +126 -0
- package/README.md +0 -9
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
/* eslint-disable max-lines -- bootstrap adapter package resolution keeps cache lookup, install, and CLI parsing together. */
|
|
2
|
+
import { createHash } from 'node:crypto'
|
|
3
|
+
import { existsSync } from 'node:fs'
|
|
4
|
+
import { readFile, readdir, rename, rm, writeFile } from 'node:fs/promises'
|
|
5
|
+
import path from 'node:path'
|
|
6
|
+
import process from 'node:process'
|
|
7
|
+
|
|
8
|
+
import { compareVersionLike, ensureDirectory, resolvePackageManagerEnv, sanitizePackageName } from './npm-package-cache'
|
|
9
|
+
import { resolveBootstrapPackageCacheDir } from './paths'
|
|
10
|
+
import { runBufferedCommand } from './process-utils'
|
|
11
|
+
import { createBootstrapProgress } from './progress'
|
|
12
|
+
|
|
13
|
+
const NPM_BIN = process.platform === 'win32' ? 'npm.cmd' : 'npm'
|
|
14
|
+
const ADAPTER_SCOPE = '@oneworks'
|
|
15
|
+
const ADAPTER_PREFIX = 'adapter-'
|
|
16
|
+
|
|
17
|
+
interface AdapterPackageVersionMetadata {
|
|
18
|
+
checkedAt: string
|
|
19
|
+
installedPackageDir: string
|
|
20
|
+
key: string
|
|
21
|
+
name: string
|
|
22
|
+
resolvedVersion: string
|
|
23
|
+
version: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface CliAdapterPackageRequest {
|
|
27
|
+
adapter: string
|
|
28
|
+
cliVersion: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const hashValue = (value: string) => createHash('sha1').update(value).digest('hex')
|
|
32
|
+
|
|
33
|
+
const resolveAdapterPackagesRoot = () => path.join(resolveBootstrapPackageCacheDir(), 'adapter-packages')
|
|
34
|
+
|
|
35
|
+
const resolveAdapterPackageCacheDir = (packageName: string, version: string) => (
|
|
36
|
+
path.join(resolveAdapterPackagesRoot(), sanitizePackageName(packageName), version)
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
const resolveAdapterPackageInstallDir = (cacheDir: string, packageName: string) => (
|
|
40
|
+
path.join(cacheDir, 'node_modules', ...packageName.split('/'))
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
const resolveAdapterPackageMetadataDir = () => path.join(resolveAdapterPackagesRoot(), 'metadata')
|
|
44
|
+
|
|
45
|
+
const resolveAdapterPackageManagerEnv = () => {
|
|
46
|
+
const npmCache = path.join(resolveAdapterPackagesRoot(), 'npm-cache')
|
|
47
|
+
return {
|
|
48
|
+
...resolvePackageManagerEnv(),
|
|
49
|
+
npm_config_cache: npmCache,
|
|
50
|
+
NPM_CONFIG_CACHE: npmCache
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const readRegistryFromEnv = () => {
|
|
55
|
+
const env = resolveAdapterPackageManagerEnv()
|
|
56
|
+
return env.npm_config_registry ?? env.NPM_CONFIG_REGISTRY ?? ''
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const resolveAdapterVersionSpec = (cliVersion: string) => {
|
|
60
|
+
const version = /^(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)/u.exec(cliVersion.trim())
|
|
61
|
+
return version == null ? cliVersion : `^${version[1]}`
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const resolveAdapterPackageLookupKey = (packageName: string, versionSpec: string) => (
|
|
65
|
+
JSON.stringify({
|
|
66
|
+
name: packageName,
|
|
67
|
+
registry: readRegistryFromEnv(),
|
|
68
|
+
version: versionSpec
|
|
69
|
+
})
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
const resolveAdapterPackageMetadataPath = (packageName: string, versionSpec: string) => {
|
|
73
|
+
const key = resolveAdapterPackageLookupKey(packageName, versionSpec)
|
|
74
|
+
return {
|
|
75
|
+
key,
|
|
76
|
+
metadataPath: path.join(resolveAdapterPackageMetadataDir(), `${hashValue(key)}.json`)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const readInstalledPackageVersion = async (packageDir: string) => {
|
|
81
|
+
try {
|
|
82
|
+
const content = await readFile(path.join(packageDir, 'package.json'), 'utf8')
|
|
83
|
+
const parsed = JSON.parse(content) as { name?: unknown; version?: unknown }
|
|
84
|
+
if (parsed.name != null && typeof parsed.name !== 'string') return undefined
|
|
85
|
+
return typeof parsed.version === 'string' ? parsed.version : undefined
|
|
86
|
+
} catch {
|
|
87
|
+
return undefined
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const formatInstallError = (message: string, stderr: string) => {
|
|
92
|
+
const detail = stderr.trim()
|
|
93
|
+
return detail ? `${message}\n${detail}` : message
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
interface ParsedSemver {
|
|
97
|
+
major: number
|
|
98
|
+
minor: number
|
|
99
|
+
patch: number
|
|
100
|
+
prerelease: string[]
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const parseSemver = (version: string): ParsedSemver | undefined => {
|
|
104
|
+
const match = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/u.exec(version.trim())
|
|
105
|
+
if (match == null) return undefined
|
|
106
|
+
return {
|
|
107
|
+
major: Number(match[1]),
|
|
108
|
+
minor: Number(match[2]),
|
|
109
|
+
patch: Number(match[3]),
|
|
110
|
+
prerelease: match[4]?.split('.') ?? []
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const comparePrereleaseIdentifiers = (left: string, right: string) => {
|
|
115
|
+
const leftNumber = /^\d+$/u.test(left) ? Number(left) : undefined
|
|
116
|
+
const rightNumber = /^\d+$/u.test(right) ? Number(right) : undefined
|
|
117
|
+
if (leftNumber != null && rightNumber != null) return leftNumber - rightNumber
|
|
118
|
+
if (leftNumber != null) return -1
|
|
119
|
+
if (rightNumber != null) return 1
|
|
120
|
+
return left.localeCompare(right)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const compareSemver = (left: ParsedSemver, right: ParsedSemver) => {
|
|
124
|
+
const coreDiff = left.major - right.major || left.minor - right.minor || left.patch - right.patch
|
|
125
|
+
if (coreDiff !== 0) return coreDiff
|
|
126
|
+
if (left.prerelease.length === 0 && right.prerelease.length === 0) return 0
|
|
127
|
+
if (left.prerelease.length === 0) return 1
|
|
128
|
+
if (right.prerelease.length === 0) return -1
|
|
129
|
+
const maxLength = Math.max(left.prerelease.length, right.prerelease.length)
|
|
130
|
+
for (let index = 0; index < maxLength; index += 1) {
|
|
131
|
+
const leftPart = left.prerelease[index]
|
|
132
|
+
const rightPart = right.prerelease[index]
|
|
133
|
+
if (leftPart == null) return -1
|
|
134
|
+
if (rightPart == null) return 1
|
|
135
|
+
const diff = comparePrereleaseIdentifiers(leftPart, rightPart)
|
|
136
|
+
if (diff !== 0) return diff
|
|
137
|
+
}
|
|
138
|
+
return 0
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const hasSameSemverCore = (left: ParsedSemver, right: ParsedSemver) => (
|
|
142
|
+
left.major === right.major && left.minor === right.minor && left.patch === right.patch
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
const compareAdapterCacheVersions = (left: string, right: string) => {
|
|
146
|
+
const leftSemver = parseSemver(left)
|
|
147
|
+
const rightSemver = parseSemver(right)
|
|
148
|
+
if (leftSemver != null && rightSemver != null) return compareSemver(leftSemver, rightSemver)
|
|
149
|
+
return compareVersionLike(left, right)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const satisfiesCaretVersionSpec = (version: string, versionSpec: string) => {
|
|
153
|
+
const trimmedSpec = versionSpec.trim()
|
|
154
|
+
const minimumVersion = trimmedSpec.startsWith('^') ? trimmedSpec.slice(1) : trimmedSpec
|
|
155
|
+
const parsedVersion = parseSemver(version)
|
|
156
|
+
const parsedMinimum = parseSemver(minimumVersion)
|
|
157
|
+
if (parsedVersion == null || parsedMinimum == null) return version === minimumVersion
|
|
158
|
+
if (compareSemver(parsedVersion, parsedMinimum) < 0) return false
|
|
159
|
+
if (!trimmedSpec.startsWith('^')) return compareSemver(parsedVersion, parsedMinimum) === 0
|
|
160
|
+
if (
|
|
161
|
+
parsedVersion.prerelease.length > 0 &&
|
|
162
|
+
(parsedMinimum.prerelease.length === 0 || !hasSameSemverCore(parsedVersion, parsedMinimum))
|
|
163
|
+
) {
|
|
164
|
+
return false
|
|
165
|
+
}
|
|
166
|
+
if (parsedMinimum.major > 0) return parsedVersion.major === parsedMinimum.major
|
|
167
|
+
if (parsedMinimum.minor > 0) {
|
|
168
|
+
return parsedVersion.major === 0 && parsedVersion.minor === parsedMinimum.minor
|
|
169
|
+
}
|
|
170
|
+
return parsedVersion.major === 0 &&
|
|
171
|
+
parsedVersion.minor === 0 &&
|
|
172
|
+
parsedVersion.patch === parsedMinimum.patch
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const readCachedAdapterPackageVersions = async (packageCacheRoot: string, versionSpec?: string) => {
|
|
176
|
+
const trimmedVersionSpec = versionSpec?.trim()
|
|
177
|
+
if (trimmedVersionSpec && !trimmedVersionSpec.startsWith('^')) {
|
|
178
|
+
return [trimmedVersionSpec]
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return (await readdir(packageCacheRoot, { withFileTypes: true }))
|
|
182
|
+
.filter(entry => entry.isDirectory())
|
|
183
|
+
.map(entry => entry.name)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const findCachedAdapterPackageDir = async (packageName: string, versionSpec?: string) => {
|
|
187
|
+
const packageCacheRoot = path.join(resolveAdapterPackagesRoot(), sanitizePackageName(packageName))
|
|
188
|
+
let entries: string[]
|
|
189
|
+
try {
|
|
190
|
+
entries = await readCachedAdapterPackageVersions(packageCacheRoot, versionSpec)
|
|
191
|
+
} catch {
|
|
192
|
+
return undefined
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const candidates: Array<{ cacheDir: string; version: string }> = []
|
|
196
|
+
for (const entry of entries) {
|
|
197
|
+
const cacheDir = path.join(packageCacheRoot, entry)
|
|
198
|
+
const packageDir = resolveAdapterPackageInstallDir(cacheDir, packageName)
|
|
199
|
+
const installedVersion = await readInstalledPackageVersion(packageDir)
|
|
200
|
+
if (installedVersion === entry && (versionSpec == null || satisfiesCaretVersionSpec(entry, versionSpec))) {
|
|
201
|
+
candidates.push({ cacheDir, version: entry })
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return candidates
|
|
206
|
+
.sort((left, right) => compareAdapterCacheVersions(right.version, left.version))[0]
|
|
207
|
+
?.cacheDir
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const readAdapterPackageVersionMetadata = async (packageName: string, versionSpec: string) => {
|
|
211
|
+
const { key, metadataPath } = resolveAdapterPackageMetadataPath(packageName, versionSpec)
|
|
212
|
+
try {
|
|
213
|
+
const content = await readFile(metadataPath, 'utf8')
|
|
214
|
+
const parsed = JSON.parse(content) as Partial<AdapterPackageVersionMetadata>
|
|
215
|
+
if (
|
|
216
|
+
parsed.key === key &&
|
|
217
|
+
parsed.name === packageName &&
|
|
218
|
+
parsed.version === versionSpec &&
|
|
219
|
+
typeof parsed.resolvedVersion === 'string' &&
|
|
220
|
+
typeof parsed.installedPackageDir === 'string'
|
|
221
|
+
) {
|
|
222
|
+
return parsed as AdapterPackageVersionMetadata
|
|
223
|
+
}
|
|
224
|
+
} catch {
|
|
225
|
+
// Ignore missing or stale metadata and fall back to the package cache.
|
|
226
|
+
}
|
|
227
|
+
return undefined
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const writeAdapterPackageVersionMetadata = async (input: {
|
|
231
|
+
installedPackageDir: string
|
|
232
|
+
packageName: string
|
|
233
|
+
resolvedVersion: string
|
|
234
|
+
versionSpec: string
|
|
235
|
+
}) => {
|
|
236
|
+
const { key, metadataPath } = resolveAdapterPackageMetadataPath(input.packageName, input.versionSpec)
|
|
237
|
+
await ensureDirectory(path.dirname(metadataPath))
|
|
238
|
+
const tempPath = `${metadataPath}.${process.pid}.${Date.now()}.tmp`
|
|
239
|
+
const metadata: AdapterPackageVersionMetadata = {
|
|
240
|
+
checkedAt: new Date().toISOString(),
|
|
241
|
+
installedPackageDir: input.installedPackageDir,
|
|
242
|
+
key,
|
|
243
|
+
name: input.packageName,
|
|
244
|
+
resolvedVersion: input.resolvedVersion,
|
|
245
|
+
version: input.versionSpec
|
|
246
|
+
}
|
|
247
|
+
await writeFile(tempPath, `${JSON.stringify(metadata, null, 2)}\n`, 'utf8')
|
|
248
|
+
await rename(tempPath, metadataPath)
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const parseVersionOutput = (spec: string, output: string) => {
|
|
252
|
+
const normalizedOutput = output.trim()
|
|
253
|
+
if (!normalizedOutput) {
|
|
254
|
+
throw new Error(`No version was returned for ${spec}.`)
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
try {
|
|
258
|
+
const parsed = JSON.parse(normalizedOutput) as unknown
|
|
259
|
+
if (typeof parsed === 'string' && parsed.trim()) {
|
|
260
|
+
return parsed.trim()
|
|
261
|
+
}
|
|
262
|
+
if (Array.isArray(parsed)) {
|
|
263
|
+
const versions = parsed.filter((item): item is string => typeof item === 'string' && item.trim() !== '')
|
|
264
|
+
const latestVersion = versions.sort(compareVersionLike).at(-1)
|
|
265
|
+
if (latestVersion != null) return latestVersion
|
|
266
|
+
}
|
|
267
|
+
} catch {
|
|
268
|
+
// Fall through to unquoted output parsing.
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const unquotedOutput = normalizedOutput.replace(/^"|"$/g, '').trim()
|
|
272
|
+
if (!unquotedOutput) {
|
|
273
|
+
throw new Error(`Invalid published version for ${spec}: ${normalizedOutput}`)
|
|
274
|
+
}
|
|
275
|
+
return unquotedOutput
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const resolvePublishedAdapterPackageVersion = async (packageName: string, versionSpec: string) => {
|
|
279
|
+
const spec = `${packageName}@${versionSpec}`
|
|
280
|
+
const result = await runBufferedCommand({
|
|
281
|
+
args: ['view', spec, 'version', '--json'],
|
|
282
|
+
command: NPM_BIN,
|
|
283
|
+
env: resolveAdapterPackageManagerEnv()
|
|
284
|
+
})
|
|
285
|
+
if (result.code !== 0) {
|
|
286
|
+
throw new Error(`Failed to resolve adapter package version for ${spec}:\n${result.stderr.trim()}`)
|
|
287
|
+
}
|
|
288
|
+
return parseVersionOutput(spec, result.stdout)
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const installAdapterPackage = async (packageName: string, version: string) => {
|
|
292
|
+
const cacheDir = resolveAdapterPackageCacheDir(packageName, version)
|
|
293
|
+
const packageDir = resolveAdapterPackageInstallDir(cacheDir, packageName)
|
|
294
|
+
const installedVersion = await readInstalledPackageVersion(packageDir)
|
|
295
|
+
if (installedVersion === version) {
|
|
296
|
+
return {
|
|
297
|
+
cacheDir,
|
|
298
|
+
packageDir
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const stagingDir = `${cacheDir}.tmp-${process.pid}-${Date.now()}`
|
|
303
|
+
await rm(stagingDir, { recursive: true, force: true })
|
|
304
|
+
await ensureDirectory(stagingDir)
|
|
305
|
+
|
|
306
|
+
const progress = createBootstrapProgress({
|
|
307
|
+
label: `installing adapter ${packageName}@${version} into bootstrap cache`
|
|
308
|
+
})
|
|
309
|
+
try {
|
|
310
|
+
const result = await runBufferedCommand({
|
|
311
|
+
args: [
|
|
312
|
+
'install',
|
|
313
|
+
'--prefix',
|
|
314
|
+
stagingDir,
|
|
315
|
+
'--no-audit',
|
|
316
|
+
'--no-fund',
|
|
317
|
+
'--loglevel=error',
|
|
318
|
+
`${packageName}@${version}`
|
|
319
|
+
],
|
|
320
|
+
command: NPM_BIN,
|
|
321
|
+
env: resolveAdapterPackageManagerEnv()
|
|
322
|
+
})
|
|
323
|
+
|
|
324
|
+
if (result.code !== 0) {
|
|
325
|
+
throw new Error(formatInstallError(
|
|
326
|
+
`Failed to install adapter package ${packageName}@${version}.`,
|
|
327
|
+
result.stderr
|
|
328
|
+
))
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
await ensureDirectory(path.dirname(cacheDir))
|
|
332
|
+
await rm(cacheDir, { recursive: true, force: true })
|
|
333
|
+
await rename(stagingDir, cacheDir)
|
|
334
|
+
progress.finish(`cached adapter ${packageName}@${version}`)
|
|
335
|
+
} catch (error) {
|
|
336
|
+
progress.fail(`failed to cache adapter ${packageName}@${version}`)
|
|
337
|
+
await rm(stagingDir, { recursive: true, force: true }).catch(() => {})
|
|
338
|
+
throw error
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return {
|
|
342
|
+
cacheDir,
|
|
343
|
+
packageDir
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const splitAdapterVersionSelector = (value: string) => {
|
|
348
|
+
const lastAt = value.lastIndexOf('@')
|
|
349
|
+
if (lastAt <= 0) {
|
|
350
|
+
return value
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
if (value.startsWith('@')) {
|
|
354
|
+
const slash = value.indexOf('/')
|
|
355
|
+
if (slash < 0 || lastAt <= slash) {
|
|
356
|
+
return value
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
return value.slice(0, lastAt)
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
export const normalizeAdapterPackageId = (type: string) => {
|
|
364
|
+
const trimmed = splitAdapterVersionSelector(type.trim())
|
|
365
|
+
if (trimmed.startsWith('@')) return trimmed
|
|
366
|
+
|
|
367
|
+
const hasAdapterPrefix = trimmed.startsWith(ADAPTER_PREFIX)
|
|
368
|
+
const adapterId = hasAdapterPrefix ? trimmed.slice(ADAPTER_PREFIX.length) : trimmed
|
|
369
|
+
const normalizedAdapterId = adapterId === 'claude' ? 'claude-code' : adapterId
|
|
370
|
+
|
|
371
|
+
return hasAdapterPrefix ? `${ADAPTER_PREFIX}${normalizedAdapterId}` : normalizedAdapterId
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
export const resolveAdapterPackageName = (type: string) => {
|
|
375
|
+
const normalizedType = normalizeAdapterPackageId(type)
|
|
376
|
+
if (normalizedType.startsWith('@')) return normalizedType
|
|
377
|
+
return normalizedType.startsWith(ADAPTER_PREFIX)
|
|
378
|
+
? `${ADAPTER_SCOPE}/${normalizedType}`
|
|
379
|
+
: `${ADAPTER_SCOPE}/${ADAPTER_PREFIX}${normalizedType}`
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
export const readCliAdapterPackageRequest = (
|
|
383
|
+
forwardedArgs: string[],
|
|
384
|
+
cliVersion: string,
|
|
385
|
+
fallbackAdapter?: string
|
|
386
|
+
): CliAdapterPackageRequest | undefined => {
|
|
387
|
+
for (let index = 0; index < forwardedArgs.length; index += 1) {
|
|
388
|
+
const arg = forwardedArgs[index]
|
|
389
|
+
if (arg === '--adapter' || arg === '-A') {
|
|
390
|
+
const adapter = forwardedArgs[index + 1]?.trim()
|
|
391
|
+
return adapter ? { adapter, cliVersion } : undefined
|
|
392
|
+
}
|
|
393
|
+
if (arg.startsWith('--adapter=')) {
|
|
394
|
+
const adapter = arg.slice('--adapter='.length).trim()
|
|
395
|
+
return adapter ? { adapter, cliVersion } : undefined
|
|
396
|
+
}
|
|
397
|
+
if (arg.startsWith('-A') && arg.length > 2) {
|
|
398
|
+
const adapter = arg.slice(2).trim()
|
|
399
|
+
return adapter ? { adapter, cliVersion } : undefined
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const adapter = fallbackAdapter?.trim()
|
|
404
|
+
return adapter ? { adapter, cliVersion } : undefined
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
export const resolveCliAdapterPackageDir = async (request: CliAdapterPackageRequest) => {
|
|
408
|
+
const packageName = resolveAdapterPackageName(request.adapter)
|
|
409
|
+
const versionSpec = resolveAdapterVersionSpec(request.cliVersion)
|
|
410
|
+
const metadata = await readAdapterPackageVersionMetadata(packageName, versionSpec)
|
|
411
|
+
if (metadata != null) {
|
|
412
|
+
const cachedByMetadata = await findCachedAdapterPackageDir(packageName, metadata.resolvedVersion)
|
|
413
|
+
if (cachedByMetadata != null && existsSync(metadata.installedPackageDir)) {
|
|
414
|
+
return cachedByMetadata
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const cachedPackageDir = await findCachedAdapterPackageDir(packageName, versionSpec)
|
|
419
|
+
if (cachedPackageDir != null) {
|
|
420
|
+
return cachedPackageDir
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const resolvedVersion = await resolvePublishedAdapterPackageVersion(packageName, versionSpec)
|
|
424
|
+
const installedPackage = await installAdapterPackage(packageName, resolvedVersion)
|
|
425
|
+
await writeAdapterPackageVersionMetadata({
|
|
426
|
+
installedPackageDir: installedPackage.packageDir,
|
|
427
|
+
packageName,
|
|
428
|
+
resolvedVersion,
|
|
429
|
+
versionSpec
|
|
430
|
+
})
|
|
431
|
+
return installedPackage.cacheDir
|
|
432
|
+
}
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process'
|
|
2
|
+
import process from 'node:process'
|
|
3
|
+
|
|
4
|
+
import { ensureDesktopInstall } from './desktop-install'
|
|
5
|
+
import type { DesktopInstallMode } from './desktop-mode'
|
|
6
|
+
import { readDesktopPreference, resolveInstallMode } from './desktop-mode'
|
|
7
|
+
import { selectDesktopAsset } from './desktop-release'
|
|
8
|
+
|
|
9
|
+
export type { DesktopInstallMode } from './desktop-mode'
|
|
10
|
+
|
|
11
|
+
export interface LaunchDesktopAppOptions {
|
|
12
|
+
forwardedArgs: string[]
|
|
13
|
+
installMode?: DesktopInstallMode
|
|
14
|
+
persistInstallMode?: boolean
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const launchDesktopApp = async (options: LaunchDesktopAppOptions) => {
|
|
18
|
+
const installMode = await resolveInstallMode({
|
|
19
|
+
explicitInstallMode: options.installMode,
|
|
20
|
+
persistInstallMode: options.persistInstallMode
|
|
21
|
+
})
|
|
22
|
+
const install = await ensureDesktopInstall(installMode)
|
|
23
|
+
const workspaceFolder = process.cwd()
|
|
24
|
+
console.error(`[bootstrap] launching desktop app from ${install.installedPath} (${installMode})`)
|
|
25
|
+
|
|
26
|
+
const child = spawn(install.executablePath, options.forwardedArgs, {
|
|
27
|
+
cwd: workspaceFolder,
|
|
28
|
+
detached: true,
|
|
29
|
+
env: {
|
|
30
|
+
...process.env,
|
|
31
|
+
ONEWORKS_DESKTOP_WORKSPACE: workspaceFolder,
|
|
32
|
+
__ONEWORKS_PROJECT_WORKSPACE_FOLDER__: workspaceFolder
|
|
33
|
+
},
|
|
34
|
+
stdio: 'ignore'
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
child.unref()
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const __TEST_ONLY__ = {
|
|
41
|
+
readDesktopPreference,
|
|
42
|
+
resolveInstallMode,
|
|
43
|
+
selectDesktopAsset
|
|
44
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process'
|
|
2
|
+
import { access, chmod, copyFile, mkdir, readFile, rm, writeFile } from 'node:fs/promises'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import process from 'node:process'
|
|
5
|
+
|
|
6
|
+
import type { DesktopInstallMode } from './desktop-mode'
|
|
7
|
+
import { downloadReleaseAsset, fetchDesktopRelease, selectDesktopAsset } from './desktop-release'
|
|
8
|
+
import { resolveBootstrapDataDir, resolveRealHomeDir } from './paths'
|
|
9
|
+
|
|
10
|
+
interface DesktopInstallMetadata {
|
|
11
|
+
executablePath: string
|
|
12
|
+
installedPath: string
|
|
13
|
+
releaseTag: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const APP_NAME = 'One Works'
|
|
17
|
+
|
|
18
|
+
const ensureDirectory = async (targetPath: string) => {
|
|
19
|
+
await mkdir(targetPath, { recursive: true })
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const hasExistingPath = async (targetPath: string) => {
|
|
23
|
+
try {
|
|
24
|
+
await access(targetPath)
|
|
25
|
+
return true
|
|
26
|
+
} catch {
|
|
27
|
+
return false
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const runSystemCommand = async (command: string, args: string[]) => {
|
|
32
|
+
const child = spawn(command, args, { stdio: 'inherit' })
|
|
33
|
+
return await new Promise<void>((resolve, reject) => {
|
|
34
|
+
child.once('error', reject)
|
|
35
|
+
child.once('exit', (code) => {
|
|
36
|
+
if (code !== 0) {
|
|
37
|
+
reject(new Error(`${command} exited with code ${code ?? 'null'}.`))
|
|
38
|
+
return
|
|
39
|
+
}
|
|
40
|
+
resolve()
|
|
41
|
+
})
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const resolveDesktopMetadataPath = (installMode: DesktopInstallMode) => (
|
|
46
|
+
path.join(resolveBootstrapDataDir(), 'desktop', `${installMode}.json`)
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
const readDesktopMetadata = async (installMode: DesktopInstallMode) => {
|
|
50
|
+
try {
|
|
51
|
+
const content = await readFile(resolveDesktopMetadataPath(installMode), 'utf8')
|
|
52
|
+
return JSON.parse(content) as DesktopInstallMetadata
|
|
53
|
+
} catch {
|
|
54
|
+
return undefined
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const writeDesktopMetadata = async (installMode: DesktopInstallMode, metadata: DesktopInstallMetadata) => {
|
|
59
|
+
const filePath = resolveDesktopMetadataPath(installMode)
|
|
60
|
+
await ensureDirectory(path.dirname(filePath))
|
|
61
|
+
await writeFile(filePath, `${JSON.stringify(metadata, null, 2)}\n`)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const resolveDownloadsDir = () => path.join(resolveBootstrapDataDir(), 'desktop', 'downloads')
|
|
65
|
+
const resolveCacheInstallRoot = (releaseTag: string) =>
|
|
66
|
+
path.join(resolveBootstrapDataDir(), 'desktop', 'apps', releaseTag)
|
|
67
|
+
|
|
68
|
+
const installDesktopForMac = async (
|
|
69
|
+
releaseTag: string,
|
|
70
|
+
asset: { digest?: string; name: string; url: string },
|
|
71
|
+
installMode: DesktopInstallMode
|
|
72
|
+
) => {
|
|
73
|
+
const installDir = installMode === 'cache'
|
|
74
|
+
? resolveCacheInstallRoot(releaseTag)
|
|
75
|
+
: path.join(resolveRealHomeDir(), 'Applications')
|
|
76
|
+
const installPath = path.join(installDir, `${APP_NAME}.app`)
|
|
77
|
+
const archivePath = path.join(resolveDownloadsDir(), asset.name)
|
|
78
|
+
const stagingDir = path.join(resolveBootstrapDataDir(), 'desktop', 'staging', `${installMode}-${releaseTag}`)
|
|
79
|
+
const extractedAppPath = path.join(stagingDir, `${APP_NAME}.app`)
|
|
80
|
+
|
|
81
|
+
await rm(stagingDir, { recursive: true, force: true })
|
|
82
|
+
await ensureDirectory(stagingDir)
|
|
83
|
+
await downloadReleaseAsset(asset, archivePath)
|
|
84
|
+
await runSystemCommand('ditto', ['-x', '-k', archivePath, stagingDir])
|
|
85
|
+
await ensureDirectory(installDir)
|
|
86
|
+
await rm(installPath, { recursive: true, force: true })
|
|
87
|
+
await runSystemCommand('ditto', [extractedAppPath, installPath])
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
executablePath: path.join(installPath, 'Contents', 'MacOS', APP_NAME),
|
|
91
|
+
installedPath: installPath,
|
|
92
|
+
releaseTag
|
|
93
|
+
} satisfies DesktopInstallMetadata
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const installDesktopForLinux = async (
|
|
97
|
+
releaseTag: string,
|
|
98
|
+
asset: { digest?: string; name: string; url: string },
|
|
99
|
+
installMode: DesktopInstallMode
|
|
100
|
+
) => {
|
|
101
|
+
const installDir = installMode === 'cache'
|
|
102
|
+
? resolveCacheInstallRoot(releaseTag)
|
|
103
|
+
: path.join(resolveRealHomeDir(), '.local', 'opt', 'oneworks')
|
|
104
|
+
const installPath = path.join(installDir, asset.name)
|
|
105
|
+
const archivePath = path.join(resolveDownloadsDir(), asset.name)
|
|
106
|
+
|
|
107
|
+
await ensureDirectory(installDir)
|
|
108
|
+
await downloadReleaseAsset(asset, archivePath)
|
|
109
|
+
await copyFile(archivePath, installPath)
|
|
110
|
+
await chmod(installPath, 0o755)
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
executablePath: installPath,
|
|
114
|
+
installedPath: installPath,
|
|
115
|
+
releaseTag
|
|
116
|
+
} satisfies DesktopInstallMetadata
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const installDesktopForWindows = async (
|
|
120
|
+
releaseTag: string,
|
|
121
|
+
asset: { digest?: string; name: string; url: string },
|
|
122
|
+
installMode: DesktopInstallMode
|
|
123
|
+
) => {
|
|
124
|
+
const localAppData = process.env.LOCALAPPDATA?.trim()
|
|
125
|
+
if (installMode === 'user' && !localAppData) {
|
|
126
|
+
throw new Error('LOCALAPPDATA is required to install the One Works desktop app on Windows.')
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const installDir = installMode === 'cache'
|
|
130
|
+
? resolveCacheInstallRoot(releaseTag)
|
|
131
|
+
: path.join(localAppData as string, 'Programs', APP_NAME)
|
|
132
|
+
|
|
133
|
+
const installPath = path.join(installDir, asset.name)
|
|
134
|
+
const archivePath = path.join(resolveDownloadsDir(), asset.name)
|
|
135
|
+
|
|
136
|
+
await ensureDirectory(installDir)
|
|
137
|
+
await downloadReleaseAsset(asset, archivePath)
|
|
138
|
+
await copyFile(archivePath, installPath)
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
executablePath: installPath,
|
|
142
|
+
installedPath: installPath,
|
|
143
|
+
releaseTag
|
|
144
|
+
} satisfies DesktopInstallMetadata
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export const ensureDesktopInstall = async (installMode: DesktopInstallMode) => {
|
|
148
|
+
const release = await fetchDesktopRelease()
|
|
149
|
+
const selectedAsset = selectDesktopAsset(release, {
|
|
150
|
+
platform: process.platform,
|
|
151
|
+
arch: process.arch
|
|
152
|
+
})
|
|
153
|
+
if (selectedAsset == null) {
|
|
154
|
+
throw new Error(
|
|
155
|
+
`No supported desktop asset was found for ${process.platform}-${process.arch} in ${release.tagName}.`
|
|
156
|
+
)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const currentMetadata = await readDesktopMetadata(installMode)
|
|
160
|
+
if (currentMetadata?.releaseTag === release.tagName && await hasExistingPath(currentMetadata.executablePath)) {
|
|
161
|
+
return currentMetadata
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const metadata = process.platform === 'darwin'
|
|
165
|
+
? await installDesktopForMac(release.tagName, selectedAsset, installMode)
|
|
166
|
+
: process.platform === 'linux'
|
|
167
|
+
? await installDesktopForLinux(release.tagName, selectedAsset, installMode)
|
|
168
|
+
: process.platform === 'win32'
|
|
169
|
+
? await installDesktopForWindows(release.tagName, selectedAsset, installMode)
|
|
170
|
+
: undefined
|
|
171
|
+
if (metadata == null) {
|
|
172
|
+
throw new Error(`Desktop bootstrap is not supported on ${process.platform}.`)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
await writeDesktopMetadata(installMode, metadata)
|
|
176
|
+
return metadata
|
|
177
|
+
}
|