rnxsim 0.1.408 → 0.1.409
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/README.md +11 -10
- package/cli/bin.ts +16 -643
- package/cli/commands/assert.ts +6 -6
- package/cli/commands/box/rnx-command.ts +3 -11
- package/cli/commands/control.ts +3 -9
- package/cli/commands/daemon-mac-app.ts +5 -11
- package/cli/commands/daemon.ts +63 -113
- package/cli/drivers/playwright-provisioning.ts +33 -29
- package/cli/drivers/playwright-sim-host.ts +501 -0
- package/cli/drivers/playwright.ts +35 -522
- package/cli/hints.ts +2 -4
- package/cli/internal-child.ts +176 -0
- package/cli/maestro-js.ts +28 -39
- package/cli/main.ts +645 -0
- package/cli/outbound-endpoints.ts +8 -0
- package/cli/self-invocation.ts +34 -0
- package/dist-lib/agent-daemon-client.cjs +35 -25
- package/dist-lib/agent-events.cjs +1 -1
- package/dist-lib/agent-identity.cjs +1 -1
- package/dist-lib/agent-sessions.cjs +35 -25
- package/dist-lib/attached-projects.cjs +1 -1
- package/dist-lib/auth/shared-session.cjs +1 -1
- package/dist-lib/backend-origin.cjs +1 -1
- package/dist-lib/beta.cjs +1 -1
- package/dist-lib/beta.mjs +1 -1
- package/dist-lib/bridge-constants.cjs +1 -1
- package/dist-lib/bridge-contract-input.cjs +1 -1
- package/dist-lib/bridge-contract-input.mjs +1 -1
- package/dist-lib/bridge-contract.cjs +1 -1
- package/dist-lib/bridge-contract.mjs +1 -1
- package/dist-lib/capture-contract.cjs +1 -1
- package/dist-lib/capture-contract.mjs +1 -1
- package/dist-lib/cli-constants.cjs +1 -1
- package/dist-lib/cloud-contract.cjs +1 -4
- package/dist-lib/cloud-contract.mjs +1 -3
- package/dist-lib/config.cjs +1 -1
- package/dist-lib/detox/index.cjs +1 -1
- package/dist-lib/dev-bundle-resolution.cjs +1 -1
- package/dist-lib/home-paths.cjs +1 -1
- package/dist-lib/host/bridge-host.cjs +25 -15
- package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
- package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
- package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
- package/dist-lib/host/replacement-module-handler.cjs +1 -1
- package/dist-lib/host/websocket-proxy.cjs +1 -1
- package/dist-lib/index.cjs +1 -1
- package/dist-lib/jump-to-source-babel.cjs +1 -1
- package/dist-lib/menu.cjs +1 -1
- package/dist-lib/menu.mjs +1 -1
- package/dist-lib/metro-fingerprint-registry.cjs +1 -1
- package/dist-lib/metro-fingerprint-registry.mjs +1 -1
- package/dist-lib/metro-production-bundle.cjs +1 -1
- package/dist-lib/metro-production-bundle.mjs +1 -1
- package/dist-lib/metro.cjs +1 -1
- package/dist-lib/profiles.cjs +1 -1
- package/dist-lib/public-brand.cjs +1 -1
- package/dist-lib/react-native-host-modules.cjs +1 -1
- package/dist-lib/react-native-host-modules.mjs +1 -1
- package/dist-lib/render-mode.cjs +1 -1
- package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
- package/dist-lib/sdk.cjs +1 -1
- package/dist-lib/sdk.mjs +1 -1
- package/dist-lib/skills.cjs +134 -608
- package/dist-lib/vite.cjs +1 -1
- package/package.json +1 -1
- package/src/agent-daemon-client.ts +2 -2
- package/src/agent-sessions.ts +29 -24
- package/src/cloud-contract.ts +0 -1
- package/src/vite-plugin.ts +62 -0
package/cli/commands/assert.ts
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
import { spawn } from 'child_process'
|
|
17
17
|
import { rnxExit } from '../run-rnx'
|
|
18
|
+
import { rnxSelfInvocation } from '../self-invocation'
|
|
18
19
|
|
|
19
20
|
// READ_VERBS lists every command whose stdout we know is valid JSON under
|
|
20
21
|
// --json. keeping this explicit means `rnx assert tap 100 200` fails
|
|
@@ -161,14 +162,13 @@ interface VerbResult {
|
|
|
161
162
|
}
|
|
162
163
|
|
|
163
164
|
async function runVerbOnce(verbArgs: string[]): Promise<VerbResult> {
|
|
164
|
-
// re-invoke self
|
|
165
|
-
//
|
|
166
|
-
|
|
167
|
-
const
|
|
168
|
-
const args = [binPath, ...verbArgs, '--json']
|
|
165
|
+
// re-invoke self through the real CLI entry, so every code path the user's
|
|
166
|
+
// verb would take, assert takes too.
|
|
167
|
+
const { executable, prefixArgs } = rnxSelfInvocation()
|
|
168
|
+
const args = [...prefixArgs, ...verbArgs, '--json']
|
|
169
169
|
|
|
170
170
|
return new Promise<VerbResult>((resolve) => {
|
|
171
|
-
const proc = spawn(
|
|
171
|
+
const proc = spawn(executable, args, {
|
|
172
172
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
173
173
|
env: process.env,
|
|
174
174
|
})
|
|
@@ -11,17 +11,9 @@ import path from 'node:path'
|
|
|
11
11
|
import { defineCommand, type CustomCommand } from '@rnx/box/shell'
|
|
12
12
|
import { PROJECT_MOUNT } from '@rnx/box/shell'
|
|
13
13
|
import { rnxPublicBrand } from '../../../src/public-brand'
|
|
14
|
+
import { rnxSelfInvocation } from '../../self-invocation'
|
|
14
15
|
import { RNX_PLACEMENT_ENV } from '../../ws-bridge'
|
|
15
16
|
|
|
16
|
-
// the standalone binary is its own entry; the npm package runs through node
|
|
17
|
-
// with the CLI as argv[1]. this is the same shape bin.ts uses to respawn
|
|
18
|
-
// itself for automatic cleanup.
|
|
19
|
-
function selfInvocation(): { command: string; prefix: string[] } {
|
|
20
|
-
const entry = process.argv[1]
|
|
21
|
-
if (!entry) return { command: process.execPath, prefix: [] }
|
|
22
|
-
return { command: process.execPath, prefix: [entry] }
|
|
23
|
-
}
|
|
24
|
-
|
|
25
17
|
// map a shell path under /project back onto the real checkout, so a child
|
|
26
18
|
// process started from `cd src && rnx describe` sees the directory the user
|
|
27
19
|
// is actually standing in.
|
|
@@ -36,8 +28,8 @@ export function createRnxCommand(options: {
|
|
|
36
28
|
shellCwd: () => string
|
|
37
29
|
}): CustomCommand {
|
|
38
30
|
return defineCommand(rnxPublicBrand.commandName, async (args) => {
|
|
39
|
-
const {
|
|
40
|
-
const child = spawn(
|
|
31
|
+
const { executable, prefixArgs } = rnxSelfInvocation()
|
|
32
|
+
const child = spawn(executable, [...prefixArgs, ...args], {
|
|
41
33
|
cwd: hostCwd(options.root, options.shellCwd()),
|
|
42
34
|
// mark the child as running inside a local box. this is the only thing
|
|
43
35
|
// that sets a placement — nothing infers one from cwd — and it is what
|
package/cli/commands/control.ts
CHANGED
|
@@ -57,6 +57,7 @@ import {
|
|
|
57
57
|
import { getDriver } from '../drivers/registry'
|
|
58
58
|
import { openUrl as openUrlInBrowser } from '../open-url'
|
|
59
59
|
import { rethrowIfExit, rnxExit } from '../run-rnx'
|
|
60
|
+
import { rnxSelfInvocation } from '../self-invocation'
|
|
60
61
|
import {
|
|
61
62
|
BridgeSimLockedError,
|
|
62
63
|
bridgePortForShellBaseUrl,
|
|
@@ -250,15 +251,8 @@ async function ensureRuntimeForOpen() {
|
|
|
250
251
|
}
|
|
251
252
|
|
|
252
253
|
function spawnSootsim(args: string[]) {
|
|
253
|
-
const
|
|
254
|
-
|
|
255
|
-
return spawn(process.execPath, [entry, ...args], {
|
|
256
|
-
detached: true,
|
|
257
|
-
stdio: 'ignore',
|
|
258
|
-
env: process.env,
|
|
259
|
-
})
|
|
260
|
-
}
|
|
261
|
-
return spawn(rnxPublicBrand.commandName, args, {
|
|
254
|
+
const { executable, prefixArgs } = rnxSelfInvocation()
|
|
255
|
+
return spawn(executable, [...prefixArgs, ...args], {
|
|
262
256
|
detached: true,
|
|
263
257
|
stdio: 'ignore',
|
|
264
258
|
env: process.env,
|
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
//
|
|
12
12
|
// the bundle is regenerated every time `daemonInstall` runs — it's tiny
|
|
13
13
|
// (a plist + a shell launcher) and the launcher embeds a snapshot of the
|
|
14
|
-
// invocation, so
|
|
15
|
-
//
|
|
14
|
+
// invocation, so `rnx upgrade` followed by `rnx daemon install --force` is
|
|
15
|
+
// the documented upgrade path. there's no notarization
|
|
16
16
|
// or Developer ID signing here yet; ad-hoc + hardened runtime is enough to
|
|
17
17
|
// fix the attribution, and a real signed bundle (shipped via the runtime
|
|
18
18
|
// CDN) is a follow-up.
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
daemonAppDir,
|
|
26
26
|
daemonAppLauncherPath,
|
|
27
27
|
} from '../../src/home-paths'
|
|
28
|
+
import type { RnxSelfInvocation } from '../self-invocation'
|
|
28
29
|
|
|
29
30
|
const BUNDLE_IDENTIFIER = 'dev.sootsim.daemon'
|
|
30
31
|
const BUNDLE_DISPLAY_NAME = 'RNX Daemon'
|
|
@@ -35,13 +36,6 @@ const BUNDLE_DISPLAY_NAME = 'RNX Daemon'
|
|
|
35
36
|
// checkouts.
|
|
36
37
|
const BUNDLE_VERSION = '0.1.36'
|
|
37
38
|
|
|
38
|
-
export interface DaemonInvocation {
|
|
39
|
-
/** absolute path to the executable launchd should spawn. */
|
|
40
|
-
executable: string
|
|
41
|
-
/** arguments passed to the executable (before server/--port flags). */
|
|
42
|
-
prefixArgs: string[]
|
|
43
|
-
}
|
|
44
|
-
|
|
45
39
|
export interface DaemonAppPaths {
|
|
46
40
|
/** the rebuilt .app bundle. */
|
|
47
41
|
bundlePath: string
|
|
@@ -53,7 +47,7 @@ export interface DaemonAppPaths {
|
|
|
53
47
|
* and re-sign it ad-hoc. safe to call repeatedly; the directory is wiped
|
|
54
48
|
* before write so stale state from earlier installs can't leak through. */
|
|
55
49
|
export function ensureDaemonApp(
|
|
56
|
-
invocation:
|
|
50
|
+
invocation: RnxSelfInvocation,
|
|
57
51
|
port: number,
|
|
58
52
|
logDir: { stdout: string; stderr: string },
|
|
59
53
|
): DaemonAppPaths {
|
|
@@ -121,7 +115,7 @@ function renderInfoPlist(): string {
|
|
|
121
115
|
}
|
|
122
116
|
|
|
123
117
|
function renderLauncherScript(
|
|
124
|
-
invocation:
|
|
118
|
+
invocation: RnxSelfInvocation,
|
|
125
119
|
port: number,
|
|
126
120
|
logDir: { stdout: string; stderr: string },
|
|
127
121
|
): string {
|
package/cli/commands/daemon.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// rnx daemon — manage the persistent rnx bridge daemon
|
|
2
2
|
//
|
|
3
3
|
// wraps `rnx serve` so it starts automatically on login and restarts if
|
|
4
|
-
// it crashes. the agent
|
|
5
|
-
//
|
|
6
|
-
//
|
|
4
|
+
// it crashes. the agent points at the executable that ran `daemon install`,
|
|
5
|
+
// so `rnx upgrade` (which replaces that file in place) is picked up by
|
|
6
|
+
// `rnx daemon restart` without touching the plist/unit file.
|
|
7
7
|
//
|
|
8
8
|
// `daemonInstall` is shared by `rnx setup` and the explicit
|
|
9
9
|
// `rnx daemon install` machine-bootstrap path.
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
// the macOS launchd Label is `dev.sootsim.daemon` (matching the .app
|
|
16
16
|
// CFBundleIdentifier).
|
|
17
17
|
|
|
18
|
-
import {
|
|
18
|
+
import { spawnSync } from 'child_process'
|
|
19
19
|
import {
|
|
20
20
|
existsSync,
|
|
21
21
|
mkdirSync,
|
|
@@ -25,11 +25,10 @@ import {
|
|
|
25
25
|
writeFileSync,
|
|
26
26
|
} from 'fs'
|
|
27
27
|
import { homedir } from 'os'
|
|
28
|
-
import {
|
|
28
|
+
import { dirname, resolve } from 'path'
|
|
29
29
|
import { DEFAULT_SOOTSIM_BRIDGE_PORT } from '../../src/bridge-constants'
|
|
30
|
-
import { getCliVersion } from '../../src/cli-version'
|
|
31
30
|
import {
|
|
32
|
-
|
|
31
|
+
daemonAppDir,
|
|
33
32
|
isDaemonLockfileFresh,
|
|
34
33
|
readDaemonLockfile,
|
|
35
34
|
shouldSkipPersistentDaemon,
|
|
@@ -38,6 +37,7 @@ import {
|
|
|
38
37
|
import { rnxPublicBrand } from '../../src/public-brand'
|
|
39
38
|
import { printCommandHelp } from '../help'
|
|
40
39
|
import { rnxExit } from '../run-rnx'
|
|
40
|
+
import { rnxSelfInvocation, type RnxSelfInvocation } from '../self-invocation'
|
|
41
41
|
import { ensureDaemonApp } from './daemon-mac-app'
|
|
42
42
|
|
|
43
43
|
// minimum seconds between launchd / systemd respawns. without this, a daemon
|
|
@@ -69,7 +69,7 @@ export async function runDaemon(args: string[], opts: DaemonOptions = {}) {
|
|
|
69
69
|
case 'install':
|
|
70
70
|
return daemonInstall({ port, force: rest.includes('--force') })
|
|
71
71
|
case 'uninstall':
|
|
72
|
-
return daemonUninstall()
|
|
72
|
+
return daemonUninstall(rest)
|
|
73
73
|
case 'status':
|
|
74
74
|
return daemonStatus()
|
|
75
75
|
case 'restart':
|
|
@@ -93,94 +93,17 @@ function printHelp() {
|
|
|
93
93
|
|
|
94
94
|
// --- resolve sootsim binary ----------------------------------------------
|
|
95
95
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
function resolveSootsimInvocation(): SootsimInvocation {
|
|
104
|
-
// launchd / systemd run agents with a stripped PATH (basically just
|
|
105
|
-
// /usr/bin:/bin:/usr/sbin:/sbin), so a `#!/usr/bin/env node` shebang
|
|
106
|
-
// resolves nothing and the daemon never starts. spawn the absolute node
|
|
107
|
-
// binary directly with the rnx entry script as an argument.
|
|
108
|
-
//
|
|
109
|
-
// process.execPath is whichever node ran the install command, which is
|
|
110
|
-
// also the node the user expects rnx to run on.
|
|
111
|
-
const npxInvocation = resolveNpxDaemonInvocation()
|
|
112
|
-
if (npxInvocation) return npxInvocation
|
|
113
|
-
const entry = resolveSootsimEntryScript()
|
|
114
|
-
return { executable: process.execPath, prefixArgs: [entry] }
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
function resolveNpxDaemonInvocation(): SootsimInvocation | null {
|
|
118
|
-
if (!isNpxSootsimInvocation()) return null
|
|
119
|
-
|
|
120
|
-
const npmCli = resolveNpmCliPath()
|
|
121
|
-
if (!npmCli) return null
|
|
122
|
-
|
|
123
|
-
const version = getCliVersion()
|
|
124
|
-
const packageSpec =
|
|
125
|
-
version === '0.0.0'
|
|
126
|
-
? rnxPublicBrand.packageName
|
|
127
|
-
: `${rnxPublicBrand.packageName}@${version}`
|
|
96
|
+
// launchd and systemd run agents with a stripped PATH (basically just
|
|
97
|
+
// /usr/bin:/bin:/usr/sbin:/sbin), so anything that depends on PATH resolution
|
|
98
|
+
// never starts. rnxSelfInvocation() answers with absolute paths for both CLI
|
|
99
|
+
// shapes; resolve symlinks on the entry script so the service spawns the real
|
|
100
|
+
// file rather than a bin shim that would itself need PATH.
|
|
101
|
+
function resolveDaemonInvocation(): RnxSelfInvocation {
|
|
102
|
+
const invocation = rnxSelfInvocation()
|
|
128
103
|
return {
|
|
129
|
-
executable:
|
|
130
|
-
prefixArgs:
|
|
131
|
-
npmCli,
|
|
132
|
-
'exec',
|
|
133
|
-
'--yes',
|
|
134
|
-
'--package',
|
|
135
|
-
packageSpec,
|
|
136
|
-
'--',
|
|
137
|
-
rnxPublicBrand.commandName,
|
|
138
|
-
],
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
function isNpxSootsimInvocation(): boolean {
|
|
143
|
-
const argv1 = process.argv[1]
|
|
144
|
-
if (process.env.npm_command === 'exec') return true
|
|
145
|
-
if (!argv1) return false
|
|
146
|
-
const real = realPathOrSelf(argv1)
|
|
147
|
-
return real.includes(`${sep}_npx${sep}`)
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function resolveNpmCliPath(): string | null {
|
|
151
|
-
try {
|
|
152
|
-
const out = execFileSync('which', ['npm'], { encoding: 'utf8' }).trim()
|
|
153
|
-
if (out && existsSync(out)) return realNpmCliPath(out)
|
|
154
|
-
} catch {}
|
|
155
|
-
|
|
156
|
-
const envPath = process.env.npm_execpath
|
|
157
|
-
if (envPath && existsSync(envPath)) return realNpmCliPath(envPath)
|
|
158
|
-
return null
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
function realNpmCliPath(path: string): string {
|
|
162
|
-
const real = realPathOrSelf(path)
|
|
163
|
-
if (basename(real) === 'npx-cli.js') {
|
|
164
|
-
const npmCli = resolve(dirname(real), 'npm-cli.js')
|
|
165
|
-
if (existsSync(npmCli)) return npmCli
|
|
104
|
+
executable: invocation.executable,
|
|
105
|
+
prefixArgs: invocation.prefixArgs.map(realPathOrSelf),
|
|
166
106
|
}
|
|
167
|
-
return real
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
function resolveSootsimEntryScript(): string {
|
|
171
|
-
// prefer which(1) → handles the user's $PATH (npm/bun/pnpm global bin dirs).
|
|
172
|
-
// fall back to process.argv[1] for dev checkouts. resolve any symlink so
|
|
173
|
-
// launchd / systemd spawn the real script, not a /node_modules/.bin shim
|
|
174
|
-
// that may itself depend on PATH.
|
|
175
|
-
try {
|
|
176
|
-
const out = execFileSync('which', [rnxPublicBrand.commandName], {
|
|
177
|
-
encoding: 'utf8',
|
|
178
|
-
}).trim()
|
|
179
|
-
if (out && existsSync(out)) return realPathOrSelf(out)
|
|
180
|
-
} catch {}
|
|
181
|
-
const argv1 = process.argv[1]
|
|
182
|
-
if (argv1 && existsSync(argv1)) return realPathOrSelf(argv1)
|
|
183
|
-
throw new Error(`could not locate the ${rnxPublicBrand.commandName} binary`)
|
|
184
107
|
}
|
|
185
108
|
|
|
186
109
|
function realPathOrSelf(p: string): string {
|
|
@@ -193,7 +116,7 @@ function realPathOrSelf(p: string): string {
|
|
|
193
116
|
|
|
194
117
|
// --- plist / unit generation ---------------------------------------------
|
|
195
118
|
|
|
196
|
-
function renderPlist(invocation:
|
|
119
|
+
function renderPlist(invocation: RnxSelfInvocation, port: number): string {
|
|
197
120
|
mkdirSync(LOG_DIR, { recursive: true })
|
|
198
121
|
const stdout = resolve(LOG_DIR, 'bridge.out.log')
|
|
199
122
|
const stderr = resolve(LOG_DIR, 'bridge.err.log')
|
|
@@ -222,7 +145,7 @@ ${programArgs}
|
|
|
222
145
|
`
|
|
223
146
|
}
|
|
224
147
|
|
|
225
|
-
function renderSystemdUnit(invocation:
|
|
148
|
+
function renderSystemdUnit(invocation: RnxSelfInvocation, port: number): string {
|
|
226
149
|
mkdirSync(LOG_DIR_LINUX, { recursive: true })
|
|
227
150
|
const exec = [invocation.executable, ...invocation.prefixArgs].map(shellQuote).join(' ')
|
|
228
151
|
return `[Unit]
|
|
@@ -293,7 +216,7 @@ export async function daemonInstall({ port, force }: { port: number; force: bool
|
|
|
293
216
|
throw new Error('background daemon setup is unavailable in this environment')
|
|
294
217
|
}
|
|
295
218
|
|
|
296
|
-
const invocation =
|
|
219
|
+
const invocation = resolveDaemonInvocation()
|
|
297
220
|
console.log(` binary: ${[invocation.executable, ...invocation.prefixArgs].join(' ')}`)
|
|
298
221
|
console.log(` port: ${port}`)
|
|
299
222
|
|
|
@@ -511,10 +434,7 @@ export function teardownLegacyMacDaemonArtifacts(
|
|
|
511
434
|
|
|
512
435
|
/** stop + disable + remove the persistent launchd / systemd registration
|
|
513
436
|
* and its support files (mac .app bundle, log dir). does NOT touch user
|
|
514
|
-
* data under ~/.rnx. idempotent: each entry is checked before remove.
|
|
515
|
-
* exported for reuse by the npm preuninstall hook (scripts/preuninstall.cjs)
|
|
516
|
-
* via the bundled CLI so a `npm rm -g rnxsim` tears down the daemon before
|
|
517
|
-
* the bin disappears. */
|
|
437
|
+
* data under ~/.rnx. idempotent: each entry is checked before remove. */
|
|
518
438
|
export function teardownDaemonService(): TeardownReport {
|
|
519
439
|
const removed: string[] = []
|
|
520
440
|
if (process.platform === 'darwin') {
|
|
@@ -530,10 +450,12 @@ export function teardownDaemonService(): TeardownReport {
|
|
|
530
450
|
rmSync(plist, { force: true })
|
|
531
451
|
removed.push(plist)
|
|
532
452
|
}
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
453
|
+
// daemon-app holds only the generated .app launcher, so the whole
|
|
454
|
+
// directory goes with the service.
|
|
455
|
+
const appDir = daemonAppDir()
|
|
456
|
+
if (existsSync(appDir)) {
|
|
457
|
+
rmSync(appDir, { recursive: true, force: true })
|
|
458
|
+
removed.push(appDir)
|
|
537
459
|
}
|
|
538
460
|
if (existsSync(LOG_DIR)) {
|
|
539
461
|
rmSync(LOG_DIR, { recursive: true, force: true })
|
|
@@ -545,8 +467,8 @@ export function teardownDaemonService(): TeardownReport {
|
|
|
545
467
|
const unit = linuxUnitPath()
|
|
546
468
|
if (existsSync(unit)) {
|
|
547
469
|
// `disable --now` stops + disables in one call. ignore exit status: a
|
|
548
|
-
// unit
|
|
549
|
-
//
|
|
470
|
+
// unit whose ExecStart no longer exists can return non-zero while
|
|
471
|
+
// still successfully disabling the unit.
|
|
550
472
|
spawnSync('systemctl', ['--user', 'disable', '--now', SERVICE_NAME_LINUX], {
|
|
551
473
|
stdio: 'ignore',
|
|
552
474
|
})
|
|
@@ -571,10 +493,12 @@ export function teardownDaemonService(): TeardownReport {
|
|
|
571
493
|
return { removed, missing: true }
|
|
572
494
|
}
|
|
573
495
|
|
|
574
|
-
/** remove
|
|
575
|
-
* profiles
|
|
576
|
-
*
|
|
577
|
-
|
|
496
|
+
/** remove EVERYTHING under ~/.rnx: installed runtimes, electron support,
|
|
497
|
+
* device profiles and their app storage, live dev-bridge state, lockfile,
|
|
498
|
+
* and config. only `rnx daemon uninstall --purge` calls this, because a
|
|
499
|
+
* user who asked to remove a background service did not ask to lose the
|
|
500
|
+
* simulators they have been working in. idempotent. honors $RNX_HOME. */
|
|
501
|
+
export function teardownRnxHome(): TeardownReport {
|
|
578
502
|
const removed: string[] = []
|
|
579
503
|
const home = rnxHomeDir()
|
|
580
504
|
if (existsSync(home)) {
|
|
@@ -584,10 +508,36 @@ export function teardownSootsimHome(): TeardownReport {
|
|
|
584
508
|
return { removed, missing: removed.length === 0 }
|
|
585
509
|
}
|
|
586
510
|
|
|
587
|
-
|
|
511
|
+
// `uninstall` removes the background service and nothing else. device
|
|
512
|
+
// profiles, their app storage, and live dev-bridge state belong to simulators
|
|
513
|
+
// the user may still be working in, so deleting them is something you ask for
|
|
514
|
+
// by name with `--purge`.
|
|
515
|
+
async function daemonUninstall(rest: string[]) {
|
|
516
|
+
const unknown = rest.filter((arg) => arg !== '--purge')
|
|
517
|
+
if (unknown.length > 0) {
|
|
518
|
+
console.error(` unknown daemon uninstall option: ${unknown[0]}`)
|
|
519
|
+
printHelp()
|
|
520
|
+
rnxExit(1)
|
|
521
|
+
return
|
|
522
|
+
}
|
|
523
|
+
const purge = rest.includes('--purge')
|
|
524
|
+
|
|
588
525
|
const service = teardownDaemonService()
|
|
589
526
|
for (const path of service.removed) console.log(` removed ${path}`)
|
|
590
|
-
|
|
527
|
+
|
|
528
|
+
if (!purge) {
|
|
529
|
+
if (service.missing) console.log(' not installed')
|
|
530
|
+
const home = rnxHomeDir()
|
|
531
|
+
if (existsSync(home)) {
|
|
532
|
+
console.log(` kept ${home} (runtimes, device profiles, open simulators)`)
|
|
533
|
+
console.log(
|
|
534
|
+
` remove that too with \`${rnxPublicBrand.commandName} daemon uninstall --purge\``,
|
|
535
|
+
)
|
|
536
|
+
}
|
|
537
|
+
return
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
const home = teardownRnxHome()
|
|
591
541
|
for (const path of home.removed) console.log(` removed ${path}`)
|
|
592
542
|
if (service.missing && home.missing) console.log(' not installed')
|
|
593
543
|
}
|
|
@@ -2,6 +2,9 @@ import { spawnSync } from 'node:child_process'
|
|
|
2
2
|
import { existsSync, readFileSync } from 'node:fs'
|
|
3
3
|
import { createRequire } from 'node:module'
|
|
4
4
|
import { dirname, join } from 'node:path'
|
|
5
|
+
import { RNX_INTERNAL_CHILDREN, RNX_INTERNAL_COMMAND } from '../internal-child'
|
|
6
|
+
import { rnxSelfInvocation } from '../self-invocation'
|
|
7
|
+
|
|
5
8
|
export interface ResolvedPlaywright {
|
|
6
9
|
// package name that resolved (for human-facing messaging)
|
|
7
10
|
spec: string
|
|
@@ -24,23 +27,6 @@ export type PlaywrightBrowserProvisioningResult =
|
|
|
24
27
|
message: string
|
|
25
28
|
}
|
|
26
29
|
|
|
27
|
-
const PLAYWRIGHT_BROWSER_PROBE = `
|
|
28
|
-
const { existsSync } = require('node:fs');
|
|
29
|
-
const modulePath = process.env.SOOTSIM_PW_MODULE;
|
|
30
|
-
try {
|
|
31
|
-
const playwright = require(modulePath);
|
|
32
|
-
const browser = playwright && playwright.chromium;
|
|
33
|
-
if (!browser || typeof browser.executablePath !== 'function') {
|
|
34
|
-
throw new Error('resolved package does not expose browser "chromium"');
|
|
35
|
-
}
|
|
36
|
-
const executablePath = browser.executablePath();
|
|
37
|
-
process.stdout.write(JSON.stringify({ executablePath, exists: existsSync(executablePath) }));
|
|
38
|
-
} catch (error) {
|
|
39
|
-
process.stderr.write(String((error && error.stack) || error));
|
|
40
|
-
process.exit(1);
|
|
41
|
-
}
|
|
42
|
-
`
|
|
43
|
-
|
|
44
30
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
45
31
|
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
46
32
|
}
|
|
@@ -93,14 +79,19 @@ function probeBrowser(
|
|
|
93
79
|
resolved: ResolvedPlaywright,
|
|
94
80
|
env: NodeJS.ProcessEnv,
|
|
95
81
|
): BrowserProbeResult {
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
82
|
+
const self = rnxSelfInvocation()
|
|
83
|
+
const probe = spawnSync(
|
|
84
|
+
self.executable,
|
|
85
|
+
[...self.prefixArgs, RNX_INTERNAL_COMMAND, RNX_INTERNAL_CHILDREN.playwrightProbe],
|
|
86
|
+
{
|
|
87
|
+
env: {
|
|
88
|
+
...env,
|
|
89
|
+
SOOTSIM_PW_MODULE: resolved.modulePath,
|
|
90
|
+
},
|
|
91
|
+
encoding: 'utf8',
|
|
92
|
+
timeout: 30_000,
|
|
100
93
|
},
|
|
101
|
-
|
|
102
|
-
timeout: 30_000,
|
|
103
|
-
})
|
|
94
|
+
)
|
|
104
95
|
if (probe.status !== 0) {
|
|
105
96
|
const detail = (probe.stderr || probe.error?.message || 'browser probe failed').trim()
|
|
106
97
|
return {
|
|
@@ -145,11 +136,24 @@ export function ensurePlaywrightBrowser(
|
|
|
145
136
|
// the resolved package is the authority for both the required executable
|
|
146
137
|
// path and the installer. never scan a shared cache or select a neighboring
|
|
147
138
|
// revision: Playwright's own CLI installs the exact revision its API named.
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
139
|
+
// that CLI is a javascript file, so this CLI runs it as itself.
|
|
140
|
+
const installer = rnxSelfInvocation()
|
|
141
|
+
const install = spawnSync(
|
|
142
|
+
installer.executable,
|
|
143
|
+
[
|
|
144
|
+
...installer.prefixArgs,
|
|
145
|
+
RNX_INTERNAL_COMMAND,
|
|
146
|
+
RNX_INTERNAL_CHILDREN.playwrightInstall,
|
|
147
|
+
resolved.cliPath,
|
|
148
|
+
'install',
|
|
149
|
+
'chromium',
|
|
150
|
+
],
|
|
151
|
+
{
|
|
152
|
+
env,
|
|
153
|
+
encoding: 'utf8',
|
|
154
|
+
timeout: 10 * 60_000,
|
|
155
|
+
},
|
|
156
|
+
)
|
|
153
157
|
if (install.status !== 0) {
|
|
154
158
|
const rawDetail = (
|
|
155
159
|
install.stderr ||
|