peachsim 0.1.527
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 +22 -0
- package/README.md +640 -0
- package/cli/app-config.ts +82 -0
- package/cli/app-fonts.ts +388 -0
- package/cli/app-project.ts +59 -0
- package/cli/app-splash.ts +171 -0
- package/cli/app-state-reset.ts +26 -0
- package/cli/auth.ts +184 -0
- package/cli/bin.ts +59 -0
- package/cli/bridge-diagnostics.ts +226 -0
- package/cli/bridge-flow-runner.ts +2806 -0
- package/cli/browser-backend.ts +44 -0
- package/cli/browser-evals.ts +96 -0
- package/cli/cli-update.ts +261 -0
- package/cli/cloud-client.ts +1530 -0
- package/cli/cloud-dispatch.ts +206 -0
- package/cli/cloud-session.ts +227 -0
- package/cli/command-contract.ts +221 -0
- package/cli/command-registry.ts +304 -0
- package/cli/commands/agent-wrapper.ts +1048 -0
- package/cli/commands/agent.ts +439 -0
- package/cli/commands/app-fonts.ts +99 -0
- package/cli/commands/assert.ts +542 -0
- package/cli/commands/auth.ts +71 -0
- package/cli/commands/box/bundle-plane.ts +71 -0
- package/cli/commands/box/checkout-plane.ts +256 -0
- package/cli/commands/box/plane-share.ts +208 -0
- package/cli/commands/box/rnx-command.ts +60 -0
- package/cli/commands/box/search-command.ts +157 -0
- package/cli/commands/box.ts +925 -0
- package/cli/commands/camera.ts +266 -0
- package/cli/commands/cleanup.ts +169 -0
- package/cli/commands/compat.ts +176 -0
- package/cli/commands/config.ts +34 -0
- package/cli/commands/control.ts +2154 -0
- package/cli/commands/cpu-profile.ts +941 -0
- package/cli/commands/daemon-mac-app.ts +165 -0
- package/cli/commands/daemon.ts +800 -0
- package/cli/commands/debug.ts +751 -0
- package/cli/commands/desktop.ts +39 -0
- package/cli/commands/detect.ts +201 -0
- package/cli/commands/detox.ts +481 -0
- package/cli/commands/device.ts +134 -0
- package/cli/commands/diagnose.ts +589 -0
- package/cli/commands/do-chain.ts +408 -0
- package/cli/commands/electron.ts +94 -0
- package/cli/commands/film.ts +379 -0
- package/cli/commands/flow.ts +1240 -0
- package/cli/commands/inspect/actions.ts +516 -0
- package/cli/commands/inspect/core.ts +2086 -0
- package/cli/commands/inspect/count.ts +17 -0
- package/cli/commands/inspect/describe.ts +119 -0
- package/cli/commands/inspect/env.ts +25 -0
- package/cli/commands/inspect/find.ts +169 -0
- package/cli/commands/inspect/get-layout.ts +53 -0
- package/cli/commands/inspect/keyboard.ts +54 -0
- package/cli/commands/inspect/list.ts +175 -0
- package/cli/commands/inspect/memory.ts +215 -0
- package/cli/commands/inspect/redaction.ts +39 -0
- package/cli/commands/inspect/resolve-target.ts +77 -0
- package/cli/commands/inspect/screens.ts +78 -0
- package/cli/commands/inspect/settle.ts +32 -0
- package/cli/commands/inspect/settling.ts +197 -0
- package/cli/commands/inspect/shared.ts +259 -0
- package/cli/commands/inspect/sleep.ts +15 -0
- package/cli/commands/inspect/tree.ts +23 -0
- package/cli/commands/inspect/url.ts +17 -0
- package/cli/commands/inspect/wait-event.ts +211 -0
- package/cli/commands/inspect/wait-idle.ts +25 -0
- package/cli/commands/inspect/wait-ready.ts +87 -0
- package/cli/commands/inspect/wait-selector.ts +59 -0
- package/cli/commands/inspect/wait.ts +32 -0
- package/cli/commands/inspect.ts +4738 -0
- package/cli/commands/install-cli.ts +275 -0
- package/cli/commands/install-desktop.ts +385 -0
- package/cli/commands/login.ts +357 -0
- package/cli/commands/logout.ts +24 -0
- package/cli/commands/maestro-generate.ts +375 -0
- package/cli/commands/maestro.ts +536 -0
- package/cli/commands/mode.ts +58 -0
- package/cli/commands/no-bridge-hint.ts +80 -0
- package/cli/commands/perf.ts +67 -0
- package/cli/commands/permissions.ts +203 -0
- package/cli/commands/platform.ts +410 -0
- package/cli/commands/preview.ts +103 -0
- package/cli/commands/profile.ts +106 -0
- package/cli/commands/react.ts +353 -0
- package/cli/commands/record.ts +1435 -0
- package/cli/commands/report-issue.ts +305 -0
- package/cli/commands/reset.ts +89 -0
- package/cli/commands/runtime.ts +348 -0
- package/cli/commands/screenshot-command.ts +106 -0
- package/cli/commands/screenshot-layers.ts +144 -0
- package/cli/commands/screenshot-mode.ts +38 -0
- package/cli/commands/screenshot.ts +492 -0
- package/cli/commands/screenshots-capture.ts +608 -0
- package/cli/commands/screenshots.ts +127 -0
- package/cli/commands/serve.ts +139 -0
- package/cli/commands/setup.ts +416 -0
- package/cli/commands/shell-boolean-mode.ts +82 -0
- package/cli/commands/skills.ts +468 -0
- package/cli/commands/slides.ts +357 -0
- package/cli/commands/state.ts +94 -0
- package/cli/commands/storage.ts +58 -0
- package/cli/commands/telemetry.ts +55 -0
- package/cli/commands/test.ts +535 -0
- package/cli/commands/three-mode.ts +771 -0
- package/cli/commands/timeline.ts +121 -0
- package/cli/commands/upgrade.ts +167 -0
- package/cli/commands/upload.ts +1203 -0
- package/cli/commands/version.ts +63 -0
- package/cli/commands/welcome.ts +17 -0
- package/cli/commands/what-happened.ts +328 -0
- package/cli/current-sim.ts +217 -0
- package/cli/desktop-companion.ts +315 -0
- package/cli/drivers/electron.ts +70 -0
- package/cli/drivers/index.ts +20 -0
- package/cli/drivers/playwright-provisioning.ts +184 -0
- package/cli/drivers/playwright-sim-host.ts +537 -0
- package/cli/drivers/playwright.ts +209 -0
- package/cli/drivers/registry.ts +65 -0
- package/cli/drivers/types.ts +102 -0
- package/cli/flow-export.ts +55 -0
- package/cli/flow-file.ts +142 -0
- package/cli/flow-live-status.ts +126 -0
- package/cli/flow-session.ts +140 -0
- package/cli/help-core.ts +143 -0
- package/cli/help.ts +44 -0
- package/cli/hidden-runtime-alias.ts +19 -0
- package/cli/hints.ts +214 -0
- package/cli/inspect-notice-state.ts +114 -0
- package/cli/internal-child.ts +176 -0
- package/cli/maestro-js.ts +323 -0
- package/cli/main.ts +704 -0
- package/cli/open-url.ts +8 -0
- package/cli/outbound-endpoints.ts +830 -0
- package/cli/parent-pid.ts +204 -0
- package/cli/parse-args.ts +189 -0
- package/cli/privacy.ts +53 -0
- package/cli/prompt.ts +56 -0
- package/cli/recording-access.ts +108 -0
- package/cli/registry.ts +2 -0
- package/cli/resolve-assets.ts +63 -0
- package/cli/rnx-in-process.ts +98 -0
- package/cli/run-registry.ts +226 -0
- package/cli/run-rnx-core.ts +148 -0
- package/cli/run-rnx.ts +137 -0
- package/cli/runtime-notes.ts +66 -0
- package/cli/runtime-summary.ts +25 -0
- package/cli/secure-file.ts +31 -0
- package/cli/self-invocation.ts +58 -0
- package/cli/send-to-box.ts +109 -0
- package/cli/setup-repository.ts +191 -0
- package/cli/shell-init.ts +98 -0
- package/cli/standalone.ts +4 -0
- package/cli/telemetry.ts +84 -0
- package/cli/ui.ts +68 -0
- package/cli/ws-bridge.ts +1020 -0
- package/detox/attempt-events.cjs +23 -0
- package/detox/colors.ts +54 -0
- package/detox/config-loader.ts +135 -0
- package/detox/element-types.ts +48 -0
- package/detox/expectations.ts +534 -0
- package/detox/gestures.ts +501 -0
- package/detox/index.ts +2694 -0
- package/detox/jest-preset.cjs +29 -0
- package/detox/jest-setup-after-env.cjs +22 -0
- package/detox/matchers.ts +30 -0
- package/detox/motion-change.cjs +109 -0
- package/detox/motion-change.d.cts +35 -0
- package/detox/navigation-lifecycle.ts +290 -0
- package/detox/navigation.ts +43 -0
- package/detox/proof-frame.cjs +114 -0
- package/detox/record-block-execution.cjs +78 -0
- package/detox/screenshots/animated-color-test-rest-norngh.png +0 -0
- package/detox/screenshots/color-test-after-drag-norngh.png +0 -0
- package/detox/screenshots/color-test-rest-norngh.png +0 -0
- package/detox/screenshots/theme-blue-toggle.png +0 -0
- package/detox/screenshots/theme-blue.png +0 -0
- package/detox/screenshots/theme-red-toggle.png +0 -0
- package/detox/screenshots/theme-red.png +0 -0
- package/dist-lib/agent-daemon-client.cjs +502 -0
- package/dist-lib/agent-events.cjs +48 -0
- package/dist-lib/agent-identity.cjs +53 -0
- package/dist-lib/agent-sessions.cjs +759 -0
- package/dist-lib/attached-projects.cjs +526 -0
- package/dist-lib/auth/shared-session.cjs +208 -0
- package/dist-lib/backend-origin.cjs +88 -0
- package/dist-lib/beta.cjs +41 -0
- package/dist-lib/beta.mjs +13 -0
- package/dist-lib/bridge-constants.cjs +161 -0
- package/dist-lib/bridge-contract-input.cjs +451 -0
- package/dist-lib/bridge-contract-input.mjs +414 -0
- package/dist-lib/bridge-contract.cjs +130 -0
- package/dist-lib/bridge-contract.mjs +94 -0
- package/dist-lib/capture-contract.cjs +220 -0
- package/dist-lib/capture-contract.mjs +190 -0
- package/dist-lib/cli-constants.cjs +32 -0
- package/dist-lib/cloud-contract.cjs +194 -0
- package/dist-lib/cloud-contract.mjs +145 -0
- package/dist-lib/cloud.cjs +670 -0
- package/dist-lib/cloud.mjs +639 -0
- package/dist-lib/config.cjs +96 -0
- package/dist-lib/detox/index.cjs +3789 -0
- package/dist-lib/detox/jest-preset.cjs +29 -0
- package/dist-lib/dev-bundle-resolution.cjs +290 -0
- package/dist-lib/home-paths.cjs +638 -0
- package/dist-lib/host/bridge-host.cjs +10903 -0
- package/dist-lib/host/fetch-proxy-handler.cjs +421 -0
- package/dist-lib/host/fetch-proxy-overrides.cjs +63 -0
- package/dist-lib/host/fetch-proxy-overrides.mjs +37 -0
- package/dist-lib/host/replacement-module-handler.cjs +469 -0
- package/dist-lib/host/websocket-proxy.cjs +298 -0
- package/dist-lib/index.cjs +4268 -0
- package/dist-lib/jump-to-source-babel.cjs +93 -0
- package/dist-lib/jump-to-source-native.cjs +112 -0
- package/dist-lib/menu.cjs +646 -0
- package/dist-lib/menu.mjs +604 -0
- package/dist-lib/metro-fingerprint-registry.cjs +186 -0
- package/dist-lib/metro-fingerprint-registry.mjs +156 -0
- package/dist-lib/metro-production-bundle.cjs +329 -0
- package/dist-lib/metro-production-bundle.mjs +295 -0
- package/dist-lib/metro.cjs +904 -0
- package/dist-lib/profiles.cjs +246 -0
- package/dist-lib/public-brand.cjs +39 -0
- package/dist-lib/react-native-host-modules.cjs +51 -0
- package/dist-lib/react-native-host-modules.mjs +25 -0
- package/dist-lib/render-mode.cjs +54 -0
- package/dist-lib/scripts/dev-server-scanner.cjs +1232 -0
- package/dist-lib/sdk.cjs +2433 -0
- package/dist-lib/sdk.mjs +2336 -0
- package/dist-lib/skills.cjs +39132 -0
- package/dist-lib/swift-submit.cjs +92 -0
- package/dist-lib/swift-submit.mjs +65 -0
- package/dist-lib/swift.cjs +1646 -0
- package/dist-lib/vite.cjs +1591 -0
- package/package.json +290 -0
- package/scripts/dev-server-scanner.ts +1291 -0
- package/scripts/optional-demo-registry.ts +113 -0
- package/skills/contrast/SKILL.md +165 -0
- package/skills/rnx-debug/SKILL.md +433 -0
- package/skills/rnx-perf/SKILL.md +399 -0
- package/skills/rnx-setup/SKILL.md +172 -0
- package/skills/rnx-test/SKILL.md +321 -0
- package/skills/rnx-visual/SKILL.md +126 -0
- package/src/agent-daemon-client.ts +410 -0
- package/src/agent-events.ts +71 -0
- package/src/agent-identity.ts +23 -0
- package/src/agent-prompt.ts +71 -0
- package/src/agent-sessions.ts +580 -0
- package/src/app-url.ts +39 -0
- package/src/attached-projects.ts +539 -0
- package/src/auth/shared-session.ts +235 -0
- package/src/backend-origin.ts +70 -0
- package/src/beta.ts +19 -0
- package/src/bridge-constants.ts +175 -0
- package/src/bridge-contract-input.ts +502 -0
- package/src/bridge-contract.ts +558 -0
- package/src/browser-cache.ts +16 -0
- package/src/capture-contract.ts +587 -0
- package/src/cli-constants.ts +1 -0
- package/src/cli-version.ts +40 -0
- package/src/cloud-contract.ts +274 -0
- package/src/cloud.ts +762 -0
- package/src/codex-client.ts +234 -0
- package/src/config.ts +132 -0
- package/src/connect.ts +101 -0
- package/src/dev-bundle-resolution.ts +346 -0
- package/src/dev-server-open.ts +198 -0
- package/src/disk-cleanup.ts +540 -0
- package/src/flow-timeout-scale.ts +15 -0
- package/src/home-paths.ts +806 -0
- package/src/host/agent-host.ts +666 -0
- package/src/host/bridge-host.ts +2788 -0
- package/src/host/fetch-proxy-handler.ts +449 -0
- package/src/host/fetch-proxy-overrides.ts +51 -0
- package/src/host/open-url.ts +89 -0
- package/src/host/plane-host.ts +203 -0
- package/src/host/replacement-module-handler.ts +158 -0
- package/src/host/websocket-proxy.ts +315 -0
- package/src/index.ts +71 -0
- package/src/jump-to-source-babel.ts +135 -0
- package/src/jump-to-source-native.ts +102 -0
- package/src/menu.ts +777 -0
- package/src/metro-fingerprint-registry.ts +302 -0
- package/src/metro-plugin.ts +341 -0
- package/src/metro-production-bundle.ts +471 -0
- package/src/native-dev-bundle-url.ts +118 -0
- package/src/native-seam-manifest.ts +341 -0
- package/src/playwright-host-log.ts +16 -0
- package/src/plist.ts +27 -0
- package/src/poll-until.ts +123 -0
- package/src/profiles.ts +179 -0
- package/src/public-brand.ts +9 -0
- package/src/react-native-host-modules.ts +26 -0
- package/src/render-mode.ts +27 -0
- package/src/rolldown-plugin-swift.ts +240 -0
- package/src/runtime-assets.ts +233 -0
- package/src/runtime-delivery.ts +54 -0
- package/src/screenshots/compose.ts +422 -0
- package/src/screenshots/frame-compose.ts +514 -0
- package/src/screenshots/orchestrate.ts +256 -0
- package/src/screenshots/org-deck.ts +91 -0
- package/src/screenshots/registry.ts +58 -0
- package/src/screenshots/schema.ts +364 -0
- package/src/sdk.ts +15 -0
- package/src/settings-bundle.ts +418 -0
- package/src/shell-bridge-identity.ts +15 -0
- package/src/sim-client.ts +697 -0
- package/src/skills/builtin/a11y-review.ts +132 -0
- package/src/skills/builtin/compat-check.ts +71 -0
- package/src/skills/builtin/maestro-test.ts +121 -0
- package/src/skills/builtin/perf-profile.ts +90 -0
- package/src/skills/builtin/screenshot-all.ts +52 -0
- package/src/skills/builtin/visual-diff.ts +100 -0
- package/src/skills/registry.ts +107 -0
- package/src/skills/types.ts +41 -0
- package/src/swift-submit.ts +185 -0
- package/src/swift.ts +7 -0
- package/src/team-machine-prompt.ts +73 -0
- package/src/vite-plugin-one.ts +264 -0
- package/src/vite-plugin-swift.ts +620 -0
- package/src/vite-plugin.ts +1946 -0
- package/src/worklets-babel.ts +250 -0
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
// installs the standalone rnx executable into the user's command path.
|
|
2
|
+
//
|
|
3
|
+
// the public shell installer owns download and checksum verification. this
|
|
4
|
+
// command mirrors its local mutation after those bytes are trusted, while the
|
|
5
|
+
// desktop app copies its bundled executable without a network download.
|
|
6
|
+
|
|
7
|
+
import { spawnSync } from 'node:child_process'
|
|
8
|
+
import {
|
|
9
|
+
appendFileSync,
|
|
10
|
+
chmodSync,
|
|
11
|
+
copyFileSync,
|
|
12
|
+
existsSync,
|
|
13
|
+
mkdirSync,
|
|
14
|
+
readFileSync,
|
|
15
|
+
renameSync,
|
|
16
|
+
unlinkSync,
|
|
17
|
+
} from 'node:fs'
|
|
18
|
+
import { createServer } from 'node:net'
|
|
19
|
+
import { homedir } from 'node:os'
|
|
20
|
+
import { dirname, join } from 'node:path'
|
|
21
|
+
import { isDaemonLockfileFresh, readDaemonLockfile } from '../../src/home-paths'
|
|
22
|
+
import { IS_STANDALONE } from '../standalone'
|
|
23
|
+
|
|
24
|
+
const INSTALL_WAIT_MS = 10_000
|
|
25
|
+
|
|
26
|
+
type CliInstallOptions = {
|
|
27
|
+
alreadyInstalled: boolean
|
|
28
|
+
handoffPid: number | null
|
|
29
|
+
port: number | null
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function runCliInstall(args: string[]): Promise<void> {
|
|
33
|
+
const options = parseCliInstallOptions(args)
|
|
34
|
+
if (!IS_STANDALONE) {
|
|
35
|
+
throw new Error('rnx cli install requires the standalone rnx executable')
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const target = installedCliPath()
|
|
39
|
+
if (!options.alreadyInstalled) installCurrentExecutable(target)
|
|
40
|
+
else if (!existsSync(target)) {
|
|
41
|
+
throw new Error(`rnx cli install expected ${target} to exist`)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const profile = installCommandPath(target)
|
|
45
|
+
console.log(` command: ${target}`)
|
|
46
|
+
if (profile) console.log(` shell: ${profile}`)
|
|
47
|
+
|
|
48
|
+
if (options.handoffPid === null) return
|
|
49
|
+
if (process.platform === 'win32') {
|
|
50
|
+
console.log(' desktop daemon: remains app-owned on Windows')
|
|
51
|
+
return
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
await handOffDaemon({ target, pid: options.handoffPid, port: options.port })
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function parseCliInstallOptions(args: string[]): CliInstallOptions {
|
|
58
|
+
const [subcommand] = args
|
|
59
|
+
if (subcommand === '--help' || subcommand === '-h') {
|
|
60
|
+
console.log(`\nrnx cli install - install the rnx command in your PATH
|
|
61
|
+
|
|
62
|
+
usage:
|
|
63
|
+
rnx cli install
|
|
64
|
+
|
|
65
|
+
internal options:
|
|
66
|
+
--installed update shell startup after an installer copied rnx
|
|
67
|
+
--handoff-pid <pid> replace this desktop-owned daemon with the service
|
|
68
|
+
--port <port> daemon bridge port to preserve during handoff
|
|
69
|
+
`)
|
|
70
|
+
process.exit(0)
|
|
71
|
+
}
|
|
72
|
+
if (subcommand !== 'install') {
|
|
73
|
+
throw new Error('usage: rnx cli install')
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const handoffPid = readHandoffPid(args)
|
|
77
|
+
const port = readPort(args)
|
|
78
|
+
if (handoffPid !== null && port === null) {
|
|
79
|
+
throw new Error('--handoff-pid requires --port')
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
alreadyInstalled: args.includes('--installed'),
|
|
83
|
+
handoffPid,
|
|
84
|
+
port,
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function readHandoffPid(args: string[]): number | null {
|
|
89
|
+
const flag = '--handoff-pid'
|
|
90
|
+
const index = args.indexOf(flag)
|
|
91
|
+
if (index < 0) return null
|
|
92
|
+
const value = Number(args[index + 1])
|
|
93
|
+
if (!Number.isSafeInteger(value) || value < 2) {
|
|
94
|
+
throw new Error(`${flag} requires a process id`)
|
|
95
|
+
}
|
|
96
|
+
return value
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function readPort(args: string[]): number | null {
|
|
100
|
+
const flag = '--port'
|
|
101
|
+
const index = args.indexOf(flag)
|
|
102
|
+
if (index < 0) return null
|
|
103
|
+
const value = Number(args[index + 1])
|
|
104
|
+
if (!Number.isInteger(value) || value < 1 || value > 65_535) {
|
|
105
|
+
throw new Error(`${flag} requires a port number`)
|
|
106
|
+
}
|
|
107
|
+
return value
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function installedCliPath(): string {
|
|
111
|
+
const home = process.env.HOME || homedir()
|
|
112
|
+
const prefix = process.env.RNX_CLI_PREFIX || join(home, '.local')
|
|
113
|
+
return join(prefix, 'bin', process.platform === 'win32' ? 'rnx.exe' : 'rnx')
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function installCurrentExecutable(target: string): void {
|
|
117
|
+
if (process.execPath === target) return
|
|
118
|
+
mkdirSync(dirname(target), { recursive: true })
|
|
119
|
+
const temporary = join(dirname(target), `.rnx-install-${process.pid}`)
|
|
120
|
+
try {
|
|
121
|
+
copyFileSync(process.execPath, temporary)
|
|
122
|
+
chmodSync(temporary, 0o755)
|
|
123
|
+
renameSync(temporary, target)
|
|
124
|
+
} finally {
|
|
125
|
+
try {
|
|
126
|
+
unlinkSync(temporary)
|
|
127
|
+
} catch {}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function installCommandPath(target: string): string | null {
|
|
132
|
+
if (process.platform === 'win32') {
|
|
133
|
+
const bin = dirname(target)
|
|
134
|
+
const pathValue = process.env.Path || process.env.PATH || ''
|
|
135
|
+
if (
|
|
136
|
+
!pathValue.split(';').some((entry) => entry.toLowerCase() === bin.toLowerCase())
|
|
137
|
+
) {
|
|
138
|
+
const next = pathValue ? `${bin};${pathValue}` : bin
|
|
139
|
+
const result = spawnSync(
|
|
140
|
+
'reg.exe',
|
|
141
|
+
[
|
|
142
|
+
'add',
|
|
143
|
+
'HKCU\\Environment',
|
|
144
|
+
'/v',
|
|
145
|
+
'Path',
|
|
146
|
+
'/t',
|
|
147
|
+
'REG_EXPAND_SZ',
|
|
148
|
+
'/d',
|
|
149
|
+
next,
|
|
150
|
+
'/f',
|
|
151
|
+
],
|
|
152
|
+
{ encoding: 'utf8' },
|
|
153
|
+
)
|
|
154
|
+
if (result.status !== 0) {
|
|
155
|
+
throw new Error(`could not add rnx to the user PATH: ${result.stderr.trim()}`)
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return 'the user PATH'
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const home = process.env.HOME || homedir()
|
|
162
|
+
const shell = process.env.SHELL || ''
|
|
163
|
+
const shellName = shell.endsWith('/zsh') ? 'zsh' : shell.endsWith('/bash') ? 'bash' : ''
|
|
164
|
+
const profile = join(
|
|
165
|
+
home,
|
|
166
|
+
shellName === 'zsh' ? '.zshrc' : shellName === 'bash' ? '.bashrc' : '.profile',
|
|
167
|
+
)
|
|
168
|
+
const bin = dirname(target)
|
|
169
|
+
const pathLine = `export PATH="${bin}:$PATH"`
|
|
170
|
+
const integration = shellName ? `eval "$(command rnx __shell-init ${shellName})"` : ''
|
|
171
|
+
const current = existsSync(profile) ? readFileSync(profile, 'utf8') : ''
|
|
172
|
+
if (current.includes(pathLine) && (!integration || current.includes(integration)))
|
|
173
|
+
return null
|
|
174
|
+
|
|
175
|
+
const additions = [
|
|
176
|
+
'',
|
|
177
|
+
'# added by the rnx installer; delete this block to remove rnx from your shell',
|
|
178
|
+
...(current.includes(pathLine) ? [] : [pathLine]),
|
|
179
|
+
...(integration && !current.includes(integration) ? [integration] : []),
|
|
180
|
+
]
|
|
181
|
+
appendFileSync(profile, `${additions.join('\n')}\n`)
|
|
182
|
+
return profile
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async function handOffDaemon({
|
|
186
|
+
target,
|
|
187
|
+
pid,
|
|
188
|
+
port,
|
|
189
|
+
}: {
|
|
190
|
+
target: string
|
|
191
|
+
pid: number
|
|
192
|
+
port: number | null
|
|
193
|
+
}): Promise<void> {
|
|
194
|
+
if (port === null) throw new Error('the desktop daemon did not report its port')
|
|
195
|
+
const deadline = Date.now() + INSTALL_WAIT_MS
|
|
196
|
+
try {
|
|
197
|
+
process.kill(pid, 'SIGTERM')
|
|
198
|
+
} catch (error) {
|
|
199
|
+
const code = error && typeof error === 'object' ? Reflect.get(error, 'code') : null
|
|
200
|
+
if (code !== 'ESRCH') throw error
|
|
201
|
+
}
|
|
202
|
+
await waitFor(
|
|
203
|
+
() => {
|
|
204
|
+
const lock = readDaemonLockfile()
|
|
205
|
+
return !isDaemonLockfileFresh(lock) || lock.pid !== pid
|
|
206
|
+
},
|
|
207
|
+
'the desktop daemon to stop',
|
|
208
|
+
deadline,
|
|
209
|
+
)
|
|
210
|
+
await waitForPortToClose(port, deadline)
|
|
211
|
+
|
|
212
|
+
const result = spawnSync(
|
|
213
|
+
target,
|
|
214
|
+
['--port', String(port), 'daemon', 'install', '--force'],
|
|
215
|
+
{
|
|
216
|
+
encoding: 'utf8',
|
|
217
|
+
timeout: Math.max(1, deadline - Date.now()),
|
|
218
|
+
env: {
|
|
219
|
+
...process.env,
|
|
220
|
+
SOOTSIM_FORCE_DAEMON_INSTALL: '1',
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
)
|
|
224
|
+
if (result.status !== 0) {
|
|
225
|
+
throw new Error(
|
|
226
|
+
`could not register the rnx daemon: ${(result.stderr || result.stdout || 'unknown error').trim()}`,
|
|
227
|
+
)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
await waitFor(
|
|
231
|
+
() => {
|
|
232
|
+
const lock = readDaemonLockfile()
|
|
233
|
+
return isDaemonLockfileFresh(lock) && lock.pid !== pid && lock.bridgePort === port
|
|
234
|
+
},
|
|
235
|
+
'the installed rnx daemon to start',
|
|
236
|
+
deadline,
|
|
237
|
+
)
|
|
238
|
+
console.log(' desktop daemon: handed off to the installed rnx command')
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async function waitFor(
|
|
242
|
+
condition: () => boolean,
|
|
243
|
+
description: string,
|
|
244
|
+
deadline: number,
|
|
245
|
+
): Promise<void> {
|
|
246
|
+
while (Date.now() < deadline) {
|
|
247
|
+
if (condition()) return
|
|
248
|
+
await new Promise((resolve) => setTimeout(resolve, 100))
|
|
249
|
+
}
|
|
250
|
+
throw new Error(`timed out waiting for ${description}`)
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
async function waitForPortToClose(port: number, deadline: number): Promise<void> {
|
|
254
|
+
while (Date.now() < deadline) {
|
|
255
|
+
const available = await new Promise<boolean>((resolve, reject) => {
|
|
256
|
+
const server = createServer()
|
|
257
|
+
server.once('error', (error) => {
|
|
258
|
+
if ('code' in error && error.code === 'EADDRINUSE') {
|
|
259
|
+
resolve(false)
|
|
260
|
+
return
|
|
261
|
+
}
|
|
262
|
+
reject(error)
|
|
263
|
+
})
|
|
264
|
+
server.listen(port, '127.0.0.1', () => {
|
|
265
|
+
server.close((error) => {
|
|
266
|
+
if (error) reject(error)
|
|
267
|
+
else resolve(true)
|
|
268
|
+
})
|
|
269
|
+
})
|
|
270
|
+
})
|
|
271
|
+
if (available) return
|
|
272
|
+
await new Promise((resolve) => setTimeout(resolve, 100))
|
|
273
|
+
}
|
|
274
|
+
throw new Error(`timed out waiting for port ${port} to close`)
|
|
275
|
+
}
|
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
// rnx desktop install — download and install the optional desktop GUI
|
|
2
|
+
//
|
|
3
|
+
// the CLI + daemon are the canonical rnx surface; this command is only
|
|
4
|
+
// for users who want the Electron-based GUI companion on top of that.
|
|
5
|
+
//
|
|
6
|
+
// fetches the right platform artifact from https://contrast.dev/api/electron-release/
|
|
7
|
+
// and installs it into the OS-native location that findDesktopCompanion() looks for:
|
|
8
|
+
// macos: ~/Applications/rnx.app (or /Applications if writable)
|
|
9
|
+
// linux: ~/Applications/rnx.AppImage
|
|
10
|
+
// win: spawns the NSIS installer
|
|
11
|
+
//
|
|
12
|
+
// post-install the electron-updater feed (same URL) handles upgrades inside the
|
|
13
|
+
// running app, so this command is a first-install bootstrap, not an update path.
|
|
14
|
+
|
|
15
|
+
import { spawn, spawnSync } from 'child_process'
|
|
16
|
+
import {
|
|
17
|
+
chmodSync,
|
|
18
|
+
copyFileSync,
|
|
19
|
+
createWriteStream,
|
|
20
|
+
existsSync,
|
|
21
|
+
mkdirSync,
|
|
22
|
+
rmSync,
|
|
23
|
+
statSync,
|
|
24
|
+
unlinkSync,
|
|
25
|
+
} from 'fs'
|
|
26
|
+
import { tmpdir } from 'os'
|
|
27
|
+
import { dirname, join, resolve } from 'path'
|
|
28
|
+
import { findDesktopCompanion } from '../desktop-companion'
|
|
29
|
+
import { confirm } from '../prompt'
|
|
30
|
+
import { rnxExit } from '../run-rnx'
|
|
31
|
+
import { accent, bold, failure, muted, success, withSpinner } from '../ui'
|
|
32
|
+
|
|
33
|
+
const DOWNLOAD_BASE = 'https://contrast.dev/api/electron-release'
|
|
34
|
+
|
|
35
|
+
type TargetPlatform = 'mac-arm64' | 'mac-x64' | 'win-x64' | 'linux-x64' | 'linux-arm64'
|
|
36
|
+
|
|
37
|
+
interface Target {
|
|
38
|
+
platform: TargetPlatform
|
|
39
|
+
filename: string
|
|
40
|
+
url: string
|
|
41
|
+
install: (downloaded: string) => Promise<string>
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function detectTarget(): Target | null {
|
|
45
|
+
const arch = process.arch
|
|
46
|
+
if (process.platform === 'darwin') {
|
|
47
|
+
const platform: TargetPlatform = arch === 'arm64' ? 'mac-arm64' : 'mac-x64'
|
|
48
|
+
const filename = `sootsim-latest-${platform}.dmg`
|
|
49
|
+
return {
|
|
50
|
+
platform,
|
|
51
|
+
filename,
|
|
52
|
+
url: `${DOWNLOAD_BASE}/${filename}`,
|
|
53
|
+
install: installMacDmg,
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (process.platform === 'linux') {
|
|
57
|
+
// ship arch-appropriate AppImages. on Apple Silicon Macs (the default
|
|
58
|
+
// host for orb/multipass VMs) the linux VM is arm64, so the x64
|
|
59
|
+
// AppImage fails at runtime with "Dynamic loader not found:
|
|
60
|
+
// /lib64/ld-linux-x86-64.so.2". detect the running arch and pick the
|
|
61
|
+
// matching artifact.
|
|
62
|
+
const platform: TargetPlatform = arch === 'arm64' ? 'linux-arm64' : 'linux-x64'
|
|
63
|
+
const filename = `sootsim-latest-${platform}.AppImage`
|
|
64
|
+
return {
|
|
65
|
+
platform,
|
|
66
|
+
filename,
|
|
67
|
+
url: `${DOWNLOAD_BASE}/${filename}`,
|
|
68
|
+
install: installLinuxAppImage,
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (process.platform === 'win32') {
|
|
72
|
+
const filename = `sootsim-latest-win-x64.exe`
|
|
73
|
+
return {
|
|
74
|
+
platform: 'win-x64',
|
|
75
|
+
filename,
|
|
76
|
+
url: `${DOWNLOAD_BASE}/${filename}`,
|
|
77
|
+
install: installWindowsExe,
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return null
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
interface DownloadOptions {
|
|
84
|
+
url: string
|
|
85
|
+
dest: string
|
|
86
|
+
onProgress?: (received: number, total: number | null) => void
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function download({ url, dest, onProgress }: DownloadOptions): Promise<void> {
|
|
90
|
+
const response = await fetch(url, { redirect: 'follow' })
|
|
91
|
+
if (!response.ok) {
|
|
92
|
+
throw new Error(`${response.status} ${response.statusText} (${url})`)
|
|
93
|
+
}
|
|
94
|
+
const contentLength = response.headers.get('content-length')
|
|
95
|
+
const total = contentLength ? Number(contentLength) : null
|
|
96
|
+
if (!response.body) {
|
|
97
|
+
throw new Error(`empty response body (${url})`)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
mkdirSync(dirname(dest), { recursive: true })
|
|
101
|
+
const file = createWriteStream(dest)
|
|
102
|
+
const reader = response.body.getReader()
|
|
103
|
+
let received = 0
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
while (true) {
|
|
107
|
+
const { done, value } = await reader.read()
|
|
108
|
+
if (done) break
|
|
109
|
+
file.write(Buffer.from(value))
|
|
110
|
+
received += value.byteLength
|
|
111
|
+
onProgress?.(received, total)
|
|
112
|
+
}
|
|
113
|
+
} finally {
|
|
114
|
+
await new Promise<void>((resolve, reject) => {
|
|
115
|
+
file.end((err?: Error | null) => (err ? reject(err) : resolve()))
|
|
116
|
+
})
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function renderProgress(received: number, total: number | null): string {
|
|
121
|
+
const mb = (received / (1024 * 1024)).toFixed(1)
|
|
122
|
+
if (!total) return ` ${mb} MB`
|
|
123
|
+
const pct = Math.min(100, Math.round((received / total) * 100))
|
|
124
|
+
const totalMb = (total / (1024 * 1024)).toFixed(1)
|
|
125
|
+
const barWidth = 24
|
|
126
|
+
const filled = Math.round((pct / 100) * barWidth)
|
|
127
|
+
const bar = accent('█'.repeat(filled)) + muted('░'.repeat(barWidth - filled))
|
|
128
|
+
return ` ${bar} ${pct}% ${mb} / ${totalMb} MB`
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function installMacDmg(dmgPath: string): Promise<string> {
|
|
132
|
+
const mountOutput = spawnSync(
|
|
133
|
+
'hdiutil',
|
|
134
|
+
['attach', '-nobrowse', '-readonly', dmgPath],
|
|
135
|
+
{
|
|
136
|
+
encoding: 'utf8',
|
|
137
|
+
},
|
|
138
|
+
)
|
|
139
|
+
if (mountOutput.status !== 0) {
|
|
140
|
+
throw new Error(
|
|
141
|
+
`hdiutil attach failed: ${mountOutput.stderr || mountOutput.stdout}`.trim(),
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
const mountLines = mountOutput.stdout.trim().split('\n')
|
|
145
|
+
const mountLine = mountLines[mountLines.length - 1]
|
|
146
|
+
const mountPoint = mountLine.split('\t').pop()?.trim()
|
|
147
|
+
if (!mountPoint || !existsSync(mountPoint)) {
|
|
148
|
+
throw new Error(`could not determine dmg mount point (output: ${mountOutput.stdout})`)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const appSource = join(mountPoint, 'rnx.app')
|
|
152
|
+
if (!existsSync(appSource)) {
|
|
153
|
+
spawnSync('hdiutil', ['detach', '-force', mountPoint])
|
|
154
|
+
throw new Error(`rnx.app not found inside ${mountPoint}`)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// prefer /Applications if writable, otherwise ~/Applications (same paths
|
|
158
|
+
// findDesktopCompanion checks, so no extra config needed post-install).
|
|
159
|
+
const systemApps = '/Applications'
|
|
160
|
+
const userApps = resolve(process.env.HOME || '', 'Applications')
|
|
161
|
+
let destRoot = systemApps
|
|
162
|
+
try {
|
|
163
|
+
const probe = join(systemApps, `.rnx-write-probe-${process.pid}`)
|
|
164
|
+
spawnSync('touch', [probe])
|
|
165
|
+
if (existsSync(probe)) rmSync(probe, { force: true })
|
|
166
|
+
else destRoot = userApps
|
|
167
|
+
} catch {
|
|
168
|
+
destRoot = userApps
|
|
169
|
+
}
|
|
170
|
+
if (destRoot === userApps) {
|
|
171
|
+
mkdirSync(destRoot, { recursive: true })
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const appDest = join(destRoot, 'rnx.app')
|
|
175
|
+
|
|
176
|
+
// copy alongside the destination and swap, rather than deleting the installed
|
|
177
|
+
// app first. removing it up front means a failed copy leaves the user with no
|
|
178
|
+
// app at all, having destroyed a working install to do it. the staging path is
|
|
179
|
+
// in the same directory so the swap is a rename on one volume.
|
|
180
|
+
const staging = join(destRoot, `.rnx-incoming-${process.pid}.app`)
|
|
181
|
+
rmSync(staging, { recursive: true, force: true })
|
|
182
|
+
|
|
183
|
+
const copy = spawnSync('cp', ['-R', appSource, staging])
|
|
184
|
+
if (copy.status !== 0) {
|
|
185
|
+
spawnSync('hdiutil', ['detach', '-force', mountPoint])
|
|
186
|
+
rmSync(staging, { recursive: true, force: true })
|
|
187
|
+
throw new Error(`copy to ${destRoot} failed: ${copy.stderr?.toString() || ''}`.trim())
|
|
188
|
+
}
|
|
189
|
+
const detach = spawnSync('hdiutil', ['detach', '-force', mountPoint])
|
|
190
|
+
|
|
191
|
+
const previous = join(destRoot, `.rnx-previous-${process.pid}.app`)
|
|
192
|
+
if (existsSync(appDest)) {
|
|
193
|
+
const park = spawnSync('mv', [appDest, previous])
|
|
194
|
+
if (park.status !== 0) {
|
|
195
|
+
rmSync(staging, { recursive: true, force: true })
|
|
196
|
+
throw new Error(
|
|
197
|
+
`could not replace ${appDest}: ${park.stderr?.toString() || ''}`.trim(),
|
|
198
|
+
)
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
const swap = spawnSync('mv', [staging, appDest])
|
|
202
|
+
if (swap.status !== 0) {
|
|
203
|
+
// put the working install back before reporting failure
|
|
204
|
+
if (existsSync(previous)) spawnSync('mv', [previous, appDest])
|
|
205
|
+
rmSync(staging, { recursive: true, force: true })
|
|
206
|
+
throw new Error(
|
|
207
|
+
`could not move into ${appDest}: ${swap.stderr?.toString() || ''}`.trim(),
|
|
208
|
+
)
|
|
209
|
+
}
|
|
210
|
+
rmSync(previous, { recursive: true, force: true })
|
|
211
|
+
|
|
212
|
+
if (detach.status !== 0) {
|
|
213
|
+
console.warn(
|
|
214
|
+
` warning: failed to unmount ${mountPoint} (${detach.stderr?.toString().trim() || 'unknown error'})`,
|
|
215
|
+
)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// clear the quarantine attribute so gatekeeper doesn't nag on first launch;
|
|
219
|
+
// the app is already notarized so this is safe.
|
|
220
|
+
spawnSync('xattr', ['-dr', 'com.apple.quarantine', appDest])
|
|
221
|
+
|
|
222
|
+
return appDest
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async function installLinuxAppImage(appImagePath: string): Promise<string> {
|
|
226
|
+
const destDir = resolve(process.env.HOME || '', 'Applications')
|
|
227
|
+
mkdirSync(destDir, { recursive: true })
|
|
228
|
+
const dest = join(destDir, 'rnx.AppImage')
|
|
229
|
+
if (existsSync(dest)) rmSync(dest, { force: true })
|
|
230
|
+
// copyFile + unlink instead of rename: /tmp and ~/Applications are
|
|
231
|
+
// routinely on separate mounts in containers / VMs, where renameSync
|
|
232
|
+
// fails EXDEV. copy is portable across mounts and the unlink at the
|
|
233
|
+
// end keeps the staging dir clean.
|
|
234
|
+
copyFileSync(appImagePath, dest)
|
|
235
|
+
unlinkSync(appImagePath)
|
|
236
|
+
chmodSync(dest, 0o755)
|
|
237
|
+
return dest
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async function installWindowsExe(exePath: string): Promise<string> {
|
|
241
|
+
// nsis installer prompts and self-elevates; run it and let it take over.
|
|
242
|
+
await new Promise<void>((resolvePromise, rejectPromise) => {
|
|
243
|
+
const child = spawn('cmd', ['/c', 'start', '""', '/wait', exePath], {
|
|
244
|
+
stdio: 'inherit',
|
|
245
|
+
})
|
|
246
|
+
child.once('error', rejectPromise)
|
|
247
|
+
child.once('exit', (code) => {
|
|
248
|
+
if (code === 0) resolvePromise()
|
|
249
|
+
else rejectPromise(new Error(`installer exited with code ${code}`))
|
|
250
|
+
})
|
|
251
|
+
})
|
|
252
|
+
const installed = findDesktopCompanion()
|
|
253
|
+
if (!installed) {
|
|
254
|
+
throw new Error('the installer finished but the rnx application was not registered')
|
|
255
|
+
}
|
|
256
|
+
return installed.path
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export async function runInstallDesktop(args: string[]) {
|
|
260
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
261
|
+
console.log(`
|
|
262
|
+
rnx desktop install
|
|
263
|
+
|
|
264
|
+
Download and install the optional rnx Desktop app.
|
|
265
|
+
The CLI works without it. Choose Desktop when you want a dedicated native window.
|
|
266
|
+
|
|
267
|
+
usage:
|
|
268
|
+
rnx desktop install [options]
|
|
269
|
+
|
|
270
|
+
options:
|
|
271
|
+
-y, --yes skip confirmation and install immediately
|
|
272
|
+
--force reinstall even if the companion is already present
|
|
273
|
+
|
|
274
|
+
examples:
|
|
275
|
+
rnx desktop install
|
|
276
|
+
rnx desktop install --yes
|
|
277
|
+
`)
|
|
278
|
+
rnxExit(0)
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const skipConfirm =
|
|
282
|
+
args.includes('--yes') ||
|
|
283
|
+
args.includes('-y') ||
|
|
284
|
+
process.env.RNX_NO_PROMPT === '1' ||
|
|
285
|
+
process.env.CI === '1' ||
|
|
286
|
+
!process.stdin.isTTY
|
|
287
|
+
const force = args.includes('--force')
|
|
288
|
+
|
|
289
|
+
const existing = findDesktopCompanion()
|
|
290
|
+
if (existing && !force) {
|
|
291
|
+
console.log(` ${success('✓')} Desktop app is already installed at ${existing.path}`)
|
|
292
|
+
console.log(` ${muted('Pass --force to reinstall.')}`)
|
|
293
|
+
return
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const target = detectTarget()
|
|
297
|
+
if (!target) {
|
|
298
|
+
console.error(
|
|
299
|
+
` ${failure('×')} No desktop build is available for ${process.platform}/${process.arch}.`,
|
|
300
|
+
)
|
|
301
|
+
console.error(
|
|
302
|
+
' Supported: darwin-arm64, darwin-x64, linux-x64, linux-arm64, win32-x64',
|
|
303
|
+
)
|
|
304
|
+
rnxExit(1)
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
console.log(` ${bold('Desktop app')}`)
|
|
308
|
+
console.log(` Platform ${target.platform}`)
|
|
309
|
+
console.log(` Download ${target.url}`)
|
|
310
|
+
console.log()
|
|
311
|
+
|
|
312
|
+
if (!skipConfirm) {
|
|
313
|
+
const ok = await confirm('Download and install now?', true)
|
|
314
|
+
if (!ok) {
|
|
315
|
+
console.log(` ${muted('Cancelled.')}`)
|
|
316
|
+
return
|
|
317
|
+
}
|
|
318
|
+
console.log()
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const workDir = join(tmpdir(), `rnx-install-${Date.now()}`)
|
|
322
|
+
const downloadPath = join(workDir, target.filename)
|
|
323
|
+
|
|
324
|
+
let lastDraw = 0
|
|
325
|
+
process.stdout.write(` Downloading ${target.filename}...\n`)
|
|
326
|
+
|
|
327
|
+
try {
|
|
328
|
+
await download({
|
|
329
|
+
url: target.url,
|
|
330
|
+
dest: downloadPath,
|
|
331
|
+
onProgress: (received, total) => {
|
|
332
|
+
const now = Date.now()
|
|
333
|
+
if (now - lastDraw < 100 && received < (total ?? Infinity)) return
|
|
334
|
+
lastDraw = now
|
|
335
|
+
if (process.stdout.isTTY) {
|
|
336
|
+
process.stdout.write(`\r\x1b[2K${renderProgress(received, total)}`)
|
|
337
|
+
}
|
|
338
|
+
},
|
|
339
|
+
})
|
|
340
|
+
} catch (err) {
|
|
341
|
+
rmSync(workDir, { recursive: true, force: true })
|
|
342
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
343
|
+
console.error(`\n ${failure('×')} Download failed: ${message}`)
|
|
344
|
+
console.error(
|
|
345
|
+
` The ${target.platform} desktop build may not be published yet, or your`,
|
|
346
|
+
)
|
|
347
|
+
console.error(
|
|
348
|
+
' network blocked the request. Check your connection and try again later.',
|
|
349
|
+
)
|
|
350
|
+
console.error(' The CLI and daemon work without the desktop app. Run `rnx --help`.')
|
|
351
|
+
rnxExit(1)
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
if (process.stdout.isTTY) process.stdout.write('\n')
|
|
355
|
+
const size = statSync(downloadPath).size
|
|
356
|
+
console.log(` ${success('✓')} Downloaded ${(size / (1024 * 1024)).toFixed(1)} MB`)
|
|
357
|
+
console.log()
|
|
358
|
+
|
|
359
|
+
try {
|
|
360
|
+
const installedAt = await withSpinner('Installing the desktop app', () =>
|
|
361
|
+
target.install(downloadPath),
|
|
362
|
+
)
|
|
363
|
+
console.log(` Installed at ${installedAt}`)
|
|
364
|
+
rmSync(workDir, { recursive: true, force: true })
|
|
365
|
+
} catch (err) {
|
|
366
|
+
console.error(
|
|
367
|
+
` ${failure('×')} Install failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
368
|
+
)
|
|
369
|
+
// the download is good, only placing the app failed. on macOS the disk
|
|
370
|
+
// image installs itself perfectly well by hand, so open it and let the user
|
|
371
|
+
// drag rather than leaving them at a dead end. keep workDir for that.
|
|
372
|
+
if (process.platform === 'darwin') {
|
|
373
|
+
spawnSync('open', [downloadPath])
|
|
374
|
+
console.error(` Opened ${target.filename} — drag rnx to Applications to finish.`)
|
|
375
|
+
} else {
|
|
376
|
+
console.error(` Keeping the download at ${downloadPath} for manual installation.`)
|
|
377
|
+
}
|
|
378
|
+
rnxExit(1)
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
console.log()
|
|
382
|
+
console.log(` ${bold('Ready')}`)
|
|
383
|
+
console.log(' rnx desktop Launch the desktop app')
|
|
384
|
+
console.log(' rnx open <target> Open a demo or bundle in it')
|
|
385
|
+
}
|