sootsim 0.1.83 → 0.1.85
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 +0 -1
- package/detox/colors.ts +54 -0
- package/detox/config-loader.ts +135 -0
- package/detox/element-types.ts +36 -0
- package/detox/expectations.ts +477 -0
- package/detox/gestures.ts +442 -0
- package/detox/index.ts +1403 -0
- package/detox/jest-environment.ts +86 -0
- package/detox/jest-preset.cjs +50 -0
- package/detox/matchers.ts +29 -0
- package/detox/navigation.ts +43 -0
- package/detox/run-test.ts +113 -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-cli/bin.js +3 -3
- package/dist-cli/chunks/{agent-MQ7GLVIB.js → agent-T3DUH5YJ.js} +2 -2
- package/dist-cli/chunks/{agent-wrapper-7KAFDQCN.js → agent-wrapper-NSBF4THI.js} +2 -2
- package/dist-cli/chunks/{assert-TV46GUNU.js → assert-X3F7TRCZ.js} +2 -2
- package/dist-cli/chunks/auto-bootstrap-47RN2V5G.js +2 -0
- package/dist-cli/chunks/beta-BRCGAF2N.js +2 -0
- package/dist-cli/chunks/chunk-36RPD6JI.js +2 -0
- package/dist-cli/chunks/{chunk-PM5NVKLP.js → chunk-3WGHC7JN.js} +2 -2
- package/dist-cli/chunks/chunk-4DBPNLGI.js +1 -0
- package/dist-cli/chunks/{chunk-J2GYISVJ.js → chunk-4EVSIUNB.js} +2 -2
- package/dist-cli/chunks/{chunk-JHJNODXN.js → chunk-4QZHZ6BC.js} +2 -2
- package/dist-cli/chunks/{chunk-F3HP444U.js → chunk-5DIGWOY7.js} +1 -1
- package/dist-cli/chunks/{chunk-DP7O5MHK.js → chunk-5N3V7OCG.js} +2 -2
- package/dist-cli/chunks/{chunk-Y4BUVURT.js → chunk-5S6D7K4L.js} +2 -2
- package/dist-cli/chunks/{chunk-ECJBV65H.js → chunk-7LKUN46F.js} +2 -2
- package/dist-cli/chunks/{chunk-WTKTOL3C.js → chunk-AC6QGW22.js} +2 -2
- package/dist-cli/chunks/{chunk-IBNRRAES.js → chunk-AFNDVS4E.js} +2 -2
- package/dist-cli/chunks/{chunk-6TNANCQC.js → chunk-BESAZ2HA.js} +2 -2
- package/dist-cli/chunks/{chunk-WN7M3QON.js → chunk-BHZJ6RIH.js} +2 -2
- package/dist-cli/chunks/{chunk-277XAALA.js → chunk-BZL6D4TV.js} +3 -3
- package/dist-cli/chunks/{chunk-CYV6Y6YV.js → chunk-CF2LPRXD.js} +2 -2
- package/dist-cli/chunks/chunk-DWTLRPEN.js +79 -0
- package/dist-cli/chunks/{chunk-CJY3AVI7.js → chunk-E2QE5FFP.js} +1 -1
- package/dist-cli/chunks/chunk-EBEL6TTJ.js +4 -0
- package/dist-cli/chunks/{chunk-DM6WT7QM.js → chunk-EFM53PZ5.js} +1 -1
- package/dist-cli/chunks/{chunk-YUELRHGB.js → chunk-EKXK3SWK.js} +2 -2
- package/dist-cli/chunks/{chunk-4LS5MZAI.js → chunk-G7CIZ5S3.js} +3 -3
- package/dist-cli/chunks/{chunk-6NN2D4EJ.js → chunk-GTAD6IUV.js} +1 -1
- package/dist-cli/chunks/{chunk-OYMFNU3M.js → chunk-H44IQHKZ.js} +1 -1
- package/dist-cli/chunks/{chunk-IP3QJLRH.js → chunk-HQDJ5BOF.js} +1 -1
- package/dist-cli/chunks/{chunk-5DJXZIFZ.js → chunk-KUSQ4NNJ.js} +1 -1
- package/dist-cli/chunks/{chunk-HAWOAQAG.js → chunk-MAO7F5PH.js} +3 -3
- package/dist-cli/chunks/{chunk-572VSFNP.js → chunk-NVTL3JQG.js} +1 -1
- package/dist-cli/chunks/{chunk-6XZOEBTZ.js → chunk-O6N2CEET.js} +2 -2
- package/dist-cli/chunks/{chunk-HNWEELAE.js → chunk-OISHLFON.js} +1 -1
- package/dist-cli/chunks/{chunk-2PY3UZVO.js → chunk-OUNLJM56.js} +2 -2
- package/dist-cli/chunks/chunk-OXOARRKR.js +67 -0
- package/dist-cli/chunks/{chunk-NXATOWWF.js → chunk-PHPXGLME.js} +1 -1
- package/dist-cli/chunks/{chunk-JQ7ZXOXJ.js → chunk-PQFFUJR6.js} +2 -2
- package/dist-cli/chunks/{chunk-KASUZ5XV.js → chunk-QLJNSOS7.js} +1 -1
- package/dist-cli/chunks/chunk-QQAECG5B.js +2 -0
- package/dist-cli/chunks/{chunk-FJYT7XL2.js → chunk-RZHREO3M.js} +2 -2
- package/dist-cli/chunks/{chunk-FRM355UL.js → chunk-SBGOUA6F.js} +2 -2
- package/dist-cli/chunks/chunk-SSCA2AEA.js +1 -0
- package/dist-cli/chunks/{chunk-Y2VJBRSP.js → chunk-UYRGCJ4N.js} +1 -1
- package/dist-cli/chunks/{chunk-2AWQ7OB2.js → chunk-WGDL5V6C.js} +1 -1
- package/dist-cli/chunks/{chunk-VMXWC2JO.js → chunk-Y5PLPEEU.js} +2 -2
- package/dist-cli/chunks/chunk-ZFAM4N5B.js +1 -0
- package/dist-cli/chunks/{chunk-RH4F2TF7.js → chunk-ZO3VHP6W.js} +1 -1
- package/dist-cli/chunks/cli-version-WPFDM2A6.js +2 -0
- package/dist-cli/chunks/{compat-QLLWBTS3.js → compat-PCXGGZBZ.js} +3 -3
- package/dist-cli/chunks/{config-2DSLDCXV.js → config-LULEVEYL.js} +2 -2
- package/dist-cli/chunks/control-6P6HY7UF.js +2 -0
- package/dist-cli/chunks/{cpu-profile-GEIKHCPC.js → cpu-profile-NOK73ZYW.js} +2 -2
- package/dist-cli/chunks/{daemon-4EBUFN4D.js → daemon-4A3DMUYL.js} +2 -2
- package/dist-cli/chunks/{debug-WGD6XWOF.js → debug-74BWB2ZG.js} +3 -3
- package/dist-cli/chunks/{detox-LNKGRZU6.js → detox-HEOMINSC.js} +2 -2
- package/dist-cli/chunks/{device-AYKXKVIQ.js → device-TTXXBJFZ.js} +2 -2
- package/dist-cli/chunks/{diagnose-TMXSDOOC.js → diagnose-QZ3GOHSE.js} +2 -2
- package/dist-cli/chunks/drivers-QRPWNOIT.js +2 -0
- package/dist-cli/chunks/{electron-QFPF7TBY.js → electron-QVOWV44R.js} +3 -3
- package/dist-cli/chunks/flow-QMA7GVN6.js +2 -0
- package/dist-cli/chunks/{hints-MXKRR4TG.js → hints-YKWRNMJC.js} +2 -2
- package/dist-cli/chunks/{home-paths-REMWQDAO.js → home-paths-SFADSTJM.js} +2 -2
- package/dist-cli/chunks/{inspect-XGSQNFV7.js → inspect-LEWGQCIU.js} +3 -3
- package/dist-cli/chunks/install-7N2N7Q32.js +2 -0
- package/dist-cli/chunks/{install-desktop-NQG3RZSA.js → install-desktop-22HYQZ2G.js} +3 -3
- package/dist-cli/chunks/{keys-5QZWXL3F.js → keys-3ZT3MICU.js} +2 -2
- package/dist-cli/chunks/{launch-SBXOZWKO.js → launch-ZXW2NFLG.js} +3 -3
- package/dist-cli/chunks/{login-EACQXE24.js → login-NJKJ7GZO.js} +4 -4
- package/dist-cli/chunks/{logout-IBQLMUML.js → logout-VMMQL7CB.js} +2 -2
- package/dist-cli/chunks/{maestro-LFYXUX7O.js → maestro-OJY4MTI7.js} +2 -2
- package/dist-cli/chunks/{preview-U4SBOEGQ.js → preview-QU2GXTEV.js} +2 -2
- package/dist-cli/chunks/{profile-GWS5ECMY.js → profile-7APWK47T.js} +2 -2
- package/dist-cli/chunks/{react-QDHLMVYL.js → react-RSVO5JZZ.js} +2 -2
- package/dist-cli/chunks/{record-BUEUWPDI.js → record-UWH4MDEO.js} +2 -2
- package/dist-cli/chunks/runtime-3FUENRHM.js +2 -0
- package/dist-cli/chunks/{runtime-delivery-G7L6RVZ7.js → runtime-delivery-QMKGRV7N.js} +2 -2
- package/dist-cli/chunks/{screenshot-T2HBA3VI.js → screenshot-43M27ALE.js} +2 -2
- package/dist-cli/chunks/{screenshot-mode-EG5HMIH3.js → screenshot-mode-EBYYN6TY.js} +2 -2
- package/dist-cli/chunks/{screenshots-S52AFHTV.js → screenshots-7TQZL6Z6.js} +2 -2
- package/dist-cli/chunks/{server-MFFVYUGG.js → server-VCFM25Z6.js} +2 -2
- package/dist-cli/chunks/setup-repo-HFH4VKJQ.js +2 -0
- package/dist-cli/chunks/{skills-HQGWBS2O.js → skills-RQA6EJQL.js} +2 -2
- package/dist-cli/chunks/{start-E3DRYY7W.js → start-ZT6MBYND.js} +4 -4
- package/dist-cli/chunks/store-BJBTDSZE.js +2 -0
- package/dist-cli/chunks/telemetry-ZZZKTILZ.js +2 -0
- package/dist-cli/chunks/{test-ZY3EF62K.js → test-RNRX5SWV.js} +3 -3
- package/dist-cli/chunks/{three-mode-WSPKQCJ5.js → three-mode-TQZH25ZO.js} +2 -2
- package/dist-cli/chunks/{timeline-3XAB5EWZ.js → timeline-GGN3AY6P.js} +2 -2
- package/dist-cli/chunks/{upgrade-WNENPFM5.js → upgrade-XT22D67C.js} +2 -2
- package/dist-cli/chunks/upload-NC2AYLC5.js +2 -0
- package/dist-cli/chunks/{web-D2AOZY44.js → web-KEHVF5MB.js} +2 -2
- package/dist-cli/chunks/{what-happened-F43KNSG6.js → what-happened-PATQRJ5T.js} +2 -2
- package/dist-cli/chunks/{whoami-T22VBR7C.js → whoami-CXVY26VV.js} +2 -2
- package/dist-lib/agent-daemon-client.cjs +1 -1
- package/dist-lib/agent-events.cjs +1 -1
- package/dist-lib/agent-sessions.cjs +1 -1
- 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 +44 -0
- package/dist-lib/bridge-constants.cjs +1 -1
- package/dist-lib/cli-constants.cjs +1 -1
- package/dist-lib/config.cjs +1 -1
- package/dist-lib/detox/index.cjs +1770 -0
- package/dist-lib/detox/jest-preset.cjs +50 -0
- package/dist-lib/dev-bundle-resolution.cjs +1 -1
- package/dist-lib/home-paths.cjs +1 -1
- package/dist-lib/host/bridge-host.cjs +1 -1
- package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
- package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
- package/dist-lib/index.cjs +136 -138
- package/dist-lib/metro.cjs +31 -26
- package/dist-lib/profiles.cjs +1 -1
- package/dist-lib/render-mode.cjs +1 -1
- package/dist-lib/scripts/demo-app-registry.cjs +809 -0
- package/dist-lib/scripts/dev-server-scanner.cjs +1269 -0
- package/dist-lib/skills.cjs +17766 -0
- package/dist-lib/vite.cjs +129 -39
- package/package.json +39 -14
- package/scripts/demo-app-registry.ts +989 -0
- package/scripts/dev-server-scanner.ts +674 -0
- package/src/agent-daemon-client.ts +390 -0
- package/src/agent-events.ts +71 -0
- package/src/agent-prompt.ts +71 -0
- package/src/agent-sessions.ts +572 -0
- package/src/attached-projects.ts +536 -0
- package/src/auth/shared-session.ts +199 -0
- package/src/backend-origin.ts +49 -0
- package/src/beta.ts +21 -0
- package/src/bridge-constants.ts +10 -0
- package/src/cli-constants.ts +1 -0
- package/src/cli-version.ts +30 -0
- package/src/codex-client.ts +215 -0
- package/src/config.ts +110 -0
- package/src/dev-bundle-resolution.ts +180 -0
- package/src/home-paths.ts +382 -0
- package/src/host/agent-host.ts +576 -0
- package/src/host/bridge-host.ts +2293 -0
- package/src/host/fetch-proxy-handler.ts +288 -0
- package/src/host/fetch-proxy-overrides.ts +39 -0
- package/src/host/open-url.ts +234 -0
- package/src/index.ts +9 -0
- package/src/metro-plugin.ts +139 -0
- package/src/native-dev-bundle-url.ts +62 -0
- package/src/native-seam-manifest.ts +313 -0
- package/src/profiles.ts +179 -0
- package/src/render-mode.ts +27 -0
- package/src/runtime-assets.ts +84 -0
- package/src/runtime-delivery.ts +334 -0
- package/src/screenshots/compose.ts +422 -0
- package/src/screenshots/frame-compose.ts +438 -0
- package/src/screenshots/orchestrate.ts +244 -0
- package/src/screenshots/registry.ts +58 -0
- package/src/screenshots/schema.ts +364 -0
- package/src/skills/builtin/a11y-review.ts +126 -0
- package/src/skills/builtin/compat-check.ts +104 -0
- package/src/skills/builtin/perf-profile.ts +84 -0
- package/src/skills/builtin/screenshot-all.ts +46 -0
- package/src/skills/builtin/test-flow.ts +118 -0
- package/src/skills/builtin/visual-diff.ts +94 -0
- package/src/skills/registry.ts +107 -0
- package/src/skills/types.ts +41 -0
- package/src/vite-plugin-one.ts +189 -0
- package/src/vite-plugin.ts +1381 -0
- package/src/worklets-babel.ts +132 -0
- package/dist-cli/chunks/auto-bootstrap-FQS4ZD2K.js +0 -2
- package/dist-cli/chunks/beta-VG7CDY2U.js +0 -2
- package/dist-cli/chunks/chunk-2OIBDYHW.js +0 -1
- package/dist-cli/chunks/chunk-6BNLVMXA.js +0 -1
- package/dist-cli/chunks/chunk-6XD6CBJM.js +0 -2
- package/dist-cli/chunks/chunk-CHQTO426.js +0 -1
- package/dist-cli/chunks/chunk-FAPYGVIU.js +0 -4
- package/dist-cli/chunks/chunk-PEHFE3LG.js +0 -64
- package/dist-cli/chunks/chunk-RXH2SLKF.js +0 -2
- package/dist-cli/chunks/chunk-UXQWC5ZR.js +0 -79
- package/dist-cli/chunks/chunk-XFQL74PF.js +0 -5
- package/dist-cli/chunks/cli-version-PWF6I6LY.js +0 -2
- package/dist-cli/chunks/control-UIOXGYXU.js +0 -2
- package/dist-cli/chunks/demo-app-registry-G3BDOFWC.js +0 -2
- package/dist-cli/chunks/drivers-IDQF34HP.js +0 -2
- package/dist-cli/chunks/flow-3JN3Y7RF.js +0 -2
- package/dist-cli/chunks/install-2N3YOOSN.js +0 -2
- package/dist-cli/chunks/runtime-PVB4VGUH.js +0 -2
- package/dist-cli/chunks/setup-repo-YOF7NV5D.js +0 -2
- package/dist-cli/chunks/store-MAI6D3UO.js +0 -2
- package/dist-cli/chunks/telemetry-RCQKCJTH.js +0 -2
- package/dist-cli/chunks/upload-YLJ4RA73.js +0 -2
- package/dist-lib/vite-base.cjs +0 -6937
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { isLoopbackHost } from './backend-origin'
|
|
2
|
+
import { METRO_BUNDLE_PATH, normalizeNativeDevBundleUrl } from './native-dev-bundle-url'
|
|
3
|
+
|
|
4
|
+
export const DEFAULT_TEST_DRIVE_PORT = 8081
|
|
5
|
+
|
|
6
|
+
export interface ResolvedDevBundle {
|
|
7
|
+
bundleUrl: string
|
|
8
|
+
port: number
|
|
9
|
+
framework: string
|
|
10
|
+
projectName?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function buildUnresolvedTargetError(targetLabel: string): Error {
|
|
14
|
+
return new Error(
|
|
15
|
+
`could not resolve a native bundle for ${targetLabel}. pass an explicit bundle URL or open Connect and choose the app there.`,
|
|
16
|
+
)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function inferManifestFramework(
|
|
20
|
+
launchUrl: string | undefined,
|
|
21
|
+
sdkVersion: unknown,
|
|
22
|
+
): string {
|
|
23
|
+
if (launchUrl?.includes('/one/metro-entry.bundle')) return 'one'
|
|
24
|
+
if (typeof sdkVersion === 'string' && sdkVersion) return 'expo'
|
|
25
|
+
return 'unknown'
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function buildBaseUrl(protocol: string, host: string, port: number) {
|
|
29
|
+
return `${protocol}//${host}:${port}`
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function shouldProxyLoopbackProbe(targetUrl: string) {
|
|
33
|
+
if (typeof window === 'undefined') return false
|
|
34
|
+
try {
|
|
35
|
+
const parsed = new URL(targetUrl)
|
|
36
|
+
return (
|
|
37
|
+
isLoopbackHost(parsed.hostname) &&
|
|
38
|
+
isLoopbackHost(window.location.hostname) &&
|
|
39
|
+
parsed.origin !== window.location.origin
|
|
40
|
+
)
|
|
41
|
+
} catch {
|
|
42
|
+
return false
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function fetchDevProbe(targetUrl: string, init?: RequestInit): Promise<Response> {
|
|
47
|
+
const requestInit: RequestInit = {
|
|
48
|
+
...init,
|
|
49
|
+
cache: init?.cache ?? 'no-store',
|
|
50
|
+
}
|
|
51
|
+
if (!shouldProxyLoopbackProbe(targetUrl)) {
|
|
52
|
+
return fetch(targetUrl, requestInit)
|
|
53
|
+
}
|
|
54
|
+
return fetch(`/__fetch-proxy?url=${encodeURIComponent(targetUrl)}`, requestInit)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function getDefaultPortForProtocol(protocol: string) {
|
|
58
|
+
return protocol === 'https:' ? 443 : 80
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function isBaseServerUrl(url: URL) {
|
|
62
|
+
const path = url.pathname || '/'
|
|
63
|
+
return (path === '/' || path === '') && !url.search && !url.hash
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function probeBaseUrlBundle(
|
|
67
|
+
baseUrl: string,
|
|
68
|
+
port: number,
|
|
69
|
+
): Promise<ResolvedDevBundle | null> {
|
|
70
|
+
const normalizedBaseUrl = baseUrl.replace(/\/+$/, '')
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
const manifestRes = await fetchDevProbe(`${normalizedBaseUrl}/`, {
|
|
74
|
+
headers: { 'expo-platform': 'ios' },
|
|
75
|
+
})
|
|
76
|
+
if (manifestRes.ok) {
|
|
77
|
+
const manifest = await manifestRes.json()
|
|
78
|
+
const client = manifest?.extra?.expoClient || manifest?.extra || {}
|
|
79
|
+
const launchUrl =
|
|
80
|
+
typeof manifest?.launchAsset?.url === 'string'
|
|
81
|
+
? manifest.launchAsset.url
|
|
82
|
+
: undefined
|
|
83
|
+
if (launchUrl || client.name) {
|
|
84
|
+
return {
|
|
85
|
+
bundleUrl: normalizeNativeDevBundleUrl(
|
|
86
|
+
launchUrl || `${normalizedBaseUrl}${METRO_BUNDLE_PATH}`,
|
|
87
|
+
),
|
|
88
|
+
port,
|
|
89
|
+
framework: inferManifestFramework(launchUrl, client.sdkVersion),
|
|
90
|
+
projectName: client.name,
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
} catch {}
|
|
95
|
+
|
|
96
|
+
// vanilla Metro (or any non-Expo dev server) — packager-status:running on
|
|
97
|
+
// /status is the canonical signature. we previously also did `HEAD
|
|
98
|
+
// /node_modules/one/metro-entry.bundle` as a framework discriminator, but
|
|
99
|
+
// Metro lazy-resolves modules at request time, so HEAD on *any* `.bundle`
|
|
100
|
+
// path can return 200 even when the underlying file doesn't exist. that
|
|
101
|
+
// caused false-positive One framework detection on non-One apps that had
|
|
102
|
+
// `node_modules/one` anywhere on disk (or that returned 200 HEAD + 404
|
|
103
|
+
// GET for missing bundles), surfacing as
|
|
104
|
+
// "failed to fetch bundle (404|502): .../node_modules/one/metro-entry.bundle…"
|
|
105
|
+
// the Expo manifest probe above already returns the correct launchAsset.url
|
|
106
|
+
// for One apps (which serve the Expo manifest format via vxrn), so we
|
|
107
|
+
// drop the bespoke One probe entirely.
|
|
108
|
+
try {
|
|
109
|
+
const statusRes = await fetchDevProbe(`${normalizedBaseUrl}/status`)
|
|
110
|
+
if (statusRes.ok) {
|
|
111
|
+
const text = await statusRes.text()
|
|
112
|
+
if (text.includes('packager-status:running')) {
|
|
113
|
+
return {
|
|
114
|
+
bundleUrl: `${normalizedBaseUrl}${METRO_BUNDLE_PATH}`,
|
|
115
|
+
port,
|
|
116
|
+
framework: 'metro',
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
} catch {}
|
|
121
|
+
|
|
122
|
+
return null
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export async function probePortBundle(port: number): Promise<ResolvedDevBundle | null> {
|
|
126
|
+
return probeBaseUrlBundle(buildBaseUrl('http:', 'localhost', port), port)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export async function resolveConnectionInput(input: string): Promise<ResolvedDevBundle> {
|
|
130
|
+
const trimmed = input.trim()
|
|
131
|
+
|
|
132
|
+
if (/^\d+$/.test(trimmed)) {
|
|
133
|
+
const port = parseInt(trimmed, 10)
|
|
134
|
+
const resolved = await probePortBundle(port)
|
|
135
|
+
if (resolved) return resolved
|
|
136
|
+
// soot's demo launcher allocates ports via `preferredPort` + shift if the
|
|
137
|
+
// preferred port is in use, so a flow YAML that hardcodes `app: 8081`
|
|
138
|
+
// breaks the day someone else binds 8081 first. probe a small window of
|
|
139
|
+
// higher ports for a metro server before giving up. only the *first* hit
|
|
140
|
+
// is used — we never scan past 3 to keep the search bounded and avoid
|
|
141
|
+
// grabbing a completely unrelated dev server.
|
|
142
|
+
const SHIFT_WINDOW = 3
|
|
143
|
+
for (let offset = 1; offset <= SHIFT_WINDOW; offset++) {
|
|
144
|
+
const shifted = await probePortBundle(port + offset)
|
|
145
|
+
if (shifted) return shifted
|
|
146
|
+
}
|
|
147
|
+
throw buildUnresolvedTargetError(
|
|
148
|
+
`localhost:${port} (also scanned +1..+${SHIFT_WINDOW})`,
|
|
149
|
+
)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const bundleUrl = trimmed.startsWith('http') ? trimmed : `http://${trimmed}`
|
|
153
|
+
let parsed: URL
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
parsed = new URL(bundleUrl)
|
|
157
|
+
} catch {
|
|
158
|
+
throw new Error(
|
|
159
|
+
`could not parse "${input}". pass a dev-server port, a dev-server base URL, or a full bundle URL.`,
|
|
160
|
+
)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const protocol = parsed.protocol || 'http:'
|
|
164
|
+
const port = parsed.port
|
|
165
|
+
? parseInt(parsed.port, 10)
|
|
166
|
+
: getDefaultPortForProtocol(protocol)
|
|
167
|
+
const baseUrl = buildBaseUrl(protocol, parsed.hostname, port)
|
|
168
|
+
|
|
169
|
+
// a bare base URL (just an origin, no path) is unambiguously a dev server
|
|
170
|
+
// to probe — regardless of host. probe its live manifest rather than
|
|
171
|
+
// guessing that the origin itself is the bundle URL. loopback vs. remote
|
|
172
|
+
// only affects how the probe fetch is routed (see shouldProxyLoopbackProbe).
|
|
173
|
+
if (isBaseServerUrl(parsed)) {
|
|
174
|
+
const resolved = await probeBaseUrlBundle(baseUrl, port)
|
|
175
|
+
if (resolved) return resolved
|
|
176
|
+
throw buildUnresolvedTargetError(baseUrl)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return { bundleUrl: parsed.toString(), port, framework: 'unknown' }
|
|
180
|
+
}
|
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
// canonical filesystem layout under ~/.sootsim/. shared by the CLI and the
|
|
2
|
+
// electron main process so every surface agrees where runtimes, the daemon
|
|
3
|
+
// lockfile, and caches live.
|
|
4
|
+
//
|
|
5
|
+
// ~/.sootsim/
|
|
6
|
+
// ├── runtimes/
|
|
7
|
+
// │ ├── <version>/ unpacked dist/ of sootsim-engine
|
|
8
|
+
// │ └── active text file with the active version string (win-safe
|
|
9
|
+
// │ alternative to a symlink)
|
|
10
|
+
// ├── electron/
|
|
11
|
+
// │ └── <version>/ pinned electron binary (future: playwright-style)
|
|
12
|
+
// ├── profiles/
|
|
13
|
+
// │ └── profiles.json storage profile metadata
|
|
14
|
+
// ├── cache/
|
|
15
|
+
// │ └── sootsim-runtime-<version>.tar.gz
|
|
16
|
+
// ├── daemon.json lockfile: pid, ports, active runtime, heartbeat
|
|
17
|
+
// └── config.json user prefs: update channel, cdn origin override
|
|
18
|
+
|
|
19
|
+
import fs from 'node:fs'
|
|
20
|
+
import { homedir } from 'node:os'
|
|
21
|
+
import path from 'node:path'
|
|
22
|
+
|
|
23
|
+
export const SOOTSIM_HOME_ENV = 'SOOTSIM_HOME'
|
|
24
|
+
export const ACTIVE_RUNTIME_FILE = 'active'
|
|
25
|
+
export const DAEMON_LOCKFILE = 'daemon.json'
|
|
26
|
+
export const CONFIG_FILE = 'config.json'
|
|
27
|
+
export const DAEMON_HEARTBEAT_STALE_MS = 30_000
|
|
28
|
+
|
|
29
|
+
export function sootsimHomeDir(): string {
|
|
30
|
+
const override = process.env[SOOTSIM_HOME_ENV]
|
|
31
|
+
if (override && override.length > 0) return path.resolve(override)
|
|
32
|
+
return path.join(homedir(), '.sootsim')
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// detect when sootsim is running from a source checkout (the soot monorepo)
|
|
36
|
+
// rather than a published npm install. used to skip auto-install of the
|
|
37
|
+
// persistent launchd / systemd agent: dev shells shouldn't register an agent
|
|
38
|
+
// whose Program path points at workspace artifacts that change between
|
|
39
|
+
// sessions, and whose served engine assets are the stale prod build instead
|
|
40
|
+
// of the live `bun dev:sootsim` output.
|
|
41
|
+
//
|
|
42
|
+
// overrides:
|
|
43
|
+
// SOOTSIM_DEV=1 / SOOTSIM_DEV=0 force the answer
|
|
44
|
+
// SOOTSIM_FORCE_DAEMON_INSTALL=1 pretend prod even from a dev
|
|
45
|
+
// checkout, for exercising the
|
|
46
|
+
// install path from this repo
|
|
47
|
+
//
|
|
48
|
+
// signal: realpath of process.argv[1] lands inside a `packages/sootsim/`
|
|
49
|
+
// directory. workspace bin symlinks (`node_modules/.bin/sootsim` →
|
|
50
|
+
// `packages/sootsim/dist-cli/bin.js`) and bun-direct invocations
|
|
51
|
+
// (`bun packages/sootsim/cli/bin.ts ...`) both match; published installs
|
|
52
|
+
// resolve under `node_modules/sootsim/` instead.
|
|
53
|
+
export function isSootsimDevCheckout(): boolean {
|
|
54
|
+
if (process.env.SOOTSIM_FORCE_DAEMON_INSTALL === '1') return false
|
|
55
|
+
const env = process.env.SOOTSIM_DEV
|
|
56
|
+
if (env === '1' || env === 'true') return true
|
|
57
|
+
if (env === '0' || env === 'false') return false
|
|
58
|
+
const argv1 = process.argv[1]
|
|
59
|
+
if (!argv1) return false
|
|
60
|
+
try {
|
|
61
|
+
const real = fs.realpathSync(argv1)
|
|
62
|
+
return real.includes(`${path.sep}packages${path.sep}sootsim${path.sep}`)
|
|
63
|
+
} catch {
|
|
64
|
+
return false
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function runtimesDir(): string {
|
|
69
|
+
return path.join(sootsimHomeDir(), 'runtimes')
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function runtimeDir(version: string): string {
|
|
73
|
+
return path.join(runtimesDir(), version)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function activeRuntimeFile(): string {
|
|
77
|
+
return path.join(runtimesDir(), ACTIVE_RUNTIME_FILE)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function electronDir(): string {
|
|
81
|
+
return path.join(sootsimHomeDir(), 'electron')
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function electronUserDataDir(): string {
|
|
85
|
+
return path.join(electronDir(), 'userData')
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function electronVersionDir(version: string): string {
|
|
89
|
+
return path.join(electronDir(), version)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function profilesDir(): string {
|
|
93
|
+
return path.join(sootsimHomeDir(), 'profiles')
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// the launchd-managed daemon spawns ProgramArguments[0] directly, and macOS
|
|
97
|
+
// Background Task Management attributes the entry to whoever code-signed
|
|
98
|
+
// that binary. pointing launchd at bun directly makes Login Items say
|
|
99
|
+
// "software from Jarred Sumner" (bun's signer); wrapping the invocation in
|
|
100
|
+
// an ad-hoc-signed .app bundle here gives BTM a CFBundleDisplayName to
|
|
101
|
+
// read instead.
|
|
102
|
+
export function daemonAppDir(): string {
|
|
103
|
+
return path.join(sootsimHomeDir(), 'daemon-app')
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function daemonAppBundlePath(): string {
|
|
107
|
+
return path.join(daemonAppDir(), 'SootSim Daemon.app')
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function daemonAppLauncherPath(): string {
|
|
111
|
+
return path.join(daemonAppBundlePath(), 'Contents', 'MacOS', 'sootsim-daemon')
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function cacheDir(): string {
|
|
115
|
+
return path.join(sootsimHomeDir(), 'cache')
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function daemonLockfilePath(): string {
|
|
119
|
+
return path.join(sootsimHomeDir(), DAEMON_LOCKFILE)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function configFilePath(): string {
|
|
123
|
+
return path.join(sootsimHomeDir(), CONFIG_FILE)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// --- shared sootsim config -----------------------------------------------
|
|
127
|
+
//
|
|
128
|
+
// ~/.sootsim/config.json is the single user-level config file every sootsim
|
|
129
|
+
// surface (cli, electron renderer, dev browser via electron ipc) agrees on.
|
|
130
|
+
//
|
|
131
|
+
// {
|
|
132
|
+
// "telemetry": true, // splash checkbox; cli + electron
|
|
133
|
+
// "settings": { // engine settings persisted across
|
|
134
|
+
// "onboardingComplete": true, // sessions. shape mirrors the
|
|
135
|
+
// "betaConsentVersion": 1, // PERSISTED_KEYS slice of
|
|
136
|
+
// "detailedTelemetry": false, // SettingsValues — see
|
|
137
|
+
// ... // sootsim-engine/settings/schema.ts
|
|
138
|
+
// }
|
|
139
|
+
// }
|
|
140
|
+
//
|
|
141
|
+
// telemetry default is opt-in (missing/corrupt config means "enabled") so a
|
|
142
|
+
// fresh install never silently drops error reports. settings default is empty
|
|
143
|
+
// (the engine's getDefaults() fills the rest).
|
|
144
|
+
|
|
145
|
+
export interface SharedConfig {
|
|
146
|
+
telemetry?: boolean
|
|
147
|
+
settings?: Record<string, unknown>
|
|
148
|
+
[key: string]: unknown
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function readSharedConfig(): SharedConfig {
|
|
152
|
+
try {
|
|
153
|
+
const raw = fs.readFileSync(configFilePath(), 'utf8')
|
|
154
|
+
const parsed = JSON.parse(raw) as SharedConfig
|
|
155
|
+
return parsed && typeof parsed === 'object' ? parsed : {}
|
|
156
|
+
} catch {
|
|
157
|
+
return {}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** merge `patch` into the shared config and atomically write to disk. nested
|
|
162
|
+
* objects (currently just `settings`) are shallow-merged so partial writes
|
|
163
|
+
* don't clobber unrelated fields. returns the new full snapshot. */
|
|
164
|
+
export function writeSharedConfig(patch: Partial<SharedConfig>): SharedConfig {
|
|
165
|
+
ensureSootsimHome()
|
|
166
|
+
const current = readSharedConfig()
|
|
167
|
+
const next: SharedConfig = { ...current, ...patch }
|
|
168
|
+
if (patch.settings && typeof patch.settings === 'object') {
|
|
169
|
+
next.settings = {
|
|
170
|
+
...(current.settings && typeof current.settings === 'object'
|
|
171
|
+
? current.settings
|
|
172
|
+
: {}),
|
|
173
|
+
...patch.settings,
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
const tmp = `${configFilePath()}.tmp`
|
|
177
|
+
fs.writeFileSync(tmp, `${JSON.stringify(next, null, 2)}\n`, 'utf8')
|
|
178
|
+
fs.renameSync(tmp, configFilePath())
|
|
179
|
+
return next
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function readTelemetryEnabled(): boolean {
|
|
183
|
+
return readSharedConfig().telemetry !== false
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function writeTelemetryEnabled(enabled: boolean): void {
|
|
187
|
+
writeSharedConfig({ telemetry: enabled })
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function ensureSootsimHome(): void {
|
|
191
|
+
fs.mkdirSync(sootsimHomeDir(), { recursive: true })
|
|
192
|
+
fs.mkdirSync(runtimesDir(), { recursive: true })
|
|
193
|
+
fs.mkdirSync(electronDir(), { recursive: true })
|
|
194
|
+
fs.mkdirSync(profilesDir(), { recursive: true })
|
|
195
|
+
fs.mkdirSync(cacheDir(), { recursive: true })
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** read the active runtime version string, or null if none is selected. */
|
|
199
|
+
export function readActiveRuntime(): string | null {
|
|
200
|
+
try {
|
|
201
|
+
const value = fs.readFileSync(activeRuntimeFile(), 'utf8').trim()
|
|
202
|
+
return value.length > 0 ? value : null
|
|
203
|
+
} catch {
|
|
204
|
+
return null
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** set the active runtime version. caller is responsible for verifying the
|
|
209
|
+
* version actually exists on disk before calling. */
|
|
210
|
+
export function writeActiveRuntime(version: string): void {
|
|
211
|
+
fs.mkdirSync(runtimesDir(), { recursive: true })
|
|
212
|
+
fs.writeFileSync(activeRuntimeFile(), `${version}\n`, 'utf8')
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** list installed runtime versions (directories under runtimes/). sorted
|
|
216
|
+
* ascending by semver when parseable, falling back to lexicographic for
|
|
217
|
+
* odd names. */
|
|
218
|
+
export function listInstalledRuntimes(): string[] {
|
|
219
|
+
try {
|
|
220
|
+
return fs
|
|
221
|
+
.readdirSync(runtimesDir(), { withFileTypes: true })
|
|
222
|
+
.filter((d) => d.isDirectory())
|
|
223
|
+
.map((d) => d.name)
|
|
224
|
+
.sort(compareSemver)
|
|
225
|
+
} catch {
|
|
226
|
+
return []
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** compare two version strings as coarse semver. treats pre-release
|
|
231
|
+
* tags as lower than release of the same major.minor.patch. callers
|
|
232
|
+
* that need strict semver compliance should use a dedicated library. */
|
|
233
|
+
export function compareSemver(a: string, b: string): number {
|
|
234
|
+
const parse = (v: string): [number[], string] => {
|
|
235
|
+
const hyphen = v.indexOf('-')
|
|
236
|
+
const core = hyphen >= 0 ? v.slice(0, hyphen) : v
|
|
237
|
+
const pre = hyphen >= 0 ? v.slice(hyphen + 1) : ''
|
|
238
|
+
const parts = core.split('.').map((n) => Number.parseInt(n, 10))
|
|
239
|
+
if (parts.some((n) => !Number.isFinite(n))) return [[Number.POSITIVE_INFINITY], v]
|
|
240
|
+
return [parts, pre]
|
|
241
|
+
}
|
|
242
|
+
const [an, ap] = parse(a)
|
|
243
|
+
const [bn, bp] = parse(b)
|
|
244
|
+
for (let i = 0; i < Math.max(an.length, bn.length); i++) {
|
|
245
|
+
const av = an[i] ?? 0
|
|
246
|
+
const bv = bn[i] ?? 0
|
|
247
|
+
if (av !== bv) return av - bv
|
|
248
|
+
}
|
|
249
|
+
// pre-release of same core sorts lower than release (empty pre).
|
|
250
|
+
if (ap === bp) return 0
|
|
251
|
+
if (!ap) return 1
|
|
252
|
+
if (!bp) return -1
|
|
253
|
+
return ap < bp ? -1 : 1
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** absolute path to the active runtime's directory, or null if none active
|
|
257
|
+
* or the active version is no longer installed. */
|
|
258
|
+
export function activeRuntimeDir(): string | null {
|
|
259
|
+
const version = readActiveRuntime()
|
|
260
|
+
if (!version) return null
|
|
261
|
+
const dir = runtimeDir(version)
|
|
262
|
+
try {
|
|
263
|
+
if (fs.statSync(dir).isDirectory()) return dir
|
|
264
|
+
} catch {}
|
|
265
|
+
return null
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// --- daemon lockfile ----------------------------------------------------
|
|
269
|
+
|
|
270
|
+
export interface DaemonLockfile {
|
|
271
|
+
/** sootsim cli/daemon version that wrote the lockfile. bumped whenever
|
|
272
|
+
* the lockfile shape changes so readers can gate on it. */
|
|
273
|
+
schema: 1
|
|
274
|
+
pid: number
|
|
275
|
+
/** platform — useful when one home dir is shared across platforms via NFS. */
|
|
276
|
+
platform: NodeJS.Platform
|
|
277
|
+
/** ws bridge port (where cli + electron open control connections). */
|
|
278
|
+
bridgePort: number
|
|
279
|
+
/** http runtime server port (where electron loads the renderer from). */
|
|
280
|
+
runtimePort: number
|
|
281
|
+
/** active runtime version at boot, or null if the daemon booted with no
|
|
282
|
+
* runtime installed. updated live when the user runs `sootsim runtime use`. */
|
|
283
|
+
activeRuntime: string | null
|
|
284
|
+
/** absolute path to the active runtime's dist directory, or null. */
|
|
285
|
+
activeRuntimeDir: string | null
|
|
286
|
+
/** epoch-ms of daemon start. */
|
|
287
|
+
startedAt: number
|
|
288
|
+
/** epoch-ms of last heartbeat. daemons update this every ~5s; readers
|
|
289
|
+
* treat the lockfile as stale if now - heartbeatAt > DAEMON_HEARTBEAT_STALE_MS. */
|
|
290
|
+
heartbeatAt: number
|
|
291
|
+
/** true while the daemon is still fetching/activating its runtime on first
|
|
292
|
+
* boot. clients (electron splash, cli) should wait for this to become
|
|
293
|
+
* false before treating the daemon as ready to serve. */
|
|
294
|
+
bootstrapping?: boolean
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const DAEMON_LOCKFILE_MAX_BYTES = 16 * 1024
|
|
298
|
+
|
|
299
|
+
export function readDaemonLockfile(): DaemonLockfile | null {
|
|
300
|
+
try {
|
|
301
|
+
// cap the read so a junk/large file on the lockfile path can't OOM
|
|
302
|
+
// the CLI when someone has been messing with ~/.sootsim/.
|
|
303
|
+
const fd = fs.openSync(daemonLockfilePath(), 'r')
|
|
304
|
+
try {
|
|
305
|
+
const buf = Buffer.alloc(DAEMON_LOCKFILE_MAX_BYTES)
|
|
306
|
+
const bytesRead = fs.readSync(fd, buf, 0, DAEMON_LOCKFILE_MAX_BYTES, 0)
|
|
307
|
+
const raw = buf.subarray(0, bytesRead).toString('utf8')
|
|
308
|
+
const parsed = JSON.parse(raw) as Partial<DaemonLockfile>
|
|
309
|
+
if (
|
|
310
|
+
parsed &&
|
|
311
|
+
parsed.schema === 1 &&
|
|
312
|
+
typeof parsed.pid === 'number' &&
|
|
313
|
+
typeof parsed.bridgePort === 'number' &&
|
|
314
|
+
typeof parsed.runtimePort === 'number' &&
|
|
315
|
+
typeof parsed.startedAt === 'number' &&
|
|
316
|
+
typeof parsed.heartbeatAt === 'number'
|
|
317
|
+
) {
|
|
318
|
+
return parsed as DaemonLockfile
|
|
319
|
+
}
|
|
320
|
+
return null
|
|
321
|
+
} finally {
|
|
322
|
+
fs.closeSync(fd)
|
|
323
|
+
}
|
|
324
|
+
} catch {
|
|
325
|
+
return null
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/** true when the lockfile exists, the named pid is alive, and the heartbeat
|
|
330
|
+
* is recent. callers should reach for this before trusting any of the
|
|
331
|
+
* ports inside.
|
|
332
|
+
*
|
|
333
|
+
* pid-reuse note: `process.kill(pid, 0)` only checks that *some* process
|
|
334
|
+
* with that pid exists. after a reboot or heavy fork churn the OS can
|
|
335
|
+
* recycle the pid onto an unrelated process owned by the same user. the
|
|
336
|
+
* heartbeat freshness check catches most of that (30s stale ⇒ reject),
|
|
337
|
+
* but a stale lockfile where pid happens to be reused < 30s ago could
|
|
338
|
+
* still slip through. the consumer paths that actually connect (electron
|
|
339
|
+
* + ws-bridge client) time out quickly, so a stale lockfile degrades to
|
|
340
|
+
* "connect attempt fails" rather than corrupt state. */
|
|
341
|
+
export function isDaemonLockfileFresh(
|
|
342
|
+
lock: DaemonLockfile | null,
|
|
343
|
+
now = Date.now(),
|
|
344
|
+
): lock is DaemonLockfile {
|
|
345
|
+
if (!lock) return false
|
|
346
|
+
if (now - lock.heartbeatAt > DAEMON_HEARTBEAT_STALE_MS) return false
|
|
347
|
+
try {
|
|
348
|
+
// signal 0 just tests whether the pid exists + we have perm to signal.
|
|
349
|
+
process.kill(lock.pid, 0)
|
|
350
|
+
return true
|
|
351
|
+
} catch {
|
|
352
|
+
return false
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
export function writeDaemonLockfile(data: DaemonLockfile): void {
|
|
357
|
+
ensureSootsimHome()
|
|
358
|
+
const tmp = `${daemonLockfilePath()}.tmp`
|
|
359
|
+
fs.writeFileSync(tmp, `${JSON.stringify(data, null, 2)}\n`, 'utf8')
|
|
360
|
+
fs.renameSync(tmp, daemonLockfilePath())
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/** try to claim the lockfile atomically on daemon boot. returns true if
|
|
364
|
+
* we now own it. returns false when another fresh daemon beat us to the
|
|
365
|
+
* punch — callers should refuse to start. a stale lockfile (dead pid or
|
|
366
|
+
* old heartbeat) is overwritten. race-safe: the final fs.renameSync is
|
|
367
|
+
* atomic on POSIX and on NTFS. */
|
|
368
|
+
export function claimDaemonLockfile(data: DaemonLockfile): boolean {
|
|
369
|
+
ensureSootsimHome()
|
|
370
|
+
const existing = readDaemonLockfile()
|
|
371
|
+
if (existing && isDaemonLockfileFresh(existing) && existing.pid !== data.pid) {
|
|
372
|
+
return false
|
|
373
|
+
}
|
|
374
|
+
writeDaemonLockfile(data)
|
|
375
|
+
return true
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
export function removeDaemonLockfile(): void {
|
|
379
|
+
try {
|
|
380
|
+
fs.unlinkSync(daemonLockfilePath())
|
|
381
|
+
} catch {}
|
|
382
|
+
}
|