@belte/belte 0.19.0

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.
Files changed (326) hide show
  1. package/CHANGELOG.md +313 -0
  2. package/LICENSE +21 -0
  3. package/README.md +559 -0
  4. package/bin/belte.ts +183 -0
  5. package/package.json +110 -0
  6. package/src/App.svelte +31 -0
  7. package/src/appEntry.ts +151 -0
  8. package/src/assets/app.html +14 -0
  9. package/src/belteResolverPlugin.ts +858 -0
  10. package/src/build.ts +147 -0
  11. package/src/buildCli.ts +129 -0
  12. package/src/buildDisconnected.ts +122 -0
  13. package/src/bundleApp.ts +149 -0
  14. package/src/bundleDisconnectedEntry.ts +17 -0
  15. package/src/cliEntry.ts +25 -0
  16. package/src/clientBuildPlugins.ts +41 -0
  17. package/src/clientEntry.ts +7 -0
  18. package/src/compile.ts +64 -0
  19. package/src/controlServerWorker.ts +422 -0
  20. package/src/dedupeSveltePlugin.ts +66 -0
  21. package/src/devEntry.ts +169 -0
  22. package/src/discoveryEntry.ts +81 -0
  23. package/src/lib/browser/applyStreamedResolution.ts +33 -0
  24. package/src/lib/browser/cacheEntryFromSnapshot.ts +48 -0
  25. package/src/lib/browser/flushUnresolvedPlaceholders.ts +16 -0
  26. package/src/lib/browser/installStreamingPlaceholders.ts +32 -0
  27. package/src/lib/browser/openResolveStream.ts +42 -0
  28. package/src/lib/browser/page.svelte.ts +258 -0
  29. package/src/lib/browser/pageStreamController.ts +17 -0
  30. package/src/lib/browser/refetchPlaceholder.ts +12 -0
  31. package/src/lib/browser/remoteProxy.ts +37 -0
  32. package/src/lib/browser/socketChannel.ts +192 -0
  33. package/src/lib/browser/socketProxy.ts +57 -0
  34. package/src/lib/browser/startClient.ts +153 -0
  35. package/src/lib/browser/subscribe.ts +131 -0
  36. package/src/lib/browser/types/Errors.ts +9 -0
  37. package/src/lib/browser/types/Layouts.ts +7 -0
  38. package/src/lib/browser/types/Pages.ts +7 -0
  39. package/src/lib/browser/types/StreamingDeferred.ts +9 -0
  40. package/src/lib/bundle/BundleMenu.ts +11 -0
  41. package/src/lib/bundle/BundleMenuItem.ts +24 -0
  42. package/src/lib/bundle/BundleWindow.ts +36 -0
  43. package/src/lib/bundle/WEBVIEW_BUILD_REVISION.ts +9 -0
  44. package/src/lib/bundle/WEBVIEW_VERSION.ts +7 -0
  45. package/src/lib/bundle/bindConnectedFlag.ts +29 -0
  46. package/src/lib/bundle/bindRequestNavigate.ts +31 -0
  47. package/src/lib/bundle/buildWebviewLib.ts +111 -0
  48. package/src/lib/bundle/disconnected.css +9 -0
  49. package/src/lib/bundle/disconnected.svelte +386 -0
  50. package/src/lib/bundle/ensureWebviewLib.ts +20 -0
  51. package/src/lib/bundle/exitWithParent.ts +28 -0
  52. package/src/lib/bundle/infoPlist.ts +46 -0
  53. package/src/lib/bundle/installDownloads.ts +24 -0
  54. package/src/lib/bundle/installMacMenu.ts +39 -0
  55. package/src/lib/bundle/listenLocalControlServer.ts +19 -0
  56. package/src/lib/bundle/native/belteMenu.mm +422 -0
  57. package/src/lib/bundle/native/webview.h +4557 -0
  58. package/src/lib/bundle/onMenu.ts +41 -0
  59. package/src/lib/bundle/openWebview.ts +104 -0
  60. package/src/lib/bundle/pngToIcns.ts +47 -0
  61. package/src/lib/bundle/probeBelteServer.ts +34 -0
  62. package/src/lib/bundle/resolveServerBinary.ts +12 -0
  63. package/src/lib/bundle/resolveWebviewLib.ts +53 -0
  64. package/src/lib/bundle/serverBinaryFilename.ts +8 -0
  65. package/src/lib/bundle/signMacApp.ts +35 -0
  66. package/src/lib/bundle/spawnEmbeddedServer.ts +65 -0
  67. package/src/lib/bundle/stableLocalPort.ts +19 -0
  68. package/src/lib/bundle/waitForServer.ts +23 -0
  69. package/src/lib/bundle/webviewCachePath.ts +23 -0
  70. package/src/lib/bundle/webviewLibName.ts +11 -0
  71. package/src/lib/cli/connectToServer.ts +23 -0
  72. package/src/lib/cli/createClient.ts +170 -0
  73. package/src/lib/cli/dispatchCommand.ts +71 -0
  74. package/src/lib/cli/loadEnvFromBinaryDir.ts +16 -0
  75. package/src/lib/cli/parseArgvForRpc.ts +97 -0
  76. package/src/lib/cli/printHelp.ts +119 -0
  77. package/src/lib/cli/printSessionHelp.ts +27 -0
  78. package/src/lib/cli/printSessionStatus.ts +21 -0
  79. package/src/lib/cli/printTrimmed.ts +8 -0
  80. package/src/lib/cli/printValue.ts +10 -0
  81. package/src/lib/cli/resolveCliTarget.ts +48 -0
  82. package/src/lib/cli/runCli.ts +139 -0
  83. package/src/lib/cli/runSession.ts +105 -0
  84. package/src/lib/cli/startLocalInstance.ts +14 -0
  85. package/src/lib/cli/tokenizeLine.ts +51 -0
  86. package/src/lib/cli/types/CliManifest.ts +9 -0
  87. package/src/lib/cli/types/CliManifestEntry.ts +17 -0
  88. package/src/lib/cli/types/CliTarget.ts +13 -0
  89. package/src/lib/mcp/annotationsForMethod.ts +29 -0
  90. package/src/lib/mcp/createMcpResourceServer.ts +101 -0
  91. package/src/lib/mcp/createMcpServer.ts +42 -0
  92. package/src/lib/mcp/dispatchMcpRequest.ts +146 -0
  93. package/src/lib/mcp/mcpResourceServerSlot.ts +18 -0
  94. package/src/lib/mcp/mcpSurface.ts +265 -0
  95. package/src/lib/mcp/toolResultFromResponse.ts +66 -0
  96. package/src/lib/mcp/types/JsonRpcRequest.ts +12 -0
  97. package/src/lib/mcp/types/JsonRpcResponse.ts +20 -0
  98. package/src/lib/mcp/types/McpResourceContents.ts +10 -0
  99. package/src/lib/mcp/types/McpResourceDescriptor.ts +6 -0
  100. package/src/lib/mcp/types/McpResourceServer.ts +12 -0
  101. package/src/lib/mcp/types/McpServer.ts +9 -0
  102. package/src/lib/mcp/types/McpServerOptions.ts +16 -0
  103. package/src/lib/server/AppModule.ts +33 -0
  104. package/src/lib/server/DELETE.ts +9 -0
  105. package/src/lib/server/GET.ts +9 -0
  106. package/src/lib/server/HEAD.ts +9 -0
  107. package/src/lib/server/PATCH.ts +9 -0
  108. package/src/lib/server/POST.ts +9 -0
  109. package/src/lib/server/PUT.ts +9 -0
  110. package/src/lib/server/agent.ts +76 -0
  111. package/src/lib/server/appDataDir.ts +15 -0
  112. package/src/lib/server/cli/buildEnvContent.ts +19 -0
  113. package/src/lib/server/cli/createTarGz.ts +76 -0
  114. package/src/lib/server/cli/handleCliDownload.ts +153 -0
  115. package/src/lib/server/cli/handleCliInstall.ts +37 -0
  116. package/src/lib/server/cli/installScript.ts +29 -0
  117. package/src/lib/server/cli/maxSourceMtime.ts +26 -0
  118. package/src/lib/server/cookies.ts +29 -0
  119. package/src/lib/server/env.ts +50 -0
  120. package/src/lib/server/error.ts +70 -0
  121. package/src/lib/server/json.ts +28 -0
  122. package/src/lib/server/jsonl.ts +46 -0
  123. package/src/lib/server/prompts/definePrompt.ts +20 -0
  124. package/src/lib/server/prompts/promptRegistry.ts +9 -0
  125. package/src/lib/server/prompts/registerPrompt.ts +6 -0
  126. package/src/lib/server/prompts/renderPromptTemplate.ts +16 -0
  127. package/src/lib/server/prompts/types/Prompt.ts +13 -0
  128. package/src/lib/server/prompts/types/PromptOptions.ts +12 -0
  129. package/src/lib/server/prompts/types/PromptRegistryEntry.ts +13 -0
  130. package/src/lib/server/prompts/types/PromptRoutes.ts +10 -0
  131. package/src/lib/server/redirect.ts +42 -0
  132. package/src/lib/server/request.ts +18 -0
  133. package/src/lib/server/rpc/defineVerb.ts +133 -0
  134. package/src/lib/server/rpc/dispatchVerbInProcess.ts +46 -0
  135. package/src/lib/server/rpc/findVerbByCommandName.ts +18 -0
  136. package/src/lib/server/rpc/parseArgs.ts +95 -0
  137. package/src/lib/server/rpc/registerVerb.ts +6 -0
  138. package/src/lib/server/rpc/types/RemoteHandler.ts +27 -0
  139. package/src/lib/server/rpc/types/RemoteRoutes.ts +13 -0
  140. package/src/lib/server/rpc/types/TypedResponse.ts +18 -0
  141. package/src/lib/server/rpc/types/VerbHelper.ts +68 -0
  142. package/src/lib/server/rpc/types/VerbRegistryEntry.ts +29 -0
  143. package/src/lib/server/rpc/unprocessed.ts +14 -0
  144. package/src/lib/server/rpc/verbRegistry.ts +11 -0
  145. package/src/lib/server/runtime/DEFAULT_PORT.ts +6 -0
  146. package/src/lib/server/runtime/DEV_REBUILD_MESSAGE.ts +4 -0
  147. package/src/lib/server/runtime/DEV_RELOAD_CLIENT_SCRIPT.ts +29 -0
  148. package/src/lib/server/runtime/acceptsZstd.ts +8 -0
  149. package/src/lib/server/runtime/buildOpenApiSpec.ts +106 -0
  150. package/src/lib/server/runtime/cacheControlForAsset.ts +22 -0
  151. package/src/lib/server/runtime/containsTraversal.ts +37 -0
  152. package/src/lib/server/runtime/createAssetHeaderCache.ts +35 -0
  153. package/src/lib/server/runtime/createPublicAssetServer.ts +63 -0
  154. package/src/lib/server/runtime/createRouteDispatcher.ts +100 -0
  155. package/src/lib/server/runtime/createServer.ts +692 -0
  156. package/src/lib/server/runtime/devReloadResponse.ts +35 -0
  157. package/src/lib/server/runtime/disableIdleTimeoutForStream.ts +27 -0
  158. package/src/lib/server/runtime/envSchemaStore.ts +15 -0
  159. package/src/lib/server/runtime/findOpenPort.ts +35 -0
  160. package/src/lib/server/runtime/getActiveServer.ts +6 -0
  161. package/src/lib/server/runtime/globToPathSet.ts +29 -0
  162. package/src/lib/server/runtime/inProcessServer.ts +20 -0
  163. package/src/lib/server/runtime/internalErrorResponse.ts +25 -0
  164. package/src/lib/server/runtime/isCrossOriginUpgrade.ts +19 -0
  165. package/src/lib/server/runtime/listenOnOpenPort.ts +36 -0
  166. package/src/lib/server/runtime/logExposedSurfaces.ts +162 -0
  167. package/src/lib/server/runtime/mimeForExtension.ts +20 -0
  168. package/src/lib/server/runtime/parseIdleTimeout.ts +10 -0
  169. package/src/lib/server/runtime/parsePort.ts +11 -0
  170. package/src/lib/server/runtime/registryManifests.ts +66 -0
  171. package/src/lib/server/runtime/requestContext.ts +5 -0
  172. package/src/lib/server/runtime/resolveStreamResponse.ts +29 -0
  173. package/src/lib/server/runtime/runWithRequestScope.ts +57 -0
  174. package/src/lib/server/runtime/safeJsonForScript.ts +17 -0
  175. package/src/lib/server/runtime/serializeCacheSnapshot.ts +45 -0
  176. package/src/lib/server/runtime/serverSlot.ts +13 -0
  177. package/src/lib/server/runtime/setActiveServer.ts +6 -0
  178. package/src/lib/server/runtime/snapshotEntryFromCache.ts +81 -0
  179. package/src/lib/server/runtime/streamCacheResolutions.ts +37 -0
  180. package/src/lib/server/runtime/streamFromIterator.ts +86 -0
  181. package/src/lib/server/runtime/streamStash.ts +64 -0
  182. package/src/lib/server/runtime/types/Assets.ts +1 -0
  183. package/src/lib/server/runtime/types/RequestStore.ts +27 -0
  184. package/src/lib/server/runtime/withResponseDefaults.ts +24 -0
  185. package/src/lib/server/server.ts +32 -0
  186. package/src/lib/server/socket.ts +31 -0
  187. package/src/lib/server/sockets/createSocketDispatcher.ts +311 -0
  188. package/src/lib/server/sockets/defineSocket.ts +167 -0
  189. package/src/lib/server/sockets/lookupSocket.ts +6 -0
  190. package/src/lib/server/sockets/recentHistory.ts +11 -0
  191. package/src/lib/server/sockets/registerSocket.ts +6 -0
  192. package/src/lib/server/sockets/socketOperations.ts +35 -0
  193. package/src/lib/server/sockets/socketRegistry.ts +9 -0
  194. package/src/lib/server/sockets/types/Socket.ts +21 -0
  195. package/src/lib/server/sockets/types/SocketClientFrame.ts +18 -0
  196. package/src/lib/server/sockets/types/SocketOperation.ts +22 -0
  197. package/src/lib/server/sockets/types/SocketOptions.ts +22 -0
  198. package/src/lib/server/sockets/types/SocketRegistryEntry.ts +17 -0
  199. package/src/lib/server/sockets/types/SocketRoutes.ts +10 -0
  200. package/src/lib/server/sockets/types/SocketServerFrame.ts +15 -0
  201. package/src/lib/server/sse.ts +53 -0
  202. package/src/lib/shared/BELTE_PACKAGE_NAME.ts +7 -0
  203. package/src/lib/shared/CACHE_CONTROL_VALUES.ts +16 -0
  204. package/src/lib/shared/HttpError.ts +19 -0
  205. package/src/lib/shared/RESOLVE_STREAM_PATH.ts +7 -0
  206. package/src/lib/shared/STREAMING_CONTENT_TYPES.ts +11 -0
  207. package/src/lib/shared/activeCacheStore.ts +20 -0
  208. package/src/lib/shared/appDataDir.ts +34 -0
  209. package/src/lib/shared/belteImportName.ts +44 -0
  210. package/src/lib/shared/browserClientFlags.ts +10 -0
  211. package/src/lib/shared/buildRpcRequest.ts +70 -0
  212. package/src/lib/shared/bundleLayout.ts +36 -0
  213. package/src/lib/shared/bundled.ts +34 -0
  214. package/src/lib/shared/cache.ts +559 -0
  215. package/src/lib/shared/cacheStoreSlot.ts +16 -0
  216. package/src/lib/shared/canonicalJson.ts +63 -0
  217. package/src/lib/shared/carriesBodyArgs.ts +13 -0
  218. package/src/lib/shared/clearLastConnection.ts +7 -0
  219. package/src/lib/shared/commandNameForUrl.ts +17 -0
  220. package/src/lib/shared/createCacheStore.ts +75 -0
  221. package/src/lib/shared/createPushIterator.ts +93 -0
  222. package/src/lib/shared/createRemoteFunction.ts +99 -0
  223. package/src/lib/shared/decodeResponse.ts +47 -0
  224. package/src/lib/shared/detectTarget.ts +27 -0
  225. package/src/lib/shared/exeSuffix.ts +9 -0
  226. package/src/lib/shared/exitOnBuildFailure.ts +17 -0
  227. package/src/lib/shared/extraForwardHeaders.ts +16 -0
  228. package/src/lib/shared/fileStem.ts +9 -0
  229. package/src/lib/shared/findExportCallSite.ts +479 -0
  230. package/src/lib/shared/forwardHeaders.ts +41 -0
  231. package/src/lib/shared/getRemoteMeta.ts +5 -0
  232. package/src/lib/shared/globalCacheStore.ts +15 -0
  233. package/src/lib/shared/globalCacheStoreSlot.ts +14 -0
  234. package/src/lib/shared/importNamesToStrip.ts +13 -0
  235. package/src/lib/shared/invalidateEvent.ts +11 -0
  236. package/src/lib/shared/isCompileTarget.ts +15 -0
  237. package/src/lib/shared/isDebugEnabled.ts +23 -0
  238. package/src/lib/shared/isModuleNotFound.ts +16 -0
  239. package/src/lib/shared/isReadOnlyMethod.ts +14 -0
  240. package/src/lib/shared/isStreamingResponse.ts +11 -0
  241. package/src/lib/shared/jsonSchemaForPromptArguments.ts +29 -0
  242. package/src/lib/shared/jsonSchemaForSchema.ts +32 -0
  243. package/src/lib/shared/jsonlErrorFrame.ts +24 -0
  244. package/src/lib/shared/keyForRemoteCall.ts +29 -0
  245. package/src/lib/shared/lastConnectionPath.ts +7 -0
  246. package/src/lib/shared/loadEnvFile.ts +17 -0
  247. package/src/lib/shared/loadEnvFromDataDir.ts +15 -0
  248. package/src/lib/shared/loadSvelteConfig.ts +18 -0
  249. package/src/lib/shared/log.ts +104 -0
  250. package/src/lib/shared/manifestModule.ts +39 -0
  251. package/src/lib/shared/memoizeByKey.ts +24 -0
  252. package/src/lib/shared/nearestLayoutPrefix.ts +36 -0
  253. package/src/lib/shared/normalizeTarget.ts +10 -0
  254. package/src/lib/shared/pageUrlForFile.ts +14 -0
  255. package/src/lib/shared/parseBoundedEnvInt.ts +20 -0
  256. package/src/lib/shared/parseEnv.ts +30 -0
  257. package/src/lib/shared/parsePromptMarkdown.ts +34 -0
  258. package/src/lib/shared/parseRouteSegments.ts +22 -0
  259. package/src/lib/shared/prepareRpcModule.ts +59 -0
  260. package/src/lib/shared/prepareSocketModule.ts +49 -0
  261. package/src/lib/shared/programNameForPackage.ts +14 -0
  262. package/src/lib/shared/promptNameForFile.ts +10 -0
  263. package/src/lib/shared/queryStringFromArgs.ts +27 -0
  264. package/src/lib/shared/readEnvFile.ts +15 -0
  265. package/src/lib/shared/readLastConnection.ts +18 -0
  266. package/src/lib/shared/readPackageJson.ts +9 -0
  267. package/src/lib/shared/recordRemoteMeta.ts +5 -0
  268. package/src/lib/shared/remoteMetaStore.ts +16 -0
  269. package/src/lib/shared/resolveClientFlags.ts +20 -0
  270. package/src/lib/shared/responseErrorText.ts +9 -0
  271. package/src/lib/shared/rpcUrlForFile.ts +19 -0
  272. package/src/lib/shared/runningAsStandaloneBinary.ts +13 -0
  273. package/src/lib/shared/serializeEnv.ts +18 -0
  274. package/src/lib/shared/setCacheStoreResolver.ts +6 -0
  275. package/src/lib/shared/setGlobalCacheStoreResolver.ts +6 -0
  276. package/src/lib/shared/socketNameForFile.ts +11 -0
  277. package/src/lib/shared/sseErrorFrame.ts +29 -0
  278. package/src/lib/shared/streamResponse.ts +169 -0
  279. package/src/lib/shared/stripImport.ts +27 -0
  280. package/src/lib/shared/subscribableFromResponse.ts +51 -0
  281. package/src/lib/shared/toBunRoutePattern.ts +28 -0
  282. package/src/lib/shared/types/CacheEntry.ts +63 -0
  283. package/src/lib/shared/types/CacheInvalidation.ts +9 -0
  284. package/src/lib/shared/types/CacheOptions.ts +33 -0
  285. package/src/lib/shared/types/CacheSnapshot.ts +16 -0
  286. package/src/lib/shared/types/CacheSnapshotEntry.ts +15 -0
  287. package/src/lib/shared/types/CacheStore.ts +32 -0
  288. package/src/lib/shared/types/ClientFlags.ts +11 -0
  289. package/src/lib/shared/types/CompileTarget.ts +6 -0
  290. package/src/lib/shared/types/HttpVerb.ts +1 -0
  291. package/src/lib/shared/types/LastConnection.ts +9 -0
  292. package/src/lib/shared/types/PromptArgument.ts +12 -0
  293. package/src/lib/shared/types/RawRemoteFunction.ts +13 -0
  294. package/src/lib/shared/types/RemoteFunction.ts +42 -0
  295. package/src/lib/shared/types/StandardSchemaV1.ts +57 -0
  296. package/src/lib/shared/types/StreamedResolution.ts +10 -0
  297. package/src/lib/shared/types/StreamingPlaceholder.ts +13 -0
  298. package/src/lib/shared/types/Subscribable.ts +15 -0
  299. package/src/lib/shared/types/SvelteConfig.ts +5 -0
  300. package/src/lib/shared/withJsonSchema.ts +20 -0
  301. package/src/lib/shared/writeLastConnection.ts +13 -0
  302. package/src/lib/shared/writeRoutesDts.ts +67 -0
  303. package/src/lib/test/clearVerbRegistry.ts +11 -0
  304. package/src/lib/test/createTestClient.ts +78 -0
  305. package/src/preload.ts +20 -0
  306. package/src/scaffold.ts +92 -0
  307. package/src/serverBuildPlugins.ts +25 -0
  308. package/src/serverEntry.ts +94 -0
  309. package/src/sveltePlugin.ts +58 -0
  310. package/src/tailwindStylePreprocessor.ts +62 -0
  311. package/template/bunfig.toml +4 -0
  312. package/template/package.json +19 -0
  313. package/template/src/app.ts +23 -0
  314. package/template/src/browser/app.css +21 -0
  315. package/template/src/browser/app.html +24 -0
  316. package/template/src/browser/pages/about/page.svelte +5 -0
  317. package/template/src/browser/pages/layout.svelte +26 -0
  318. package/template/src/browser/pages/page.svelte +20 -0
  319. package/template/src/bundle/icon.png +0 -0
  320. package/template/src/cli/banner.txt +3 -0
  321. package/template/src/cli/footer.txt +1 -0
  322. package/template/src/server/config.ts +17 -0
  323. package/template/src/server/rpc/getHello.ts +35 -0
  324. package/template/svelte.config.js +12 -0
  325. package/template/tsconfig.json +18 -0
  326. package/tsconfig.app.json +16 -0
