electrobun 1.16.0 → 1.17.0-beta.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.
- package/README.md +36 -2
- package/dist/api/bun/ElectrobunConfig.ts +16 -0
- package/dist/api/bun/core/BrowserView.ts +41 -13
- package/dist/api/bun/core/Socket.ts +8 -0
- package/dist/api/bun/core/Updater.ts +2 -4
- package/dist/api/bun/core/Utils.ts +18 -15
- package/dist/api/bun/index.ts +137 -0
- package/dist/api/bun/preload/build.ts +6 -4
- package/dist/api/bun/proc/native.ts +271 -238
- package/dist/api/shared/bun-version.ts +1 -1
- package/package.json +3 -2
- package/src/cli/index.ts +281 -21
package/README.md
CHANGED
|
@@ -27,9 +27,43 @@ Visit <a href="https://blackboard.sh/electrobun/">https://blackboard.sh/electrob
|
|
|
27
27
|
- Provide everything you need in one tightly integrated workflow to start writing code in 5 minutes and distribute in 10.
|
|
28
28
|
|
|
29
29
|
## Apps Built with Electrobun
|
|
30
|
+
- [24agents](https://github.com/jhsu/24agents) - Hyperprompter
|
|
31
|
+
- [act-track-ai](https://github.com/IrdanGu/act-track-ai) - personal desktop productivity tracker
|
|
32
|
+
- [Agents Council](https://github.com/MrLesk/agents-council) - agent-to-agent MCP communication tool for feedback requests
|
|
33
|
+
- [ai-wrapped](https://github.com/gulivan/ai-wrapped) - Wrapped-style desktop dashboard for your AI coding agent activity
|
|
30
34
|
- [Audio TTS](https://github.com/blackboardsh/audio-tts) - desktop text-to-speech app using Qwen3-TTS for voice design, cloning, and generation
|
|
35
|
+
- [aueio-player-desktop](https://github.com/tuomashatakka/aueio-player-desktop) - beautiful, minimal cross-platform audio player
|
|
36
|
+
- [bestdiff](https://github.com/tesmond/bestdiff) - a git diff checker with curved connectors
|
|
37
|
+
- [BuddyWriter](https://github.com/OxFrancesco/BuddyWriter) - BuddyWriter desktop and mobile apps
|
|
38
|
+
- [burns](https://github.com/l3wi/burns) - a Smithers manager
|
|
39
|
+
- [cbx-tool](https://github.com/jebin2/cbx-tool) - desktop app for reading and editing comic book archives (.cbz/.cbr)
|
|
31
40
|
- [Co(lab)](https://blackboard.sh/colab/) - a hybrid web browser + code editor for deep work
|
|
41
|
+
- [codlogs](https://github.com/tobitege/codlogs) - search and export local Codex sessions via CLI or desktop app
|
|
42
|
+
- [Codex Agents Composer](https://github.com/MrLesk/codex-agents-composer) - desktop app for managing your Codex agents and their skills
|
|
43
|
+
- [codex-devtools](https://github.com/gulivan/codex-devtools) - desktop inspector for Codex session data; browse conversations, search messages, and analyze agent activity
|
|
44
|
+
- [Deskdown](https://github.com/guarana-studio/deskdown) - transform any web address into a desktop app in under 20 seconds
|
|
45
|
+
- [dev-3.0](https://github.com/h0x91b/dev-3.0) - helps you not get lost while managing multiple AI agents across projects
|
|
32
46
|
- [DOOM](https://github.com/blackboardsh/electrobun-doom) - DOOM implemented in 2 ways: bun -> (c doom -> bundled wgpu) and (full ts port bun -> bundled wgpu)
|
|
47
|
+
- [electrobun-rms](https://github.com/khanhthanhdev/electrobun-rms) - fast Electrobun desktop app template with React, Tailwind CSS, and Vite
|
|
48
|
+
- [golb](https://github.com/chrisdadev13/golb) - desktop AI coding workspace built with React, Vite, and Tailwind
|
|
49
|
+
- [GOG Achievements GUI](https://github.com/timendum/gog-achievements-gui) - desktop app for managing GOG achievements
|
|
50
|
+
- [groov](https://github.com/laurenzcodes/groov) - desktop audio deck monitor
|
|
51
|
+
- [Guerilla Glass](https://github.com/okikeSolutions/guerillaglass) - open-source cross-platform creator studio for fast Record -> Edit -> Deliver workflows
|
|
52
|
+
- [Marginalia](https://github.com/lars-hoeijmans/Marginalia) - a simple note taking app
|
|
53
|
+
- [md-browse](https://github.com/needle-tools/md-browse) - a markdown-first browser that converts web pages to clean markdown
|
|
54
|
+
- [peekachu](https://github.com/needle-tools/peekachu) - password manager for AIs; store secrets in your OS keychain and scrub output so AI assistants never see actual values
|
|
55
|
+
- [PLEXI](https://github.com/ianjamesburke/PLEXI) - a multi-dimensional terminal multiplexer for the agentic era
|
|
56
|
+
- [Prometheus](https://github.com/opensourcectl/prometheus) - desktop utility toolbox for file cleanup, document manipulation, and image processing
|
|
57
|
+
- [Quiver](https://ataraxy-labs.github.io/quiver/) - desktop app for GitHub PR reviews, merge conflict resolution, and AI commit messages
|
|
58
|
+
- [remotecode.io](https://github.com/samuelfaj/remotecode.io) - continue local AI coding sessions (Claude Code or Codex) from your mobile device
|
|
59
|
+
- [sirene](https://github.com/KevinBonnoron/sirene) - self-hosted multi-backend text-to-speech platform with voice cloning
|
|
60
|
+
- [StoryForge](https://github.com/vrrdnt/StoryForge) - desktop app for Vintage Story players to switch between game versions, modpacks, servers, and accounts
|
|
61
|
+
- [Tensamin Client](https://github.com/Tensamin/Client) - web, desktop, and mobile app for accessing Tensamin
|
|
62
|
+
- [tokenpass-desktop](https://github.com/b-open-io/tokenpass-desktop) - desktop app that runs the Sigma Identity stack locally for Bitcoin-backed authentication
|
|
63
|
+
- [typsmthng-desktop](https://github.com/aaditagrawal/typsmthng-desktop) - experimental desktop typing application
|
|
64
|
+
- [VibesOS](https://github.com/popmechanic/VibesOS) - A GUI for Claude Code that makes it easy to vibe code simple, un-hackable apps
|
|
65
|
+
- [VoiceVault](https://github.com/PJH720/VoiceVault) - AI-powered voice recorder with transcription, summarization, and RAG search
|
|
66
|
+
- [warren](https://github.com/Loa212/warren) - open-source, peer-to-peer terminal mesh for accessing your machines from any device without SSH keys or config files
|
|
33
67
|
|
|
34
68
|
### Video Demos
|
|
35
69
|
|
|
@@ -41,12 +75,12 @@ Visit <a href="https://blackboard.sh/electrobun/">https://blackboard.sh/electrob
|
|
|
41
75
|
|
|
42
76
|
## Star History
|
|
43
77
|
|
|
44
|
-
[](https://www.star-history.com/#blackboardsh/electrobun&type=date&legend=top-left)
|
|
45
79
|
|
|
46
80
|
## Contributing
|
|
47
81
|
Ways to get involved:
|
|
48
82
|
|
|
49
|
-
- Follow us on X for updates <a href="https://twitter.com/BlackboardTech">@BlackboardTech</a> or <a href="https://bsky.app/profile/yoav.codes">@yoav.codes</a>
|
|
83
|
+
- Follow us on X for updates <a href="https://twitter.com/BlackboardTech">@BlackboardTech</a> and <a href="https://twitter.com/YoavCodes">@YoavCodes</a> or on bluesky <a href="https://bsky.app/profile/yoav.codes">@yoav.codes</a>
|
|
50
84
|
- Join the conversation on <a href="https://discord.gg/ueKE4tjaCE">Discord</a>
|
|
51
85
|
- Create and participate in Github issues and discussions
|
|
52
86
|
- Let me know what you're building with Electrobun
|
|
@@ -198,6 +198,22 @@ export interface ElectrobunConfig {
|
|
|
198
198
|
*/
|
|
199
199
|
watchIgnore?: string[];
|
|
200
200
|
|
|
201
|
+
/**
|
|
202
|
+
* Carrot build configuration.
|
|
203
|
+
* When present, the build also produces a carrot artifact alongside the standalone app.
|
|
204
|
+
* Set `carrotOnly: true` to skip the standalone app build entirely.
|
|
205
|
+
*/
|
|
206
|
+
carrot?: {
|
|
207
|
+
id: string;
|
|
208
|
+
name: string;
|
|
209
|
+
description?: string;
|
|
210
|
+
mode?: "window" | "background";
|
|
211
|
+
carrotOnly?: boolean;
|
|
212
|
+
permissions?: Record<string, unknown>;
|
|
213
|
+
dependencies?: Record<string, string>;
|
|
214
|
+
remoteUIs?: Record<string, { entrypoint: string; [key: string]: unknown }>;
|
|
215
|
+
};
|
|
216
|
+
|
|
201
217
|
/**
|
|
202
218
|
* macOS-specific build configuration
|
|
203
219
|
*/
|
|
@@ -9,7 +9,11 @@ import {
|
|
|
9
9
|
} from "../../shared/rpc.js";
|
|
10
10
|
import { Updater } from "./Updater";
|
|
11
11
|
import { BuildConfig } from "./BuildConfig";
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
rpcPort,
|
|
14
|
+
sendMessageToWebviewViaSocket,
|
|
15
|
+
removeSocketForWebview,
|
|
16
|
+
} from "./Socket";
|
|
13
17
|
import { randomBytes } from "crypto";
|
|
14
18
|
import { type Pointer } from "bun:ffi";
|
|
15
19
|
|
|
@@ -102,6 +106,7 @@ export class BrowserView<T extends RPCWithTransport = RPCWithTransport> {
|
|
|
102
106
|
sandbox: boolean = false;
|
|
103
107
|
startTransparent: boolean = false;
|
|
104
108
|
startPassthrough: boolean = false;
|
|
109
|
+
isRemoved: boolean = false;
|
|
105
110
|
|
|
106
111
|
constructor(options: Partial<BrowserViewOptions<T>> = defaultOptions) {
|
|
107
112
|
// const rpc = options.rpc;
|
|
@@ -211,13 +216,16 @@ export class BrowserView<T extends RPCWithTransport = RPCWithTransport> {
|
|
|
211
216
|
// so we have to chunk it
|
|
212
217
|
// TODO: is this still needed after switching from named pipes
|
|
213
218
|
executeJavascript(js: string) {
|
|
219
|
+
if (!this.ptr || this.isRemoved) {
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
214
222
|
ffi.request.evaluateJavascriptWithNoCompletion({ id: this.id, js });
|
|
215
223
|
}
|
|
216
224
|
|
|
217
225
|
loadURL(url: string) {
|
|
218
226
|
console.log(`DEBUG: loadURL called for webview ${this.id}: ${url}`);
|
|
219
227
|
this.url = url;
|
|
220
|
-
native
|
|
228
|
+
native!.symbols.loadURLInWebView(this.ptr, toCString(this.url));
|
|
221
229
|
}
|
|
222
230
|
|
|
223
231
|
loadHTML(html: string) {
|
|
@@ -229,18 +237,18 @@ export class BrowserView<T extends RPCWithTransport = RPCWithTransport> {
|
|
|
229
237
|
|
|
230
238
|
if (this.renderer === "cef") {
|
|
231
239
|
// For CEF, store HTML content in native map and use scheme handler
|
|
232
|
-
native
|
|
240
|
+
native!.symbols.setWebviewHTMLContent(this.id, toCString(html));
|
|
233
241
|
this.loadURL("views://internal/index.html");
|
|
234
242
|
} else {
|
|
235
243
|
// For WKWebView, load HTML content directly
|
|
236
|
-
native
|
|
244
|
+
native!.symbols.loadHTMLInWebView(this.ptr, toCString(html));
|
|
237
245
|
}
|
|
238
246
|
}
|
|
239
247
|
|
|
240
248
|
setNavigationRules(rules: string[]) {
|
|
241
249
|
this.navigationRules = JSON.stringify(rules);
|
|
242
250
|
const rulesJson = JSON.stringify(rules);
|
|
243
|
-
native
|
|
251
|
+
native!.symbols.setWebviewNavigationRules(this.ptr, toCString(rulesJson));
|
|
244
252
|
}
|
|
245
253
|
|
|
246
254
|
findInPage(
|
|
@@ -249,7 +257,7 @@ export class BrowserView<T extends RPCWithTransport = RPCWithTransport> {
|
|
|
249
257
|
) {
|
|
250
258
|
const forward = options?.forward ?? true;
|
|
251
259
|
const matchCase = options?.matchCase ?? false;
|
|
252
|
-
native
|
|
260
|
+
native!.symbols.webviewFindInPage(
|
|
253
261
|
this.ptr,
|
|
254
262
|
toCString(searchText),
|
|
255
263
|
forward,
|
|
@@ -258,19 +266,19 @@ export class BrowserView<T extends RPCWithTransport = RPCWithTransport> {
|
|
|
258
266
|
}
|
|
259
267
|
|
|
260
268
|
stopFindInPage() {
|
|
261
|
-
native
|
|
269
|
+
native!.symbols.webviewStopFind(this.ptr);
|
|
262
270
|
}
|
|
263
271
|
|
|
264
272
|
openDevTools() {
|
|
265
|
-
native
|
|
273
|
+
native!.symbols.webviewOpenDevTools(this.ptr);
|
|
266
274
|
}
|
|
267
275
|
|
|
268
276
|
closeDevTools() {
|
|
269
|
-
native
|
|
277
|
+
native!.symbols.webviewCloseDevTools(this.ptr);
|
|
270
278
|
}
|
|
271
279
|
|
|
272
280
|
toggleDevTools() {
|
|
273
|
-
native
|
|
281
|
+
native!.symbols.webviewToggleDevTools(this.ptr);
|
|
274
282
|
}
|
|
275
283
|
|
|
276
284
|
/**
|
|
@@ -278,7 +286,7 @@ export class BrowserView<T extends RPCWithTransport = RPCWithTransport> {
|
|
|
278
286
|
* @param zoomLevel - The zoom level (1.0 = 100%, 1.5 = 150%, etc.)
|
|
279
287
|
*/
|
|
280
288
|
setPageZoom(zoomLevel: number) {
|
|
281
|
-
native
|
|
289
|
+
native!.symbols.webviewSetPageZoom(this.ptr, zoomLevel);
|
|
282
290
|
}
|
|
283
291
|
|
|
284
292
|
/**
|
|
@@ -286,7 +294,7 @@ export class BrowserView<T extends RPCWithTransport = RPCWithTransport> {
|
|
|
286
294
|
* @returns The current zoom level (1.0 = 100%)
|
|
287
295
|
*/
|
|
288
296
|
getPageZoom(): number {
|
|
289
|
-
return native
|
|
297
|
+
return native!.symbols.webviewGetPageZoom(this.ptr) as number;
|
|
290
298
|
}
|
|
291
299
|
|
|
292
300
|
// todo (yoav): move this to a class that also has off, append, prepend, etc.
|
|
@@ -315,6 +323,9 @@ export class BrowserView<T extends RPCWithTransport = RPCWithTransport> {
|
|
|
315
323
|
|
|
316
324
|
return {
|
|
317
325
|
send(message: any) {
|
|
326
|
+
if (!that.ptr || that.isRemoved) {
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
318
329
|
const sentOverSocket = sendMessageToWebviewViaSocket(that.id, message);
|
|
319
330
|
|
|
320
331
|
if (!sentOverSocket) {
|
|
@@ -327,14 +338,31 @@ export class BrowserView<T extends RPCWithTransport = RPCWithTransport> {
|
|
|
327
338
|
}
|
|
328
339
|
},
|
|
329
340
|
registerHandler(handler: (msg: unknown) => void) {
|
|
341
|
+
if (that.isRemoved) {
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
330
344
|
that.rpcHandler = handler;
|
|
331
345
|
},
|
|
332
346
|
};
|
|
333
347
|
};
|
|
334
348
|
|
|
335
349
|
remove() {
|
|
336
|
-
|
|
350
|
+
if (!this.ptr || this.isRemoved) {
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
const ptr = this.ptr;
|
|
354
|
+
this.isRemoved = true;
|
|
355
|
+
// Drop JS-side references first so late callbacks cannot target a stale view.
|
|
337
356
|
delete BrowserViewMap[this.id];
|
|
357
|
+
removeSocketForWebview(this.id);
|
|
358
|
+
this.rpc?.setTransport({
|
|
359
|
+
send() {},
|
|
360
|
+
registerHandler() {},
|
|
361
|
+
unregisterHandler() {},
|
|
362
|
+
});
|
|
363
|
+
this.rpcHandler = undefined;
|
|
364
|
+
this.ptr = null as any;
|
|
365
|
+
native!.symbols.webviewRemove(ptr);
|
|
338
366
|
}
|
|
339
367
|
|
|
340
368
|
static getById(id: number) {
|
|
@@ -47,6 +47,14 @@ export const socketMap: {
|
|
|
47
47
|
};
|
|
48
48
|
} = {};
|
|
49
49
|
|
|
50
|
+
export const removeSocketForWebview = (webviewId: number) => {
|
|
51
|
+
const rpc = socketMap[webviewId];
|
|
52
|
+
if (!rpc) return;
|
|
53
|
+
|
|
54
|
+
rpc.socket = null;
|
|
55
|
+
delete socketMap[webviewId];
|
|
56
|
+
};
|
|
57
|
+
|
|
50
58
|
const startRPCServer = () => {
|
|
51
59
|
const startPort = 50000;
|
|
52
60
|
const endPort = 65535;
|
|
@@ -1119,11 +1119,9 @@ del "%~f0"
|
|
|
1119
1119
|
localInfo = await Bun.file(`../${resourcesDir}/version.json`).json();
|
|
1120
1120
|
return localInfo;
|
|
1121
1121
|
} catch (error) {
|
|
1122
|
-
// Handle the error
|
|
1123
1122
|
console.error("Failed to read version.json", error);
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
throw error;
|
|
1123
|
+
localInfo = { identifier: "", channel: "", version: "", hash: "", baseUrl: "", name: "" };
|
|
1124
|
+
return localInfo;
|
|
1127
1125
|
}
|
|
1128
1126
|
},
|
|
1129
1127
|
};
|
|
@@ -136,25 +136,27 @@ export const quit = () => {
|
|
|
136
136
|
return;
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
// a Worker thread can fail to terminate when the main thread is blocked in FFI.
|
|
147
|
-
native.symbols.forceExit(0);
|
|
139
|
+
if (native) {
|
|
140
|
+
native.symbols.stopEventLoop();
|
|
141
|
+
native.symbols.waitForShutdownComplete(5000);
|
|
142
|
+
native.symbols.forceExit(0);
|
|
143
|
+
} else {
|
|
144
|
+
process.exit(0);
|
|
145
|
+
}
|
|
148
146
|
};
|
|
149
147
|
|
|
150
148
|
// Override process.exit so that calling it triggers proper native cleanup
|
|
149
|
+
const _originalProcessExit = process.exit;
|
|
151
150
|
process.exit = ((code?: number) => {
|
|
152
|
-
if (
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
151
|
+
if (native) {
|
|
152
|
+
if (isQuitting) {
|
|
153
|
+
native.symbols.forceExit(code ?? 0);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
quit();
|
|
157
|
+
} else {
|
|
158
|
+
_originalProcessExit(code ?? 0);
|
|
156
159
|
}
|
|
157
|
-
quit();
|
|
158
160
|
}) as typeof process.exit;
|
|
159
161
|
|
|
160
162
|
export const openFileDialog = async (
|
|
@@ -358,7 +360,8 @@ function getVersionInfo(): { identifier: string; channel: string } {
|
|
|
358
360
|
return _versionInfo;
|
|
359
361
|
} catch (error) {
|
|
360
362
|
console.error("Failed to read version.json", error);
|
|
361
|
-
|
|
363
|
+
_versionInfo = { identifier: "", channel: "" };
|
|
364
|
+
return _versionInfo;
|
|
362
365
|
}
|
|
363
366
|
}
|
|
364
367
|
|
package/dist/api/bun/index.ts
CHANGED
|
@@ -44,6 +44,143 @@ import type {
|
|
|
44
44
|
ApplicationMenuItemConfig,
|
|
45
45
|
} from "./proc/native";
|
|
46
46
|
import { BuildConfig, type BuildConfigType } from "./core/BuildConfig";
|
|
47
|
+
import { bridge, hasFFI } from "./proc/native";
|
|
48
|
+
|
|
49
|
+
// Carrot boot state — populated from __bunnyCarrotBootstrap injected by Bunny Ears
|
|
50
|
+
let _carrotManifest: Record<string, unknown> | null = null;
|
|
51
|
+
let _carrotContext: { currentDir?: string; statePath?: string; logsPath?: string; permissions?: string[]; grantedPermissions?: Record<string, unknown>; authToken?: string | null; channel?: string } | null = null;
|
|
52
|
+
|
|
53
|
+
const _bootstrap = (globalThis as any).__bunnyCarrotBootstrap as { manifest?: any; context?: any } | undefined;
|
|
54
|
+
if (_bootstrap) {
|
|
55
|
+
_carrotManifest = _bootstrap.manifest ?? null;
|
|
56
|
+
_carrotContext = _bootstrap.context ?? null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (bridge) {
|
|
60
|
+
bridge.on("init", (payload: any) => {
|
|
61
|
+
if (payload?.manifest) _carrotManifest = payload.manifest;
|
|
62
|
+
if (payload?.context) _carrotContext = payload.context;
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// Forward host events to the local event emitter so ApplicationMenu.on(),
|
|
66
|
+
// ContextMenu.on(), etc. work in carrot workers
|
|
67
|
+
for (const eventName of ["application-menu-clicked", "context-menu-clicked"]) {
|
|
68
|
+
bridge.on(eventName, (payload: unknown) => {
|
|
69
|
+
electobunEventEmmitter.emitEvent({ type: eventName, data: payload } as any);
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Update local auth token when the host notifies of a change (e.g., Farm login)
|
|
74
|
+
bridge.on("auth-token-changed", (payload: unknown) => {
|
|
75
|
+
const token = (payload as any)?.token;
|
|
76
|
+
if (token && _carrotContext) {
|
|
77
|
+
_carrotContext.authToken = token;
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// Clear local auth token on logout
|
|
82
|
+
bridge.on("auth-token-cleared", () => {
|
|
83
|
+
if (_carrotContext) {
|
|
84
|
+
_carrotContext.authToken = null;
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export const Carrots = {
|
|
90
|
+
async invoke<T = unknown>(
|
|
91
|
+
carrotId: string,
|
|
92
|
+
method: string,
|
|
93
|
+
params?: unknown,
|
|
94
|
+
options?: { windowId?: string },
|
|
95
|
+
): Promise<T> {
|
|
96
|
+
if (!bridge) throw new Error("Carrots.invoke() is only available when running as a carrot inside Bunny Ears");
|
|
97
|
+
return bridge.requestHost<T>("invoke-carrot", { carrotId, method, params, windowId: options?.windowId });
|
|
98
|
+
},
|
|
99
|
+
emit(carrotId: string, name: string, payload?: unknown) {
|
|
100
|
+
if (!bridge) throw new Error("Carrots.emit() is only available when running as a carrot inside Bunny Ears");
|
|
101
|
+
bridge.sendAction("emit-carrot-event", { carrotId, name, payload });
|
|
102
|
+
},
|
|
103
|
+
async list() {
|
|
104
|
+
if (!bridge) throw new Error("Carrots.list() is only available when running as a carrot inside Bunny Ears");
|
|
105
|
+
return bridge.requestHost<Array<{
|
|
106
|
+
id: string; name: string; description: string; version: string;
|
|
107
|
+
mode: string; permissions: string[]; status: string; devMode: boolean;
|
|
108
|
+
}>>("list-carrots");
|
|
109
|
+
},
|
|
110
|
+
async start(carrotId: string) {
|
|
111
|
+
if (!bridge) throw new Error("Carrots.start() is only available when running as a carrot inside Bunny Ears");
|
|
112
|
+
return bridge.requestHost<{ ok: boolean }>("start-carrot", { id: carrotId });
|
|
113
|
+
},
|
|
114
|
+
async stop(carrotId: string) {
|
|
115
|
+
if (!bridge) throw new Error("Carrots.stop() is only available when running as a carrot inside Bunny Ears");
|
|
116
|
+
return bridge.requestHost<{ ok: boolean }>("stop-carrot", { id: carrotId });
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
export const app = {
|
|
121
|
+
on(name: string, handler: (payload: unknown) => void) {
|
|
122
|
+
if (bridge) {
|
|
123
|
+
return bridge.on(name, handler);
|
|
124
|
+
}
|
|
125
|
+
electobunEventEmmitter.on(name, (e: { data: unknown }) => handler(e.data));
|
|
126
|
+
return () => {};
|
|
127
|
+
},
|
|
128
|
+
quit() {
|
|
129
|
+
Utils.quit();
|
|
130
|
+
},
|
|
131
|
+
get isCarrotMode() {
|
|
132
|
+
return !hasFFI;
|
|
133
|
+
},
|
|
134
|
+
get manifest() {
|
|
135
|
+
return _carrotManifest;
|
|
136
|
+
},
|
|
137
|
+
get permissions() {
|
|
138
|
+
return _carrotContext?.permissions ?? [];
|
|
139
|
+
},
|
|
140
|
+
get grantedPermissions() {
|
|
141
|
+
return _carrotContext?.grantedPermissions ?? {};
|
|
142
|
+
},
|
|
143
|
+
get currentDir() {
|
|
144
|
+
return _carrotContext?.currentDir ?? "";
|
|
145
|
+
},
|
|
146
|
+
get statePath() {
|
|
147
|
+
return _carrotContext?.statePath ?? "";
|
|
148
|
+
},
|
|
149
|
+
get logsPath() {
|
|
150
|
+
return _carrotContext?.logsPath ?? "";
|
|
151
|
+
},
|
|
152
|
+
get authToken() {
|
|
153
|
+
return _carrotContext?.authToken ?? null;
|
|
154
|
+
},
|
|
155
|
+
async fetchAuthToken(): Promise<string | null> {
|
|
156
|
+
if (!bridge) return null;
|
|
157
|
+
const result = await bridge.requestHost<{ token: string | null }>("get-auth-token");
|
|
158
|
+
if (result?.token && _carrotContext) {
|
|
159
|
+
_carrotContext.authToken = result.token;
|
|
160
|
+
}
|
|
161
|
+
return result?.token ?? null;
|
|
162
|
+
},
|
|
163
|
+
async setAuthToken(token: string): Promise<void> {
|
|
164
|
+
if (!bridge) return;
|
|
165
|
+
await bridge.requestHost("set-auth-token", { token });
|
|
166
|
+
if (_carrotContext) {
|
|
167
|
+
_carrotContext.authToken = token;
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
get channel() {
|
|
171
|
+
return _carrotContext?.channel ?? "";
|
|
172
|
+
},
|
|
173
|
+
openManager() {
|
|
174
|
+
if (bridge) bridge.sendAction("open-manager");
|
|
175
|
+
},
|
|
176
|
+
openBunnyWindow(payload?: { screenX?: number; screenY?: number }) {
|
|
177
|
+
if (bridge) bridge.sendAction("open-bunny-window", payload);
|
|
178
|
+
},
|
|
179
|
+
async getWindowFrame(windowId?: string) {
|
|
180
|
+
if (!bridge) return null;
|
|
181
|
+
return bridge.requestHost<{ x: number; y: number; width: number; height: number } | null>("window-get-frame", { windowId });
|
|
182
|
+
},
|
|
183
|
+
};
|
|
47
184
|
|
|
48
185
|
// Named Exports
|
|
49
186
|
export {
|
|
@@ -17,7 +17,7 @@ async function buildPreload() {
|
|
|
17
17
|
const fullResult = await Bun.build({
|
|
18
18
|
entrypoints: [fullPreloadEntry],
|
|
19
19
|
target: "browser",
|
|
20
|
-
format: "
|
|
20
|
+
format: "esm",
|
|
21
21
|
minify: false,
|
|
22
22
|
});
|
|
23
23
|
|
|
@@ -31,7 +31,7 @@ async function buildPreload() {
|
|
|
31
31
|
const sandboxedResult = await Bun.build({
|
|
32
32
|
entrypoints: [sandboxedPreloadEntry],
|
|
33
33
|
target: "browser",
|
|
34
|
-
format: "
|
|
34
|
+
format: "esm",
|
|
35
35
|
minify: false,
|
|
36
36
|
});
|
|
37
37
|
|
|
@@ -40,8 +40,10 @@ async function buildPreload() {
|
|
|
40
40
|
throw new Error("Failed to build sandboxed preload script");
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
// Bun does not currently support iife output, so we wrap the ESM bundle manually
|
|
44
|
+
// to keep preload globals scoped for script injection.
|
|
45
|
+
const fullPreloadJs = `(function(){${await fullResult.outputs[0]!.text()}})();`;
|
|
46
|
+
const sandboxedPreloadJs = `(function(){${await sandboxedResult.outputs[0]!.text()}})();`;
|
|
45
47
|
|
|
46
48
|
const outputContent = `// Auto-generated file. Do not edit directly.
|
|
47
49
|
// Run "bun build.ts" or "bun build:dev" from the package folder to regenerate.
|