@zhangferry-dev/tokendash 1.3.0 → 1.4.2

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.
@@ -0,0 +1,291 @@
1
+ const { app, BrowserWindow, screen, shell } = require('electron');
2
+ const path = require('node:path');
3
+ const fs = require('node:fs');
4
+ const http = require('node:http');
5
+ const { spawn } = require('node:child_process');
6
+
7
+ // Global debug logger (writes to file since stdout is lost in packaged apps)
8
+ const DEBUG_LOG = '/tmp/tokendash-debug.log';
9
+ try { fs.writeFileSync(DEBUG_LOG, 'main.js loaded\n'); } catch(_){}
10
+
11
+ // Import from bundled server (created by esbuild)
12
+ let createApp;
13
+ try {
14
+ createApp = require('../dist/electron-server.cjs').createApp;
15
+ } catch (e) {
16
+ console.error('Failed to load bundled server. Did you run the build?', e.message);
17
+ app.quit();
18
+ }
19
+
20
+ const { formatCost } = require('./trayBadge.js');
21
+
22
+ // Resolve trayHelper binary: extract from asar if needed
23
+ function resolveTrayHelperPath() {
24
+ const srcPath = path.join(__dirname, 'trayHelper');
25
+ const isAsar = srcPath.includes('.asar');
26
+ const debugLog = (msg) => {
27
+ const logPath = '/tmp/tokendash-debug.log';
28
+ fs.appendFileSync(logPath, msg + '\n');
29
+ };
30
+ debugLog('[trayHelper] __dirname: ' + __dirname);
31
+ debugLog('[trayHelper] srcPath: ' + srcPath + ' isAsar: ' + isAsar);
32
+ if (isAsar) {
33
+ const destDir = path.join(app.getPath('userData'), 'helpers');
34
+ const destPath = path.join(destDir, 'trayHelper');
35
+ debugLog('[trayHelper] extracting to: ' + destPath);
36
+ if (!fs.existsSync(destDir)) fs.mkdirSync(destDir, { recursive: true });
37
+ fs.copyFileSync(srcPath, destPath);
38
+ fs.chmodSync(destPath, 0o755);
39
+ debugLog('[trayHelper] extracted OK');
40
+ return destPath;
41
+ }
42
+ return srcPath;
43
+ }
44
+
45
+ // ---------------------------------------------------------------------------
46
+ // State
47
+ // ---------------------------------------------------------------------------
48
+
49
+ let popover = null;
50
+ let server = null;
51
+ let trayProcess = null;
52
+ const serverPort = parseInt(process.env.TOKENDASH_PORT || '3456', 10);
53
+
54
+ // ---------------------------------------------------------------------------
55
+ // Helpers
56
+ // ---------------------------------------------------------------------------
57
+
58
+ function listenWithFallback(expressApp, port) {
59
+ return new Promise((resolve, reject) => {
60
+ let currentPort = port;
61
+ let attempts = 0;
62
+
63
+ function tryListen() {
64
+ const s = expressApp.listen(currentPort);
65
+ s.once('listening', () => resolve({ server: s, port: currentPort }));
66
+ s.once('error', (err) => {
67
+ if (err.code === 'EADDRINUSE' && attempts < 20) {
68
+ attempts++;
69
+ currentPort++;
70
+ tryListen();
71
+ } else {
72
+ reject(err);
73
+ }
74
+ });
75
+ }
76
+
77
+ tryListen();
78
+ });
79
+ }
80
+
81
+ function fetchJson(url) {
82
+ return new Promise((resolve, reject) => {
83
+ http.get(url, (res) => {
84
+ let data = '';
85
+ res.on('data', (chunk) => { data += chunk; });
86
+ res.on('end', () => {
87
+ try { resolve(JSON.parse(data)); }
88
+ catch (e) { reject(e); }
89
+ });
90
+ }).on('error', reject);
91
+ });
92
+ }
93
+
94
+ function positionPopoverBelowTray() {
95
+ if (!trayProcess || !popover) return;
96
+
97
+ // Use primary screen top-right area for positioning
98
+ const primaryDisplay = screen.getPrimaryDisplay();
99
+ const { width: screenW } = primaryDisplay.workArea;
100
+ const popoverWidth = 340;
101
+ const popoverHeight = 460;
102
+
103
+ // Position in top-right area, below menu bar
104
+ const x = screenW - popoverWidth - 16;
105
+ const y = 32; // below menu bar
106
+
107
+ popover.setPosition(Math.round(x), Math.round(y), false);
108
+ }
109
+
110
+ function togglePopover() {
111
+ if (!popover) return;
112
+
113
+ if (popover.isVisible()) {
114
+ popover.hide();
115
+ } else {
116
+ positionPopoverBelowTray();
117
+ popover.show();
118
+ popover.focus();
119
+ }
120
+ }
121
+
122
+ // ---------------------------------------------------------------------------
123
+ // Native tray helper (Swift binary for macOS 26+ compatibility)
124
+ // ---------------------------------------------------------------------------
125
+
126
+ function startTrayHelper() {
127
+ const helperPath = resolveTrayHelperPath();
128
+ trayProcess = spawn(helperPath, [], {
129
+ stdio: ['pipe', 'pipe', 'inherit'],
130
+ });
131
+
132
+ let buffer = '';
133
+
134
+ trayProcess.stdout.on('data', (data) => {
135
+ buffer += data.toString();
136
+ const lines = buffer.split('\n');
137
+ buffer = lines.pop(); // keep incomplete line in buffer
138
+
139
+ for (const line of lines) {
140
+ const event = line.trim();
141
+ if (event === 'click') {
142
+ togglePopover();
143
+ } else if (event === 'open-dashboard') {
144
+ const port = serverPort;
145
+ shell.openExternal(`http://localhost:${port}`);
146
+ } else if (event === 'quit-request') {
147
+ app.quit();
148
+ } else if (event === 'ready') {
149
+ // Helper is ready, start badge updates
150
+ startBadgeUpdates();
151
+ }
152
+ }
153
+ });
154
+
155
+ trayProcess.on('close', (code) => {
156
+ console.log('Tray helper exited with code', code);
157
+ trayProcess = null;
158
+ });
159
+
160
+ trayProcess.on('error', (err) => {
161
+ console.error('Failed to start tray helper:', err.message);
162
+ trayProcess = null;
163
+ });
164
+ }
165
+
166
+ function sendTrayCommand(command) {
167
+ if (trayProcess && trayProcess.stdin && !trayProcess.stdin.destroyed) {
168
+ trayProcess.stdin.write(command + '\n');
169
+ }
170
+ }
171
+
172
+ function stopTrayHelper() {
173
+ if (trayProcess) {
174
+ sendTrayCommand('quit');
175
+ trayProcess = null;
176
+ }
177
+ }
178
+
179
+ // ---------------------------------------------------------------------------
180
+ // Tray badge updater
181
+ // ---------------------------------------------------------------------------
182
+
183
+ let updateTimer = null;
184
+
185
+ function updateTrayBadge() {
186
+ fetchJson(`http://localhost:${serverPort}/api/daily`)
187
+ .then((data) => {
188
+ if (!data || !data.totals) return;
189
+ const cost = data.totals.totalCost || 0;
190
+ const tokens = data.totals.totalTokens || 0;
191
+
192
+ const badgeText = formatCost(cost);
193
+ sendTrayCommand('title:' + badgeText);
194
+
195
+ const tokenStr = tokens >= 1e6
196
+ ? (tokens / 1e6).toFixed(1) + 'M'
197
+ : tokens >= 1e3
198
+ ? (tokens / 1e3).toFixed(1) + 'K'
199
+ : String(tokens);
200
+ sendTrayCommand('tooltip:TokenDash - $' + cost.toFixed(2) + ' today - ' + tokenStr + ' tokens');
201
+ })
202
+ .catch((err) => {
203
+ if (err.code !== 'ECONNREFUSED') {
204
+ console.error('Tray badge update error:', err.message);
205
+ }
206
+ });
207
+ }
208
+
209
+ function startBadgeUpdates() {
210
+ updateTrayBadge();
211
+ updateTimer = setInterval(updateTrayBadge, 30000);
212
+ }
213
+
214
+ function stopBadgeUpdates() {
215
+ if (updateTimer) {
216
+ clearInterval(updateTimer);
217
+ updateTimer = null;
218
+ }
219
+ }
220
+
221
+ // ---------------------------------------------------------------------------
222
+ // Create popover window
223
+ // ---------------------------------------------------------------------------
224
+
225
+ function createPopoverWindow() {
226
+ popover = new BrowserWindow({
227
+ width: 340,
228
+ height: 460,
229
+ frame: false,
230
+ resizable: false,
231
+ hasShadow: true,
232
+ alwaysOnTop: true,
233
+ skipTaskbar: true,
234
+ show: false,
235
+ fullscreenable: false,
236
+ transparent: false,
237
+ webPreferences: {
238
+ nodeIntegration: false,
239
+ contextIsolation: true,
240
+ },
241
+ });
242
+
243
+ popover.loadURL(`http://localhost:${serverPort}/popover.html`);
244
+
245
+ popover.on('blur', () => {
246
+ popover.hide();
247
+ });
248
+
249
+ popover.on('close', (e) => {
250
+ if (!app.isQuitting) {
251
+ e.preventDefault();
252
+ popover.hide();
253
+ }
254
+ });
255
+ }
256
+
257
+ // ---------------------------------------------------------------------------
258
+ // App lifecycle
259
+ // ---------------------------------------------------------------------------
260
+
261
+ app.whenReady().then(async () => {
262
+ app.on('before-quit', () => {
263
+ app.isQuitting = true;
264
+ stopBadgeUpdates();
265
+ stopTrayHelper();
266
+ if (server) server.close();
267
+ });
268
+
269
+ // Create and bind Express server
270
+ const expressApp = createApp(serverPort);
271
+ try {
272
+ const result = await listenWithFallback(expressApp, serverPort);
273
+ server = result.server;
274
+ console.log(`tokendash running on http://localhost:${result.port}`);
275
+ } catch (err) {
276
+ console.error('Failed to start server:', err);
277
+ app.quit();
278
+ return;
279
+ }
280
+
281
+ // Start native tray helper
282
+ startTrayHelper();
283
+
284
+ // Create popover
285
+ createPopoverWindow();
286
+ });
287
+
288
+ process.on('uncaughtException', (err) => {
289
+ console.error('Fatal error in Electron main:', err);
290
+ app.quit();
291
+ });
@@ -0,0 +1,25 @@
1
+ const { contextBridge, ipcRenderer } = require('electron');
2
+
3
+ contextBridge.exposeInMainWorld('electronAPI', {
4
+ openDashboard(url) {
5
+ return ipcRenderer.invoke('tokendash:open-dashboard', url);
6
+ },
7
+ getAppInfo() {
8
+ return ipcRenderer.invoke('tokendash:get-app-info');
9
+ },
10
+ setLaunchAtLogin(enabled) {
11
+ return ipcRenderer.invoke('tokendash:set-launch-at-login', enabled);
12
+ },
13
+ checkForUpdates() {
14
+ return ipcRenderer.invoke('tokendash:check-for-updates');
15
+ },
16
+ quitApp() {
17
+ return ipcRenderer.invoke('tokendash:quit');
18
+ },
19
+ setSelectedAgents(agents) {
20
+ return ipcRenderer.invoke('tokendash:set-selected-agents', agents);
21
+ },
22
+ updateTraySnapshot(snapshot) {
23
+ return ipcRenderer.invoke('tokendash:update-tray-snapshot', snapshot);
24
+ },
25
+ });
@@ -0,0 +1,27 @@
1
+ // electron/trayBadge.cjs
2
+
3
+ /**
4
+ * Format token count as compact string for tray badge.
5
+ * Examples: 1234 -> "1.2K", 567890 -> "567.9K", 1500000 -> "1.5M"
6
+ */
7
+ function formatTokens(tokens) {
8
+ tokens = Number(tokens) || 0;
9
+
10
+ if (tokens >= 1e6) return (tokens / 1e6).toFixed(1) + 'M';
11
+ if (tokens >= 1e3) return (tokens / 1e3).toFixed(1) + 'K';
12
+ if (tokens > 0) return String(tokens);
13
+ return '0';
14
+ }
15
+
16
+ /**
17
+ * Format cost as compact string for tray badge (max 5 chars).
18
+ * Examples: 1.234 -> "$1.2", 12.5 -> "$12", 0.05 -> "$0.1", 123.4 -> "$123"
19
+ */
20
+ function formatCost(cost) {
21
+ if (cost < 0.05) return '$0';
22
+ if (cost < 10) return '$' + cost.toFixed(1);
23
+ if (cost < 100) return '$' + Math.round(cost);
24
+ return '$' + Math.round(cost);
25
+ }
26
+
27
+ module.exports = { formatCost, formatTokens };
@@ -0,0 +1,30 @@
1
+ // electron/trayBadge.js
2
+ const { nativeImage } = require('electron');
3
+
4
+ /**
5
+ * Format cost as compact string for tray badge (max 5 chars).
6
+ * Examples: 1.234 -> "$1.2", 12.5 -> "$12", 0.05 -> "$0.1", 123.4 -> "$123"
7
+ */
8
+ function formatCost(cost) {
9
+ if (cost < 0.05) return '$0';
10
+ if (cost < 10) return '$' + cost.toFixed(1);
11
+ if (cost < 100) return '$' + Math.round(cost);
12
+ return '$' + Math.round(cost);
13
+ }
14
+
15
+ // Embedded 22x22 PNG: white circle on transparent background
16
+ // Used as a template image — macOS auto-adapts to menu bar appearance
17
+ const TRAY_PNG_BASE64 =
18
+ 'iVBORw0KGgoAAAANSUhEUgAAABYAAAAWCAYAAADEtGw7AAAAWElEQVR4nO2U2QkAMAjFnNatOqsFP+2BJxRpBgjh0QrQCiIadGZ4hHgRSjCj0ldvLNWXB6RM5gSSdZIEKfPFzcQ1zy2jeiuFyi8dmER3QkvOpqHefuifZgKh/EKNb7YAbgAAAABJRU5ErkJggg==';
19
+
20
+ /**
21
+ * Create a macOS tray icon template.
22
+ * setTemplate(true) lets macOS automatically invert colors for light/dark menu bars.
23
+ */
24
+ function createBadgeIcon(_text) {
25
+ const img = nativeImage.createFromDataURL(`data:image/png;base64,${TRAY_PNG_BASE64}`);
26
+ img.setTemplateImage(true);
27
+ return img;
28
+ }
29
+
30
+ module.exports = { createBadgeIcon, formatCost };
Binary file
@@ -0,0 +1,152 @@
1
+ import Cocoa
2
+
3
+ // TokenDash Native Tray Helper for macOS 26+
4
+ // Communicates with Electron main process via stdin/stdout
5
+ // Protocol:
6
+ // stdin commands: "title:<text>\n" "tooltip:<text>\n" "quit\n"
7
+ // stdout events: "click:<screenX>,<screenY>\n"
8
+
9
+ class AppDelegate: NSObject, NSApplicationDelegate {
10
+ var statusItem: NSStatusItem!
11
+ var readHandle: FileHandle?
12
+ var currentTitle = "0"
13
+
14
+ func applicationDidFinishLaunching(_ notification: Notification) {
15
+ // Create status bar item
16
+ statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
17
+
18
+ statusItem.button?.image = renderCombinedImage(title: currentTitle)
19
+ statusItem.button?.imagePosition = .imageOnly
20
+ statusItem.button?.imageScaling = .scaleProportionallyDown
21
+ statusItem.button?.isBordered = false
22
+ statusItem.button?.title = ""
23
+ statusItem.button?.toolTip = "TokenDash"
24
+
25
+ // Set up click actions — both left and right click
26
+ statusItem.button?.target = self
27
+ statusItem.button?.action = #selector(handleClick(_:))
28
+ statusItem.button?.sendAction(on: [.leftMouseUp, .rightMouseUp])
29
+
30
+ // Read commands from stdin
31
+ readHandle = FileHandle.standardInput
32
+ NotificationCenter.default.addObserver(
33
+ self,
34
+ selector: #selector(handleStdin),
35
+ name: .NSFileHandleDataAvailable,
36
+ object: readHandle
37
+ )
38
+ readHandle?.waitForDataInBackgroundAndNotify()
39
+
40
+ // Signal ready
41
+ sendEvent("ready")
42
+ }
43
+
44
+ /// Render icon + title text into a single template image for the status bar.
45
+ func renderCombinedImage(title: String) -> NSImage {
46
+ let iconW: CGFloat = 18
47
+ let iconH: CGFloat = 18
48
+ let fontSize: CGFloat = 13
49
+ let font = NSFont.monospacedSystemFont(ofSize: fontSize, weight: .medium)
50
+ let textAttrs: [NSAttributedString.Key: Any] = [.font: font]
51
+ let textWidth = (title as NSString).size(withAttributes: textAttrs).width
52
+ let padding: CGFloat = 4 // gap between icon and text
53
+
54
+ let totalWidth = iconW + padding + textWidth
55
+ // Status bar height is ~22pt; center vertically
56
+ let totalHeight: CGFloat = 20
57
+
58
+ let image = NSImage(size: NSSize(width: totalWidth, height: totalHeight))
59
+ image.lockFocus()
60
+
61
+ // Draw icon centered vertically
62
+ let icon = createTemplateIcon(size: NSSize(width: iconW, height: iconH))
63
+ let iconY = (totalHeight - iconH) / 2.0
64
+ icon.draw(in: NSRect(x: 0, y: iconY, width: iconW, height: iconH))
65
+
66
+ // Draw text centered vertically (baseline-adjusted)
67
+ let textY = (totalHeight - fontSize) / 2.0 - 1
68
+ (title as NSString).draw(at: NSPoint(x: iconW + padding, y: textY), withAttributes: textAttrs)
69
+
70
+ image.unlockFocus()
71
+ image.isTemplate = true
72
+ return image
73
+ }
74
+
75
+ @objc func handleClick(_ sender: Any?) {
76
+ guard let _ = NSApp.currentEvent else { return }
77
+ let loc = NSEvent.mouseLocation
78
+ sendEvent("click:\(Int(loc.x)),\(Int(loc.y))")
79
+ }
80
+
81
+ @objc func handleStdin() {
82
+ guard let data = readHandle?.availableData, data.count > 0 else {
83
+ readHandle?.waitForDataInBackgroundAndNotify()
84
+ return
85
+ }
86
+
87
+ if let line = String(data: data, encoding: .utf8) {
88
+ for command in line.split(separator: "\n") {
89
+ let cmd = command.trimmingCharacters(in: .whitespacesAndNewlines)
90
+ if cmd.hasPrefix("title:") {
91
+ let title = String(cmd.dropFirst(6))
92
+ currentTitle = title
93
+ statusItem.button?.image = renderCombinedImage(title: title)
94
+ } else if cmd.hasPrefix("tooltip:") {
95
+ let tooltip = String(cmd.dropFirst(8))
96
+ statusItem.button?.toolTip = tooltip
97
+ } else if cmd == "quit" {
98
+ NSApp.terminate(nil)
99
+ return
100
+ }
101
+ }
102
+ }
103
+
104
+ readHandle?.waitForDataInBackgroundAndNotify()
105
+ }
106
+
107
+ func sendEvent(_ event: String) {
108
+ print(event)
109
+ fflush(stdout)
110
+ }
111
+
112
+ func createTemplateIcon(size: NSSize) -> NSImage {
113
+ let image = NSImage(size: size)
114
+ image.lockFocus()
115
+
116
+ let sx = size.width / 64.0
117
+ let sy = size.height / 64.0
118
+
119
+ let path = NSBezierPath()
120
+ path.move(to: NSPoint(x: 6 * sx, y: (64 - 32) * sy))
121
+ path.line(to: NSPoint(x: 18 * sx, y: (64 - 32) * sy))
122
+ path.curve(to: NSPoint(x: 24.5 * sx, y: (64 - 39) * sy),
123
+ controlPoint1: NSPoint(x: 21 * sx, y: (64 - 32) * sy),
124
+ controlPoint2: NSPoint(x: 22.5 * sx, y: (64 - 34) * sy))
125
+ path.curve(to: NSPoint(x: 34 * sx, y: (64 - 50) * sy),
126
+ controlPoint1: NSPoint(x: 27 * sx, y: (64 - 45.5) * sy),
127
+ controlPoint2: NSPoint(x: 30 * sx, y: (64 - 50) * sy))
128
+ path.curve(to: NSPoint(x: 44 * sx, y: (64 - 22) * sy),
129
+ controlPoint1: NSPoint(x: 38 * sx, y: (64 - 50) * sy),
130
+ controlPoint2: NSPoint(x: 40.5 * sx, y: (64 - 42) * sy))
131
+ path.curve(to: NSPoint(x: 52 * sx, y: (64 - 8) * sy),
132
+ controlPoint1: NSPoint(x: 46 * sx, y: (64 - 11) * sy),
133
+ controlPoint2: NSPoint(x: 49 * sx, y: (64 - 8) * sy))
134
+ path.curve(to: NSPoint(x: 60 * sx, y: (64 - 22) * sy),
135
+ controlPoint1: NSPoint(x: 55 * sx, y: (64 - 8) * sy),
136
+ controlPoint2: NSPoint(x: 57.5 * sx, y: (64 - 13) * sy))
137
+
138
+ path.lineWidth = 5 * sx
139
+ path.lineCapStyle = .round
140
+ path.lineJoinStyle = .round
141
+ NSColor.black.setStroke()
142
+ path.stroke()
143
+
144
+ image.unlockFocus()
145
+ image.isTemplate = true
146
+ return image
147
+ }
148
+ }
149
+
150
+ let delegate = AppDelegate()
151
+ NSApplication.shared.delegate = delegate
152
+ NSApp.run()
@@ -0,0 +1,20 @@
1
+ appId: com.zhangferry-dev.tokendash
2
+ productName: TokenDash
3
+ directories:
4
+ output: release
5
+ files:
6
+ - dist/**/*
7
+ - bin/**/*
8
+ - electron/**/*
9
+ - package.json
10
+ mac:
11
+ target: dmg
12
+ category: public.app-category.developer-tools
13
+ icon: resources/icon.icns
14
+ identity: null
15
+ darkModeSupport: true
16
+ minimumSystemVersion: '14.0'
17
+ extendInfo:
18
+ LSUIElement: true
19
+ extraMetadata:
20
+ main: electron/main.cjs
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhangferry-dev/tokendash",
3
- "version": "1.3.0",
3
+ "version": "1.4.2",
4
4
  "type": "module",
