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
package/src/profiles.ts
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
import { electronUserDataDir, ensureSootsimHome, profilesDir } from './home-paths'
|
|
4
|
+
|
|
5
|
+
export const DEFAULT_PROFILE_ID = 'default'
|
|
6
|
+
const PROFILE_INDEX_FILE = 'profiles.json'
|
|
7
|
+
const PROFILE_INDEX_VERSION = 1 as const
|
|
8
|
+
|
|
9
|
+
export interface StorageProfile {
|
|
10
|
+
id: string
|
|
11
|
+
createdAt: string
|
|
12
|
+
updatedAt: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface ProfileIndex {
|
|
16
|
+
version: 1
|
|
17
|
+
profiles: StorageProfile[]
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function normalizeProfileId(input: string): string {
|
|
21
|
+
const id = input.trim()
|
|
22
|
+
if (!id) throw new Error('profile id is required')
|
|
23
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(id)) {
|
|
24
|
+
throw new Error(
|
|
25
|
+
'profile ids must start with a letter or number and contain only letters, numbers, dot, dash, or underscore',
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
if (id === '.' || id === '..') throw new Error(`invalid profile id: ${id}`)
|
|
29
|
+
return id
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function profileIndexPath(): string {
|
|
33
|
+
return path.join(profilesDir(), PROFILE_INDEX_FILE)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function electronProfilePartitionName(profileId: string): string {
|
|
37
|
+
return `sootsim-profile-${normalizeProfileId(profileId)}`
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function electronProfilePartition(profileId: string): string {
|
|
41
|
+
return `persist:${electronProfilePartitionName(profileId)}`
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function electronProfilePartitionDir(profileId: string): string {
|
|
45
|
+
return path.join(
|
|
46
|
+
electronUserDataDir(),
|
|
47
|
+
'Partitions',
|
|
48
|
+
electronProfilePartitionName(profileId),
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function playwrightProfileUserDataDir(profileId: string): string {
|
|
53
|
+
return path.join(profilesDir(), 'playwright', normalizeProfileId(profileId))
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function readIndexRaw(): ProfileIndex {
|
|
57
|
+
try {
|
|
58
|
+
const parsed = JSON.parse(
|
|
59
|
+
fs.readFileSync(profileIndexPath(), 'utf8'),
|
|
60
|
+
) as Partial<ProfileIndex> | null
|
|
61
|
+
if (!parsed || parsed.version !== PROFILE_INDEX_VERSION) {
|
|
62
|
+
return { version: PROFILE_INDEX_VERSION, profiles: [] }
|
|
63
|
+
}
|
|
64
|
+
const profiles = Array.isArray(parsed.profiles)
|
|
65
|
+
? parsed.profiles
|
|
66
|
+
.filter(
|
|
67
|
+
(profile): profile is StorageProfile =>
|
|
68
|
+
!!profile &&
|
|
69
|
+
typeof profile.id === 'string' &&
|
|
70
|
+
typeof profile.createdAt === 'string' &&
|
|
71
|
+
typeof profile.updatedAt === 'string',
|
|
72
|
+
)
|
|
73
|
+
.map((profile) => ({
|
|
74
|
+
id: normalizeProfileId(profile.id),
|
|
75
|
+
createdAt: profile.createdAt,
|
|
76
|
+
updatedAt: profile.updatedAt,
|
|
77
|
+
}))
|
|
78
|
+
: []
|
|
79
|
+
return { version: PROFILE_INDEX_VERSION, profiles }
|
|
80
|
+
} catch {
|
|
81
|
+
return { version: PROFILE_INDEX_VERSION, profiles: [] }
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function sortProfiles(profiles: StorageProfile[]): StorageProfile[] {
|
|
86
|
+
return [...profiles].sort((a, b) => {
|
|
87
|
+
if (a.id === DEFAULT_PROFILE_ID) return -1
|
|
88
|
+
if (b.id === DEFAULT_PROFILE_ID) return 1
|
|
89
|
+
return a.id.localeCompare(b.id)
|
|
90
|
+
})
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function withDefault(index: ProfileIndex): ProfileIndex {
|
|
94
|
+
if (index.profiles.some((profile) => profile.id === DEFAULT_PROFILE_ID)) {
|
|
95
|
+
return { ...index, profiles: sortProfiles(index.profiles) }
|
|
96
|
+
}
|
|
97
|
+
const now = new Date().toISOString()
|
|
98
|
+
return {
|
|
99
|
+
version: PROFILE_INDEX_VERSION,
|
|
100
|
+
profiles: sortProfiles([
|
|
101
|
+
{ id: DEFAULT_PROFILE_ID, createdAt: now, updatedAt: now },
|
|
102
|
+
...index.profiles,
|
|
103
|
+
]),
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function writeIndex(index: ProfileIndex): ProfileIndex {
|
|
108
|
+
ensureSootsimHome()
|
|
109
|
+
const next = withDefault(index)
|
|
110
|
+
const tmp = `${profileIndexPath()}.tmp`
|
|
111
|
+
fs.writeFileSync(tmp, `${JSON.stringify(next, null, 2)}\n`, 'utf8')
|
|
112
|
+
fs.renameSync(tmp, profileIndexPath())
|
|
113
|
+
return next
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function listProfiles(): StorageProfile[] {
|
|
117
|
+
return writeIndex(readIndexRaw()).profiles
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function getProfile(id: string): StorageProfile | null {
|
|
121
|
+
const normalized = normalizeProfileId(id)
|
|
122
|
+
return listProfiles().find((profile) => profile.id === normalized) ?? null
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function ensureProfile(id = DEFAULT_PROFILE_ID): StorageProfile {
|
|
126
|
+
const normalized = normalizeProfileId(id)
|
|
127
|
+
const existing = getProfile(normalized)
|
|
128
|
+
if (existing) return existing
|
|
129
|
+
return createProfile(normalized)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function createProfile(id: string): StorageProfile {
|
|
133
|
+
const normalized = normalizeProfileId(id)
|
|
134
|
+
const index = withDefault(readIndexRaw())
|
|
135
|
+
if (index.profiles.some((profile) => profile.id === normalized)) {
|
|
136
|
+
throw new Error(`profile already exists: ${normalized}`)
|
|
137
|
+
}
|
|
138
|
+
const now = new Date().toISOString()
|
|
139
|
+
const profile = { id: normalized, createdAt: now, updatedAt: now }
|
|
140
|
+
writeIndex({ version: PROFILE_INDEX_VERSION, profiles: [...index.profiles, profile] })
|
|
141
|
+
return profile
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function nextGeneratedProfileId(): string {
|
|
145
|
+
const existing = new Set(listProfiles().map((profile) => profile.id))
|
|
146
|
+
for (let i = 1; i < 10_000; i++) {
|
|
147
|
+
const id = `profile-${i}`
|
|
148
|
+
if (!existing.has(id)) return id
|
|
149
|
+
}
|
|
150
|
+
throw new Error('could not allocate a new profile id')
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function deleteProfile(id: string): StorageProfile {
|
|
154
|
+
const normalized = normalizeProfileId(id)
|
|
155
|
+
if (normalized === DEFAULT_PROFILE_ID) {
|
|
156
|
+
throw new Error('the default profile cannot be deleted; clear it instead')
|
|
157
|
+
}
|
|
158
|
+
const index = withDefault(readIndexRaw())
|
|
159
|
+
const profile = index.profiles.find((entry) => entry.id === normalized)
|
|
160
|
+
if (!profile) throw new Error(`profile not found: ${normalized}`)
|
|
161
|
+
writeIndex({
|
|
162
|
+
version: PROFILE_INDEX_VERSION,
|
|
163
|
+
profiles: index.profiles.filter((entry) => entry.id !== normalized),
|
|
164
|
+
})
|
|
165
|
+
clearProfileStorage(normalized)
|
|
166
|
+
return profile
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function clearProfileStorage(id: string): void {
|
|
170
|
+
const normalized = normalizeProfileId(id)
|
|
171
|
+
for (const dir of [
|
|
172
|
+
electronProfilePartitionDir(normalized),
|
|
173
|
+
playwrightProfileUserDataDir(normalized),
|
|
174
|
+
]) {
|
|
175
|
+
try {
|
|
176
|
+
fs.rmSync(dir, { recursive: true, force: true })
|
|
177
|
+
} catch {}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export type RenderMode = 'worker'
|
|
2
|
+
|
|
3
|
+
// worker rendering is now the only supported mode.
|
|
4
|
+
export function getRenderMode(_searchParams: URLSearchParams): RenderMode {
|
|
5
|
+
return 'worker'
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function isRenderWorkerMode(_searchParams: URLSearchParams): boolean {
|
|
9
|
+
return true
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function isRenderMainMode(_searchParams: URLSearchParams): boolean {
|
|
13
|
+
return false
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function hasRenderWorkerBootstrap(
|
|
17
|
+
workerModuleBaseUrl: string | null | undefined,
|
|
18
|
+
): boolean {
|
|
19
|
+
return typeof workerModuleBaseUrl === 'string' && workerModuleBaseUrl.length > 0
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function shouldEnableRenderWorker(
|
|
23
|
+
_searchParams: URLSearchParams,
|
|
24
|
+
workerModuleBaseUrl: string | null | undefined,
|
|
25
|
+
): boolean {
|
|
26
|
+
return hasRenderWorkerBootstrap(workerModuleBaseUrl)
|
|
27
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// helpers for serving the installed sootsim engine runtime from framework
|
|
2
|
+
// dev servers. the runtime is managed by `sootsim runtime install` and lives
|
|
3
|
+
// under ~/.sootsim/runtimes/<version>.
|
|
4
|
+
|
|
5
|
+
import fs from 'fs'
|
|
6
|
+
import path from 'path'
|
|
7
|
+
import { readActiveRuntime, runtimeDir } from './home-paths'
|
|
8
|
+
|
|
9
|
+
export const SOOTSIM_RUNTIME_MISSING_MESSAGE =
|
|
10
|
+
'[sootsim] no engine runtime installed — run `sootsim setup-repo` in your project, or `sootsim runtime install`'
|
|
11
|
+
|
|
12
|
+
const MIME_TYPES: Record<string, string> = {
|
|
13
|
+
'.js': 'application/javascript',
|
|
14
|
+
'.mjs': 'application/javascript',
|
|
15
|
+
'.css': 'text/css',
|
|
16
|
+
'.html': 'text/html',
|
|
17
|
+
'.wasm': 'application/wasm',
|
|
18
|
+
'.json': 'application/json',
|
|
19
|
+
'.jpg': 'image/jpeg',
|
|
20
|
+
'.jpeg': 'image/jpeg',
|
|
21
|
+
'.png': 'image/png',
|
|
22
|
+
'.svg': 'image/svg+xml',
|
|
23
|
+
'.webp': 'image/webp',
|
|
24
|
+
'.glb': 'model/gltf-binary',
|
|
25
|
+
'.ttf': 'font/ttf',
|
|
26
|
+
'.otf': 'font/otf',
|
|
27
|
+
'.woff': 'font/woff',
|
|
28
|
+
'.woff2': 'font/woff2',
|
|
29
|
+
'.mp3': 'audio/mpeg',
|
|
30
|
+
'.wav': 'audio/wav',
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const ROOT_RUNTIME_PATHS = [
|
|
34
|
+
'/assets/',
|
|
35
|
+
'/engine/',
|
|
36
|
+
'/engine-tenant/',
|
|
37
|
+
'/photos/',
|
|
38
|
+
'/three-mode/',
|
|
39
|
+
'/canvaskit.wasm',
|
|
40
|
+
'/fonts/',
|
|
41
|
+
'/icons/',
|
|
42
|
+
'/sounds/',
|
|
43
|
+
'/spike/',
|
|
44
|
+
'/test-wallpaper.jpg',
|
|
45
|
+
'/preview-sw.js',
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
export function resolveActiveRuntimeRoot(): string | null {
|
|
49
|
+
const active = readActiveRuntime()
|
|
50
|
+
if (!active) return null
|
|
51
|
+
const dir = runtimeDir(active)
|
|
52
|
+
if (!fs.existsSync(path.join(dir, 'index.html'))) return null
|
|
53
|
+
return dir
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function isRootRuntimeAssetPath(pathname: string): boolean {
|
|
57
|
+
return ROOT_RUNTIME_PATHS.some((p) => pathname === p || pathname.startsWith(p))
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function resolveRuntimeFilePath(
|
|
61
|
+
runtimeRoot: string,
|
|
62
|
+
pathname: string,
|
|
63
|
+
): string | null {
|
|
64
|
+
if (!pathname.startsWith('/')) return null
|
|
65
|
+
if (pathname.includes('\0') || pathname.includes('\\')) return null
|
|
66
|
+
for (const segment of pathname.split('/')) {
|
|
67
|
+
if (segment === '..') return null
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const fullPath = path.resolve(runtimeRoot, pathname.replace(/^\/+/, ''))
|
|
71
|
+
const rootWithSep = runtimeRoot.endsWith(path.sep)
|
|
72
|
+
? runtimeRoot
|
|
73
|
+
: runtimeRoot + path.sep
|
|
74
|
+
if (!fullPath.startsWith(rootWithSep) && fullPath !== runtimeRoot) return null
|
|
75
|
+
if (!fs.existsSync(fullPath) || !fs.statSync(fullPath).isFile()) return null
|
|
76
|
+
return fullPath
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function serveRuntimeFile(res: any, fullPath: string) {
|
|
80
|
+
const ext = path.extname(fullPath)
|
|
81
|
+
res.setHeader('content-type', MIME_TYPES[ext] || 'application/octet-stream')
|
|
82
|
+
res.setHeader('cache-control', 'max-age=31536000,immutable')
|
|
83
|
+
fs.createReadStream(fullPath).pipe(res)
|
|
84
|
+
}
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
import { spawn } from 'child_process'
|
|
2
|
+
import crypto from 'crypto'
|
|
3
|
+
import fs from 'fs'
|
|
4
|
+
import path from 'path'
|
|
5
|
+
import { Readable } from 'stream'
|
|
6
|
+
import { pipeline } from 'stream/promises'
|
|
7
|
+
import {
|
|
8
|
+
cacheDir,
|
|
9
|
+
compareSemver,
|
|
10
|
+
configFilePath,
|
|
11
|
+
ensureSootsimHome,
|
|
12
|
+
readActiveRuntime,
|
|
13
|
+
runtimeDir,
|
|
14
|
+
writeActiveRuntime,
|
|
15
|
+
} from './home-paths'
|
|
16
|
+
|
|
17
|
+
export const DEFAULT_RUNTIME_CDN_ORIGIN = 'https://sootbean.com'
|
|
18
|
+
export const RUNTIME_CDN_ORIGIN_ENV = 'SOOTSIM_CDN_ORIGIN'
|
|
19
|
+
export const RUNTIME_CHANNEL_ENV = 'SOOTSIM_RUNTIME_CHANNEL'
|
|
20
|
+
|
|
21
|
+
export interface RuntimeManifestChannel {
|
|
22
|
+
latest: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface RuntimeManifestVersion {
|
|
26
|
+
tarball?: string
|
|
27
|
+
sha256: string
|
|
28
|
+
size?: number
|
|
29
|
+
published?: string
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface RuntimeManifest {
|
|
33
|
+
channels: Record<string, RuntimeManifestChannel>
|
|
34
|
+
versions: Record<string, RuntimeManifestVersion>
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface RuntimeInstallOptions {
|
|
38
|
+
version?: string | null
|
|
39
|
+
channel?: string
|
|
40
|
+
cdnOrigin?: string
|
|
41
|
+
force?: boolean
|
|
42
|
+
setActive?: boolean
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface RuntimeInstallResult {
|
|
46
|
+
version: string
|
|
47
|
+
channel: string
|
|
48
|
+
cdnOrigin: string
|
|
49
|
+
runtimeDir: string
|
|
50
|
+
installed: boolean
|
|
51
|
+
activated: boolean
|
|
52
|
+
manifest: RuntimeManifest
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface RuntimeUpdateResult {
|
|
56
|
+
checked: boolean
|
|
57
|
+
updated: boolean
|
|
58
|
+
reason?: string
|
|
59
|
+
activeVersion: string | null
|
|
60
|
+
latestVersion?: string
|
|
61
|
+
install?: RuntimeInstallResult
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
interface SootsimConfig {
|
|
65
|
+
runtimeChannel?: string
|
|
66
|
+
cdnOrigin?: string
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function readConfig(): SootsimConfig {
|
|
70
|
+
try {
|
|
71
|
+
const parsed = JSON.parse(fs.readFileSync(configFilePath(), 'utf8')) as SootsimConfig
|
|
72
|
+
return parsed && typeof parsed === 'object' ? parsed : {}
|
|
73
|
+
} catch {
|
|
74
|
+
return {}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function resolveRuntimeCdnOrigin(input?: string): string {
|
|
79
|
+
const config = readConfig()
|
|
80
|
+
const value =
|
|
81
|
+
input ||
|
|
82
|
+
process.env[RUNTIME_CDN_ORIGIN_ENV] ||
|
|
83
|
+
config.cdnOrigin ||
|
|
84
|
+
DEFAULT_RUNTIME_CDN_ORIGIN
|
|
85
|
+
return value.replace(/\/+$/, '')
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function resolveRuntimeChannel(input?: string): string {
|
|
89
|
+
const config = readConfig()
|
|
90
|
+
return input || process.env[RUNTIME_CHANNEL_ENV] || config.runtimeChannel || 'stable'
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function runtimeManifestUrl(cdnOrigin?: string): string {
|
|
94
|
+
const url = new URL(`${resolveRuntimeCdnOrigin(cdnOrigin)}/runtimes/manifest.json`)
|
|
95
|
+
// cdn edges can keep the no-query manifest around longer than its
|
|
96
|
+
// max-age. runtime checks need the latest manifest every time.
|
|
97
|
+
url.searchParams.set('t', String(Date.now()))
|
|
98
|
+
return url.toString()
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function runtimeTarballUrl(version: string, cdnOrigin?: string): string {
|
|
102
|
+
return `${resolveRuntimeCdnOrigin(cdnOrigin)}/runtimes/sootsim-runtime-${version}.tar.gz`
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export async function fetchRuntimeManifest(cdnOrigin?: string): Promise<RuntimeManifest> {
|
|
106
|
+
const url = runtimeManifestUrl(cdnOrigin)
|
|
107
|
+
const res = await fetch(url, { headers: { Accept: 'application/json' } })
|
|
108
|
+
if (!res.ok) {
|
|
109
|
+
throw new Error(`manifest fetch failed: ${res.status} ${res.statusText} (${url})`)
|
|
110
|
+
}
|
|
111
|
+
return (await res.json()) as RuntimeManifest
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function resolveRuntimeVersion(
|
|
115
|
+
manifest: RuntimeManifest,
|
|
116
|
+
opts: Pick<RuntimeInstallOptions, 'version' | 'channel'> = {},
|
|
117
|
+
): { version: string; channel: string; entry: RuntimeManifestVersion } {
|
|
118
|
+
const channel = resolveRuntimeChannel(opts.channel)
|
|
119
|
+
const version = opts.version || manifest.channels[channel]?.latest
|
|
120
|
+
if (!version) {
|
|
121
|
+
throw new Error(
|
|
122
|
+
`no version specified and channel '${channel}' has no latest entry in the manifest`,
|
|
123
|
+
)
|
|
124
|
+
}
|
|
125
|
+
const entry = manifest.versions[version]
|
|
126
|
+
if (!entry) {
|
|
127
|
+
throw new Error(
|
|
128
|
+
`version ${version} not found in manifest; available: ${
|
|
129
|
+
Object.keys(manifest.versions).slice(-10).join(', ') || '(none)'
|
|
130
|
+
}`,
|
|
131
|
+
)
|
|
132
|
+
}
|
|
133
|
+
return { version, channel, entry }
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export async function installRuntime(
|
|
137
|
+
opts: RuntimeInstallOptions = {},
|
|
138
|
+
): Promise<RuntimeInstallResult> {
|
|
139
|
+
ensureSootsimHome()
|
|
140
|
+
|
|
141
|
+
const cdnOrigin = resolveRuntimeCdnOrigin(opts.cdnOrigin)
|
|
142
|
+
const manifest = await fetchRuntimeManifest(cdnOrigin)
|
|
143
|
+
const { version, channel, entry } = resolveRuntimeVersion(manifest, opts)
|
|
144
|
+
const destDir = runtimeDir(version)
|
|
145
|
+
const setActive = opts.setActive !== false
|
|
146
|
+
|
|
147
|
+
if (!opts.force && fs.existsSync(path.join(destDir, 'index.html'))) {
|
|
148
|
+
if (setActive) writeActiveRuntime(version)
|
|
149
|
+
return {
|
|
150
|
+
version,
|
|
151
|
+
channel,
|
|
152
|
+
cdnOrigin,
|
|
153
|
+
runtimeDir: destDir,
|
|
154
|
+
installed: false,
|
|
155
|
+
activated: setActive,
|
|
156
|
+
manifest,
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const tarUrl = entry.tarball || runtimeTarballUrl(version, cdnOrigin)
|
|
161
|
+
const tarCachePath = path.join(cacheDir(), `sootsim-runtime-${version}.tar.gz`)
|
|
162
|
+
process.stderr.write(`sootsim: downloading runtime ${version}…\n`)
|
|
163
|
+
await downloadToFile(tarUrl, tarCachePath)
|
|
164
|
+
process.stderr.write(`sootsim: extracting runtime ${version}…\n`)
|
|
165
|
+
|
|
166
|
+
const actualSha = await sha256File(tarCachePath)
|
|
167
|
+
if (actualSha !== entry.sha256) {
|
|
168
|
+
fs.rmSync(tarCachePath, { force: true })
|
|
169
|
+
throw new Error(
|
|
170
|
+
`sha256 mismatch for runtime ${version}: expected ${entry.sha256}, actual ${actualSha}`,
|
|
171
|
+
)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const tmpDir = path.join(path.dirname(destDir), `.installing-${version}-${process.pid}`)
|
|
175
|
+
fs.rmSync(tmpDir, { recursive: true, force: true })
|
|
176
|
+
fs.mkdirSync(tmpDir, { recursive: true })
|
|
177
|
+
try {
|
|
178
|
+
await extractTarball(tarCachePath, tmpDir)
|
|
179
|
+
if (!fs.existsSync(path.join(tmpDir, 'index.html'))) {
|
|
180
|
+
throw new Error(`extracted tarball for runtime ${version} is missing index.html`)
|
|
181
|
+
}
|
|
182
|
+
fs.rmSync(destDir, { recursive: true, force: true })
|
|
183
|
+
fs.renameSync(tmpDir, destDir)
|
|
184
|
+
} catch (err) {
|
|
185
|
+
fs.rmSync(tmpDir, { recursive: true, force: true })
|
|
186
|
+
throw err
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (setActive) writeActiveRuntime(version)
|
|
190
|
+
return {
|
|
191
|
+
version,
|
|
192
|
+
channel,
|
|
193
|
+
cdnOrigin,
|
|
194
|
+
runtimeDir: destDir,
|
|
195
|
+
installed: true,
|
|
196
|
+
activated: setActive,
|
|
197
|
+
manifest,
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export async function updateRuntimeToLatest(
|
|
202
|
+
opts: Pick<RuntimeInstallOptions, 'channel' | 'cdnOrigin'> = {},
|
|
203
|
+
): Promise<RuntimeUpdateResult> {
|
|
204
|
+
ensureSootsimHome()
|
|
205
|
+
const cdnOrigin = resolveRuntimeCdnOrigin(opts.cdnOrigin)
|
|
206
|
+
const channel = resolveRuntimeChannel(opts.channel)
|
|
207
|
+
const manifest = await fetchRuntimeManifest(cdnOrigin)
|
|
208
|
+
const latestVersion = manifest.channels[channel]?.latest
|
|
209
|
+
if (!latestVersion) {
|
|
210
|
+
return {
|
|
211
|
+
checked: true,
|
|
212
|
+
updated: false,
|
|
213
|
+
reason: `channel '${channel}' has no latest runtime`,
|
|
214
|
+
activeVersion: readActiveRuntime(),
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
const entry = manifest.versions[latestVersion]
|
|
218
|
+
if (!entry) {
|
|
219
|
+
return {
|
|
220
|
+
checked: true,
|
|
221
|
+
updated: false,
|
|
222
|
+
reason: `manifest is missing version ${latestVersion}`,
|
|
223
|
+
activeVersion: readActiveRuntime(),
|
|
224
|
+
latestVersion,
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const activeVersion = readActiveRuntime()
|
|
229
|
+
const activeDir = activeVersion ? runtimeDir(activeVersion) : null
|
|
230
|
+
const activeInstalled = activeDir
|
|
231
|
+
? fs.existsSync(path.join(activeDir, 'index.html'))
|
|
232
|
+
: false
|
|
233
|
+
const shouldInstall =
|
|
234
|
+
!activeVersion || !activeInstalled || compareSemver(latestVersion, activeVersion) > 0
|
|
235
|
+
|
|
236
|
+
if (!shouldInstall) {
|
|
237
|
+
return {
|
|
238
|
+
checked: true,
|
|
239
|
+
updated: false,
|
|
240
|
+
reason: 'active runtime is current',
|
|
241
|
+
activeVersion,
|
|
242
|
+
latestVersion,
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// setActive=false: callers like the daemon flip the active runtime
|
|
247
|
+
// themselves so they can broadcast runtime:changed to connected
|
|
248
|
+
// browsers. CLI install uses installRuntime() directly with
|
|
249
|
+
// setActive=true and is unaffected.
|
|
250
|
+
const install = await installRuntime({
|
|
251
|
+
version: latestVersion,
|
|
252
|
+
channel,
|
|
253
|
+
cdnOrigin,
|
|
254
|
+
setActive: false,
|
|
255
|
+
})
|
|
256
|
+
return {
|
|
257
|
+
checked: true,
|
|
258
|
+
updated: true,
|
|
259
|
+
activeVersion: latestVersion,
|
|
260
|
+
latestVersion,
|
|
261
|
+
install,
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export interface RuntimeUpToDateCheck {
|
|
266
|
+
active: string | null
|
|
267
|
+
latest: string | null
|
|
268
|
+
// true only when a latest version is known and it is newer than the
|
|
269
|
+
// active one (or nothing is active yet). network failures resolve to
|
|
270
|
+
// false so callers can treat this as a best-effort hint.
|
|
271
|
+
outdated: boolean
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** read-only check: is the active runtime the latest on its channel?
|
|
275
|
+
* unlike updateRuntimeToLatest this never downloads or activates — it's
|
|
276
|
+
* for surfacing a "newer runtime available" hint at startup / in help. */
|
|
277
|
+
export async function checkRuntimeUpToDate(
|
|
278
|
+
opts: Pick<RuntimeInstallOptions, 'channel' | 'cdnOrigin'> = {},
|
|
279
|
+
): Promise<RuntimeUpToDateCheck> {
|
|
280
|
+
const active = readActiveRuntime()
|
|
281
|
+
try {
|
|
282
|
+
const manifest = await fetchRuntimeManifest(resolveRuntimeCdnOrigin(opts.cdnOrigin))
|
|
283
|
+
const channel = resolveRuntimeChannel(opts.channel)
|
|
284
|
+
const latest = manifest.channels[channel]?.latest ?? null
|
|
285
|
+
const outdated = !!(latest && (!active || compareSemver(latest, active) > 0))
|
|
286
|
+
return { active, latest, outdated }
|
|
287
|
+
} catch {
|
|
288
|
+
return { active, latest: null, outdated: false }
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
async function downloadToFile(url: string, destPath: string): Promise<void> {
|
|
293
|
+
const res = await fetch(url)
|
|
294
|
+
if (!res.ok || !res.body) {
|
|
295
|
+
throw new Error(`download failed: ${res.status} ${res.statusText} (${url})`)
|
|
296
|
+
}
|
|
297
|
+
fs.mkdirSync(path.dirname(destPath), { recursive: true })
|
|
298
|
+
const tmp = `${destPath}.partial`
|
|
299
|
+
try {
|
|
300
|
+
await pipeline(
|
|
301
|
+
Readable.fromWeb(res.body as unknown as Parameters<typeof Readable.fromWeb>[0]),
|
|
302
|
+
fs.createWriteStream(tmp),
|
|
303
|
+
)
|
|
304
|
+
fs.renameSync(tmp, destPath)
|
|
305
|
+
} catch (err) {
|
|
306
|
+
try {
|
|
307
|
+
fs.unlinkSync(tmp)
|
|
308
|
+
} catch {}
|
|
309
|
+
throw err
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function sha256File(filePath: string): Promise<string> {
|
|
314
|
+
return new Promise((resolve, reject) => {
|
|
315
|
+
const hash = crypto.createHash('sha256')
|
|
316
|
+
const stream = fs.createReadStream(filePath)
|
|
317
|
+
stream.on('data', (chunk) => hash.update(chunk))
|
|
318
|
+
stream.on('error', reject)
|
|
319
|
+
stream.on('end', () => resolve(hash.digest('hex')))
|
|
320
|
+
})
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function extractTarball(tarPath: string, destDir: string): Promise<void> {
|
|
324
|
+
return new Promise((resolve, reject) => {
|
|
325
|
+
const child = spawn('tar', ['-xzf', tarPath, '-C', destDir], {
|
|
326
|
+
stdio: ['ignore', 'inherit', 'inherit'],
|
|
327
|
+
})
|
|
328
|
+
child.on('error', reject)
|
|
329
|
+
child.on('exit', (code) => {
|
|
330
|
+
if (code === 0) resolve()
|
|
331
|
+
else reject(new Error(`tar exited with code ${code}`))
|
|
332
|
+
})
|
|
333
|
+
})
|
|
334
|
+
}
|