@arach/lattices 0.2.0 → 0.6.1
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/LICENSE +21 -0
- package/README.md +172 -86
- package/apps/mac/Info.plist +43 -0
- package/apps/mac/Lattices.app/Contents/Info.plist +43 -0
- package/apps/mac/Lattices.app/Contents/MacOS/Lattices +0 -0
- package/apps/mac/Lattices.app/Contents/Resources/AppIcon.icns +0 -0
- package/apps/mac/Lattices.app/Contents/Resources/docs/assistant-knowledge.md +130 -0
- package/apps/mac/Lattices.app/Contents/Resources/tap.wav +0 -0
- package/apps/mac/Lattices.app/Contents/_CodeSignature/CodeResources +150 -0
- package/apps/mac/Lattices.entitlements +21 -0
- package/apps/mac/Resources/Pets/assistant-spark/pet.json +62 -0
- package/apps/mac/Resources/Pets/assistant-spark/spritesheet.webp +0 -0
- package/apps/mac/Resources/Pets/scout-ranger/pet.json +6 -0
- package/apps/mac/Resources/Pets/scout-ranger/spritesheet.webp +0 -0
- package/apps/mac/Resources/tap.wav +0 -0
- package/assets/AppIcon.icns +0 -0
- package/bin/assistant-intelligence.ts +912 -0
- package/bin/cli/capture.ts +252 -0
- package/bin/cli/daemon.ts +22 -0
- package/bin/cli/helpers.ts +105 -0
- package/bin/cli/layer.ts +178 -0
- package/bin/cli/runs.ts +43 -0
- package/bin/cli/search.ts +141 -0
- package/bin/cli/session.ts +32 -0
- package/bin/client.ts +17 -0
- package/bin/cua.ts +26 -0
- package/bin/{daemon-client.js → daemon-client.ts} +49 -30
- package/bin/handsoff-infer.ts +96 -0
- package/bin/handsoff-worker.ts +531 -0
- package/bin/infer.ts +424 -0
- package/bin/keychain.ts +75 -0
- package/bin/lattices-app.ts +655 -0
- package/bin/lattices-build +125 -0
- package/bin/lattices-build-env.ts +77 -0
- package/bin/lattices-dev +362 -0
- package/bin/lattices.ts +3260 -0
- package/bin/project-twin.ts +645 -0
- package/docs/agent-execution-plan.md +562 -0
- package/docs/agent-layer-guide.md +207 -0
- package/docs/agents.md +233 -0
- package/docs/ai-chat-ux-review.md +416 -0
- package/docs/api.md +1041 -47
- package/docs/app.md +96 -13
- package/docs/assistant-knowledge.md +130 -0
- package/docs/companion-deck.md +209 -0
- package/docs/component-extraction-roadmap.md +392 -0
- package/docs/concepts.md +13 -12
- package/docs/config.md +83 -10
- package/docs/gesture-customization-proposal.md +520 -0
- package/docs/handsoff-test-scenarios.md +84 -0
- package/docs/hyperspace-grid-snappiness.md +210 -0
- package/docs/layers.md +176 -28
- package/docs/mouse-gestures.md +244 -0
- package/docs/ocr.md +21 -9
- package/docs/overview.md +42 -23
- package/docs/presentation-execution-review.md +491 -0
- package/docs/prompts/hands-off-system.md +382 -0
- package/docs/prompts/hands-off-turn.md +30 -0
- package/docs/prompts/voice-advisor.md +31 -0
- package/docs/prompts/voice-fallback.md +23 -0
- package/docs/proposals/LAT-001-gesture-visual-customization.md +522 -0
- package/docs/proposals/LAT-002-shared-overlay-canvas.md +353 -0
- package/docs/proposals/LAT-003-menu-bar-controller-architecture.md +291 -0
- package/docs/proposals/LAT-004-interactive-overlay-actors.md +534 -0
- package/docs/proposals/LAT-005-action-runtime-product-spine.md +914 -0
- package/docs/proposals/LAT-006-followup-gaps.md +103 -0
- package/docs/proposals/LAT-006-runs-and-capture-in-lattices.md +566 -0
- package/docs/proposals/LAT-007-unified-app-shell.md +128 -0
- package/docs/quickstart.md +8 -12
- package/docs/reference/dewey.config.ts +74 -0
- package/docs/reference/install-agent.md +79 -0
- package/docs/release.md +172 -0
- package/docs/repo-structure.md +100 -0
- package/docs/terminal-kit.md +87 -0
- package/docs/tiling-reference.md +224 -0
- package/docs/twins.md +138 -0
- package/docs/voice-command-protocol.md +278 -0
- package/docs/voice-error-model.md +73 -0
- package/docs/voice.md +221 -0
- package/package.json +69 -16
- package/packages/npm/sdk/cua.d.mts +1 -0
- package/packages/npm/sdk/cua.d.ts +188 -0
- package/packages/npm/sdk/cua.mjs +376 -0
- package/app/Lattices.app/Contents/Info.plist +0 -24
- package/app/Package.swift +0 -13
- package/app/Sources/ActionRow.swift +0 -61
- package/app/Sources/App.swift +0 -10
- package/app/Sources/AppDelegate.swift +0 -234
- package/app/Sources/AppShellView.swift +0 -62
- package/app/Sources/AppTypeClassifier.swift +0 -70
- package/app/Sources/AppWindowShell.swift +0 -63
- package/app/Sources/CheatSheetHUD.swift +0 -332
- package/app/Sources/CommandModeState.swift +0 -1362
- package/app/Sources/CommandModeView.swift +0 -1405
- package/app/Sources/CommandModeWindow.swift +0 -192
- package/app/Sources/CommandPaletteView.swift +0 -307
- package/app/Sources/CommandPaletteWindow.swift +0 -134
- package/app/Sources/DaemonProtocol.swift +0 -101
- package/app/Sources/DaemonServer.swift +0 -414
- package/app/Sources/DesktopModel.swift +0 -121
- package/app/Sources/DesktopModelTypes.swift +0 -71
- package/app/Sources/DiagnosticLog.swift +0 -271
- package/app/Sources/EventBus.swift +0 -30
- package/app/Sources/HotkeyManager.swift +0 -250
- package/app/Sources/HotkeyStore.swift +0 -338
- package/app/Sources/InventoryManager.swift +0 -35
- package/app/Sources/InventoryPath.swift +0 -43
- package/app/Sources/KeyRecorderView.swift +0 -210
- package/app/Sources/LatticesApi.swift +0 -1125
- package/app/Sources/MainView.swift +0 -467
- package/app/Sources/MainWindow.swift +0 -83
- package/app/Sources/OcrModel.swift +0 -309
- package/app/Sources/OcrStore.swift +0 -295
- package/app/Sources/OmniSearchState.swift +0 -283
- package/app/Sources/OmniSearchView.swift +0 -288
- package/app/Sources/OmniSearchWindow.swift +0 -105
- package/app/Sources/OrphanRow.swift +0 -129
- package/app/Sources/PaletteCommand.swift +0 -419
- package/app/Sources/PermissionChecker.swift +0 -125
- package/app/Sources/Preferences.swift +0 -92
- package/app/Sources/ProcessModel.swift +0 -199
- package/app/Sources/ProcessQuery.swift +0 -151
- package/app/Sources/Project.swift +0 -28
- package/app/Sources/ProjectRow.swift +0 -368
- package/app/Sources/ProjectScanner.swift +0 -121
- package/app/Sources/ScreenMapState.swift +0 -2387
- package/app/Sources/ScreenMapView.swift +0 -2820
- package/app/Sources/ScreenMapWindowController.swift +0 -89
- package/app/Sources/SessionManager.swift +0 -72
- package/app/Sources/SettingsView.swift +0 -1053
- package/app/Sources/SettingsWindow.swift +0 -20
- package/app/Sources/TabGroupRow.swift +0 -178
- package/app/Sources/Terminal.swift +0 -259
- package/app/Sources/TerminalQuery.swift +0 -156
- package/app/Sources/TerminalSynthesizer.swift +0 -200
- package/app/Sources/Theme.swift +0 -163
- package/app/Sources/TilePickerView.swift +0 -209
- package/app/Sources/TmuxModel.swift +0 -53
- package/app/Sources/TmuxQuery.swift +0 -81
- package/app/Sources/WindowTiler.swift +0 -1755
- package/app/Sources/WorkspaceManager.swift +0 -434
- package/bin/lattices-app.js +0 -221
- package/bin/lattices.js +0 -1418
|
@@ -0,0 +1,655 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { execFileSync, execSync, spawn } from "node:child_process";
|
|
4
|
+
import { existsSync, mkdirSync, chmodSync, createWriteStream, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { join, resolve } from "node:path";
|
|
7
|
+
import { get } from "node:https";
|
|
8
|
+
import type { IncomingMessage } from "node:http";
|
|
9
|
+
import { resolveBuildEnv } from "./lattices-build-env";
|
|
10
|
+
|
|
11
|
+
const __dirname = import.meta.dir;
|
|
12
|
+
const appDir = resolve(__dirname, "../apps/mac");
|
|
13
|
+
const cliRoot = resolve(__dirname, "..");
|
|
14
|
+
const bundlePath = resolve(appDir, "Lattices.app");
|
|
15
|
+
const binaryDir = resolve(bundlePath, "Contents/MacOS");
|
|
16
|
+
const binaryPath = resolve(binaryDir, "Lattices");
|
|
17
|
+
const entitlementsPath = resolve(__dirname, "../apps/mac/Lattices.entitlements");
|
|
18
|
+
const resourcesDir = resolve(bundlePath, "Contents/Resources");
|
|
19
|
+
const iconPath = resolve(__dirname, "../assets/AppIcon.icns");
|
|
20
|
+
const tapSoundPath = resolve(__dirname, "../apps/mac/Resources/tap.wav");
|
|
21
|
+
|
|
22
|
+
const REPO = "arach/lattices";
|
|
23
|
+
const RELEASE_APP_ASSET_NAMES = ["Lattices.dmg"];
|
|
24
|
+
const RELEASE_BINARY_ASSET_NAMES = ["Lattices-macos-arm64", "LatticeApp-macos-arm64"];
|
|
25
|
+
type ReleaseAsset = { name: string; browser_download_url: string };
|
|
26
|
+
const selfScriptPath = resolve(__dirname, "lattices-app.ts");
|
|
27
|
+
const CAPS_LOCK_HID_USAGE = 0x700000039;
|
|
28
|
+
const F18_HID_USAGE = 0x70000006D;
|
|
29
|
+
|
|
30
|
+
// ── Helpers ──────────────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
function isRunning(): boolean {
|
|
33
|
+
try {
|
|
34
|
+
execSync("pgrep -x Lattices", { stdio: "pipe" });
|
|
35
|
+
return true;
|
|
36
|
+
} catch {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
type RunningProcess = { pid: string; command: string };
|
|
42
|
+
|
|
43
|
+
function runningLatticesProcesses(): RunningProcess[] {
|
|
44
|
+
let pids: string[];
|
|
45
|
+
try {
|
|
46
|
+
pids = execFileSync("pgrep", ["-x", "Lattices"], {
|
|
47
|
+
encoding: "utf8",
|
|
48
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
49
|
+
}).trim().split(/\s+/).filter(Boolean);
|
|
50
|
+
} catch {
|
|
51
|
+
return [];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const processes: RunningProcess[] = [];
|
|
55
|
+
for (const pid of pids) {
|
|
56
|
+
try {
|
|
57
|
+
const command = execFileSync("ps", ["-p", pid, "-o", "command="], {
|
|
58
|
+
encoding: "utf8",
|
|
59
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
60
|
+
}).trim();
|
|
61
|
+
if (command) processes.push({ pid, command });
|
|
62
|
+
} catch {}
|
|
63
|
+
}
|
|
64
|
+
return processes;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function runningBundleProcesses(targetBundlePath = bundlePath): RunningProcess[] {
|
|
68
|
+
const executable = resolve(targetBundlePath, "Contents/MacOS/Lattices");
|
|
69
|
+
return runningLatticesProcesses().filter(({ command }) =>
|
|
70
|
+
command === executable || command.startsWith(`${executable} `)
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function requireBundleNotRunningForBuild(): void {
|
|
75
|
+
const running = runningBundleProcesses();
|
|
76
|
+
if (!running.length) return;
|
|
77
|
+
|
|
78
|
+
console.error("Refusing to rebuild Lattices.app while that bundle is running.");
|
|
79
|
+
console.error("Rewriting or re-signing a live Mach-O can make macOS kill it later with Code Signature Invalid.");
|
|
80
|
+
console.error(`Running PID(s): ${running.map((proc) => proc.pid).join(", ")}`);
|
|
81
|
+
console.error("Use `lattices app restart` to quit, rebuild, and relaunch, or run `lattices app quit` before `lattices app build`.");
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function sleep(ms: number): void {
|
|
86
|
+
execFileSync("/bin/sleep", [(ms / 1000).toString()], { stdio: "ignore" });
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function waitForExit(timeoutMs: number): boolean {
|
|
90
|
+
const deadline = Date.now() + timeoutMs;
|
|
91
|
+
while (Date.now() < deadline) {
|
|
92
|
+
if (!isRunning()) return true;
|
|
93
|
+
sleep(100);
|
|
94
|
+
}
|
|
95
|
+
return !isRunning();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
type HIDKeyboardMapping = { src: number; dst: number };
|
|
99
|
+
|
|
100
|
+
function hasOwnedCapsLockTransportMapping(): boolean {
|
|
101
|
+
try {
|
|
102
|
+
return execFileSync(
|
|
103
|
+
"/usr/bin/defaults",
|
|
104
|
+
["read", "dev.lattices.app", "keyboardRemaps.capsLockHIDTransportOwned"],
|
|
105
|
+
{ encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }
|
|
106
|
+
).trim() === "1";
|
|
107
|
+
} catch {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function readHIDKeyboardMappings(): HIDKeyboardMapping[] {
|
|
113
|
+
try {
|
|
114
|
+
const output = execFileSync(
|
|
115
|
+
"/usr/bin/hidutil",
|
|
116
|
+
["property", "--get", "UserKeyMapping"],
|
|
117
|
+
{ encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }
|
|
118
|
+
);
|
|
119
|
+
return Array.from(output.matchAll(/HIDKeyboardModifierMappingDst\s*=\s*(\d+);\s*HIDKeyboardModifierMappingSrc\s*=\s*(\d+);/g))
|
|
120
|
+
.map((match) => ({ dst: Number(match[1]), src: Number(match[2]) }));
|
|
121
|
+
} catch {
|
|
122
|
+
return [];
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function writeHIDKeyboardMappings(mappings: HIDKeyboardMapping[]): void {
|
|
127
|
+
const pairs = mappings.map((mapping) => ({
|
|
128
|
+
HIDKeyboardModifierMappingSrc: mapping.src,
|
|
129
|
+
HIDKeyboardModifierMappingDst: mapping.dst,
|
|
130
|
+
}));
|
|
131
|
+
execFileSync(
|
|
132
|
+
"/usr/bin/hidutil",
|
|
133
|
+
["property", "--set", JSON.stringify({ UserKeyMapping: pairs })],
|
|
134
|
+
{ stdio: "ignore" }
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function clearOwnedCapsLockTransportMapping(): void {
|
|
139
|
+
if (!hasOwnedCapsLockTransportMapping()) return;
|
|
140
|
+
|
|
141
|
+
const mappings = readHIDKeyboardMappings();
|
|
142
|
+
const filtered = mappings.filter((mapping) => (
|
|
143
|
+
mapping.src !== CAPS_LOCK_HID_USAGE || mapping.dst !== F18_HID_USAGE
|
|
144
|
+
));
|
|
145
|
+
|
|
146
|
+
if (filtered.length !== mappings.length) {
|
|
147
|
+
try {
|
|
148
|
+
writeHIDKeyboardMappings(filtered);
|
|
149
|
+
} catch {
|
|
150
|
+
console.log("Warning: failed to clear Caps Lock HID transport mapping.");
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
execFileSync("/usr/bin/defaults", ["delete", "dev.lattices.app", "keyboardRemaps.capsLockHIDTransportOwned"], { stdio: "ignore" });
|
|
157
|
+
} catch {}
|
|
158
|
+
try {
|
|
159
|
+
execFileSync("/usr/bin/defaults", ["delete", "dev.lattices.app", "keyboardRemaps.capsLockHIDTransportOriginalMappings"], { stdio: "ignore" });
|
|
160
|
+
} catch {}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function quit(): boolean {
|
|
164
|
+
if (!isRunning()) {
|
|
165
|
+
clearOwnedCapsLockTransportMapping();
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
try {
|
|
170
|
+
execFileSync(
|
|
171
|
+
"/usr/bin/osascript",
|
|
172
|
+
["-e", 'tell application id "dev.lattices.app" to quit'],
|
|
173
|
+
{ stdio: "ignore" }
|
|
174
|
+
);
|
|
175
|
+
} catch {}
|
|
176
|
+
|
|
177
|
+
if (!waitForExit(2_000)) {
|
|
178
|
+
try { execSync("pkill -x Lattices", { stdio: "pipe" }); } catch {}
|
|
179
|
+
waitForExit(1_000);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (isRunning()) {
|
|
183
|
+
try { execSync("pkill -9 -x Lattices", { stdio: "pipe" }); } catch {}
|
|
184
|
+
waitForExit(500);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
clearOwnedCapsLockTransportMapping();
|
|
188
|
+
return !isRunning();
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function hasSwift(): boolean {
|
|
192
|
+
try {
|
|
193
|
+
execSync("which swift", { stdio: "pipe" });
|
|
194
|
+
return true;
|
|
195
|
+
} catch {
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function packageVersion(): string {
|
|
201
|
+
try {
|
|
202
|
+
const pkg = JSON.parse(readFileSync(resolve(__dirname, "../package.json"), "utf8"));
|
|
203
|
+
return typeof pkg.version === "string" ? pkg.version : "0.1.0";
|
|
204
|
+
} catch {
|
|
205
|
+
return "0.1.0";
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function gitRevision(): string {
|
|
210
|
+
try {
|
|
211
|
+
return execSync("git rev-parse --short HEAD", {
|
|
212
|
+
cwd: cliRoot,
|
|
213
|
+
encoding: "utf8",
|
|
214
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
215
|
+
}).trim();
|
|
216
|
+
} catch {
|
|
217
|
+
return "unknown";
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function xmlEscape(value: string): string {
|
|
222
|
+
return value
|
|
223
|
+
.replaceAll("&", "&")
|
|
224
|
+
.replaceAll("<", "<")
|
|
225
|
+
.replaceAll(">", ">")
|
|
226
|
+
.replaceAll('"', """)
|
|
227
|
+
.replaceAll("'", "'");
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function launch(extraArgs: string[] = []): void {
|
|
231
|
+
if (isRunning()) {
|
|
232
|
+
console.log("lattices app is already running.");
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
const args = [bundlePath];
|
|
236
|
+
const appArgs = ["--lattices-cli-root", cliRoot, ...extraArgs];
|
|
237
|
+
if (appArgs.length) args.push("--args", ...appArgs);
|
|
238
|
+
spawn("open", args, { detached: true, stdio: "ignore" }).unref();
|
|
239
|
+
console.log("lattices app launched.");
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function relaunchIfNeeded(shouldLaunch: boolean, extraArgs: string[] = []): void {
|
|
243
|
+
if (!shouldLaunch) {
|
|
244
|
+
console.log("App updated. Launch with: lattices app");
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
launch(extraArgs);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function resolveSigningIdentity(): string | null {
|
|
251
|
+
try {
|
|
252
|
+
const identities = execSync("security find-identity -v -p codesigning", { stdio: "pipe" }).toString();
|
|
253
|
+
return identities.match(/^\s*\d+\)\s+([A-F0-9]{40})\s+"Developer ID Application:[^"]+"/m)?.[1]
|
|
254
|
+
|| identities.match(/^\s*\d+\)\s+([A-F0-9]{40})\s+"Apple Development:[^"]+"/m)?.[1]
|
|
255
|
+
|| null;
|
|
256
|
+
} catch {
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function bundleTeamIdentifier(): string | null {
|
|
262
|
+
try {
|
|
263
|
+
const output = execSync(`codesign -dv '${bundlePath}' 2>&1`, { encoding: "utf8" });
|
|
264
|
+
const match = output.match(/TeamIdentifier=(.+)/);
|
|
265
|
+
const team = match?.[1]?.trim();
|
|
266
|
+
return team && team !== "not set" ? team : null;
|
|
267
|
+
} catch {
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function runCodesign(args: string[]): void {
|
|
273
|
+
try {
|
|
274
|
+
execFileSync("codesign", args, { stdio: "pipe" });
|
|
275
|
+
} catch (error) {
|
|
276
|
+
const stderr = (error as { stderr?: Buffer | string }).stderr;
|
|
277
|
+
const detail = stderr ? `\n${String(stderr).trim()}` : "";
|
|
278
|
+
throw new Error(`codesign ${args.join(" ")}${detail}`);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function signBundle(): void {
|
|
283
|
+
const identity = resolveSigningIdentity();
|
|
284
|
+
const entitlements = existsSync(entitlementsPath) ? entitlementsPath : null;
|
|
285
|
+
const tempBinaryPath = `${binaryPath}.cstemp`;
|
|
286
|
+
|
|
287
|
+
try {
|
|
288
|
+
if (existsSync(tempBinaryPath)) rmSync(tempBinaryPath, { force: true });
|
|
289
|
+
} catch {}
|
|
290
|
+
|
|
291
|
+
const sign = (signer: string, label: string) => {
|
|
292
|
+
// Sign the Mach-O first, then the bundle — more reliable than --deep and
|
|
293
|
+
// keeps a stable TeamIdentifier so macOS TCC grants survive rebuilds.
|
|
294
|
+
const binaryArgs = ["--force", "--options", "runtime", "--sign", signer];
|
|
295
|
+
if (entitlements) binaryArgs.push("--entitlements", entitlements);
|
|
296
|
+
binaryArgs.push(binaryPath);
|
|
297
|
+
runCodesign(binaryArgs);
|
|
298
|
+
|
|
299
|
+
const bundleArgs = ["--force", "--options", "runtime", "--sign", signer];
|
|
300
|
+
if (entitlements) bundleArgs.push("--entitlements", entitlements);
|
|
301
|
+
bundleArgs.push("--identifier", "dev.lattices.app", bundlePath);
|
|
302
|
+
runCodesign(bundleArgs);
|
|
303
|
+
|
|
304
|
+
const team = bundleTeamIdentifier();
|
|
305
|
+
if (team) {
|
|
306
|
+
console.log(`Signed (${label}) — TeamIdentifier=${team}`);
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
if (label === "ad-hoc") return;
|
|
310
|
+
throw new Error(`codesign reported no TeamIdentifier after ${label} signing`);
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
if (identity) {
|
|
314
|
+
console.log(`Signing with: ${identity}`);
|
|
315
|
+
try {
|
|
316
|
+
sign(identity, "developer");
|
|
317
|
+
return;
|
|
318
|
+
} catch (error) {
|
|
319
|
+
console.log(`Warning: signing with '${identity}' failed — ${(error as Error).message}`);
|
|
320
|
+
console.log("Warning: falling back to ad-hoc. Privacy permissions will not persist across rebuilds.");
|
|
321
|
+
}
|
|
322
|
+
} else {
|
|
323
|
+
console.log("Warning: no local signing identity found — falling back to ad-hoc.");
|
|
324
|
+
console.log("Warning: grant Accessibility/Screen Recording again after each rebuild, or install a signing cert.");
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
sign("-", "ad-hoc");
|
|
328
|
+
|
|
329
|
+
try {
|
|
330
|
+
if (existsSync(tempBinaryPath)) rmSync(tempBinaryPath, { force: true });
|
|
331
|
+
} catch {}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
type BundleBuildMetadata = {
|
|
335
|
+
channel?: "dev" | "release";
|
|
336
|
+
track?: string;
|
|
337
|
+
revision?: string;
|
|
338
|
+
timestamp?: string;
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
function buildMetadataPlist(metadata: BundleBuildMetadata): string {
|
|
342
|
+
if (metadata.channel !== "dev") return "";
|
|
343
|
+
|
|
344
|
+
const track = metadata.track ?? "latest";
|
|
345
|
+
const revision = metadata.revision ?? gitRevision();
|
|
346
|
+
const timestamp = metadata.timestamp ?? new Date().toISOString();
|
|
347
|
+
|
|
348
|
+
return ` <key>LatticesBuildChannel</key>
|
|
349
|
+
<string>dev</string>
|
|
350
|
+
<key>LatticesBuildTrack</key>
|
|
351
|
+
<string>${xmlEscape(track)}</string>
|
|
352
|
+
<key>LatticesBuildRevision</key>
|
|
353
|
+
<string>${xmlEscape(revision)}</string>
|
|
354
|
+
<key>LatticesBuildTimestamp</key>
|
|
355
|
+
<string>${xmlEscape(timestamp)}</string>
|
|
356
|
+
`;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function writeInfoPlist(metadata: BundleBuildMetadata = {}): void {
|
|
360
|
+
mkdirSync(resolve(bundlePath, "Contents"), { recursive: true });
|
|
361
|
+
const version = packageVersion();
|
|
362
|
+
const buildMetadata = buildMetadataPlist(metadata);
|
|
363
|
+
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
364
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
365
|
+
<plist version="1.0">
|
|
366
|
+
<dict>
|
|
367
|
+
<key>CFBundleIdentifier</key>
|
|
368
|
+
<string>dev.lattices.app</string>
|
|
369
|
+
<key>CFBundleName</key>
|
|
370
|
+
<string>Lattices</string>
|
|
371
|
+
<key>CFBundleDisplayName</key>
|
|
372
|
+
<string>Lattices</string>
|
|
373
|
+
<key>CFBundleExecutable</key>
|
|
374
|
+
<string>Lattices</string>
|
|
375
|
+
<key>CFBundleIconFile</key>
|
|
376
|
+
<string>AppIcon</string>
|
|
377
|
+
<key>CFBundlePackageType</key>
|
|
378
|
+
<string>APPL</string>
|
|
379
|
+
<key>CFBundleURLTypes</key>
|
|
380
|
+
<array>
|
|
381
|
+
<dict>
|
|
382
|
+
<key>CFBundleURLName</key>
|
|
383
|
+
<string>dev.lattices.app</string>
|
|
384
|
+
<key>CFBundleURLSchemes</key>
|
|
385
|
+
<array>
|
|
386
|
+
<string>lattices</string>
|
|
387
|
+
</array>
|
|
388
|
+
</dict>
|
|
389
|
+
</array>
|
|
390
|
+
<key>CFBundleVersion</key>
|
|
391
|
+
<string>${version}</string>
|
|
392
|
+
<key>CFBundleShortVersionString</key>
|
|
393
|
+
<string>${version}</string>
|
|
394
|
+
${buildMetadata} <key>LSMinimumSystemVersion</key>
|
|
395
|
+
<string>13.0</string>
|
|
396
|
+
<key>LSUIElement</key>
|
|
397
|
+
<true/>
|
|
398
|
+
<key>NSHighResolutionCapable</key>
|
|
399
|
+
<true/>
|
|
400
|
+
<key>NSMicrophoneUsageDescription</key>
|
|
401
|
+
<string>Lattices uses the microphone for Hudson Voice dictation and voice commands.</string>
|
|
402
|
+
<key>NSSupportsAutomaticTermination</key>
|
|
403
|
+
<true/>
|
|
404
|
+
</dict>
|
|
405
|
+
</plist>
|
|
406
|
+
`;
|
|
407
|
+
writeFileSync(resolve(bundlePath, "Contents/Info.plist"), plist);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function syncBundleResources(): void {
|
|
411
|
+
mkdirSync(resourcesDir, { recursive: true });
|
|
412
|
+
if (existsSync(iconPath)) {
|
|
413
|
+
execSync(`cp '${iconPath}' '${resolve(resourcesDir, "AppIcon.icns")}'`);
|
|
414
|
+
}
|
|
415
|
+
if (existsSync(tapSoundPath)) {
|
|
416
|
+
execSync(`cp '${tapSoundPath}' '${resolve(resourcesDir, "tap.wav")}'`);
|
|
417
|
+
}
|
|
418
|
+
// Bundle the assistant knowledge base so the in-app chat assistant can load it
|
|
419
|
+
// in shipped builds (dev builds fall back to the repo docs/ path).
|
|
420
|
+
const assistantDoc = resolve(cliRoot, "docs/assistant-knowledge.md");
|
|
421
|
+
if (existsSync(assistantDoc)) {
|
|
422
|
+
const docsDir = resolve(resourcesDir, "docs");
|
|
423
|
+
mkdirSync(docsDir, { recursive: true });
|
|
424
|
+
execSync(`cp '${assistantDoc}' '${resolve(docsDir, "assistant-knowledge.md")}'`);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// ── Build from source (current arch only) ────────────────────────────
|
|
429
|
+
|
|
430
|
+
function buildFromSource(): boolean {
|
|
431
|
+
console.log("Building lattices app from source...");
|
|
432
|
+
try {
|
|
433
|
+
execSync("swift build -c release", {
|
|
434
|
+
cwd: appDir,
|
|
435
|
+
stdio: "inherit",
|
|
436
|
+
// Build features are declared in apps/mac/build.json and resolved to the
|
|
437
|
+
// HUDSONKIT_WITH_* env HudsonKit gates on — one source of truth across
|
|
438
|
+
// every build entrypoint (see bin/lattices-build-env.ts).
|
|
439
|
+
env: { ...process.env, ...resolveBuildEnv() },
|
|
440
|
+
});
|
|
441
|
+
} catch {
|
|
442
|
+
return false;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const builtPath = resolve(appDir, ".build/release/Lattices");
|
|
446
|
+
if (!existsSync(builtPath)) return false;
|
|
447
|
+
|
|
448
|
+
mkdirSync(binaryDir, { recursive: true });
|
|
449
|
+
execSync(`cp '${builtPath}' '${binaryPath}'`);
|
|
450
|
+
writeInfoPlist();
|
|
451
|
+
syncBundleResources();
|
|
452
|
+
|
|
453
|
+
// Re-sign the bundle so macOS TCC recognizes a stable identity across rebuilds.
|
|
454
|
+
// Prefer a real local signing identity; only fall back to ad-hoc when necessary.
|
|
455
|
+
try {
|
|
456
|
+
signBundle();
|
|
457
|
+
} catch {
|
|
458
|
+
// Non-fatal — app still works, just permissions won't persist across rebuilds
|
|
459
|
+
console.log("Warning: code signing failed — permissions may not persist across rebuilds.");
|
|
460
|
+
}
|
|
461
|
+
// Update bundle timestamp so Finder shows the correct modified date
|
|
462
|
+
try { execSync(`touch '${bundlePath}'`, { stdio: "pipe" }); } catch {}
|
|
463
|
+
console.log("Build complete.");
|
|
464
|
+
return true;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// ── Download from GitHub releases ────────────────────────────────────
|
|
468
|
+
|
|
469
|
+
function httpsGet(url: string): Promise<IncomingMessage> {
|
|
470
|
+
return new Promise((resolve, reject) => {
|
|
471
|
+
get(url, { headers: { "User-Agent": "lattices" } }, (res) => {
|
|
472
|
+
if (res.statusCode! >= 300 && res.statusCode! < 400 && res.headers.location) {
|
|
473
|
+
return httpsGet(res.headers.location).then(resolve, reject);
|
|
474
|
+
}
|
|
475
|
+
if (res.statusCode !== 200) {
|
|
476
|
+
reject(new Error(`HTTP ${res.statusCode}`));
|
|
477
|
+
res.resume();
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
resolve(res);
|
|
481
|
+
}).on("error", reject);
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
async function downloadToFile(url: string, destination: string): Promise<void> {
|
|
486
|
+
const res = await httpsGet(url);
|
|
487
|
+
const ws = createWriteStream(destination);
|
|
488
|
+
await new Promise<void>((resolve, reject) => {
|
|
489
|
+
res.pipe(ws);
|
|
490
|
+
ws.on("finish", resolve);
|
|
491
|
+
ws.on("error", reject);
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function installBundleFromDmg(dmgPath: string): void {
|
|
496
|
+
const mountPoint = mkdtempSync(join(tmpdir(), "lattices-mount-"));
|
|
497
|
+
try {
|
|
498
|
+
execSync(`hdiutil attach -nobrowse -readonly -mountpoint '${mountPoint}' '${dmgPath}'`, { stdio: "pipe" });
|
|
499
|
+
const mountedBundle = resolve(mountPoint, "Lattices.app");
|
|
500
|
+
if (!existsSync(mountedBundle)) {
|
|
501
|
+
throw new Error("Lattices.app not found in mounted disk image");
|
|
502
|
+
}
|
|
503
|
+
rmSync(bundlePath, { recursive: true, force: true });
|
|
504
|
+
execSync(`cp -R '${mountedBundle}' '${bundlePath}'`);
|
|
505
|
+
} finally {
|
|
506
|
+
try {
|
|
507
|
+
execSync(`hdiutil detach '${mountPoint}' -quiet`, { stdio: "pipe" });
|
|
508
|
+
} catch {}
|
|
509
|
+
rmSync(mountPoint, { recursive: true, force: true });
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
async function download(): Promise<boolean> {
|
|
514
|
+
console.log("Downloading pre-built lattices app...");
|
|
515
|
+
|
|
516
|
+
try {
|
|
517
|
+
const apiUrl = `https://api.github.com/repos/${REPO}/releases/latest`;
|
|
518
|
+
const apiRes = await httpsGet(apiUrl);
|
|
519
|
+
const chunks: Buffer[] = [];
|
|
520
|
+
for await (const chunk of apiRes) chunks.push(chunk as Buffer);
|
|
521
|
+
const release = JSON.parse(Buffer.concat(chunks).toString());
|
|
522
|
+
|
|
523
|
+
const assets: ReleaseAsset[] = Array.isArray(release.assets) ? release.assets : [];
|
|
524
|
+
const appAsset = assets.find((a) =>
|
|
525
|
+
RELEASE_APP_ASSET_NAMES.includes(a.name) || (a.name.endsWith(".dmg") && a.name.startsWith("Lattices"))
|
|
526
|
+
);
|
|
527
|
+
if (appAsset) {
|
|
528
|
+
const tempDir = mkdtempSync(join(tmpdir(), "lattices-download-"));
|
|
529
|
+
const dmgPath = resolve(tempDir, appAsset.name);
|
|
530
|
+
try {
|
|
531
|
+
await downloadToFile(appAsset.browser_download_url, dmgPath);
|
|
532
|
+
installBundleFromDmg(dmgPath);
|
|
533
|
+
} finally {
|
|
534
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
535
|
+
}
|
|
536
|
+
console.log("Download complete.");
|
|
537
|
+
return true;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
const binaryAsset = assets.find((a) => RELEASE_BINARY_ASSET_NAMES.includes(a.name));
|
|
541
|
+
if (!binaryAsset) throw new Error("App bundle not found in release assets");
|
|
542
|
+
|
|
543
|
+
mkdirSync(binaryDir, { recursive: true });
|
|
544
|
+
await downloadToFile(binaryAsset.browser_download_url, binaryPath);
|
|
545
|
+
chmodSync(binaryPath, 0o755);
|
|
546
|
+
writeInfoPlist();
|
|
547
|
+
syncBundleResources();
|
|
548
|
+
console.log("Download complete.");
|
|
549
|
+
return true;
|
|
550
|
+
} catch (e) {
|
|
551
|
+
console.log(`Download failed: ${(e as Error).message}`);
|
|
552
|
+
return false;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// ── Commands ─────────────────────────────────────────────────────────
|
|
557
|
+
|
|
558
|
+
async function ensureBinary(): Promise<void> {
|
|
559
|
+
if (existsSync(binaryPath)) return;
|
|
560
|
+
|
|
561
|
+
const downloaded = await download();
|
|
562
|
+
if (downloaded) return;
|
|
563
|
+
|
|
564
|
+
console.error(
|
|
565
|
+
"Could not find a bundled lattices app or download one.\n" +
|
|
566
|
+
"Options:\n" +
|
|
567
|
+
" \u2022 Reinstall or update @arach/lattices\n" +
|
|
568
|
+
" \u2022 Developers can build from source with: lattices-app build\n" +
|
|
569
|
+
" \u2022 Download manually from: https://github.com/" + REPO + "/releases"
|
|
570
|
+
);
|
|
571
|
+
process.exit(1);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
function spawnDetachedUpdateWorker(extraArgs: string[] = [], shouldLaunch = false): void {
|
|
575
|
+
const workerArgs = [
|
|
576
|
+
selfScriptPath,
|
|
577
|
+
"update",
|
|
578
|
+
"--worker",
|
|
579
|
+
...(shouldLaunch ? ["--launch"] : []),
|
|
580
|
+
...extraArgs,
|
|
581
|
+
];
|
|
582
|
+
const child = spawn(process.execPath, workerArgs, {
|
|
583
|
+
cwd: cliRoot,
|
|
584
|
+
detached: true,
|
|
585
|
+
stdio: "ignore",
|
|
586
|
+
});
|
|
587
|
+
child.unref();
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
async function updateApp(extraArgs: string[] = [], shouldLaunch = false): Promise<void> {
|
|
591
|
+
const wasRunning = isRunning();
|
|
592
|
+
if (wasRunning) {
|
|
593
|
+
quit();
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
const downloaded = await download();
|
|
597
|
+
if (!downloaded) {
|
|
598
|
+
console.error("Update failed.");
|
|
599
|
+
if (wasRunning || shouldLaunch || extraArgs.length > 0) {
|
|
600
|
+
launch(extraArgs);
|
|
601
|
+
}
|
|
602
|
+
process.exit(1);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
relaunchIfNeeded(shouldLaunch || wasRunning || extraArgs.length > 0, extraArgs);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
const cmd = process.argv[2];
|
|
609
|
+
const flags = process.argv.slice(3);
|
|
610
|
+
const launchFlags: string[] = [];
|
|
611
|
+
if (flags.includes("--diagnostics") || flags.includes("-d")) launchFlags.push("--diagnostics");
|
|
612
|
+
if (flags.includes("--screen-map") || flags.includes("-m")) launchFlags.push("--screen-map");
|
|
613
|
+
const shouldLaunchAfterUpdate = flags.includes("--launch") || launchFlags.length > 0;
|
|
614
|
+
const shouldDetachUpdate = flags.includes("--detach");
|
|
615
|
+
const isUpdateWorker = flags.includes("--worker");
|
|
616
|
+
|
|
617
|
+
if (cmd === "build") {
|
|
618
|
+
requireBundleNotRunningForBuild();
|
|
619
|
+
if (!hasSwift()) {
|
|
620
|
+
console.error("Swift is required. Install with: xcode-select --install");
|
|
621
|
+
process.exit(1);
|
|
622
|
+
}
|
|
623
|
+
if (!buildFromSource()) {
|
|
624
|
+
console.error("Build failed.");
|
|
625
|
+
process.exit(1);
|
|
626
|
+
}
|
|
627
|
+
} else if (cmd === "quit") {
|
|
628
|
+
if (quit()) {
|
|
629
|
+
console.log("lattices app stopped.");
|
|
630
|
+
} else {
|
|
631
|
+
console.log("lattices app is not running.");
|
|
632
|
+
}
|
|
633
|
+
} else if (cmd === "restart") {
|
|
634
|
+
// Quit → rebuild → relaunch
|
|
635
|
+
quit();
|
|
636
|
+
if (!hasSwift()) {
|
|
637
|
+
console.error("Swift is required. Install with: xcode-select --install");
|
|
638
|
+
process.exit(1);
|
|
639
|
+
}
|
|
640
|
+
if (!buildFromSource()) {
|
|
641
|
+
console.error("Build failed.");
|
|
642
|
+
process.exit(1);
|
|
643
|
+
}
|
|
644
|
+
launch(launchFlags);
|
|
645
|
+
} else if (cmd === "update") {
|
|
646
|
+
if (shouldDetachUpdate && !isUpdateWorker) {
|
|
647
|
+
spawnDetachedUpdateWorker(launchFlags, shouldLaunchAfterUpdate);
|
|
648
|
+
console.log("lattices app update started.");
|
|
649
|
+
} else {
|
|
650
|
+
await updateApp(launchFlags, shouldLaunchAfterUpdate);
|
|
651
|
+
}
|
|
652
|
+
} else {
|
|
653
|
+
await ensureBinary();
|
|
654
|
+
launch(launchFlags);
|
|
655
|
+
}
|