5
5
  "description": "Token Usage Analytics Dashboard",
6
6
  "publishConfig": {
@@ -13,20 +13,26 @@
13
13
  },
14
14
  "files": [
15
15
  "dist",
16
- "bin"
16
+ "bin",
17
+ "electron",
18
+ "resources",
19
+ "electron-builder.yml"
17
20
  ],
18
21
  "scripts": {
19
22
  "dev": "concurrently \"npm run dev:server\" \"npm run dev:client\"",
20
23
  "dev:server": "tsx watch src/server/index.ts",
21
24
  "dev:client": "vite",
22
- "build": "vite build && tsc -p tsconfig.json",
25
+ "build": "vite build && tsc -p tsconfig.json && cp public/popover.html dist/client/popover.html && node esbuild.config.mjs",
26
+ "dev:electron": "ELECTRON_DEV=1 electron .",
27
+ "build:electron": "electron-builder --mac",
28
+ "build:all": "npm run build && npm run build:electron",
23
29
  "start": "node dist/server/index.js",
24
30
  "typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.frontend.json --noEmit",
25
31
  "test": "vitest run",
26
32
  "test:watch": "vitest",
27
33
  "test:e2e": "playwright test",
28
34
  "prepack": "npm run build",
29
- "publish": "npm publish --access public --registry https://registry.npmjs.org"
35
+ "release": "npm publish --access public --registry https://registry.npmjs.org"
30
36
  },
31
37
  "dependencies": {
32
38
  "express": "^5.1.0",
@@ -45,6 +51,9 @@
45
51
  "@types/react-dom": "^19.1.2",
46
52
  "@vitejs/plugin-react": "^4.4.1",
47
53
  "concurrently": "^9.1.2",
54
+ "electron": "^41.5.0",
55
+ "electron-builder": "^26.0.0",
56
+ "esbuild": "^0.25.0",
48
57
  "tailwindcss": "^4.1.4",
49
58
  "tsx": "^4.19.3",
50
59
  "typescript": "^5.8.3",
@@ -0,0 +1,10 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+ <plist version="1.0">
4
+ <dict>
5
+ <key>com.apple.security.cs.allow-jit</key>
6
+ <true/>
7
+ <key>com.apple.security.cs.allow-unsigned-executable-memory</key>
8
+ <true/>
9
+ </dict>
10
+ </plist>
Binary file
Binary file
Binary file