paneful 0.7.2 → 0.8.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 +24 -3
- package/assets/icons/Contents.json +318 -0
- package/assets/icons/icon-mac-128x128.png +0 -0
- package/assets/icons/icon-mac-128x128@2x.png +0 -0
- package/assets/icons/icon-mac-16x16.png +0 -0
- package/assets/icons/icon-mac-16x16@2x.png +0 -0
- package/assets/icons/icon-mac-256x256.png +0 -0
- package/assets/icons/icon-mac-256x256@2x.png +0 -0
- package/assets/icons/icon-mac-32x32.png +0 -0
- package/assets/icons/icon-mac-32x32@2x.png +0 -0
- package/assets/icons/icon-mac-512x512.png +0 -0
- package/assets/icons/icon-mac-512x512@2x.png +0 -0
- package/dist/server/browser.js +12 -10
- package/dist/server/index.js +85 -10
- package/dist/server/install-app.js +348 -139
- package/dist/server/ipc.js +5 -4
- package/dist/server/pty-manager.js +1 -1
- package/dist/server/settings-store.js +64 -0
- package/dist/server/ws-handler.js +23 -1
- package/dist/web/assets/{index-Dbp11YgF.css → index-B4-CUyGw.css} +1 -1
- package/dist/web/assets/{index-Cg_s9WdK.js → index-CoI7fA_K.js} +72 -72
- package/dist/web/icon-192.png +0 -0
- package/dist/web/icon-512.png +0 -0
- package/dist/web/index.html +2 -2
- package/package.json +3 -2
|
@@ -2,36 +2,42 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
import { execFileSync } from 'node:child_process';
|
|
5
|
-
const
|
|
5
|
+
const DEFAULT_APP_PATH = '/Applications/Paneful.app';
|
|
6
6
|
const BUNDLE_ID = 'com.paneful.app';
|
|
7
|
-
function
|
|
8
|
-
//
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
7
|
+
function findIconDir() {
|
|
8
|
+
// Walk up from import.meta.dirname to find AppIcon.appiconset
|
|
9
|
+
// Works from both dist/server/ (built) and server/ (dev)
|
|
10
|
+
let dir = import.meta.dirname;
|
|
11
|
+
for (let i = 0; i < 5; i++) {
|
|
12
|
+
const candidate = path.join(dir, 'assets', 'icons');
|
|
13
|
+
if (fs.existsSync(path.join(candidate, 'icon-mac-512x512.png')))
|
|
14
|
+
return candidate;
|
|
15
|
+
dir = path.dirname(dir);
|
|
16
|
+
}
|
|
16
17
|
return null;
|
|
17
18
|
}
|
|
18
|
-
function buildIcns(
|
|
19
|
+
function buildIcns(iconDir, contentsDir) {
|
|
19
20
|
const iconsetDir = path.join(os.tmpdir(), 'Paneful.iconset');
|
|
20
21
|
const icnsPath = path.join(contentsDir, 'Resources', 'Paneful.icns');
|
|
21
22
|
try {
|
|
22
23
|
fs.mkdirSync(iconsetDir, { recursive: true });
|
|
23
|
-
//
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
24
|
+
// Map pre-sized PNGs to iconset naming convention
|
|
25
|
+
const mappings = [
|
|
26
|
+
['icon-mac-16x16.png', 'icon_16x16.png'],
|
|
27
|
+
['icon-mac-16x16@2x.png', 'icon_16x16@2x.png'],
|
|
28
|
+
['icon-mac-32x32.png', 'icon_32x32.png'],
|
|
29
|
+
['icon-mac-32x32@2x.png', 'icon_32x32@2x.png'],
|
|
30
|
+
['icon-mac-128x128.png', 'icon_128x128.png'],
|
|
31
|
+
['icon-mac-128x128@2x.png', 'icon_128x128@2x.png'],
|
|
32
|
+
['icon-mac-256x256.png', 'icon_256x256.png'],
|
|
33
|
+
['icon-mac-256x256@2x.png', 'icon_256x256@2x.png'],
|
|
34
|
+
['icon-mac-512x512.png', 'icon_512x512.png'],
|
|
35
|
+
['icon-mac-512x512@2x.png', 'icon_512x512@2x.png'],
|
|
36
|
+
];
|
|
37
|
+
for (const [src, dest] of mappings) {
|
|
38
|
+
const srcPath = path.join(iconDir, src);
|
|
39
|
+
if (fs.existsSync(srcPath)) {
|
|
40
|
+
fs.copyFileSync(srcPath, path.join(iconsetDir, dest));
|
|
35
41
|
}
|
|
36
42
|
}
|
|
37
43
|
fs.mkdirSync(path.dirname(icnsPath), { recursive: true });
|
|
@@ -72,123 +78,301 @@ ${hasIcon ? ` <key>CFBundleIconFile</key>
|
|
|
72
78
|
</plist>
|
|
73
79
|
`;
|
|
74
80
|
}
|
|
75
|
-
function
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
if
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
if
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
81
|
+
function escapeSwiftString(s) {
|
|
82
|
+
return s.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
83
|
+
}
|
|
84
|
+
function getSwiftSource() {
|
|
85
|
+
const bakedNode = escapeSwiftString(process.execPath);
|
|
86
|
+
const bakedPaneful = escapeSwiftString(fs.realpathSync(process.argv[1]));
|
|
87
|
+
return `import Cocoa
|
|
88
|
+
import WebKit
|
|
89
|
+
|
|
90
|
+
// Port is determined at runtime; read from lockfile after server starts
|
|
91
|
+
let HOME = NSHomeDirectory()
|
|
92
|
+
let DATA_DIR = HOME + "/.paneful"
|
|
93
|
+
let LOCKFILE = DATA_DIR + "/paneful.lock"
|
|
94
|
+
let LOG_FILE = DATA_DIR + "/app.log"
|
|
95
|
+
let BAKED_NODE = "${bakedNode}"
|
|
96
|
+
let BAKED_PANEFUL = "${bakedPaneful}"
|
|
97
|
+
|
|
98
|
+
var logHandle: FileHandle?
|
|
99
|
+
|
|
100
|
+
func log(_ msg: String) {
|
|
101
|
+
guard let h = logHandle else { return }
|
|
102
|
+
let df = DateFormatter()
|
|
103
|
+
df.dateFormat = "yyyy-MM-dd HH:mm:ss"
|
|
104
|
+
if let data = "[\\(df.string(from: Date()))] \\(msg)\\n".data(using: .utf8) {
|
|
105
|
+
h.write(data)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
func findNode() -> String? {
|
|
110
|
+
let fm = FileManager.default
|
|
111
|
+
if fm.isExecutableFile(atPath: BAKED_NODE) { return BAKED_NODE }
|
|
112
|
+
for p in ["/usr/local/bin/node", "/opt/homebrew/bin/node"] {
|
|
113
|
+
if fm.isExecutableFile(atPath: p) { return p }
|
|
114
|
+
}
|
|
115
|
+
// nvm
|
|
116
|
+
let nvmDir = HOME + "/.nvm/versions/node"
|
|
117
|
+
if let vs = try? fm.contentsOfDirectory(atPath: nvmDir) {
|
|
118
|
+
if let latest = vs.filter({ $0.hasPrefix("v") }).sorted().last {
|
|
119
|
+
let p = "\\(nvmDir)/\\(latest)/bin/node"
|
|
120
|
+
if fm.isExecutableFile(atPath: p) { return p }
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
// fnm
|
|
124
|
+
let fnmDir = HOME + "/.fnm/node-versions"
|
|
125
|
+
if let vs = try? fm.contentsOfDirectory(atPath: fnmDir) {
|
|
126
|
+
if let latest = vs.filter({ $0.hasPrefix("v") }).sorted().last {
|
|
127
|
+
let p = "\\(fnmDir)/\\(latest)/installation/bin/node"
|
|
128
|
+
if fm.isExecutableFile(atPath: p) { return p }
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
// volta
|
|
132
|
+
let volta = HOME + "/.volta/bin/node"
|
|
133
|
+
if fm.isExecutableFile(atPath: volta) { return volta }
|
|
134
|
+
return nil
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
func findPaneful(nodeDir: String) -> String? {
|
|
138
|
+
let fm = FileManager.default
|
|
139
|
+
if fm.fileExists(atPath: BAKED_PANEFUL) { return BAKED_PANEFUL }
|
|
140
|
+
for p in ["/usr/local/bin/paneful", "/opt/homebrew/bin/paneful", nodeDir + "/paneful"] {
|
|
141
|
+
if fm.isExecutableFile(atPath: p) {
|
|
142
|
+
// Resolve symlink to get actual .js entry point
|
|
143
|
+
if let dest = try? fm.destinationOfSymbolicLink(atPath: p) {
|
|
144
|
+
let base = (p as NSString).deletingLastPathComponent
|
|
145
|
+
return dest.hasPrefix("/") ? dest : base + "/" + dest
|
|
146
|
+
}
|
|
147
|
+
return p
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return nil
|
|
134
151
|
}
|
|
135
152
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
if [[ -x "$p" ]]; then
|
|
143
|
-
# Resolve symlink to get the actual .js file
|
|
144
|
-
local resolved
|
|
145
|
-
resolved=$(readlink -f "$p" 2>/dev/null || readlink "$p" 2>/dev/null || echo "$p")
|
|
146
|
-
echo "$resolved"; return
|
|
147
|
-
fi
|
|
148
|
-
done
|
|
149
|
-
|
|
150
|
-
# 3. npm global prefix
|
|
151
|
-
local node_bin
|
|
152
|
-
node_bin=$(dirname "$1")
|
|
153
|
-
if [[ -x "$node_bin/paneful" ]]; then
|
|
154
|
-
local resolved
|
|
155
|
-
resolved=$(readlink -f "$node_bin/paneful" 2>/dev/null || readlink "$node_bin/paneful" 2>/dev/null || echo "$node_bin/paneful")
|
|
156
|
-
echo "$resolved"; return
|
|
157
|
-
fi
|
|
158
|
-
|
|
159
|
-
return 1
|
|
153
|
+
func readLockfile() -> (pid: pid_t, port: Int)? {
|
|
154
|
+
guard let content = try? String(contentsOfFile: LOCKFILE, encoding: .utf8) else { return nil }
|
|
155
|
+
let lines = content.trimmingCharacters(in: .whitespacesAndNewlines).split(separator: "\\n")
|
|
156
|
+
guard lines.count >= 2, let pid = pid_t(lines[0]), let port = Int(lines[1]) else { return nil }
|
|
157
|
+
guard kill(pid, 0) == 0 else { return nil }
|
|
158
|
+
return (pid, port)
|
|
160
159
|
}
|
|
161
160
|
|
|
162
|
-
|
|
163
|
-
|
|
161
|
+
class AppDelegate: NSObject, NSApplicationDelegate {
|
|
162
|
+
var window: NSWindow!
|
|
163
|
+
var webView: WKWebView!
|
|
164
|
+
var serverProcess: Process?
|
|
165
|
+
var ownedServer = false
|
|
166
|
+
var port = 0
|
|
167
|
+
|
|
168
|
+
func applicationDidFinishLaunching(_ notification: Notification) {
|
|
169
|
+
try? FileManager.default.createDirectory(atPath: DATA_DIR, withIntermediateDirectories: true)
|
|
170
|
+
FileManager.default.createFile(atPath: LOG_FILE, contents: nil)
|
|
171
|
+
logHandle = FileHandle(forWritingAtPath: LOG_FILE)
|
|
172
|
+
logHandle?.seekToEndOfFile()
|
|
173
|
+
log("--- App launched ---")
|
|
174
|
+
|
|
175
|
+
setupMenuBar()
|
|
176
|
+
|
|
177
|
+
if let lock = readLockfile() {
|
|
178
|
+
log("Server already running on port \\(lock.port), connecting")
|
|
179
|
+
port = lock.port
|
|
180
|
+
createWindow()
|
|
181
|
+
} else {
|
|
182
|
+
log("Starting server")
|
|
183
|
+
if startServer() {
|
|
184
|
+
ownedServer = true
|
|
185
|
+
waitForLockfile(attempt: 0)
|
|
186
|
+
} else {
|
|
187
|
+
let a = NSAlert()
|
|
188
|
+
a.messageText = "Paneful"
|
|
189
|
+
a.informativeText = "Failed to start server.\\nMake sure Node.js and paneful are installed.\\nCheck ~/.paneful/app.log"
|
|
190
|
+
a.alertStyle = .critical
|
|
191
|
+
a.runModal()
|
|
192
|
+
NSApp.terminate(nil)
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
func startServer() -> Bool {
|
|
198
|
+
guard let node = findNode() else { log("ERROR: node not found"); return false }
|
|
199
|
+
log("Using node: \\(node)")
|
|
200
|
+
let nodeDir = (node as NSString).deletingLastPathComponent
|
|
201
|
+
guard let paneful = findPaneful(nodeDir: nodeDir) else { log("ERROR: paneful not found"); return false }
|
|
202
|
+
log("Using paneful: \\(paneful)")
|
|
203
|
+
|
|
204
|
+
let proc = Process()
|
|
205
|
+
proc.executableURL = URL(fileURLWithPath: node)
|
|
206
|
+
proc.arguments = [paneful]
|
|
207
|
+
var env = ProcessInfo.processInfo.environment
|
|
208
|
+
env["PANEFUL_APP"] = "1"
|
|
209
|
+
proc.environment = env
|
|
210
|
+
do {
|
|
211
|
+
try proc.run()
|
|
212
|
+
serverProcess = proc
|
|
213
|
+
log("Server started (PID: \\(proc.processIdentifier))")
|
|
214
|
+
return true
|
|
215
|
+
} catch {
|
|
216
|
+
log("Failed to start: \\(error)")
|
|
217
|
+
return false
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
func waitForLockfile(attempt: Int) {
|
|
222
|
+
if attempt > 50 {
|
|
223
|
+
log("Server did not start in time")
|
|
224
|
+
let a = NSAlert()
|
|
225
|
+
a.messageText = "Paneful"
|
|
226
|
+
a.informativeText = "Server failed to start in time.\\nCheck ~/.paneful/app.log"
|
|
227
|
+
a.alertStyle = .critical
|
|
228
|
+
a.runModal()
|
|
229
|
+
NSApp.terminate(nil)
|
|
230
|
+
return
|
|
231
|
+
}
|
|
232
|
+
if let lock = readLockfile() {
|
|
233
|
+
log("Server ready on port \\(lock.port)")
|
|
234
|
+
port = lock.port
|
|
235
|
+
DispatchQueue.main.async { self.createWindow() }
|
|
236
|
+
} else {
|
|
237
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
|
238
|
+
self.waitForLockfile(attempt: attempt + 1)
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
func createWindow() {
|
|
244
|
+
let config = WKWebViewConfiguration()
|
|
245
|
+
config.preferences.setValue(true, forKey: "developerExtrasEnabled")
|
|
246
|
+
|
|
247
|
+
webView = WKWebView(frame: .zero, configuration: config)
|
|
248
|
+
webView.load(URLRequest(url: URL(string: "http://localhost:\\(self.port)")!))
|
|
249
|
+
|
|
250
|
+
window = NSWindow(
|
|
251
|
+
contentRect: NSRect(x: 0, y: 0, width: 1200, height: 800),
|
|
252
|
+
styleMask: [.titled, .closable, .resizable, .miniaturizable],
|
|
253
|
+
backing: .buffered,
|
|
254
|
+
defer: false
|
|
255
|
+
)
|
|
256
|
+
window.title = "Paneful"
|
|
257
|
+
window.contentView = webView
|
|
258
|
+
window.center()
|
|
259
|
+
window.makeKeyAndOrderFront(nil)
|
|
260
|
+
log("Window created")
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
func setupMenuBar() {
|
|
264
|
+
let mainMenu = NSMenu()
|
|
265
|
+
|
|
266
|
+
// App menu
|
|
267
|
+
let appMenu = NSMenu()
|
|
268
|
+
appMenu.addItem(withTitle: "About Paneful", action: #selector(NSApplication.orderFrontStandardAboutPanel(_:)), keyEquivalent: "")
|
|
269
|
+
appMenu.addItem(.separator())
|
|
270
|
+
appMenu.addItem(withTitle: "Hide Paneful", action: #selector(NSApplication.hide(_:)), keyEquivalent: "h")
|
|
271
|
+
let ho = appMenu.addItem(withTitle: "Hide Others", action: #selector(NSApplication.hideOtherApplications(_:)), keyEquivalent: "h")
|
|
272
|
+
ho.keyEquivalentModifierMask = [.command, .option]
|
|
273
|
+
appMenu.addItem(withTitle: "Show All", action: #selector(NSApplication.unhideAllApplications(_:)), keyEquivalent: "")
|
|
274
|
+
appMenu.addItem(.separator())
|
|
275
|
+
appMenu.addItem(withTitle: "Quit Paneful", action: #selector(NSApplication.terminate(_:)), keyEquivalent: "q")
|
|
276
|
+
let appItem = NSMenuItem(); appItem.submenu = appMenu
|
|
277
|
+
mainMenu.addItem(appItem)
|
|
278
|
+
|
|
279
|
+
// Edit menu (enables Cmd+C/V/X/A in WebView)
|
|
280
|
+
let editMenu = NSMenu(title: "Edit")
|
|
281
|
+
editMenu.addItem(withTitle: "Undo", action: Selector(("undo:")), keyEquivalent: "z")
|
|
282
|
+
editMenu.addItem(withTitle: "Redo", action: Selector(("redo:")), keyEquivalent: "Z")
|
|
283
|
+
editMenu.addItem(.separator())
|
|
284
|
+
editMenu.addItem(withTitle: "Cut", action: #selector(NSText.cut(_:)), keyEquivalent: "x")
|
|
285
|
+
editMenu.addItem(withTitle: "Copy", action: #selector(NSText.copy(_:)), keyEquivalent: "c")
|
|
286
|
+
editMenu.addItem(withTitle: "Paste", action: #selector(NSText.paste(_:)), keyEquivalent: "v")
|
|
287
|
+
editMenu.addItem(withTitle: "Select All", action: #selector(NSText.selectAll(_:)), keyEquivalent: "a")
|
|
288
|
+
let editItem = NSMenuItem(); editItem.submenu = editMenu
|
|
289
|
+
mainMenu.addItem(editItem)
|
|
290
|
+
|
|
291
|
+
// View menu
|
|
292
|
+
let viewMenu = NSMenu(title: "View")
|
|
293
|
+
viewMenu.addItem(withTitle: "Reload", action: #selector(reloadPage(_:)), keyEquivalent: "r")
|
|
294
|
+
let viewItem = NSMenuItem(); viewItem.submenu = viewMenu
|
|
295
|
+
mainMenu.addItem(viewItem)
|
|
296
|
+
|
|
297
|
+
// Window menu
|
|
298
|
+
let windowMenu = NSMenu(title: "Window")
|
|
299
|
+
windowMenu.addItem(withTitle: "Minimize", action: #selector(NSWindow.performMiniaturize(_:)), keyEquivalent: "m")
|
|
300
|
+
windowMenu.addItem(withTitle: "Close", action: #selector(NSWindow.performClose(_:)), keyEquivalent: "w")
|
|
301
|
+
let windowItem = NSMenuItem(); windowItem.submenu = windowMenu
|
|
302
|
+
mainMenu.addItem(windowItem)
|
|
303
|
+
|
|
304
|
+
NSApp.mainMenu = mainMenu
|
|
305
|
+
NSApp.windowsMenu = windowMenu
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
@objc func reloadPage(_ sender: Any?) {
|
|
309
|
+
webView?.reload()
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
|
|
313
|
+
return true
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
func applicationWillTerminate(_ notification: Notification) {
|
|
317
|
+
if ownedServer, let proc = serverProcess, proc.isRunning {
|
|
318
|
+
log("Terminating server (PID: \\(proc.processIdentifier))")
|
|
319
|
+
proc.terminate()
|
|
320
|
+
proc.waitUntilExit()
|
|
321
|
+
}
|
|
322
|
+
log("App terminated")
|
|
323
|
+
logHandle?.closeFile()
|
|
324
|
+
}
|
|
164
325
|
}
|
|
165
326
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
echo "Using node: $NODE"
|
|
173
|
-
|
|
174
|
-
PANEFUL=$(find_paneful "$NODE")
|
|
175
|
-
if [[ -z "$PANEFUL" ]]; then
|
|
176
|
-
echo "ERROR: paneful not found"
|
|
177
|
-
show_error "Could not find paneful.\\n\\nRun: npm install -g paneful && paneful --install-app"
|
|
178
|
-
exit 1
|
|
179
|
-
fi
|
|
180
|
-
echo "Using paneful: $PANEFUL"
|
|
181
|
-
|
|
182
|
-
exec "$NODE" "$PANEFUL"
|
|
327
|
+
let app = NSApplication.shared
|
|
328
|
+
app.setActivationPolicy(.regular)
|
|
329
|
+
let delegate = AppDelegate()
|
|
330
|
+
app.delegate = delegate
|
|
331
|
+
app.activate(ignoringOtherApps: true)
|
|
332
|
+
app.run()
|
|
183
333
|
`;
|
|
184
334
|
}
|
|
185
|
-
export async function installApp() {
|
|
335
|
+
export async function installApp(appPath) {
|
|
186
336
|
if (process.platform !== 'darwin') {
|
|
187
337
|
console.error('paneful --install-app is only supported on macOS.');
|
|
188
338
|
process.exit(1);
|
|
189
339
|
}
|
|
190
|
-
|
|
191
|
-
|
|
340
|
+
// Check for swiftc
|
|
341
|
+
try {
|
|
342
|
+
execFileSync('swiftc', ['--version'], { stdio: 'pipe' });
|
|
343
|
+
}
|
|
344
|
+
catch {
|
|
345
|
+
console.error('swiftc not found. Install Xcode Command Line Tools:');
|
|
346
|
+
console.error(' xcode-select --install');
|
|
347
|
+
process.exit(1);
|
|
348
|
+
}
|
|
349
|
+
let resolvedPath = appPath;
|
|
350
|
+
if (!resolvedPath) {
|
|
351
|
+
// Show a folder picker dialog so the user can choose where to install
|
|
352
|
+
try {
|
|
353
|
+
const chosen = execFileSync('osascript', ['-e', `
|
|
354
|
+
set defaultDir to POSIX file "/Applications" as alias
|
|
355
|
+
try
|
|
356
|
+
set chosenFolder to choose folder with prompt "Choose where to install Paneful.app:" default location defaultDir
|
|
357
|
+
return POSIX path of chosenFolder
|
|
358
|
+
on error
|
|
359
|
+
return ""
|
|
360
|
+
end try
|
|
361
|
+
`], { encoding: 'utf-8', timeout: 60_000 }).trim();
|
|
362
|
+
if (!chosen) {
|
|
363
|
+
console.log('Installation cancelled.');
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
resolvedPath = path.join(chosen, 'Paneful.app');
|
|
367
|
+
}
|
|
368
|
+
catch {
|
|
369
|
+
// Fallback if osascript dialog fails (e.g. no GUI session)
|
|
370
|
+
resolvedPath = DEFAULT_APP_PATH;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
const installDir = path.dirname(resolvedPath);
|
|
374
|
+
console.log(`Creating Paneful.app in ${installDir}...`);
|
|
375
|
+
const contentsDir = path.join(resolvedPath, 'Contents');
|
|
192
376
|
const macosDir = path.join(contentsDir, 'MacOS');
|
|
193
377
|
const resourcesDir = path.join(contentsDir, 'Resources');
|
|
194
378
|
try {
|
|
@@ -196,32 +380,57 @@ export async function installApp() {
|
|
|
196
380
|
fs.mkdirSync(macosDir, { recursive: true });
|
|
197
381
|
fs.mkdirSync(resourcesDir, { recursive: true });
|
|
198
382
|
// Icon
|
|
199
|
-
const
|
|
383
|
+
const iconDir = findIconDir();
|
|
200
384
|
let hasIcon = false;
|
|
201
|
-
if (
|
|
202
|
-
const icnsPath = buildIcns(
|
|
385
|
+
if (iconDir) {
|
|
386
|
+
const icnsPath = buildIcns(iconDir, contentsDir);
|
|
203
387
|
hasIcon = icnsPath !== null;
|
|
204
388
|
if (hasIcon) {
|
|
205
|
-
console.log(' Icon:
|
|
389
|
+
console.log(' Icon: built .icns from assets');
|
|
206
390
|
}
|
|
207
391
|
else {
|
|
208
392
|
console.log(' Icon: conversion failed, using generic icon');
|
|
209
393
|
}
|
|
210
394
|
}
|
|
211
395
|
else {
|
|
212
|
-
console.log(' Icon:
|
|
396
|
+
console.log(' Icon: assets not found, using generic icon');
|
|
213
397
|
}
|
|
214
398
|
// Info.plist
|
|
215
399
|
fs.writeFileSync(path.join(contentsDir, 'Info.plist'), getInfoPlist(hasIcon));
|
|
216
400
|
console.log(' Info.plist: written');
|
|
217
|
-
//
|
|
218
|
-
const
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
401
|
+
// Compile Swift wrapper
|
|
402
|
+
const swiftSource = getSwiftSource();
|
|
403
|
+
const tmpSwift = path.join(os.tmpdir(), 'PanefulApp.swift');
|
|
404
|
+
const binaryPath = path.join(macosDir, 'Paneful');
|
|
405
|
+
fs.writeFileSync(tmpSwift, swiftSource);
|
|
406
|
+
console.log(' Compiling native wrapper...');
|
|
407
|
+
try {
|
|
408
|
+
execFileSync('swiftc', [
|
|
409
|
+
tmpSwift,
|
|
410
|
+
'-o', binaryPath,
|
|
411
|
+
'-framework', 'Cocoa',
|
|
412
|
+
'-framework', 'WebKit',
|
|
413
|
+
'-O',
|
|
414
|
+
], { stdio: 'pipe', timeout: 60_000 });
|
|
415
|
+
}
|
|
416
|
+
finally {
|
|
417
|
+
try {
|
|
418
|
+
fs.unlinkSync(tmpSwift);
|
|
419
|
+
}
|
|
420
|
+
catch { /* ok */ }
|
|
421
|
+
}
|
|
422
|
+
console.log(' Binary: compiled');
|
|
423
|
+
// Ad-hoc code sign to avoid Gatekeeper warning
|
|
424
|
+
try {
|
|
425
|
+
execFileSync('codesign', ['--force', '--deep', '--sign', '-', resolvedPath], { stdio: 'pipe' });
|
|
426
|
+
console.log(' Signed: ad-hoc');
|
|
427
|
+
}
|
|
428
|
+
catch {
|
|
429
|
+
console.log(' Signing: skipped (codesign not available)');
|
|
430
|
+
}
|
|
222
431
|
// Touch the app so Finder picks up changes
|
|
223
432
|
try {
|
|
224
|
-
execFileSync('touch', [
|
|
433
|
+
execFileSync('touch', [resolvedPath], { stdio: 'pipe' });
|
|
225
434
|
}
|
|
226
435
|
catch { /* ok */ }
|
|
227
436
|
console.log('\nPaneful.app installed successfully!');
|
package/dist/server/ipc.js
CHANGED
|
@@ -41,10 +41,11 @@ export function startIpcListener(socketPath, ptyManager, projectStore, wsHandler
|
|
|
41
41
|
function handleIpcRequest(request, ptyManager, projectStore, wsHandler) {
|
|
42
42
|
switch (request.command) {
|
|
43
43
|
case 'spawn': {
|
|
44
|
-
|
|
45
|
-
const id = uuidv4();
|
|
46
|
-
|
|
47
|
-
|
|
44
|
+
const existing = projectStore.findByCwd(request.cwd);
|
|
45
|
+
const id = existing?.id ?? uuidv4();
|
|
46
|
+
if (!existing) {
|
|
47
|
+
projectStore.create(newProject(id, request.name, request.cwd));
|
|
48
|
+
}
|
|
48
49
|
wsHandler.send({
|
|
49
50
|
type: 'project:spawned',
|
|
50
51
|
projectId: id,
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
const DEFAULTS = {
|
|
4
|
+
favourites: {},
|
|
5
|
+
ui: {
|
|
6
|
+
theme: 'system',
|
|
7
|
+
sidebarWidth: 224,
|
|
8
|
+
editorSyncEnabled: true,
|
|
9
|
+
},
|
|
10
|
+
activeProjectId: null,
|
|
11
|
+
};
|
|
12
|
+
export class SettingsStore {
|
|
13
|
+
filePath;
|
|
14
|
+
data;
|
|
15
|
+
constructor(dataDir) {
|
|
16
|
+
this.filePath = path.join(dataDir, 'settings.json');
|
|
17
|
+
this.data = { ...DEFAULTS, ui: { ...DEFAULTS.ui } };
|
|
18
|
+
if (fs.existsSync(this.filePath)) {
|
|
19
|
+
try {
|
|
20
|
+
const raw = JSON.parse(fs.readFileSync(this.filePath, 'utf-8'));
|
|
21
|
+
this.data = {
|
|
22
|
+
favourites: raw.favourites ?? DEFAULTS.favourites,
|
|
23
|
+
ui: {
|
|
24
|
+
theme: raw.ui?.theme ?? DEFAULTS.ui.theme,
|
|
25
|
+
sidebarWidth: raw.ui?.sidebarWidth ?? DEFAULTS.ui.sidebarWidth,
|
|
26
|
+
editorSyncEnabled: raw.ui?.editorSyncEnabled ?? DEFAULTS.ui.editorSyncEnabled,
|
|
27
|
+
},
|
|
28
|
+
activeProjectId: raw.activeProjectId ?? DEFAULTS.activeProjectId,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
// Corrupted file, use defaults
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
get() {
|
|
37
|
+
return this.data;
|
|
38
|
+
}
|
|
39
|
+
update(partial) {
|
|
40
|
+
if (partial.favourites !== undefined) {
|
|
41
|
+
this.data.favourites = partial.favourites;
|
|
42
|
+
}
|
|
43
|
+
if (partial.ui !== undefined) {
|
|
44
|
+
this.data.ui = { ...this.data.ui, ...partial.ui };
|
|
45
|
+
}
|
|
46
|
+
if (partial.activeProjectId !== undefined) {
|
|
47
|
+
this.data.activeProjectId = partial.activeProjectId;
|
|
48
|
+
}
|
|
49
|
+
this.persist();
|
|
50
|
+
return this.data;
|
|
51
|
+
}
|
|
52
|
+
persist() {
|
|
53
|
+
try {
|
|
54
|
+
const dir = path.dirname(this.filePath);
|
|
55
|
+
if (!fs.existsSync(dir)) {
|
|
56
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
57
|
+
}
|
|
58
|
+
fs.writeFileSync(this.filePath, JSON.stringify(this.data, null, 2));
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
console.error('Failed to persist settings');
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|