oneworks 0.0.0 → 0.1.0-alpha.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 +21 -0
- 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__/runtime-package.spec.ts +211 -0
- package/cli.js +5 -0
- package/package-version-refresh-worker.cjs +324 -0
- package/package.json +26 -13
- 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 -3
- package/index.js +0 -3
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { Buffer } from 'node:buffer'
|
|
2
|
+
import { spawn } from 'node:child_process'
|
|
3
|
+
import { createRequire } from 'node:module'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import process from 'node:process'
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
readPublishedPackageVersionMetadata,
|
|
9
|
+
resolvePackageLookupTimeoutMs,
|
|
10
|
+
resolvePackageManagerEnv,
|
|
11
|
+
resolvePackageTag,
|
|
12
|
+
shouldUseCachedPackageVersionFirst,
|
|
13
|
+
writePublishedPackageVersionMetadata
|
|
14
|
+
} from './npm-package-cache'
|
|
15
|
+
import { findInstalledPublishedPackageVersion } from './npm-package-install'
|
|
16
|
+
import { runBufferedCommand } from './process-utils'
|
|
17
|
+
|
|
18
|
+
const NPM_BIN = process.platform === 'win32' ? 'npm.cmd' : 'npm'
|
|
19
|
+
|
|
20
|
+
export { resolvePackageManagerEnv } from './npm-package-cache'
|
|
21
|
+
export { installPublishedPackage, resolvePackageBinEntrypoint } from './npm-package-install'
|
|
22
|
+
|
|
23
|
+
const resolveRefreshWorkerEntrypoint = () => {
|
|
24
|
+
const requireFromHere = createRequire(import.meta.url)
|
|
25
|
+
const packageJsonPath = requireFromHere.resolve('oneworks/package.json')
|
|
26
|
+
return path.join(path.dirname(packageJsonPath), 'package-version-refresh-worker.cjs')
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const spawnPackageVersionRefresh = (packageName: string) => {
|
|
30
|
+
if (process.env.ONEWORKS_BOOTSTRAP_DISABLE_BACKGROUND_REFRESH === '1') {
|
|
31
|
+
return
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
const payload = Buffer.from(JSON.stringify({ packageName }), 'utf8').toString('base64url')
|
|
36
|
+
const child = spawn(
|
|
37
|
+
process.execPath,
|
|
38
|
+
[
|
|
39
|
+
resolveRefreshWorkerEntrypoint(),
|
|
40
|
+
payload
|
|
41
|
+
],
|
|
42
|
+
{
|
|
43
|
+
cwd: process.cwd(),
|
|
44
|
+
detached: true,
|
|
45
|
+
env: resolvePackageManagerEnv(),
|
|
46
|
+
stdio: 'ignore'
|
|
47
|
+
}
|
|
48
|
+
)
|
|
49
|
+
child.unref()
|
|
50
|
+
} catch {
|
|
51
|
+
// Keep bootstrap startup independent from background metadata refresh.
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const resolvePublishedPackageVersionFromRegistry = async (
|
|
56
|
+
packageName: string,
|
|
57
|
+
options: { timeoutMs?: number } = {}
|
|
58
|
+
) => {
|
|
59
|
+
const spec = `${packageName}@${resolvePackageTag()}`
|
|
60
|
+
const result = await runBufferedCommand({
|
|
61
|
+
command: NPM_BIN,
|
|
62
|
+
args: ['view', spec, 'version', '--json'],
|
|
63
|
+
env: resolvePackageManagerEnv(),
|
|
64
|
+
timeoutMs: options.timeoutMs
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
if (result.timedOut === true) {
|
|
68
|
+
return {
|
|
69
|
+
spec,
|
|
70
|
+
timedOut: true as const
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (result.code !== 0) {
|
|
75
|
+
throw new Error(`Failed to resolve published version for ${spec}:\n${result.stderr.trim()}`)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const normalizedOutput = result.stdout.trim()
|
|
79
|
+
if (!normalizedOutput) {
|
|
80
|
+
throw new Error(`No version was returned for ${spec}.`)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
const parsed = JSON.parse(normalizedOutput) as unknown
|
|
85
|
+
if (typeof parsed === 'string' && parsed.trim()) {
|
|
86
|
+
return {
|
|
87
|
+
spec,
|
|
88
|
+
version: parsed.trim()
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
} catch {
|
|
92
|
+
// fall through
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const unquotedOutput = normalizedOutput.replace(/^"|"$/g, '').trim()
|
|
96
|
+
if (!unquotedOutput) {
|
|
97
|
+
throw new Error(`Invalid published version for ${spec}: ${normalizedOutput}`)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
spec,
|
|
102
|
+
version: unquotedOutput
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export const resolvePublishedPackageVersion = async (
|
|
107
|
+
packageName: string,
|
|
108
|
+
options: { cacheFirst?: boolean } = {}
|
|
109
|
+
) => {
|
|
110
|
+
const cachedMetadata = await readPublishedPackageVersionMetadata(packageName)
|
|
111
|
+
if (cachedMetadata != null && (options.cacheFirst ?? shouldUseCachedPackageVersionFirst())) {
|
|
112
|
+
spawnPackageVersionRefresh(packageName)
|
|
113
|
+
return cachedMetadata.version
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const cachedInstalledVersion = await findInstalledPublishedPackageVersion(packageName)
|
|
117
|
+
if (cachedInstalledVersion != null && (options.cacheFirst ?? shouldUseCachedPackageVersionFirst())) {
|
|
118
|
+
spawnPackageVersionRefresh(packageName)
|
|
119
|
+
return cachedInstalledVersion
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const registryResult = await resolvePublishedPackageVersionFromRegistry(
|
|
123
|
+
packageName,
|
|
124
|
+
cachedMetadata == null
|
|
125
|
+
? {}
|
|
126
|
+
: {
|
|
127
|
+
timeoutMs: resolvePackageLookupTimeoutMs()
|
|
128
|
+
}
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
if ('version' in registryResult) {
|
|
132
|
+
await writePublishedPackageVersionMetadata(packageName, registryResult.version)
|
|
133
|
+
return registryResult.version
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (cachedMetadata == null) {
|
|
137
|
+
// This is not expected because uncached lookups do not use a timeout.
|
|
138
|
+
const retryResult = await resolvePublishedPackageVersionFromRegistry(packageName)
|
|
139
|
+
if ('version' in retryResult) {
|
|
140
|
+
await writePublishedPackageVersionMetadata(packageName, retryResult.version)
|
|
141
|
+
return retryResult.version
|
|
142
|
+
}
|
|
143
|
+
throw new Error(`Failed to resolve published version for ${retryResult.spec}.`)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
console.error(
|
|
147
|
+
`[bootstrap] npm view ${registryResult.spec} timed out after ${resolvePackageLookupTimeoutMs()}ms, using cached ${packageName}@${cachedMetadata.version}`
|
|
148
|
+
)
|
|
149
|
+
spawnPackageVersionRefresh(packageName)
|
|
150
|
+
return cachedMetadata.version
|
|
151
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
let packageJson: Record<string, unknown> | undefined
|
|
2
|
+
|
|
3
|
+
const getPackageJson = () => {
|
|
4
|
+
if (!packageJson) {
|
|
5
|
+
try {
|
|
6
|
+
// eslint-disable-next-line ts/no-require-imports
|
|
7
|
+
packageJson = require('oneworks/package.json') as Record<string, unknown>
|
|
8
|
+
} catch {
|
|
9
|
+
packageJson = {}
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return packageJson
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const getBootstrapPackageConfig = (key: string, defaultValue = '') => {
|
|
17
|
+
const value = getPackageJson()[key]
|
|
18
|
+
return typeof value === 'string' ? value : defaultValue
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const getBootstrapVersion = () => getBootstrapPackageConfig('version', '0.0.0')
|
|
22
|
+
|
|
23
|
+
export const getBootstrapDescription = () => getBootstrapPackageConfig('description', 'One Works bootstrap launcher')
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import process from 'node:process'
|
|
4
|
+
|
|
5
|
+
import { readCliAdapterPackageRequest, resolveCliAdapterPackageDir } from './adapter-package-cache'
|
|
6
|
+
import { installPublishedPackage, resolvePackageBinEntrypoint, resolvePublishedPackageVersion } from './npm-package'
|
|
7
|
+
import { resolvePackageCacheDir, resolvePackageInstallDir, sanitizePackageName } from './npm-package-cache'
|
|
8
|
+
import { resolveBootstrapDataDir } from './paths'
|
|
9
|
+
import { runNodeEntrypoint } from './process-utils'
|
|
10
|
+
|
|
11
|
+
export interface LaunchInstalledPackageOptions {
|
|
12
|
+
commandName?: string
|
|
13
|
+
forwardedArgs: string[]
|
|
14
|
+
packageName: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const normalizeRuntimePackageDir = (value: string | undefined) => {
|
|
18
|
+
const trimmed = value?.trim()
|
|
19
|
+
return trimmed == null || trimmed === '' ? undefined : path.resolve(trimmed)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const shouldResolveCliAdapterPackage = () => {
|
|
23
|
+
const cliPackageDir = normalizeRuntimePackageDir(process.env.__ONEWORKS_PROJECT_CLI_PACKAGE_DIR__)
|
|
24
|
+
const packageDir = normalizeRuntimePackageDir(process.env.__ONEWORKS_PROJECT_PACKAGE_DIR__)
|
|
25
|
+
return cliPackageDir == null || cliPackageDir === packageDir
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const readActivePackageVersion = (packageName: string) => {
|
|
29
|
+
try {
|
|
30
|
+
const metadataPath = path.join(
|
|
31
|
+
resolveBootstrapDataDir(),
|
|
32
|
+
'module-updates',
|
|
33
|
+
`${sanitizePackageName(packageName)}.json`
|
|
34
|
+
)
|
|
35
|
+
const parsed = JSON.parse(readFileSync(metadataPath, 'utf8')) as {
|
|
36
|
+
packageName?: unknown
|
|
37
|
+
version?: unknown
|
|
38
|
+
}
|
|
39
|
+
if (parsed.packageName !== packageName || typeof parsed.version !== 'string') {
|
|
40
|
+
return undefined
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const packageDir = resolvePackageInstallDir(resolvePackageCacheDir(packageName, parsed.version), packageName)
|
|
44
|
+
const packageJson = JSON.parse(readFileSync(path.join(packageDir, 'package.json'), 'utf8')) as {
|
|
45
|
+
name?: unknown
|
|
46
|
+
version?: unknown
|
|
47
|
+
}
|
|
48
|
+
return packageJson.name === packageName && packageJson.version === parsed.version
|
|
49
|
+
? parsed.version
|
|
50
|
+
: undefined
|
|
51
|
+
} catch {
|
|
52
|
+
return undefined
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export const launchInstalledPackage = async (input: LaunchInstalledPackageOptions) => {
|
|
57
|
+
const version = readActivePackageVersion(input.packageName) ?? await resolvePublishedPackageVersion(input.packageName)
|
|
58
|
+
console.error(`[bootstrap] using ${input.packageName}@${version}`)
|
|
59
|
+
const installedPackage = await installPublishedPackage(input.packageName, version)
|
|
60
|
+
const entryPath = await resolvePackageBinEntrypoint(installedPackage.packageDir, input.commandName)
|
|
61
|
+
const adapterPackageRequest = input.packageName === '@oneworks/cli' &&
|
|
62
|
+
shouldResolveCliAdapterPackage()
|
|
63
|
+
? readCliAdapterPackageRequest(
|
|
64
|
+
input.forwardedArgs,
|
|
65
|
+
version,
|
|
66
|
+
process.env.__ONEWORKS_RUNTIME_PROTOCOL_CONSUMER_ADAPTER__
|
|
67
|
+
)
|
|
68
|
+
: undefined
|
|
69
|
+
const adapterPackageDir = adapterPackageRequest == null
|
|
70
|
+
? undefined
|
|
71
|
+
: await resolveCliAdapterPackageDir(adapterPackageRequest)
|
|
72
|
+
|
|
73
|
+
return await runNodeEntrypoint(
|
|
74
|
+
entryPath,
|
|
75
|
+
input.forwardedArgs,
|
|
76
|
+
adapterPackageDir == null
|
|
77
|
+
? {}
|
|
78
|
+
: {
|
|
79
|
+
env: {
|
|
80
|
+
...process.env,
|
|
81
|
+
__ONEWORKS_PROJECT_CLI_PACKAGE_DIR__: adapterPackageDir
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
)
|
|
85
|
+
}
|
package/src/paths.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import os from 'node:os'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import process from 'node:process'
|
|
4
|
+
|
|
5
|
+
export const resolveRealHomeDir = () => {
|
|
6
|
+
const realHome = process.env.__ONEWORKS_PROJECT_REAL_HOME__?.trim()
|
|
7
|
+
if (realHome) {
|
|
8
|
+
return realHome
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
return os.homedir()
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const resolveBootstrapDataDir = () => path.join(resolveRealHomeDir(), '.oneworks', 'bootstrap')
|
|
15
|
+
|
|
16
|
+
export const resolveBootstrapPackageCacheDir = () => {
|
|
17
|
+
const packageCacheDir = process.env.__ONEWORKS_PROJECT_PACKAGE_CACHE_DIR__?.trim()
|
|
18
|
+
return packageCacheDir || resolveBootstrapDataDir()
|
|
19
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import type { Buffer } from 'node:buffer'
|
|
2
|
+
import { spawn } from 'node:child_process'
|
|
3
|
+
import process from 'node:process'
|
|
4
|
+
|
|
5
|
+
interface RunBufferedCommandOptions {
|
|
6
|
+
args: string[]
|
|
7
|
+
command: string
|
|
8
|
+
cwd?: string
|
|
9
|
+
env?: NodeJS.ProcessEnv
|
|
10
|
+
stdio?: 'ignore' | 'inherit' | 'pipe'
|
|
11
|
+
timeoutMs?: number
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const runBufferedCommand = async (input: RunBufferedCommandOptions) => {
|
|
15
|
+
const child = spawn(input.command, input.args, {
|
|
16
|
+
cwd: input.cwd,
|
|
17
|
+
env: input.env,
|
|
18
|
+
stdio: input.stdio ?? 'pipe'
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
let stdout = ''
|
|
22
|
+
let stderr = ''
|
|
23
|
+
let timedOut = false
|
|
24
|
+
let killTimeout: NodeJS.Timeout | undefined
|
|
25
|
+
const timeout = input.timeoutMs != null && input.timeoutMs > 0
|
|
26
|
+
? setTimeout(() => {
|
|
27
|
+
timedOut = true
|
|
28
|
+
child.kill('SIGTERM')
|
|
29
|
+
killTimeout = setTimeout(() => {
|
|
30
|
+
if (child.exitCode == null && child.signalCode == null) {
|
|
31
|
+
child.kill('SIGKILL')
|
|
32
|
+
}
|
|
33
|
+
}, 1_000)
|
|
34
|
+
killTimeout.unref()
|
|
35
|
+
}, input.timeoutMs)
|
|
36
|
+
: undefined
|
|
37
|
+
|
|
38
|
+
if (input.stdio !== 'inherit') {
|
|
39
|
+
child.stdout?.on('data', (chunk: Buffer | string) => {
|
|
40
|
+
stdout += String(chunk)
|
|
41
|
+
})
|
|
42
|
+
child.stderr?.on('data', (chunk: Buffer | string) => {
|
|
43
|
+
stderr += String(chunk)
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return await new Promise<{
|
|
48
|
+
code: number
|
|
49
|
+
stderr: string
|
|
50
|
+
timedOut?: boolean
|
|
51
|
+
stdout: string
|
|
52
|
+
}>((resolve, reject) => {
|
|
53
|
+
child.once('error', (error) => {
|
|
54
|
+
if (timeout != null) {
|
|
55
|
+
clearTimeout(timeout)
|
|
56
|
+
}
|
|
57
|
+
if (killTimeout != null) {
|
|
58
|
+
clearTimeout(killTimeout)
|
|
59
|
+
}
|
|
60
|
+
reject(error)
|
|
61
|
+
})
|
|
62
|
+
child.once('exit', (code) => {
|
|
63
|
+
if (timeout != null) {
|
|
64
|
+
clearTimeout(timeout)
|
|
65
|
+
}
|
|
66
|
+
if (killTimeout != null) {
|
|
67
|
+
clearTimeout(killTimeout)
|
|
68
|
+
}
|
|
69
|
+
resolve({
|
|
70
|
+
code: code ?? 0,
|
|
71
|
+
stderr,
|
|
72
|
+
timedOut,
|
|
73
|
+
stdout
|
|
74
|
+
})
|
|
75
|
+
})
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export const runNodeEntrypoint = async (
|
|
80
|
+
entryPath: string,
|
|
81
|
+
forwardedArgs: string[],
|
|
82
|
+
options: { env?: NodeJS.ProcessEnv } = {}
|
|
83
|
+
) => {
|
|
84
|
+
const child = spawn(process.execPath, [entryPath, ...forwardedArgs], {
|
|
85
|
+
cwd: process.cwd(),
|
|
86
|
+
env: options.env ?? process.env,
|
|
87
|
+
stdio: 'inherit'
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
const forwardSignal = (signal: NodeJS.Signals) => {
|
|
91
|
+
if (!child.killed) {
|
|
92
|
+
child.kill(signal)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const handleSigint = () => {
|
|
97
|
+
forwardSignal('SIGINT')
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const handleSigterm = () => {
|
|
101
|
+
forwardSignal('SIGTERM')
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const cleanup = () => {
|
|
105
|
+
process.off('SIGINT', handleSigint)
|
|
106
|
+
process.off('SIGTERM', handleSigterm)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
process.on('SIGINT', handleSigint)
|
|
110
|
+
process.on('SIGTERM', handleSigterm)
|
|
111
|
+
|
|
112
|
+
return await new Promise<number>((resolve, reject) => {
|
|
113
|
+
child.once('error', (error) => {
|
|
114
|
+
cleanup()
|
|
115
|
+
reject(error)
|
|
116
|
+
})
|
|
117
|
+
child.once('exit', (code, signal) => {
|
|
118
|
+
cleanup()
|
|
119
|
+
if (signal != null) {
|
|
120
|
+
process.kill(process.pid, signal)
|
|
121
|
+
return
|
|
122
|
+
}
|
|
123
|
+
resolve(code ?? 0)
|
|
124
|
+
})
|
|
125
|
+
})
|
|
126
|
+
}
|
package/src/program.ts
ADDED
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
/* eslint-disable max-lines -- bootstrap CLI command routing is intentionally centralized. */
|
|
2
|
+
import { Command } from 'commander'
|
|
3
|
+
import process from 'node:process'
|
|
4
|
+
|
|
5
|
+
import { launchDesktopApp } from './desktop-app'
|
|
6
|
+
import type { DesktopInstallMode, LaunchDesktopAppOptions } from './desktop-app'
|
|
7
|
+
import { getBootstrapDescription, getBootstrapVersion } from './package-config'
|
|
8
|
+
import { launchInstalledPackage } from './package-launcher'
|
|
9
|
+
import { checkRuntimePackage, formatRuntimePackageStatus, installRuntimePackage } from './runtime-package'
|
|
10
|
+
import type { RuntimePackageAction, RuntimePackageOptions, RuntimePackageStatus } from './runtime-package'
|
|
11
|
+
|
|
12
|
+
interface BootstrapCliDeps {
|
|
13
|
+
checkRuntimePackage: (target?: string, options?: RuntimePackageOptions) => Promise<RuntimePackageStatus>
|
|
14
|
+
installRuntimePackage: (target?: string, options?: RuntimePackageOptions) => Promise<RuntimePackageStatus>
|
|
15
|
+
launchDesktopApp: (options: LaunchDesktopAppOptions) => Promise<void>
|
|
16
|
+
launchInstalledPackage: (input: {
|
|
17
|
+
commandName?: string
|
|
18
|
+
forwardedArgs: string[]
|
|
19
|
+
packageName: string
|
|
20
|
+
}) => Promise<number>
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
type BootstrapTarget =
|
|
24
|
+
| {
|
|
25
|
+
forwardedArgs: string[]
|
|
26
|
+
installMode?: DesktopInstallMode
|
|
27
|
+
kind: 'desktop'
|
|
28
|
+
persistInstallMode?: boolean
|
|
29
|
+
}
|
|
30
|
+
| {
|
|
31
|
+
commandName?: string
|
|
32
|
+
forwardedArgs: string[]
|
|
33
|
+
kind: 'package'
|
|
34
|
+
packageName: string
|
|
35
|
+
}
|
|
36
|
+
| {
|
|
37
|
+
action: RuntimePackageAction
|
|
38
|
+
json: boolean
|
|
39
|
+
kind: 'runtime-package'
|
|
40
|
+
target?: string
|
|
41
|
+
version?: string
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const DEFAULT_DEPS: BootstrapCliDeps = {
|
|
45
|
+
checkRuntimePackage,
|
|
46
|
+
installRuntimePackage,
|
|
47
|
+
launchDesktopApp,
|
|
48
|
+
launchInstalledPackage
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const parseDesktopTarget = (args: string[]): BootstrapTarget => {
|
|
52
|
+
const forwardedArgs = [...args]
|
|
53
|
+
let installMode: DesktopInstallMode | undefined
|
|
54
|
+
let persistInstallMode = false
|
|
55
|
+
|
|
56
|
+
if (forwardedArgs[0] === 'cache') {
|
|
57
|
+
forwardedArgs.shift()
|
|
58
|
+
installMode = 'cache'
|
|
59
|
+
persistInstallMode = true
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const noCacheIndex = forwardedArgs.indexOf('--no-cache')
|
|
63
|
+
if (noCacheIndex >= 0) {
|
|
64
|
+
forwardedArgs.splice(noCacheIndex, 1)
|
|
65
|
+
installMode = 'user'
|
|
66
|
+
persistInstallMode = true
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
forwardedArgs,
|
|
71
|
+
installMode,
|
|
72
|
+
kind: 'desktop',
|
|
73
|
+
persistInstallMode
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const parseRuntimePackageTarget = (args: string[]): BootstrapTarget => {
|
|
78
|
+
const forwardedArgs = [...args]
|
|
79
|
+
const jsonIndex = forwardedArgs.indexOf('--json')
|
|
80
|
+
const json = jsonIndex >= 0
|
|
81
|
+
if (json) {
|
|
82
|
+
forwardedArgs.splice(jsonIndex, 1)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const action = forwardedArgs.shift()
|
|
86
|
+
if (action !== 'check' && action !== 'install') {
|
|
87
|
+
throw new Error('Runtime package command must be `check` or `install`.')
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
let version: string | undefined
|
|
91
|
+
for (let index = 0; index < forwardedArgs.length; index += 1) {
|
|
92
|
+
const arg = forwardedArgs[index]
|
|
93
|
+
if (arg === '--version') {
|
|
94
|
+
const value = forwardedArgs[index + 1]
|
|
95
|
+
if (value == null || value.trim() === '') {
|
|
96
|
+
throw new Error('Runtime package --version requires a value.')
|
|
97
|
+
}
|
|
98
|
+
version = value
|
|
99
|
+
forwardedArgs.splice(index, 2)
|
|
100
|
+
index -= 1
|
|
101
|
+
continue
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (arg?.startsWith('--version=')) {
|
|
105
|
+
const value = arg.slice('--version='.length)
|
|
106
|
+
if (value.trim() === '') {
|
|
107
|
+
throw new Error('Runtime package --version requires a value.')
|
|
108
|
+
}
|
|
109
|
+
version = value
|
|
110
|
+
forwardedArgs.splice(index, 1)
|
|
111
|
+
index -= 1
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const targetSelector = forwardedArgs.shift()
|
|
116
|
+
if (forwardedArgs.length > 0) {
|
|
117
|
+
throw new Error(`Unsupported runtime package arguments: ${forwardedArgs.join(' ')}`)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const selectorAtIndex = targetSelector?.lastIndexOf('@') ?? -1
|
|
121
|
+
const selectorTarget = selectorAtIndex > 0 ? targetSelector?.slice(0, selectorAtIndex) : targetSelector
|
|
122
|
+
const selectorVersion = selectorAtIndex > 0 ? targetSelector?.slice(selectorAtIndex + 1) : undefined
|
|
123
|
+
if (selectorVersion != null && selectorVersion.trim() === '') {
|
|
124
|
+
throw new Error(`Runtime package target selector requires a version: ${targetSelector}.`)
|
|
125
|
+
}
|
|
126
|
+
if (version != null && selectorVersion != null && version !== selectorVersion) {
|
|
127
|
+
throw new Error(`Conflicting runtime package versions: ${selectorVersion} and ${version}.`)
|
|
128
|
+
}
|
|
129
|
+
const resolvedVersion = version ?? selectorVersion
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
action,
|
|
133
|
+
json,
|
|
134
|
+
kind: 'runtime-package',
|
|
135
|
+
target: selectorTarget,
|
|
136
|
+
...(resolvedVersion != null ? { version: resolvedVersion } : {})
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export const routeBootstrapCommand = (command: string | undefined, args: string[]): BootstrapTarget | undefined => {
|
|
141
|
+
if (command == null || command.trim() === '') {
|
|
142
|
+
return undefined
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (command === 'web') {
|
|
146
|
+
return {
|
|
147
|
+
commandName: 'oneworks-web',
|
|
148
|
+
forwardedArgs: args,
|
|
149
|
+
kind: 'package',
|
|
150
|
+
packageName: '@oneworks/web'
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (command === 'server') {
|
|
155
|
+
return {
|
|
156
|
+
commandName: 'oneworks-server',
|
|
157
|
+
forwardedArgs: args,
|
|
158
|
+
kind: 'package',
|
|
159
|
+
packageName: '@oneworks/server'
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (command === 'runtime') {
|
|
164
|
+
return parseRuntimePackageTarget(args)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (command === 'app') {
|
|
168
|
+
return parseDesktopTarget(args)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (command === 'cli') {
|
|
172
|
+
return {
|
|
173
|
+
commandName: 'oneworks',
|
|
174
|
+
forwardedArgs: args,
|
|
175
|
+
kind: 'package',
|
|
176
|
+
packageName: '@oneworks/cli'
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return {
|
|
181
|
+
commandName: 'oneworks',
|
|
182
|
+
forwardedArgs: [command, ...args],
|
|
183
|
+
kind: 'package',
|
|
184
|
+
packageName: '@oneworks/cli'
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export const createBootstrapCli = (inputDeps: Partial<BootstrapCliDeps> = {}) => {
|
|
189
|
+
const deps: BootstrapCliDeps = {
|
|
190
|
+
...DEFAULT_DEPS,
|
|
191
|
+
...inputDeps
|
|
192
|
+
}
|
|
193
|
+
const program = new Command()
|
|
194
|
+
|
|
195
|
+
program
|
|
196
|
+
.name('oneworks')
|
|
197
|
+
.description(getBootstrapDescription())
|
|
198
|
+
.helpOption(false)
|
|
199
|
+
.allowUnknownOption(true)
|
|
200
|
+
.allowExcessArguments(true)
|
|
201
|
+
.argument('[command]', 'Reserved commands: web, server, app, cli')
|
|
202
|
+
.argument('[args...]', 'Forwarded arguments for the delegated runtime')
|
|
203
|
+
.showHelpAfterError()
|
|
204
|
+
.addHelpText(
|
|
205
|
+
'after',
|
|
206
|
+
`
|
|
207
|
+
Top-level flags:
|
|
208
|
+
--help, -h Show bootstrap help
|
|
209
|
+
--version, -V Print bootstrap version
|
|
210
|
+
|
|
211
|
+
Examples:
|
|
212
|
+
npx oneworks summarize the repo
|
|
213
|
+
npx oneworks web --port 8787
|
|
214
|
+
npx oneworks server --host 0.0.0.0 --allow-cors
|
|
215
|
+
npx oneworks runtime check cli
|
|
216
|
+
npx oneworks runtime check cli@0.1.0-alpha.0
|
|
217
|
+
npx oneworks runtime install server
|
|
218
|
+
npx oneworks runtime install server --version 0.1.0-alpha.0
|
|
219
|
+
npx oneworks app
|
|
220
|
+
npx oneworks app cache
|
|
221
|
+
npx oneworks app --no-cache
|
|
222
|
+
cd ~/work/project-a && npx oneworks app
|
|
223
|
+
cd ~/work/project-b && npx oneworks app
|
|
224
|
+
`
|
|
225
|
+
)
|
|
226
|
+
.action(async (command: string | undefined, args: string[] = []) => {
|
|
227
|
+
const target = routeBootstrapCommand(command, args)
|
|
228
|
+
if (target == null) {
|
|
229
|
+
program.outputHelp()
|
|
230
|
+
return
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (target.kind === 'desktop') {
|
|
234
|
+
await deps.launchDesktopApp({
|
|
235
|
+
forwardedArgs: target.forwardedArgs,
|
|
236
|
+
installMode: target.installMode,
|
|
237
|
+
persistInstallMode: target.persistInstallMode
|
|
238
|
+
})
|
|
239
|
+
return
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (target.kind === 'runtime-package') {
|
|
243
|
+
let status: RuntimePackageStatus
|
|
244
|
+
if (target.action === 'install') {
|
|
245
|
+
status = target.version == null
|
|
246
|
+
? await deps.installRuntimePackage(target.target)
|
|
247
|
+
: await deps.installRuntimePackage(target.target, { version: target.version })
|
|
248
|
+
} else {
|
|
249
|
+
status = target.version == null
|
|
250
|
+
? await deps.checkRuntimePackage(target.target)
|
|
251
|
+
: await deps.checkRuntimePackage(target.target, { version: target.version })
|
|
252
|
+
}
|
|
253
|
+
const output = target.json ? JSON.stringify(status) : formatRuntimePackageStatus(status)
|
|
254
|
+
process.stdout.write(`${output}\n`)
|
|
255
|
+
return
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const exitCode = await deps.launchInstalledPackage({
|
|
259
|
+
packageName: target.packageName,
|
|
260
|
+
commandName: target.commandName,
|
|
261
|
+
forwardedArgs: target.forwardedArgs
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
if (exitCode !== 0) {
|
|
265
|
+
process.exit(exitCode)
|
|
266
|
+
}
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
return program
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export const runBootstrapCli = async (argv = process.argv) => {
|
|
273
|
+
const command = createBootstrapCli()
|
|
274
|
+
const userArgs = argv.slice(2)
|
|
275
|
+
if (userArgs.length === 0 || (userArgs.length === 1 && ['-h', '--help'].includes(userArgs[0] ?? ''))) {
|
|
276
|
+
command.outputHelp()
|
|
277
|
+
return
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (userArgs.length === 1 && ['-V', '--version'].includes(userArgs[0] ?? '')) {
|
|
281
|
+
process.stdout.write(`${getBootstrapVersion()}\n`)
|
|
282
|
+
return
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
await command.parseAsync(argv)
|
|
286
|
+
}
|