@lattices/cli 0.3.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 +155 -0
- package/app/Lattices.app/Contents/Info.plist +24 -0
- package/app/Package.swift +13 -0
- package/app/Sources/AccessibilityTextExtractor.swift +111 -0
- package/app/Sources/ActionRow.swift +61 -0
- package/app/Sources/App.swift +10 -0
- package/app/Sources/AppDelegate.swift +242 -0
- package/app/Sources/AppShellView.swift +62 -0
- package/app/Sources/AppTypeClassifier.swift +70 -0
- package/app/Sources/AppWindowShell.swift +63 -0
- package/app/Sources/CheatSheetHUD.swift +332 -0
- package/app/Sources/CommandModeState.swift +1362 -0
- package/app/Sources/CommandModeView.swift +1405 -0
- package/app/Sources/CommandModeWindow.swift +192 -0
- package/app/Sources/CommandPaletteView.swift +307 -0
- package/app/Sources/CommandPaletteWindow.swift +134 -0
- package/app/Sources/DaemonProtocol.swift +101 -0
- package/app/Sources/DaemonServer.swift +414 -0
- package/app/Sources/DesktopModel.swift +149 -0
- package/app/Sources/DesktopModelTypes.swift +71 -0
- package/app/Sources/DiagnosticLog.swift +271 -0
- package/app/Sources/EventBus.swift +30 -0
- package/app/Sources/HotkeyManager.swift +254 -0
- package/app/Sources/HotkeyStore.swift +338 -0
- package/app/Sources/InventoryManager.swift +35 -0
- package/app/Sources/InventoryPath.swift +43 -0
- package/app/Sources/KeyRecorderView.swift +210 -0
- package/app/Sources/LatticesApi.swift +1234 -0
- package/app/Sources/LayerBezel.swift +203 -0
- package/app/Sources/MainView.swift +479 -0
- package/app/Sources/MainWindow.swift +83 -0
- package/app/Sources/OcrModel.swift +430 -0
- package/app/Sources/OcrStore.swift +329 -0
- package/app/Sources/OmniSearchState.swift +283 -0
- package/app/Sources/OmniSearchView.swift +288 -0
- package/app/Sources/OmniSearchWindow.swift +105 -0
- package/app/Sources/OrphanRow.swift +129 -0
- package/app/Sources/PaletteCommand.swift +419 -0
- package/app/Sources/PermissionChecker.swift +125 -0
- package/app/Sources/Preferences.swift +99 -0
- package/app/Sources/ProcessModel.swift +199 -0
- package/app/Sources/ProcessQuery.swift +151 -0
- package/app/Sources/Project.swift +28 -0
- package/app/Sources/ProjectRow.swift +368 -0
- package/app/Sources/ProjectScanner.swift +128 -0
- package/app/Sources/ScreenMapState.swift +2387 -0
- package/app/Sources/ScreenMapView.swift +2820 -0
- package/app/Sources/ScreenMapWindowController.swift +89 -0
- package/app/Sources/SessionManager.swift +72 -0
- package/app/Sources/SettingsView.swift +1064 -0
- package/app/Sources/SettingsWindow.swift +20 -0
- package/app/Sources/TabGroupRow.swift +178 -0
- package/app/Sources/Terminal.swift +259 -0
- package/app/Sources/TerminalQuery.swift +156 -0
- package/app/Sources/TerminalSynthesizer.swift +200 -0
- package/app/Sources/Theme.swift +163 -0
- package/app/Sources/TilePickerView.swift +209 -0
- package/app/Sources/TmuxModel.swift +53 -0
- package/app/Sources/TmuxQuery.swift +81 -0
- package/app/Sources/WindowTiler.swift +1778 -0
- package/app/Sources/WorkspaceManager.swift +575 -0
- package/bin/client.js +4 -0
- package/bin/daemon-client.js +187 -0
- package/bin/lattices-app.js +221 -0
- package/bin/lattices.js +1551 -0
- package/docs/api.md +924 -0
- package/docs/app.md +297 -0
- package/docs/concepts.md +135 -0
- package/docs/config.md +245 -0
- package/docs/layers.md +410 -0
- package/docs/ocr.md +185 -0
- package/docs/overview.md +94 -0
- package/docs/quickstart.md +75 -0
- package/package.json +42 -0
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
// Lightweight WebSocket client for lattices daemon (ws://127.0.0.1:9399)
|
|
2
|
+
// Uses Node `net` module with manual HTTP upgrade + minimal WS framing.
|
|
3
|
+
// Zero npm dependencies. Requires Node >= 18.
|
|
4
|
+
|
|
5
|
+
import { createConnection } from "node:net";
|
|
6
|
+
import { randomBytes } from "node:crypto";
|
|
7
|
+
|
|
8
|
+
const DAEMON_HOST = "127.0.0.1";
|
|
9
|
+
const DAEMON_PORT = 9399;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Send a JSON-RPC-style request to the daemon and return the response.
|
|
13
|
+
* @param {string} method
|
|
14
|
+
* @param {object} [params]
|
|
15
|
+
* @param {number} [timeoutMs=3000]
|
|
16
|
+
* @returns {Promise<object>} The result field from the response
|
|
17
|
+
*/
|
|
18
|
+
export async function daemonCall(method, params, timeoutMs = 3000) {
|
|
19
|
+
const id = randomBytes(4).toString("hex");
|
|
20
|
+
const request = JSON.stringify({ id, method, params: params ?? null });
|
|
21
|
+
|
|
22
|
+
return new Promise((resolve, reject) => {
|
|
23
|
+
const socket = createConnection({ host: DAEMON_HOST, port: DAEMON_PORT });
|
|
24
|
+
let settled = false;
|
|
25
|
+
let buffer = Buffer.alloc(0);
|
|
26
|
+
let upgraded = false;
|
|
27
|
+
|
|
28
|
+
const timer = setTimeout(() => {
|
|
29
|
+
if (!settled) {
|
|
30
|
+
settled = true;
|
|
31
|
+
socket.destroy();
|
|
32
|
+
reject(new Error("Daemon request timed out"));
|
|
33
|
+
}
|
|
34
|
+
}, timeoutMs);
|
|
35
|
+
|
|
36
|
+
const cleanup = () => {
|
|
37
|
+
clearTimeout(timer);
|
|
38
|
+
socket.destroy();
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
socket.on("error", (err) => {
|
|
42
|
+
if (!settled) {
|
|
43
|
+
settled = true;
|
|
44
|
+
cleanup();
|
|
45
|
+
reject(err);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
socket.on("connect", () => {
|
|
50
|
+
// Send HTTP upgrade request
|
|
51
|
+
const key = randomBytes(16).toString("base64");
|
|
52
|
+
const upgrade = [
|
|
53
|
+
`GET / HTTP/1.1`,
|
|
54
|
+
`Host: ${DAEMON_HOST}:${DAEMON_PORT}`,
|
|
55
|
+
`Upgrade: websocket`,
|
|
56
|
+
`Connection: Upgrade`,
|
|
57
|
+
`Sec-WebSocket-Key: ${key}`,
|
|
58
|
+
`Sec-WebSocket-Version: 13`,
|
|
59
|
+
``,
|
|
60
|
+
``,
|
|
61
|
+
].join("\r\n");
|
|
62
|
+
socket.write(upgrade);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
socket.on("data", (chunk) => {
|
|
66
|
+
buffer = Buffer.concat([buffer, chunk]);
|
|
67
|
+
|
|
68
|
+
if (!upgraded) {
|
|
69
|
+
const headerEnd = buffer.indexOf("\r\n\r\n");
|
|
70
|
+
if (headerEnd === -1) return;
|
|
71
|
+
const header = buffer.subarray(0, headerEnd).toString();
|
|
72
|
+
if (!header.includes("101")) {
|
|
73
|
+
settled = true;
|
|
74
|
+
cleanup();
|
|
75
|
+
reject(new Error("WebSocket upgrade failed"));
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
upgraded = true;
|
|
79
|
+
buffer = buffer.subarray(headerEnd + 4);
|
|
80
|
+
|
|
81
|
+
// Send the request as a masked WebSocket text frame
|
|
82
|
+
sendFrame(socket, request);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Try to parse a WebSocket frame from the buffer
|
|
86
|
+
const result = parseFrame(buffer);
|
|
87
|
+
if (result) {
|
|
88
|
+
buffer = result.rest;
|
|
89
|
+
if (!settled) {
|
|
90
|
+
settled = true;
|
|
91
|
+
cleanup();
|
|
92
|
+
try {
|
|
93
|
+
const parsed = JSON.parse(result.payload);
|
|
94
|
+
if (parsed.error) {
|
|
95
|
+
reject(new Error(parsed.error));
|
|
96
|
+
} else {
|
|
97
|
+
resolve(parsed.result);
|
|
98
|
+
}
|
|
99
|
+
} catch (e) {
|
|
100
|
+
reject(new Error("Invalid JSON response from daemon"));
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Check if the daemon is reachable.
|
|
110
|
+
* @returns {Promise<boolean>}
|
|
111
|
+
*/
|
|
112
|
+
export async function isDaemonRunning() {
|
|
113
|
+
try {
|
|
114
|
+
await daemonCall("daemon.status", null, 1000);
|
|
115
|
+
return true;
|
|
116
|
+
} catch {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// MARK: - WebSocket framing helpers
|
|
122
|
+
|
|
123
|
+
function sendFrame(socket, text) {
|
|
124
|
+
const payload = Buffer.from(text, "utf8");
|
|
125
|
+
const mask = randomBytes(4);
|
|
126
|
+
const len = payload.length;
|
|
127
|
+
|
|
128
|
+
let header;
|
|
129
|
+
if (len < 126) {
|
|
130
|
+
header = Buffer.alloc(2);
|
|
131
|
+
header[0] = 0x81; // FIN + text opcode
|
|
132
|
+
header[1] = 0x80 | len; // masked + length
|
|
133
|
+
} else if (len < 65536) {
|
|
134
|
+
header = Buffer.alloc(4);
|
|
135
|
+
header[0] = 0x81;
|
|
136
|
+
header[1] = 0x80 | 126;
|
|
137
|
+
header.writeUInt16BE(len, 2);
|
|
138
|
+
} else {
|
|
139
|
+
header = Buffer.alloc(10);
|
|
140
|
+
header[0] = 0x81;
|
|
141
|
+
header[1] = 0x80 | 127;
|
|
142
|
+
header.writeBigUInt64BE(BigInt(len), 2);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Mask payload
|
|
146
|
+
const masked = Buffer.alloc(payload.length);
|
|
147
|
+
for (let i = 0; i < payload.length; i++) {
|
|
148
|
+
masked[i] = payload[i] ^ mask[i % 4];
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
socket.write(Buffer.concat([header, mask, masked]));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function parseFrame(buf) {
|
|
155
|
+
if (buf.length < 2) return null;
|
|
156
|
+
|
|
157
|
+
const masked = (buf[1] & 0x80) !== 0;
|
|
158
|
+
let payloadLen = buf[1] & 0x7f;
|
|
159
|
+
let offset = 2;
|
|
160
|
+
|
|
161
|
+
if (payloadLen === 126) {
|
|
162
|
+
if (buf.length < 4) return null;
|
|
163
|
+
payloadLen = buf.readUInt16BE(2);
|
|
164
|
+
offset = 4;
|
|
165
|
+
} else if (payloadLen === 127) {
|
|
166
|
+
if (buf.length < 10) return null;
|
|
167
|
+
payloadLen = Number(buf.readBigUInt64BE(2));
|
|
168
|
+
offset = 10;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (masked) offset += 4;
|
|
172
|
+
if (buf.length < offset + payloadLen) return null;
|
|
173
|
+
|
|
174
|
+
let payload = buf.subarray(offset, offset + payloadLen);
|
|
175
|
+
if (masked) {
|
|
176
|
+
const maskKey = buf.subarray(offset - 4, offset);
|
|
177
|
+
payload = Buffer.alloc(payloadLen);
|
|
178
|
+
for (let i = 0; i < payloadLen; i++) {
|
|
179
|
+
payload[i] = buf[offset + i] ^ maskKey[i % 4];
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
payload: payload.toString("utf8"),
|
|
185
|
+
rest: buf.subarray(offset + payloadLen),
|
|
186
|
+
};
|
|
187
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { execSync, spawn } from "node:child_process";
|
|
4
|
+
import { existsSync, mkdirSync, chmodSync, createWriteStream } from "node:fs";
|
|
5
|
+
import { resolve, dirname } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { get } from "node:https";
|
|
8
|
+
|
|
9
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
const appDir = resolve(__dirname, "../app");
|
|
11
|
+
const bundlePath = resolve(appDir, "Lattices.app");
|
|
12
|
+
const binaryDir = resolve(bundlePath, "Contents/MacOS");
|
|
13
|
+
const binaryPath = resolve(binaryDir, "Lattices");
|
|
14
|
+
|
|
15
|
+
const REPO = "arach/lattices";
|
|
16
|
+
const ASSET_NAME = "Lattices-macos-arm64";
|
|
17
|
+
|
|
18
|
+
// ── Helpers ──────────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
function isRunning() {
|
|
21
|
+
try {
|
|
22
|
+
execSync("pgrep -x Lattices", { stdio: "pipe" });
|
|
23
|
+
return true;
|
|
24
|
+
} catch {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function quit() {
|
|
30
|
+
try {
|
|
31
|
+
execSync("pkill -x Lattices", { stdio: "pipe" });
|
|
32
|
+
// Wait briefly for process to exit
|
|
33
|
+
try { execSync("sleep 0.5", { stdio: "pipe" }); } catch {}
|
|
34
|
+
// Force kill if still running
|
|
35
|
+
if (isRunning()) {
|
|
36
|
+
execSync("pkill -9 -x Lattices", { stdio: "pipe" });
|
|
37
|
+
}
|
|
38
|
+
return true;
|
|
39
|
+
} catch {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function hasSwift() {
|
|
45
|
+
try {
|
|
46
|
+
execSync("which swift", { stdio: "pipe" });
|
|
47
|
+
return true;
|
|
48
|
+
} catch {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function launch(extraArgs = []) {
|
|
54
|
+
if (isRunning()) {
|
|
55
|
+
console.log("lattices app is already running.");
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const args = [bundlePath];
|
|
59
|
+
if (extraArgs.length) args.push("--args", ...extraArgs);
|
|
60
|
+
spawn("open", args, { detached: true, stdio: "ignore" }).unref();
|
|
61
|
+
console.log("lattices app launched.");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ── Build from source (current arch only) ────────────────────────────
|
|
65
|
+
|
|
66
|
+
function buildFromSource() {
|
|
67
|
+
console.log("Building lattices app from source...");
|
|
68
|
+
try {
|
|
69
|
+
execSync("swift build -c release", {
|
|
70
|
+
cwd: appDir,
|
|
71
|
+
stdio: "inherit",
|
|
72
|
+
});
|
|
73
|
+
} catch {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const builtPath = resolve(appDir, ".build/release/Lattices");
|
|
78
|
+
if (!existsSync(builtPath)) return false;
|
|
79
|
+
|
|
80
|
+
mkdirSync(binaryDir, { recursive: true });
|
|
81
|
+
execSync(`cp '${builtPath}' '${binaryPath}'`);
|
|
82
|
+
|
|
83
|
+
// Copy app icon into bundle
|
|
84
|
+
const iconSrc = resolve(__dirname, "../assets/AppIcon.icns");
|
|
85
|
+
const resourcesDir = resolve(bundlePath, "Contents/Resources");
|
|
86
|
+
mkdirSync(resourcesDir, { recursive: true });
|
|
87
|
+
if (existsSync(iconSrc)) {
|
|
88
|
+
execSync(`cp '${iconSrc}' '${resolve(resourcesDir, "AppIcon.icns")}'`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Re-sign the bundle so macOS TCC recognizes a stable identity across rebuilds.
|
|
92
|
+
// Without this, each build gets a new ad-hoc signature and permission grants are lost.
|
|
93
|
+
try {
|
|
94
|
+
// Prefer a real signing identity for stable TCC grants; fall back to ad-hoc with fixed identifier
|
|
95
|
+
const identities = execSync("security find-identity -v -p codesigning", { stdio: "pipe" }).toString();
|
|
96
|
+
const devId = identities.match(/"(Developer ID Application:[^"]+)"/)?.[1]
|
|
97
|
+
|| identities.match(/"(Apple Development:[^"]+)"/)?.[1];
|
|
98
|
+
const signArg = devId ? `'${devId}'` : "-";
|
|
99
|
+
execSync(
|
|
100
|
+
`codesign --force --sign ${signArg} --identifier com.arach.lattices '${bundlePath}'`,
|
|
101
|
+
{ stdio: "pipe" }
|
|
102
|
+
);
|
|
103
|
+
} catch (e) {
|
|
104
|
+
// Non-fatal — app still works, just permissions won't persist across rebuilds
|
|
105
|
+
console.log("Warning: code signing failed — permissions may not persist across rebuilds.");
|
|
106
|
+
}
|
|
107
|
+
// Update bundle timestamp so Finder shows the correct modified date
|
|
108
|
+
try { execSync(`touch '${bundlePath}'`, { stdio: "pipe" }); } catch {}
|
|
109
|
+
console.log("Build complete.");
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ── Download from GitHub releases ────────────────────────────────────
|
|
114
|
+
|
|
115
|
+
function httpsGet(url) {
|
|
116
|
+
return new Promise((resolve, reject) => {
|
|
117
|
+
get(url, { headers: { "User-Agent": "lattices" } }, (res) => {
|
|
118
|
+
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
119
|
+
return httpsGet(res.headers.location).then(resolve, reject);
|
|
120
|
+
}
|
|
121
|
+
if (res.statusCode !== 200) {
|
|
122
|
+
reject(new Error(`HTTP ${res.statusCode}`));
|
|
123
|
+
res.resume();
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
resolve(res);
|
|
127
|
+
}).on("error", reject);
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function download() {
|
|
132
|
+
console.log("Downloading pre-built binary...");
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
const apiUrl = `https://api.github.com/repos/${REPO}/releases/latest`;
|
|
136
|
+
const apiRes = await httpsGet(apiUrl);
|
|
137
|
+
const chunks = [];
|
|
138
|
+
for await (const chunk of apiRes) chunks.push(chunk);
|
|
139
|
+
const release = JSON.parse(Buffer.concat(chunks).toString());
|
|
140
|
+
|
|
141
|
+
const asset = release.assets?.find((a) => a.name === ASSET_NAME);
|
|
142
|
+
if (!asset) throw new Error("Binary not found in release assets");
|
|
143
|
+
|
|
144
|
+
const dlRes = await httpsGet(asset.browser_download_url);
|
|
145
|
+
|
|
146
|
+
mkdirSync(binaryDir, { recursive: true });
|
|
147
|
+
const ws = createWriteStream(binaryPath);
|
|
148
|
+
await new Promise((resolve, reject) => {
|
|
149
|
+
dlRes.pipe(ws);
|
|
150
|
+
ws.on("finish", resolve);
|
|
151
|
+
ws.on("error", reject);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
chmodSync(binaryPath, 0o755);
|
|
155
|
+
console.log("Download complete.");
|
|
156
|
+
return true;
|
|
157
|
+
} catch (e) {
|
|
158
|
+
console.log(`Download failed: ${e.message}`);
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ── Commands ─────────────────────────────────────────────────────────
|
|
164
|
+
|
|
165
|
+
async function ensureBinary() {
|
|
166
|
+
if (existsSync(binaryPath)) return;
|
|
167
|
+
|
|
168
|
+
// 1. Try local compile (fast, matches exact system)
|
|
169
|
+
if (hasSwift()) {
|
|
170
|
+
if (buildFromSource()) return;
|
|
171
|
+
console.log("Local build failed, trying download...");
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// 2. Fall back to pre-built binary from GitHub releases
|
|
175
|
+
const downloaded = await download();
|
|
176
|
+
if (downloaded) return;
|
|
177
|
+
|
|
178
|
+
// 3. Nothing worked
|
|
179
|
+
console.error(
|
|
180
|
+
"Could not build or download the lattices app.\n" +
|
|
181
|
+
"Options:\n" +
|
|
182
|
+
" • Install Xcode CLI tools: xcode-select --install\n" +
|
|
183
|
+
" • Download manually from: https://github.com/" + REPO + "/releases"
|
|
184
|
+
);
|
|
185
|
+
process.exit(1);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const cmd = process.argv[2];
|
|
189
|
+
const flags = process.argv.slice(3);
|
|
190
|
+
const launchFlags = [];
|
|
191
|
+
if (flags.includes("--diagnostics") || flags.includes("-d")) launchFlags.push("--diagnostics");
|
|
192
|
+
if (flags.includes("--screen-map") || flags.includes("-m")) launchFlags.push("--screen-map");
|
|
193
|
+
|
|
194
|
+
if (cmd === "build") {
|
|
195
|
+
if (!hasSwift()) {
|
|
196
|
+
console.error("Swift is required. Install with: xcode-select --install");
|
|
197
|
+
process.exit(1);
|
|
198
|
+
}
|
|
199
|
+
buildFromSource();
|
|
200
|
+
} else if (cmd === "quit") {
|
|
201
|
+
if (quit()) {
|
|
202
|
+
console.log("lattices app stopped.");
|
|
203
|
+
} else {
|
|
204
|
+
console.log("lattices app is not running.");
|
|
205
|
+
}
|
|
206
|
+
} else if (cmd === "restart") {
|
|
207
|
+
// Quit → rebuild → relaunch
|
|
208
|
+
quit();
|
|
209
|
+
if (!hasSwift()) {
|
|
210
|
+
console.error("Swift is required. Install with: xcode-select --install");
|
|
211
|
+
process.exit(1);
|
|
212
|
+
}
|
|
213
|
+
if (!buildFromSource()) {
|
|
214
|
+
console.error("Build failed.");
|
|
215
|
+
process.exit(1);
|
|
216
|
+
}
|
|
217
|
+
launch(launchFlags);
|
|
218
|
+
} else {
|
|
219
|
+
await ensureBinary();
|
|
220
|
+
launch(launchFlags);
|
|
221
|
+
}
|