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,199 @@
|
|
|
1
|
+
import {
|
|
2
|
+
chmodSync,
|
|
3
|
+
existsSync,
|
|
4
|
+
mkdirSync,
|
|
5
|
+
readFileSync,
|
|
6
|
+
rmSync,
|
|
7
|
+
writeFileSync,
|
|
8
|
+
} from 'node:fs'
|
|
9
|
+
import { dirname, join, resolve } from 'node:path'
|
|
10
|
+
import { electronUserDataDir } from '../home-paths'
|
|
11
|
+
|
|
12
|
+
export type SharedDesktopAuthTeamSummary = {
|
|
13
|
+
id: string
|
|
14
|
+
name: string
|
|
15
|
+
role: 'owner' | 'member'
|
|
16
|
+
githubOrg: string | null
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type SharedDesktopAuthUser = {
|
|
20
|
+
id: string
|
|
21
|
+
name?: string
|
|
22
|
+
email?: string
|
|
23
|
+
image?: string
|
|
24
|
+
teams?: SharedDesktopAuthTeamSummary[]
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type SharedDesktopAuthSession = {
|
|
28
|
+
version: 1
|
|
29
|
+
token: string
|
|
30
|
+
user: SharedDesktopAuthUser | null
|
|
31
|
+
origin: string
|
|
32
|
+
source: 'cli' | 'electron' | 'browser' | 'unknown'
|
|
33
|
+
updatedAt: string
|
|
34
|
+
validatedAt?: string
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const SESSION_VERSION = 1 as const
|
|
38
|
+
const SESSION_FILE_ENV = 'SOOTSIM_SHARED_AUTH_FILE'
|
|
39
|
+
const DEFAULT_ORIGIN = 'https://sootbean.com'
|
|
40
|
+
|
|
41
|
+
function getBaseDir() {
|
|
42
|
+
const explicit = process.env[SESSION_FILE_ENV]
|
|
43
|
+
if (explicit?.trim()) {
|
|
44
|
+
return dirname(resolve(explicit))
|
|
45
|
+
}
|
|
46
|
+
return electronUserDataDir()
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function getSharedDesktopAuthFilePath() {
|
|
50
|
+
const explicit = process.env[SESSION_FILE_ENV]
|
|
51
|
+
if (explicit?.trim()) return resolve(explicit)
|
|
52
|
+
return join(getBaseDir(), 'desktop-auth.json')
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function normalizeUser(input: unknown): SharedDesktopAuthUser | null {
|
|
56
|
+
if (!input || typeof input !== 'object') return null
|
|
57
|
+
const value = input as Record<string, unknown>
|
|
58
|
+
if (typeof value.id !== 'string' || !value.id.trim()) return null
|
|
59
|
+
return {
|
|
60
|
+
id: value.id.trim(),
|
|
61
|
+
name: typeof value.name === 'string' ? value.name : undefined,
|
|
62
|
+
email: typeof value.email === 'string' ? value.email : undefined,
|
|
63
|
+
image: typeof value.image === 'string' ? value.image : undefined,
|
|
64
|
+
teams: Array.isArray(value.teams)
|
|
65
|
+
? value.teams
|
|
66
|
+
.map((team): SharedDesktopAuthTeamSummary | null => {
|
|
67
|
+
if (!team || typeof team !== 'object') return null
|
|
68
|
+
const t = team as Record<string, unknown>
|
|
69
|
+
if (typeof t.id !== 'string' || typeof t.name !== 'string') return null
|
|
70
|
+
return {
|
|
71
|
+
id: t.id,
|
|
72
|
+
name: t.name,
|
|
73
|
+
role: t.role === 'owner' ? 'owner' : 'member',
|
|
74
|
+
githubOrg: typeof t.githubOrg === 'string' ? t.githubOrg : null,
|
|
75
|
+
}
|
|
76
|
+
})
|
|
77
|
+
.filter((team): team is SharedDesktopAuthTeamSummary => !!team)
|
|
78
|
+
: undefined,
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function normalizeSession(input: unknown): SharedDesktopAuthSession | null {
|
|
83
|
+
if (!input || typeof input !== 'object') return null
|
|
84
|
+
const value = input as Record<string, unknown>
|
|
85
|
+
if (value.version !== SESSION_VERSION) return null
|
|
86
|
+
if (typeof value.token !== 'string' || !value.token.trim()) return null
|
|
87
|
+
const origin =
|
|
88
|
+
typeof value.origin === 'string' && value.origin.trim()
|
|
89
|
+
? value.origin.trim()
|
|
90
|
+
: DEFAULT_ORIGIN
|
|
91
|
+
const source =
|
|
92
|
+
value.source === 'cli' ||
|
|
93
|
+
value.source === 'electron' ||
|
|
94
|
+
value.source === 'browser' ||
|
|
95
|
+
value.source === 'unknown'
|
|
96
|
+
? value.source
|
|
97
|
+
: 'unknown'
|
|
98
|
+
const updatedAt =
|
|
99
|
+
typeof value.updatedAt === 'string' && value.updatedAt
|
|
100
|
+
? value.updatedAt
|
|
101
|
+
: new Date().toISOString()
|
|
102
|
+
const validatedAt =
|
|
103
|
+
typeof value.validatedAt === 'string' && value.validatedAt
|
|
104
|
+
? value.validatedAt
|
|
105
|
+
: undefined
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
version: SESSION_VERSION,
|
|
109
|
+
token: value.token.trim(),
|
|
110
|
+
user: normalizeUser(value.user),
|
|
111
|
+
origin,
|
|
112
|
+
source,
|
|
113
|
+
updatedAt,
|
|
114
|
+
validatedAt,
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function readSharedDesktopAuthSession(): SharedDesktopAuthSession | null {
|
|
119
|
+
const filepath = getSharedDesktopAuthFilePath()
|
|
120
|
+
if (!existsSync(filepath)) return null
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
const parsed = JSON.parse(readFileSync(filepath, 'utf8')) as unknown
|
|
124
|
+
const normalized = normalizeSession(parsed)
|
|
125
|
+
if (!normalized) {
|
|
126
|
+
rmSync(filepath, { force: true })
|
|
127
|
+
return null
|
|
128
|
+
}
|
|
129
|
+
return normalized
|
|
130
|
+
} catch {
|
|
131
|
+
rmSync(filepath, { force: true })
|
|
132
|
+
return null
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function writeSharedDesktopAuthSession(
|
|
137
|
+
session: Omit<SharedDesktopAuthSession, 'version' | 'updatedAt'> & {
|
|
138
|
+
updatedAt?: string
|
|
139
|
+
},
|
|
140
|
+
) {
|
|
141
|
+
const filepath = getSharedDesktopAuthFilePath()
|
|
142
|
+
mkdirSync(dirname(filepath), { recursive: true })
|
|
143
|
+
const normalized: SharedDesktopAuthSession = {
|
|
144
|
+
version: SESSION_VERSION,
|
|
145
|
+
token: session.token.trim(),
|
|
146
|
+
user: session.user ? normalizeUser(session.user) : null,
|
|
147
|
+
origin: session.origin?.trim() || DEFAULT_ORIGIN,
|
|
148
|
+
source: session.source,
|
|
149
|
+
updatedAt: session.updatedAt || new Date().toISOString(),
|
|
150
|
+
validatedAt: session.validatedAt,
|
|
151
|
+
}
|
|
152
|
+
writeFileSync(filepath, JSON.stringify(normalized, null, 2) + '\n')
|
|
153
|
+
try {
|
|
154
|
+
chmodSync(filepath, 0o600)
|
|
155
|
+
} catch {
|
|
156
|
+
// best-effort only
|
|
157
|
+
}
|
|
158
|
+
return normalized
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function clearSharedDesktopAuthSession() {
|
|
162
|
+
rmSync(getSharedDesktopAuthFilePath(), { force: true })
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export async function refreshSharedDesktopAuthSession(
|
|
166
|
+
originOverride?: string,
|
|
167
|
+
): Promise<SharedDesktopAuthSession | null> {
|
|
168
|
+
const current = readSharedDesktopAuthSession()
|
|
169
|
+
if (!current?.token) return null
|
|
170
|
+
|
|
171
|
+
const origin = originOverride || current.origin || DEFAULT_ORIGIN
|
|
172
|
+
|
|
173
|
+
try {
|
|
174
|
+
const res = await fetch(`${origin.replace(/\/$/, '')}/api/auth/me`, {
|
|
175
|
+
headers: { authorization: `Bearer ${current.token}` },
|
|
176
|
+
})
|
|
177
|
+
if (res.status === 401) {
|
|
178
|
+
clearSharedDesktopAuthSession()
|
|
179
|
+
return null
|
|
180
|
+
}
|
|
181
|
+
if (!res.ok) return current
|
|
182
|
+
|
|
183
|
+
const data = (await res.json()) as { user?: SharedDesktopAuthUser | null }
|
|
184
|
+
if (!data.user?.id) {
|
|
185
|
+
clearSharedDesktopAuthSession()
|
|
186
|
+
return null
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return writeSharedDesktopAuthSession({
|
|
190
|
+
token: current.token,
|
|
191
|
+
user: data.user,
|
|
192
|
+
origin,
|
|
193
|
+
source: current.source,
|
|
194
|
+
validatedAt: new Date().toISOString(),
|
|
195
|
+
})
|
|
196
|
+
} catch {
|
|
197
|
+
return current
|
|
198
|
+
}
|
|
199
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
const DEFAULT_SOOT_BACKEND_ORIGIN = 'https://sootbean.com'
|
|
2
|
+
|
|
3
|
+
function normalizeHostname(hostname: string): string {
|
|
4
|
+
return hostname.replace(/^\[|\]$/g, '').toLowerCase()
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function isLoopbackHost(hostname: string): boolean {
|
|
8
|
+
const normalized = normalizeHostname(hostname)
|
|
9
|
+
return (
|
|
10
|
+
normalized === 'localhost' ||
|
|
11
|
+
normalized.endsWith('.localhost') ||
|
|
12
|
+
normalized === '0.0.0.0' ||
|
|
13
|
+
normalized === '::1' ||
|
|
14
|
+
/^127(?:\.\d{1,3}){3}$/.test(normalized)
|
|
15
|
+
)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function isSootBackendHost(hostname: string): boolean {
|
|
19
|
+
const normalized = normalizeHostname(hostname)
|
|
20
|
+
return (
|
|
21
|
+
isLoopbackHost(normalized) ||
|
|
22
|
+
normalized === 'sootbean.com' ||
|
|
23
|
+
normalized.endsWith('.sootbean.com')
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function isSootBackendOrigin(origin: string): boolean {
|
|
28
|
+
try {
|
|
29
|
+
return isSootBackendHost(new URL(origin).hostname)
|
|
30
|
+
} catch {
|
|
31
|
+
return false
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function resolveSootBackendOrigin(currentOrigin?: string | null): string {
|
|
36
|
+
const origin = currentOrigin?.trim()
|
|
37
|
+
if (!origin) return DEFAULT_SOOT_BACKEND_ORIGIN
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
const parsed = new URL(origin)
|
|
41
|
+
if (isLoopbackHost(parsed.hostname) || isSootBackendHost(parsed.hostname)) {
|
|
42
|
+
return parsed.origin
|
|
43
|
+
}
|
|
44
|
+
} catch {}
|
|
45
|
+
|
|
46
|
+
return DEFAULT_SOOT_BACKEND_ORIGIN
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export { DEFAULT_SOOT_BACKEND_ORIGIN }
|
package/src/beta.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// public beta flag + copy. single source of truth for the badge that
|
|
2
|
+
// appears on the CLI banner, electron title, shell rail pill, and the
|
|
3
|
+
// download page. flip IS_BETA to false when 1.0 ships and the badge
|
|
4
|
+
// disappears everywhere automatically.
|
|
5
|
+
//
|
|
6
|
+
// see plans/sootsim-public-beta.md for the rollout plan.
|
|
7
|
+
|
|
8
|
+
export const IS_BETA = true
|
|
9
|
+
|
|
10
|
+
export const BETA_VERSION_TARGET = '1.0.0'
|
|
11
|
+
|
|
12
|
+
// short label — used on the rail pill, electron title, etc.
|
|
13
|
+
export const BETA_LABEL = 'public beta'
|
|
14
|
+
|
|
15
|
+
// one-line tagline — used in the CLI banner + privacy disclosures.
|
|
16
|
+
export const BETA_TAGLINE = `${BETA_LABEL} · free until ${BETA_VERSION_TARGET}`
|
|
17
|
+
|
|
18
|
+
// what we ask for in return — kept here so the onboarding flow, the
|
|
19
|
+
// privacy page, and any future surface render the same words.
|
|
20
|
+
export const BETA_ASK_HEADLINE =
|
|
21
|
+
'free for everyone until 1.0 — we send anonymous error and dependency data while you use it'
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// canonical bridge port for sootsim's WebSocket connection between the CLI
|
|
2
|
+
// and the running engine. matches main — kept in a single module so CLI,
|
|
3
|
+
// dev plugin, and runtime agree on the same value.
|
|
4
|
+
export const DEFAULT_SOOTSIM_BRIDGE_PORT = 7668
|
|
5
|
+
|
|
6
|
+
// private websocket close code used when the daemon intentionally closes a
|
|
7
|
+
// sim. browser clients treat it as terminal and skip their normal reconnect
|
|
8
|
+
// loop; playwright-owned sims use it to close their hosting context.
|
|
9
|
+
export const SOOTSIM_BRIDGE_SIM_CLOSE_CODE = 4001
|
|
10
|
+
export const SOOTSIM_BRIDGE_SIM_CLOSE_REASON = 'sootsim close'
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const DEFAULT_SOOTSIM_SHELL_URL = 'http://localhost:5173/'
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// sootsim CLI version — the `sootsim` npm package version. resolved once
|
|
2
|
+
// and cached. shared by `--version`, `--help`, the `upgrade` command, the
|
|
3
|
+
// startup flow, and the bridge-host (which injects it into the served
|
|
4
|
+
// runtime html as window.__sootsimCliVersion).
|
|
5
|
+
|
|
6
|
+
import { readFileSync } from 'node:fs'
|
|
7
|
+
import { fileURLToPath } from 'node:url'
|
|
8
|
+
|
|
9
|
+
let cached: string | null = null
|
|
10
|
+
|
|
11
|
+
export function getCliVersion(): string {
|
|
12
|
+
if (cached != null) return cached
|
|
13
|
+
// prefer package resolution (works for npm/global installs); fall back to
|
|
14
|
+
// the package.json next to this module's source for repo / bundled runs.
|
|
15
|
+
const candidates: Array<() => string> = [
|
|
16
|
+
() => fileURLToPath(import.meta.resolve('sootsim/package.json')),
|
|
17
|
+
() => fileURLToPath(new URL('../package.json', import.meta.url)),
|
|
18
|
+
]
|
|
19
|
+
for (const resolve of candidates) {
|
|
20
|
+
try {
|
|
21
|
+
const version = JSON.parse(readFileSync(resolve(), 'utf8')).version
|
|
22
|
+
if (typeof version === 'string' && version) {
|
|
23
|
+
cached = version
|
|
24
|
+
return cached
|
|
25
|
+
}
|
|
26
|
+
} catch {}
|
|
27
|
+
}
|
|
28
|
+
cached = '0.0.0'
|
|
29
|
+
return cached
|
|
30
|
+
}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
// minimal client for `codex app-server` — a long-lived child process that
|
|
2
|
+
// speaks line-delimited JSON-RPC 2.0 over stdio.
|
|
3
|
+
//
|
|
4
|
+
// scope: just the subset sootsim needs for the attached-projects agent
|
|
5
|
+
// wrapper. each connection owns exactly one spawned app-server child; caller
|
|
6
|
+
// is responsible for lifecycle. bidirectional: client can issue requests +
|
|
7
|
+
// receive server-initiated notifications. we treat every method we don't
|
|
8
|
+
// recognize as an observable event that we pass straight to the handler map.
|
|
9
|
+
|
|
10
|
+
import { spawn, type ChildProcessWithoutNullStreams } from 'node:child_process'
|
|
11
|
+
import readline from 'node:readline'
|
|
12
|
+
|
|
13
|
+
export interface CodexClientOptions {
|
|
14
|
+
/** resolved path to the codex binary (via `which codex` or user override). */
|
|
15
|
+
bin: string
|
|
16
|
+
/** working directory for the spawned process. defaults to process.cwd(). */
|
|
17
|
+
cwd?: string
|
|
18
|
+
/** extra environment overrides forwarded to the child. */
|
|
19
|
+
env?: Record<string, string | undefined>
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type CodexNotificationHandler = (params: unknown) => void
|
|
23
|
+
|
|
24
|
+
export interface CodexClient {
|
|
25
|
+
/** promise that resolves when the child exits. use to await shutdown. */
|
|
26
|
+
readonly exited: Promise<{ code: number | null; signal: NodeJS.Signals | null }>
|
|
27
|
+
/** register a handler for a server-sent notification method. returns
|
|
28
|
+
* a dispose fn. multiple handlers per method are allowed. */
|
|
29
|
+
on(method: string, handler: CodexNotificationHandler): () => void
|
|
30
|
+
/** send a JSON-RPC request and await its response. rejects on error
|
|
31
|
+
* response or child death. */
|
|
32
|
+
request<TResult = unknown>(method: string, params?: unknown): Promise<TResult>
|
|
33
|
+
/** send a JSON-RPC notification (no id, no response). */
|
|
34
|
+
notify(method: string, params?: unknown): void
|
|
35
|
+
/** close stdin (graceful shutdown), then SIGTERM after timeout. */
|
|
36
|
+
shutdown(timeoutMs?: number): Promise<void>
|
|
37
|
+
/** force-kill immediately. */
|
|
38
|
+
kill(signal?: NodeJS.Signals): void
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface JsonRpcResponse {
|
|
42
|
+
jsonrpc?: '2.0'
|
|
43
|
+
id?: number | string | null
|
|
44
|
+
result?: unknown
|
|
45
|
+
error?: { code: number; message: string; data?: unknown }
|
|
46
|
+
method?: string
|
|
47
|
+
params?: unknown
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export class CodexRpcError extends Error {
|
|
51
|
+
code: number
|
|
52
|
+
data: unknown
|
|
53
|
+
constructor(message: string, code: number, data?: unknown) {
|
|
54
|
+
super(message)
|
|
55
|
+
this.name = 'CodexRpcError'
|
|
56
|
+
this.code = code
|
|
57
|
+
this.data = data
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function spawnCodexClient(options: CodexClientOptions): CodexClient {
|
|
62
|
+
const child: ChildProcessWithoutNullStreams = spawn(options.bin, ['app-server'], {
|
|
63
|
+
cwd: options.cwd,
|
|
64
|
+
env: { ...process.env, ...options.env },
|
|
65
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
const pending = new Map<
|
|
69
|
+
number,
|
|
70
|
+
{
|
|
71
|
+
resolve: (value: unknown) => void
|
|
72
|
+
reject: (err: unknown) => void
|
|
73
|
+
method: string
|
|
74
|
+
}
|
|
75
|
+
>()
|
|
76
|
+
const handlers = new Map<string, Set<CodexNotificationHandler>>()
|
|
77
|
+
let nextId = 1
|
|
78
|
+
let closed = false
|
|
79
|
+
|
|
80
|
+
const exited = new Promise<{
|
|
81
|
+
code: number | null
|
|
82
|
+
signal: NodeJS.Signals | null
|
|
83
|
+
}>((resolve) => {
|
|
84
|
+
child.on('exit', (code, signal) => {
|
|
85
|
+
closed = true
|
|
86
|
+
const err = new Error(
|
|
87
|
+
`codex app-server exited (code=${code}, signal=${signal ?? ''})`,
|
|
88
|
+
)
|
|
89
|
+
for (const { reject } of pending.values()) reject(err)
|
|
90
|
+
pending.clear()
|
|
91
|
+
resolve({ code, signal })
|
|
92
|
+
})
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
// stdout carries JSON-RPC messages, one per line.
|
|
96
|
+
const rl = readline.createInterface({ input: child.stdout, crlfDelay: Infinity })
|
|
97
|
+
rl.on('line', (line) => {
|
|
98
|
+
const trimmed = line.trim()
|
|
99
|
+
if (!trimmed) return
|
|
100
|
+
let msg: JsonRpcResponse
|
|
101
|
+
try {
|
|
102
|
+
msg = JSON.parse(trimmed) as JsonRpcResponse
|
|
103
|
+
} catch {
|
|
104
|
+
// app-server occasionally emits non-JSON warnings; ignore.
|
|
105
|
+
return
|
|
106
|
+
}
|
|
107
|
+
if (msg.id != null && (msg.result !== undefined || msg.error !== undefined)) {
|
|
108
|
+
const idNum = typeof msg.id === 'string' ? Number(msg.id) : msg.id
|
|
109
|
+
const entry = idNum != null ? pending.get(idNum) : undefined
|
|
110
|
+
if (!entry) return
|
|
111
|
+
pending.delete(idNum!)
|
|
112
|
+
if (msg.error) {
|
|
113
|
+
entry.reject(new CodexRpcError(msg.error.message, msg.error.code, msg.error.data))
|
|
114
|
+
} else {
|
|
115
|
+
entry.resolve(msg.result)
|
|
116
|
+
}
|
|
117
|
+
return
|
|
118
|
+
}
|
|
119
|
+
if (msg.method) {
|
|
120
|
+
const set = handlers.get(msg.method)
|
|
121
|
+
if (!set) return
|
|
122
|
+
for (const h of set) {
|
|
123
|
+
try {
|
|
124
|
+
h(msg.params)
|
|
125
|
+
} catch (err) {
|
|
126
|
+
// a buggy handler must not kill the bridge, but it MUST be visible —
|
|
127
|
+
// silent swallowing turned a notification-handler typo into a
|
|
128
|
+
// "nothing happens" bug once already.
|
|
129
|
+
console.error(
|
|
130
|
+
`[codex-client] handler for "${msg.method}" threw:`,
|
|
131
|
+
err instanceof Error ? (err.stack ?? err.message) : err,
|
|
132
|
+
)
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
// stderr is diagnostic only; surfaced via the "stderr" notification channel
|
|
139
|
+
// so callers can forward it into transcripts if they want.
|
|
140
|
+
child.stderr.setEncoding('utf8')
|
|
141
|
+
child.stderr.on('data', (chunk: string) => {
|
|
142
|
+
const set = handlers.get('__stderr__')
|
|
143
|
+
if (!set) return
|
|
144
|
+
for (const h of set) {
|
|
145
|
+
try {
|
|
146
|
+
h({ text: chunk })
|
|
147
|
+
} catch {}
|
|
148
|
+
}
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
function write(msg: unknown): void {
|
|
152
|
+
if (closed) return
|
|
153
|
+
try {
|
|
154
|
+
child.stdin.write(JSON.stringify(msg) + '\n')
|
|
155
|
+
} catch {
|
|
156
|
+
// if stdin has been closed, subsequent writes will throw; ignore.
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
exited,
|
|
162
|
+
on(method, handler) {
|
|
163
|
+
let set = handlers.get(method)
|
|
164
|
+
if (!set) {
|
|
165
|
+
set = new Set()
|
|
166
|
+
handlers.set(method, set)
|
|
167
|
+
}
|
|
168
|
+
set.add(handler)
|
|
169
|
+
return () => {
|
|
170
|
+
set?.delete(handler)
|
|
171
|
+
}
|
|
172
|
+
},
|
|
173
|
+
request<TResult>(method: string, params?: unknown): Promise<TResult> {
|
|
174
|
+
if (closed) {
|
|
175
|
+
return Promise.reject(new Error(`codex app-server closed; cannot call ${method}`))
|
|
176
|
+
}
|
|
177
|
+
const id = nextId++
|
|
178
|
+
return new Promise<TResult>((resolve, reject) => {
|
|
179
|
+
pending.set(id, {
|
|
180
|
+
resolve: (v) => resolve(v as TResult),
|
|
181
|
+
reject,
|
|
182
|
+
method,
|
|
183
|
+
})
|
|
184
|
+
write({ jsonrpc: '2.0', id, method, params })
|
|
185
|
+
})
|
|
186
|
+
},
|
|
187
|
+
notify(method, params) {
|
|
188
|
+
write({ jsonrpc: '2.0', method, params })
|
|
189
|
+
},
|
|
190
|
+
async shutdown(timeoutMs = 1500) {
|
|
191
|
+
if (closed) return
|
|
192
|
+
try {
|
|
193
|
+
child.stdin.end()
|
|
194
|
+
} catch {}
|
|
195
|
+
const t = setTimeout(() => {
|
|
196
|
+
if (!closed) {
|
|
197
|
+
try {
|
|
198
|
+
child.kill('SIGTERM')
|
|
199
|
+
} catch {}
|
|
200
|
+
}
|
|
201
|
+
}, timeoutMs)
|
|
202
|
+
try {
|
|
203
|
+
await exited
|
|
204
|
+
} finally {
|
|
205
|
+
clearTimeout(t)
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
kill(signal = 'SIGTERM') {
|
|
209
|
+
if (closed) return
|
|
210
|
+
try {
|
|
211
|
+
child.kill(signal)
|
|
212
|
+
} catch {}
|
|
213
|
+
},
|
|
214
|
+
}
|
|
215
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// sootsim.config.ts — per-app module resolution, turbo modules, env, state, and settings.
|
|
2
|
+
//
|
|
3
|
+
// usage:
|
|
4
|
+
// import { defineConfig } from 'sootsim/config'
|
|
5
|
+
// export default defineConfig({ modules: { ... }, env: { ... } })
|
|
6
|
+
|
|
7
|
+
export type ModuleResolution =
|
|
8
|
+
| 'noop' // empty module
|
|
9
|
+
| false // disable builtin stub, use original
|
|
10
|
+
| { use: string } // redirect to existing compat stub
|
|
11
|
+
| { file: string } // resolve to a file relative to the config
|
|
12
|
+
| { inline: Record<string, any> } // inline stub object
|
|
13
|
+
|
|
14
|
+
export type NativeModuleResolution = ModuleResolution | Record<string, any> // direct native module object for non-url configs
|
|
15
|
+
|
|
16
|
+
export interface SootSimConfig<
|
|
17
|
+
Settings extends Record<string, any> = Record<string, any>,
|
|
18
|
+
> {
|
|
19
|
+
modules?: Record<string, ModuleResolution>
|
|
20
|
+
turboModules?: Record<string, NativeModuleResolution>
|
|
21
|
+
nativeModules?: Record<string, NativeModuleResolution>
|
|
22
|
+
env?: Record<string, string>
|
|
23
|
+
settings?: Partial<Settings>
|
|
24
|
+
initialState?: {
|
|
25
|
+
authenticated?: boolean
|
|
26
|
+
locale?: string
|
|
27
|
+
colorScheme?: 'light' | 'dark' | 'auto'
|
|
28
|
+
featureFlags?: Record<string, boolean | string | number>
|
|
29
|
+
[key: string]: any
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const SOOTSIM_CONFIG_QUERY_PARAM = 'sootsimConfig'
|
|
34
|
+
|
|
35
|
+
export function defineConfig<Settings extends Record<string, any> = Record<string, any>>(
|
|
36
|
+
config: SootSimConfig<Settings>,
|
|
37
|
+
): SootSimConfig<Settings> {
|
|
38
|
+
return config
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function hasOwnKeys(value: Record<string, unknown> | undefined): boolean {
|
|
42
|
+
return !!value && Object.keys(value).length > 0
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function hasSootSimConfig<
|
|
46
|
+
Settings extends Record<string, any> = Record<string, any>,
|
|
47
|
+
>(config?: SootSimConfig<Settings> | null): config is SootSimConfig<Settings> {
|
|
48
|
+
if (!config) return false
|
|
49
|
+
return (
|
|
50
|
+
hasOwnKeys(config.modules as Record<string, unknown> | undefined) ||
|
|
51
|
+
hasOwnKeys(config.turboModules as Record<string, unknown> | undefined) ||
|
|
52
|
+
hasOwnKeys(config.nativeModules as Record<string, unknown> | undefined) ||
|
|
53
|
+
hasOwnKeys(config.env as Record<string, unknown> | undefined) ||
|
|
54
|
+
hasOwnKeys(config.settings as Record<string, unknown> | undefined) ||
|
|
55
|
+
hasOwnKeys(config.initialState as Record<string, unknown> | undefined)
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function mergeSootSimConfig<
|
|
60
|
+
Settings extends Record<string, any> = Record<string, any>,
|
|
61
|
+
>(
|
|
62
|
+
base?: SootSimConfig<Settings> | null,
|
|
63
|
+
override?: SootSimConfig<Settings> | null,
|
|
64
|
+
): SootSimConfig<Settings> | undefined {
|
|
65
|
+
if (!hasSootSimConfig(base) && !hasSootSimConfig(override)) return undefined
|
|
66
|
+
if (!hasSootSimConfig(base)) return override ? { ...override } : undefined
|
|
67
|
+
if (!hasSootSimConfig(override)) return base ? { ...base } : undefined
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
modules: { ...(base.modules || {}), ...(override.modules || {}) },
|
|
71
|
+
turboModules: { ...(base.turboModules || {}), ...(override.turboModules || {}) },
|
|
72
|
+
nativeModules: {
|
|
73
|
+
...(base.nativeModules || {}),
|
|
74
|
+
...(override.nativeModules || {}),
|
|
75
|
+
},
|
|
76
|
+
env: { ...(base.env || {}), ...(override.env || {}) },
|
|
77
|
+
settings: {
|
|
78
|
+
...(base.settings || {}),
|
|
79
|
+
...(override.settings || {}),
|
|
80
|
+
} as Partial<Settings>,
|
|
81
|
+
initialState: { ...(base.initialState || {}), ...(override.initialState || {}) },
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function readSootSimConfigFromSearchParams<
|
|
86
|
+
Settings extends Record<string, any> = Record<string, any>,
|
|
87
|
+
>(searchParams: URLSearchParams): SootSimConfig<Settings> | undefined {
|
|
88
|
+
const raw = searchParams.get(SOOTSIM_CONFIG_QUERY_PARAM)
|
|
89
|
+
if (!raw) return undefined
|
|
90
|
+
try {
|
|
91
|
+
const parsed = JSON.parse(raw) as SootSimConfig<Settings>
|
|
92
|
+
return hasSootSimConfig(parsed) ? parsed : undefined
|
|
93
|
+
} catch (error) {
|
|
94
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
95
|
+
console.warn(`[sootsim] invalid ${SOOTSIM_CONFIG_QUERY_PARAM}: ${message}`)
|
|
96
|
+
return undefined
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function applySootSimConfigToUrl<
|
|
101
|
+
Settings extends Record<string, any> = Record<string, any>,
|
|
102
|
+
>(url: string, config?: SootSimConfig<Settings> | null): string {
|
|
103
|
+
const parsed = new URL(url)
|
|
104
|
+
if (hasSootSimConfig(config)) {
|
|
105
|
+
parsed.searchParams.set(SOOTSIM_CONFIG_QUERY_PARAM, JSON.stringify(config))
|
|
106
|
+
} else {
|
|
107
|
+
parsed.searchParams.delete(SOOTSIM_CONFIG_QUERY_PARAM)
|
|
108
|
+
}
|
|
109
|
+
return parsed.toString()
|
|
110
|
+
}
|