@@ -0,0 +1,36 @@
1
+ import type { StandardSchemaV1 } from '../shared/types/StandardSchemaV1.ts'
2
+ import type { BundleMenu } from './BundleMenu.ts'
3
+
4
+ /*
5
+ User-authored bundle window configuration, default-exported from an
6
+ optional `src/bundle/window.ts`. Baked into the launcher at build time
7
+ (via the `belte:bundle-window` virtual) and read directly in dev. Every
8
+ field is optional — the launcher falls back to the program name for the
9
+ title and to openWebview's defaults for size.
10
+
11
+ The standard App/Edit/Window menus (Quit, copy/paste, minimize/close) plus the
12
+ built-in File menu (Start server / Connect / Disconnect) are always installed.
13
+ `menu` adds custom top-level menus between the Edit and Window menus; their items
14
+ emit `belte:menu` events the app handles. See BundleMenuItem.
15
+ */
16
+ export type BundleWindow = {
17
+ title?: string
18
+ width?: number
19
+ height?: number
20
+ menu?: BundleMenu[]
21
+ /*
22
+ Overrides the bundle setup form's schema. By default the form is derived from
23
+ src/server/config.ts's env schema, so one declaration drives both boot
24
+ validation and the form — set this only when the bundle form should differ
25
+ from the env schema (it replaces, not merges; compose with the env schema
26
+ yourself via its `.extend(...)` if you want both).
27
+
28
+ A Standard Schema (the same kind belte accepts for RPC/MCP). Its JSON Schema
29
+ drives the connect screen's first-run form, shown as a modal when Start is
30
+ clicked with a required key still unset; the user's answers persist to the
31
+ data-dir `.env` the server loads at boot. Each property maps to one env var of
32
+ the same name; `title` is the field label, `description` the hint,
33
+ `format: 'password'` masks the input, and `default` pre-fills it.
34
+ */
35
+ config?: StandardSchemaV1
36
+ }
@@ -0,0 +1,9 @@
1
+ /*
2
+ Revision of belte's own contribution to the compiled webview library — the
3
+ native shim sources linked in beside the vendored header (e.g. belteMenu.mm)
4
+ and the flags buildWebviewLib compiles them with. It participates in the build
5
+ cache key alongside the upstream version, so changing belte's native build
6
+ selects a fresh cache path and bypasses any library built before the change.
7
+ Bump this whenever the shim sources or their compile invocation change.
8
+ */
9
+ export const WEBVIEW_BUILD_REVISION = 9
@@ -0,0 +1,7 @@
1
+ /*
2
+ Upstream `webview/webview` release the vendored `native/webview.h` is taken
3
+ from (https://github.com/webview/webview, MIT). Used to namespace the build
4
+ cache so bumping the header naturally bypasses any previously built library.
5
+ Bump this whenever `native/webview.h` is re-vendored.
6
+ */
7
+ export const WEBVIEW_VERSION = '0.12.0'
@@ -0,0 +1,29 @@
1
+ import { dlopen, FFIType } from 'bun:ffi'
2
+
3
+ /*
4
+ Binds belte_set_connected — the native flag the macOS Server menu's
5
+ validateMenuItem: reads to enable/disable Start/Disconnect. The symbol is
6
+ compiled only into the macOS lib (the Cocoa menu shim), so a failed lookup
7
+ degrades to a no-op setter rather than throwing on Linux/Windows.
8
+
9
+ The flag is a process-global, which is what lets the bundle's control server set
10
+ it from off the main thread: that server runs in a Worker because `webview_run`
11
+ blocks the main thread's JS event loop, yet the main-thread menu still reads the
12
+ same value through the shared dylib image. The returned close releases the handle.
13
+ */
14
+ export function bindConnectedFlag(libPath: string): {
15
+ setConnected: (connected: boolean) => void
16
+ close: () => void
17
+ } {
18
+ try {
19
+ const lib = dlopen(libPath, {
20
+ belte_set_connected: { args: [FFIType.i32], returns: FFIType.void },
21
+ })
22
+ return {
23
+ setConnected: (connected) => lib.symbols.belte_set_connected(connected ? 1 : 0),
24
+ close: () => lib.close(),
25
+ }
26
+ } catch {
27
+ return { setConnected: () => {}, close: () => {} }
28
+ }
29
+ }
@@ -0,0 +1,31 @@
1
+ import { dlopen, FFIType } from 'bun:ffi'
2
+
3
+ /*
4
+ Binds belte_request_navigate — points the live webview window at a URL from any
5
+ thread by hopping onto the UI thread via webview_dispatch. The launcher's control
6
+ server runs in a Worker (off the main thread that webview_run blocks), so when it
7
+ detects the connected server has died it uses this to bounce the window back to the
8
+ connect screen. macOS-only symbol (the Cocoa shim), so a failed lookup degrades to
9
+ a no-op — elsewhere the worker can still correct the menu flag, just not the window.
10
+
11
+ `handle` is the webview pointer created on the main thread, forwarded to the worker
12
+ as a number; bun:ffi accepts it for a ptr argument from either thread because the
13
+ pointer addresses the same process heap.
14
+ */
15
+ export function bindRequestNavigate(libPath: string): {
16
+ requestNavigate: (handle: number, url: string) => void
17
+ close: () => void
18
+ } {
19
+ try {
20
+ const lib = dlopen(libPath, {
21
+ belte_request_navigate: { args: [FFIType.ptr, FFIType.ptr], returns: FFIType.void },
22
+ })
23
+ return {
24
+ requestNavigate: (handle, url) =>
25
+ lib.symbols.belte_request_navigate(handle, new TextEncoder().encode(`${url}\0`)),
26
+ close: () => lib.close(),
27
+ }
28
+ } catch {
29
+ return { requestNavigate: () => {}, close: () => {} }
30
+ }
31
+ }
@@ -0,0 +1,111 @@
1
+ import { dirname } from 'node:path'
2
+ import { log } from '../shared/log.ts'
3
+ import { WEBVIEW_VERSION } from './WEBVIEW_VERSION.ts'
4
+ import { webviewCachePath } from './webviewCachePath.ts'
5
+
6
+ // Vendored upstream amalgamation; the host compiler turns it into a lib.
7
+ const HEADER = new URL('./native/webview.h', import.meta.url).pathname
8
+
9
+ // belte's own native shim, linked into the same dylib on macOS to add the
10
+ // standard application menu bar the upstream webview omits.
11
+ const MAC_MENU_SOURCE = new URL('./native/belteMenu.mm', import.meta.url).pathname
12
+
13
+ /*
14
+ Linux GTK/WebKit pkg-config module sets, newest first. The vendored header
15
+ auto-selects the GTK4 or GTK3 backend from GTK_MAJOR_VERSION in the include
16
+ path, so supplying the right cflags/libs is all that's needed — no backend
17
+ macro. The first set whose packages are installed wins.
18
+ */
19
+ const LINUX_PKG_SETS = [
20
+ ['gtk4', 'webkitgtk-6.0'],
21
+ ['gtk+-3.0', 'webkit2gtk-4.1'],
22
+ ['gtk+-3.0', 'webkit2gtk-4.0'],
23
+ ]
24
+
25
+ /*
26
+ Compiles the vendored webview header into a native shared library for the
27
+ host platform and caches it (webviewCachePath), so `belte bundle` needs
28
+ no prebuilt binary and no third-party webview package — just
29
+ the platform's C++ toolchain. Returns the cached path. Throws actionable
30
+ guidance when the toolchain or native webview dev packages are missing,
31
+ rather than letting the compiler fail opaquely.
32
+ */
33
+ export async function buildWebviewLib(): Promise<string> {
34
+ const outfile = webviewCachePath()
35
+ await Bun.$`mkdir -p ${dirname(outfile)}`.quiet()
36
+ log.info(`building webview ${WEBVIEW_VERSION} for ${process.platform}-${process.arch}…`)
37
+
38
+ if (process.platform === 'darwin') {
39
+ await compileDarwin(outfile)
40
+ } else if (process.platform === 'linux') {
41
+ await compileLinux(outfile)
42
+ } else {
43
+ /*
44
+ Windows needs MSVC + the WebView2 SDK header (WebView2.h), which
45
+ isn't a turnkey shell invocation. Until that build path lands,
46
+ point Windows users at the explicit-path escape hatch.
47
+ */
48
+ throw new Error(
49
+ `[belte] building the webview library on ${process.platform} isn't supported yet. ` +
50
+ 'Set BELTE_WEBVIEW_LIB to a prebuilt webview library to continue.',
51
+ )
52
+ }
53
+
54
+ log.success(`built webview library: ${outfile}`)
55
+ return outfile
56
+ }
57
+
58
+ // macOS: clang against the WebKit + Cocoa frameworks (always present with the
59
+ // Command Line Tools), linking belte's Objective-C++ menu shim into the same
60
+ // dylib. The `-x` flags switch the input language per file: the vendored
61
+ // header compiles as C++ (it uses the C objc runtime, not objc syntax), the
62
+ // shim as Objective-C++. Maps a missing compiler to the install hint.
63
+ async function compileDarwin(outfile: string): Promise<void> {
64
+ try {
65
+ await Bun.$`clang++ -std=c++17 -DWEBVIEW_BUILD_SHARED -fvisibility=hidden -shared -framework WebKit -framework Cocoa -x c++ ${HEADER} -x objective-c++ ${MAC_MENU_SOURCE} -o ${outfile}`.quiet()
66
+ } catch (error) {
67
+ throw new Error(
68
+ '[belte] failed to compile the webview library. Install the Xcode Command ' +
69
+ 'Line Tools with `xcode-select --install` and try again.\n' +
70
+ describeShellError(error),
71
+ )
72
+ }
73
+ }
74
+
75
+ // Linux: detect an installed GTK/WebKit set via pkg-config, then compile a
76
+ // position-independent shared object. Maps missing packages to install hints.
77
+ async function compileLinux(outfile: string): Promise<void> {
78
+ const flags = await linuxPkgFlags()
79
+ try {
80
+ await Bun.$`c++ -std=c++17 -DWEBVIEW_BUILD_SHARED -fvisibility=hidden -fPIC -shared -x c++ ${HEADER} ${flags} -o ${outfile}`.quiet()
81
+ } catch (error) {
82
+ throw new Error(
83
+ '[belte] failed to compile the webview library. Ensure a C++ compiler ' +
84
+ '(e.g. `build-essential`) is installed.\n' +
85
+ describeShellError(error),
86
+ )
87
+ }
88
+ }
89
+
90
+ // Returns combined cflags + libs for the first available GTK/WebKit set, or
91
+ // throws with the package names to install when none resolve.
92
+ async function linuxPkgFlags(): Promise<string[]> {
93
+ for (const modules of LINUX_PKG_SETS) {
94
+ const probe = await Bun.$`pkg-config --exists ${modules}`.nothrow().quiet()
95
+ if (probe.exitCode === 0) {
96
+ const flags = await Bun.$`pkg-config --cflags --libs ${modules}`.quiet().text()
97
+ return flags.trim().split(/\s+/)
98
+ }
99
+ }
100
+ throw new Error(
101
+ '[belte] no GTK/WebKit development packages found. Install one set, e.g. ' +
102
+ '`libgtk-4-dev libwebkitgtk-6.0-dev` or `libgtk-3-dev libwebkit2gtk-4.1-dev`, ' +
103
+ 'or set BELTE_WEBVIEW_LIB to a prebuilt webview library.',
104
+ )
105
+ }
106
+
107
+ // Surfaces the compiler's own stderr when a Bun shell command throws.
108
+ function describeShellError(error: unknown): string {
109
+ const stderr = (error as { stderr?: Uint8Array }).stderr
110
+ return stderr ? new TextDecoder().decode(stderr) : String(error)
111
+ }
@@ -0,0 +1,9 @@
1
+ /*
2
+ Tailwind entry for the bundle connect screen. `@import "tailwindcss"` pulls the
3
+ engine in; the explicit `@source` guarantees the default component is scanned for
4
+ utility classes even if automatic source detection misses it. bun-plugin-tailwind
5
+ compiles this at build time; buildDisconnected inlines the result into the served
6
+ HTML so the screen needs zero external requests.
7
+ */
8
+ @import 'tailwindcss';
9
+ @source './disconnected.svelte';
@@ -0,0 +1,386 @@
1
+ <script lang="ts">
2
+ /*
3
+ Default bundle connect screen. The launcher serves this (with the logo baked in
4
+ at build time and the app title injected at runtime) instead of a blank window,
5
+ and overriding it is a matter of dropping a `src/bundle/disconnected.svelte`.
6
+
7
+ It both connects to a remote server by URL and boots the embedded server, talking
8
+ to the launcher's in-process control server: POST /connect and POST /start each
9
+ reply with a `{ redirect }` the page follows, while the launcher records the
10
+ connection so the native File menu's enabled state stays authoritative.
11
+ */
12
+
13
+ /*
14
+ The last remote URL that successfully connected — only prefills the form's input
15
+ on a later visit; it never drives a reconnect (the launcher owns auto-resume now,
16
+ deciding before the window even opens). Survives disconnect and quitting.
17
+ */
18
+ const LAST_URL_KEY = 'belte:last-server-url'
19
+
20
+ // Injected globals: app title from the launcher, logo data URI from the build.
21
+ const heading = (globalThis as { __BELTE_TITLE__?: string }).__BELTE_TITLE__ ?? 'belte app'
22
+ const logo = (globalThis as { __BELTE_LOGO__?: string }).__BELTE_LOGO__
23
+
24
+ const placeholder = 'https://example.com'
25
+
26
+ // Prefill the form with the last server we connected to, from any prior launch.
27
+ let url = $state(localStorage.getItem(LAST_URL_KEY) ?? '')
28
+ let error = $state<string | undefined>(undefined)
29
+
30
+ /*
31
+ `?action=` set by the File menu (Start/Disconnect) or the launcher when a live
32
+ connection dies (`lost`). Auto-resume of a saved connection now happens in the
33
+ launcher before the window opens, so this screen only ever loads as a real
34
+ destination — there's no no-action auto-resume left to handle here.
35
+ */
36
+ const launchAction = new URLSearchParams(location.search).get('action')
37
+
38
+ /*
39
+ Two phases so the screen never flashes before a redirect. A menu Start may boot
40
+ straight through, so it opens on a neutral splash; every other entry is a genuine
41
+ destination, so the connect screen shows immediately. Boot/connect re-enter the
42
+ splash so a redirect (including after saving config) never flashes the form.
43
+ */
44
+ let phase = $state<'splash' | 'connect'>(launchAction === 'start' ? 'splash' : 'connect')
45
+
46
+ /*
47
+ First-run config form, surfaced as a modal only when Start is clicked (or
48
+ auto-start fires) with a required key still unset. Fields are derived from the
49
+ app's config JSON Schema served by the launcher; answers post back to the
50
+ data-dir `.env` the embedded server loads at boot.
51
+ */
52
+ type ConfigField = {
53
+ key: string
54
+ label: string
55
+ description?: string
56
+ inputType: 'text' | 'password' | 'number' | 'checkbox'
57
+ required: boolean
58
+ }
59
+ let configFields = $state<ConfigField[]>([])
60
+ let configValues = $state<Record<string, string>>({})
61
+ let showConfig = $state(false)
62
+ let savingConfig = $state(false)
63
+ // Every required field has a value — gates the modal's Save button.
64
+ const canSaveConfig = $derived(
65
+ configFields.every(
66
+ (field) =>
67
+ !field.required ||
68
+ field.inputType === 'checkbox' ||
69
+ (configValues[field.key] ?? '').trim() !== '',
70
+ ),
71
+ )
72
+
73
+ /*
74
+ Interpret the boot intent once on load. `?action=` is set by the native File
75
+ menu's navigate items (or the launcher when a live connection dies); absent it, a
76
+ remembered server reconnects automatically:
77
+ - start → boot the embedded server (matches the Start Server menu item).
78
+ - lost → the connected server stopped responding; explain and wait (the
79
+ form is already prefilled with the last URL for a one-click retry).
80
+ - disconnect → forget the URL + tear down any embedded server, stay here so the
81
+ form is the place to point at another server.
82
+ - (none) → reconnect to the saved server if there is one.
83
+ */
84
+ $effect(() => {
85
+ if (launchAction === 'start') {
86
+ // File-menu Start Server is an explicit click → run the Start flow.
87
+ void start()
88
+ return
89
+ }
90
+ if (launchAction === 'lost') {
91
+ error = 'The server stopped responding.'
92
+ return
93
+ }
94
+ if (launchAction === 'disconnect') {
95
+ // Have the launcher forget the auto-resume choice and reap any embedded
96
+ // server; LAST_URL_KEY stays so the form is still prefilled to reconnect.
97
+ void fetch('/__belte/disconnect').catch(() => {})
98
+ }
99
+ })
100
+
101
+ /*
102
+ Ask the launcher to connect to a server by URL. It verifies the URL really is a
103
+ belte server — POST /connect probes the target's identity endpoint — and flips the
104
+ native menu's connected flag before replying with the `{ redirect }` to follow; a
105
+ URL that isn't a belte server comes back as an error shown below the form. Only a
106
+ confirmed URL is remembered, so a relaunch never auto-retries a dead or wrong
107
+ address. The saved-URL reconnect path runs through here too.
108
+ */
109
+ async function connect(target: string = url.trim()): Promise<void> {
110
+ const cleaned = target.trim()
111
+ if (!cleaned) {
112
+ return
113
+ }
114
+ error = undefined
115
+ // Hide the form while connecting so a successful redirect doesn't flash it.
116
+ phase = 'splash'
117
+ try {
118
+ const response = await fetch('/connect', {
119
+ method: 'POST',
120
+ headers: { 'content-type': 'application/json' },
121
+ body: JSON.stringify({ url: cleaned }),
122
+ })
123
+ if (!response.ok) {
124
+ const body = (await response.json()) as { error?: string }
125
+ throw new Error(body.error ?? `connect failed (${response.status})`)
126
+ }
127
+ const { redirect } = (await response.json()) as { redirect: string }
128
+ // Prefill the form with this server on a later visit (the launcher records
129
+ // the auto-resume choice itself, on the /connect it just handled).
130
+ localStorage.setItem(LAST_URL_KEY, cleaned)
131
+ location.href = redirect
132
+ } catch (cause) {
133
+ error = `Could not connect: ${String(cause)}`
134
+ // Failed — bring the form back so the error and a retry are visible.
135
+ phase = 'connect'
136
+ }
137
+ }
138
+
139
+ /*
140
+ Start, always an explicit click (button or File-menu) — auto-resume happens in
141
+ the launcher before the window opens, so this is never a launch path. Asks the
142
+ launcher what config the app needs: if it declares any, open the modal (prefilled
143
+ with the last-used values) so the user can review or change settings before
144
+ booting — re-running Start after a disconnect is how you reconfigure. With no
145
+ config schema, boot straight through. The modal's save path resumes the boot.
146
+ */
147
+ async function start(): Promise<void> {
148
+ error = undefined
149
+ const config = await loadConfig().catch(() => undefined)
150
+ if (config) {
151
+ configFields = config.fields
152
+ configValues = { ...config.values }
153
+ // Reveal the connect screen as the modal's backdrop.
154
+ phase = 'connect'
155
+ showConfig = true
156
+ return
157
+ }
158
+ await boot()
159
+ }
160
+
161
+ // Boot the embedded server via the launcher, then follow it once it answers.
162
+ async function boot(): Promise<void> {
163
+ // Splash while booting so the connect screen doesn't flash before the redirect
164
+ // (including straight after saving config).
165
+ phase = 'splash'
166
+ try {
167
+ const response = await fetch('/start', { method: 'POST' })
168
+ if (!response.ok) {
169
+ const body = (await response.json()) as { error?: string }
170
+ throw new Error(body.error ?? `start failed (${response.status})`)
171
+ }
172
+ const { redirect } = (await response.json()) as { redirect: string }
173
+ location.href = redirect
174
+ } catch (cause) {
175
+ error = `Could not start the server: ${String(cause)}`
176
+ // Boot failed — bring the connect screen back to show the error.
177
+ phase = 'connect'
178
+ }
179
+ }
180
+
181
+ /*
182
+ Fetches the app's config schema + resolved current values from the launcher and
183
+ turns the JSON Schema into render-ready fields. Returns undefined when no schema
184
+ is declared, so Start never gates.
185
+ */
186
+ async function loadConfig(): Promise<
187
+ { fields: ConfigField[]; values: Record<string, string> } | undefined
188
+ > {
189
+ const response = await fetch('/__belte/config')
190
+ const { schema, values } = (await response.json()) as {
191
+ schema: Record<string, unknown> | null
192
+ values: Record<string, string>
193
+ }
194
+ if (!schema) {
195
+ return undefined
196
+ }
197
+ return { fields: fieldsFromSchema(schema), values: values ?? {} }
198
+ }
199
+
200
+ // Derives one render-ready field per JSON Schema property, reusing the standard
201
+ // slots: `title` → label, `description` → hint, `format`/`type` → input kind.
202
+ function fieldsFromSchema(schema: Record<string, unknown>): ConfigField[] {
203
+ const properties = (schema.properties ?? {}) as Record<string, Record<string, unknown>>
204
+ const required = new Set((schema.required as string[]) ?? [])
205
+ return Object.entries(properties).map(([key, property]) => ({
206
+ key,
207
+ label: (property.title as string) ?? key,
208
+ description: property.description as string | undefined,
209
+ inputType: inputType(property),
210
+ required: required.has(key),
211
+ }))
212
+ }
213
+
214
+ // Maps a JSON Schema property to an HTML input kind (directory falls back to text
215
+ // until a native picker exists).
216
+ function inputType(property: Record<string, unknown>): ConfigField['inputType'] {
217
+ if (property.type === 'boolean') {
218
+ return 'checkbox'
219
+ }
220
+ if (property.type === 'number' || property.type === 'integer') {
221
+ return 'number'
222
+ }
223
+ if (property.format === 'password') {
224
+ return 'password'
225
+ }
226
+ return 'text'
227
+ }
228
+
229
+ // Persist the form's answers to the data-dir `.env`, then resume the boot.
230
+ async function saveConfig(): Promise<void> {
231
+ error = undefined
232
+ savingConfig = true
233
+ try {
234
+ const response = await fetch('/__belte/config', {
235
+ method: 'POST',
236
+ headers: { 'content-type': 'application/json' },
237
+ body: JSON.stringify({ values: configValues }),
238
+ })
239
+ if (!response.ok) {
240
+ throw new Error(`save failed (${response.status})`)
241
+ }
242
+ showConfig = false
243
+ savingConfig = false
244
+ await boot()
245
+ } catch (cause) {
246
+ error = `Could not save settings: ${String(cause)}`
247
+ savingConfig = false
248
+ }
249
+ }
250
+ </script>
251
+
252
+ {#if phase === 'splash'}
253
+ <!-- Neutral splash shown while an auto-start/auto-reconnect resolves, so the
254
+ connect screen never flashes before it redirects. Same background as the card. -->
255
+ <div class="flex min-h-screen items-center justify-center bg-gray-50 dark:bg-gray-950">
256
+ {#if logo}
257
+ <img src={logo} alt="" class="h-16 w-16 rounded-xl object-contain opacity-90">
258
+ {/if}
259
+ </div>
260
+ {:else}
261
+ <main
262
+ class="flex min-h-screen items-center justify-center bg-gray-50 p-6 text-gray-900 dark:bg-gray-950 dark:text-gray-100">
263
+ <div
264
+ class="w-full max-w-sm rounded-2xl bg-white p-8 shadow-sm ring-1 ring-gray-200 dark:bg-gray-900 dark:ring-gray-800">
265
+ {#if logo}
266
+ <img src={logo} alt="" class="mx-auto mb-5 h-16 w-16 rounded-xl object-contain">
267
+ {/if}
268
+ <h1 class="mb-6 text-center text-xl font-semibold tracking-tight">{heading}</h1>
269
+
270
+ <form
271
+ class="flex flex-col gap-3"
272
+ onsubmit={(event) => {
273
+ event.preventDefault()
274
+ void connect()
275
+ }}>
276
+ <input
277
+ type="url"
278
+ bind:value={url}
279
+ {placeholder}
280
+ autocomplete="url"
281
+ class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm outline-none focus:border-gray-900 focus:ring-1 focus:ring-gray-900 dark:border-gray-700 dark:bg-gray-800 dark:focus:border-gray-100 dark:focus:ring-gray-100">
282
+ <button
283
+ type="submit"
284
+ class="w-full rounded-lg bg-gray-900 px-3 py-2 text-sm font-medium text-white hover:bg-gray-700 dark:bg-gray-100 dark:text-gray-900 dark:hover:bg-gray-300">
285
+ Connect
286
+ </button>
287
+ </form>
288
+
289
+ <div class="my-5 flex items-center gap-3 text-xs text-gray-400 dark:text-gray-500">
290
+ <span class="h-px flex-1 bg-gray-200 dark:bg-gray-800"></span>
291
+ or
292
+ <span class="h-px flex-1 bg-gray-200 dark:bg-gray-800"></span>
293
+ </div>
294
+
295
+ <button
296
+ type="button"
297
+ onclick={() => void start()}
298
+ class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm font-medium hover:bg-gray-50 dark:border-gray-700 dark:hover:bg-gray-800">
299
+ Start server
300
+ </button>
301
+
302
+ {#if error}
303
+ <p class="mt-4 text-center text-sm text-red-600 dark:text-red-400">{error}</p>
304
+ {/if}
305
+
306
+ <p class="mt-8 text-center text-xs text-gray-400 dark:text-gray-500">
307
+ made with
308
+ <a
309
+ href="https://github.com/briancray/belte"
310
+ class="underline hover:text-gray-600 dark:hover:text-gray-300">
311
+ belte
312
+ </a>
313
+ </p>
314
+ </div>
315
+ </main>
316
+ {/if}
317
+
318
+ {#if showConfig}
319
+ <!-- First-run config modal — shown only when Start needs settings the app lacks. -->
320
+ <div
321
+ class="fixed inset-0 z-10 flex items-center justify-center bg-black/40 p-6 text-gray-900 dark:text-gray-100">
322
+ <div
323
+ class="w-full max-w-sm rounded-2xl bg-white p-8 shadow-lg ring-1 ring-gray-200 dark:bg-gray-900 dark:ring-gray-800">
324
+ <h2 class="mb-5 text-lg font-semibold tracking-tight">Set up{heading}</h2>
325
+
326
+ <form
327
+ class="flex flex-col gap-4"
328
+ onsubmit={(event) => {
329
+ event.preventDefault()
330
+ void saveConfig()
331
+ }}>
332
+ {#each configFields as field (field.key)}
333
+ <label class="flex flex-col gap-1 text-sm">
334
+ <span class="font-medium">
335
+ {field.label}
336
+ {#if field.required}
337
+ <span class="text-red-500">*</span>
338
+ {/if}
339
+ </span>
340
+ {#if field.inputType === 'checkbox'}
341
+ <input
342
+ type="checkbox"
343
+ checked={configValues[field.key] === 'true'}
344
+ onchange={(event) =>
345
+ (configValues[field.key] = event.currentTarget.checked
346
+ ? 'true'
347
+ : 'false')}
348
+ class="mt-1 size-4 self-start rounded border-gray-300 dark:border-gray-700">
349
+ {:else}
350
+ <input
351
+ type={field.inputType}
352
+ value={configValues[field.key] ?? ''}
353
+ oninput={(event) =>
354
+ (configValues[field.key] = event.currentTarget.value)}
355
+ class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm outline-none focus:border-gray-900 focus:ring-1 focus:ring-gray-900 dark:border-gray-700 dark:bg-gray-800 dark:focus:border-gray-100 dark:focus:ring-gray-100">
356
+ {/if}
357
+ {#if field.description}
358
+ <span class="text-xs text-gray-400 dark:text-gray-500">
359
+ {field.description}
360
+ </span>
361
+ {/if}
362
+ </label>
363
+ {/each}
364
+
365
+ <div class="mt-1 flex gap-3">
366
+ <button
367
+ type="button"
368
+ onclick={() => (showConfig = false)}
369
+ class="flex-1 rounded-lg border border-gray-300 px-3 py-2 text-sm font-medium hover:bg-gray-50 dark:border-gray-700 dark:hover:bg-gray-800">
370
+ Cancel
371
+ </button>
372
+ <button
373
+ type="submit"
374
+ disabled={!canSaveConfig || savingConfig}
375
+ class="flex-1 rounded-lg bg-gray-900 px-3 py-2 text-sm font-medium text-white hover:bg-gray-700 disabled:opacity-60 dark:bg-gray-100 dark:text-gray-900 dark:hover:bg-gray-300">
376
+ {savingConfig ? 'Saving…' : 'Save & start'}
377
+ </button>
378
+ </div>
379
+ </form>
380
+
381
+ {#if error}
382
+ <p class="mt-4 text-center text-sm text-red-600 dark:text-red-400">{error}</p>
383
+ {/if}
384
+ </div>
385
+ </div>
386
+ {/if}
@@ -0,0 +1,20 @@
1
+ import { buildWebviewLib } from './buildWebviewLib.ts'
2
+ import { resolveWebviewLib } from './resolveWebviewLib.ts'
3
+
4
+ /*
5
+ Build-time guarantee that a webview library exists, returning its path.
6
+ Tries plain resolution first (an explicit BELTE_WEBVIEW_LIB, a bundle-local
7
+ copy, or a previously built cache); on a miss it compiles the vendored
8
+ header for the host (buildWebviewLib) and caches the result.
9
+
10
+ Used by `belte bundle` (bundleApp), which runs under bun on a developer's
11
+ machine — never by the compiled launcher, which only ever resolves the copy
12
+ shipped beside it and must not invoke a compiler on an end user's machine.
13
+ */
14
+ export async function ensureWebviewLib(cwd: string = process.cwd()): Promise<string> {
15
+ try {
16
+ return await resolveWebviewLib(cwd)
17
+ } catch {
18
+ return await buildWebviewLib()
19
+ }
20
+ }
@@ -0,0 +1,28 @@
1
+ /*
2
+ Tie the embedded server's lifetime to the bundle launcher's.
3
+
4
+ The launcher spawns this server with BELTE_PARENT_PID set to its own pid. On a
5
+ clean window close the launcher reaps the child directly, but a force-quit (or
6
+ crash) of the launcher can't run that cleanup, which would leave the server
7
+ orphaned and holding its port. So when that env var is present, poll the parent
8
+ and exit once it's gone. A no-op when the var is absent (standalone `belte
9
+ start`), so it only ever activates inside a bundle.
10
+ */
11
+ export function exitWithParent(): void {
12
+ const parent = process.env.BELTE_PARENT_PID
13
+ if (!parent) {
14
+ return
15
+ }
16
+ const parentPid = Number(parent)
17
+ const timer = setInterval(() => {
18
+ try {
19
+ // Signal 0 sends nothing — it only probes existence, throwing when the
20
+ // parent has exited (or its pid is no longer reachable).
21
+ process.kill(parentPid, 0)
22
+ } catch {
23
+ process.exit(0)
24
+ }
25
+ }, 1000)
26
+ // The watchdog alone shouldn't keep the server process alive.
27
+ timer.unref()
28
+ }