@arach/lattices 0.2.1 → 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.
Files changed (143) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +144 -69
  3. package/apps/mac/Info.plist +43 -0
  4. package/apps/mac/Lattices.app/Contents/Info.plist +43 -0
  5. package/apps/mac/Lattices.app/Contents/MacOS/Lattices +0 -0
  6. package/apps/mac/Lattices.app/Contents/Resources/AppIcon.icns +0 -0
  7. package/apps/mac/Lattices.app/Contents/Resources/docs/assistant-knowledge.md +130 -0
  8. package/apps/mac/Lattices.app/Contents/Resources/tap.wav +0 -0
  9. package/apps/mac/Lattices.app/Contents/_CodeSignature/CodeResources +150 -0
  10. package/apps/mac/Lattices.entitlements +21 -0
  11. package/apps/mac/Resources/Pets/assistant-spark/pet.json +62 -0
  12. package/apps/mac/Resources/Pets/assistant-spark/spritesheet.webp +0 -0
  13. package/apps/mac/Resources/Pets/scout-ranger/pet.json +6 -0
  14. package/apps/mac/Resources/Pets/scout-ranger/spritesheet.webp +0 -0
  15. package/apps/mac/Resources/tap.wav +0 -0
  16. package/assets/AppIcon.icns +0 -0
  17. package/bin/assistant-intelligence.ts +912 -0
  18. package/bin/cli/capture.ts +252 -0
  19. package/bin/cli/daemon.ts +22 -0
  20. package/bin/cli/helpers.ts +105 -0
  21. package/bin/cli/layer.ts +178 -0
  22. package/bin/cli/runs.ts +43 -0
  23. package/bin/cli/search.ts +141 -0
  24. package/bin/cli/session.ts +32 -0
  25. package/bin/client.ts +17 -0
  26. package/bin/cua.ts +26 -0
  27. package/bin/{daemon-client.js → daemon-client.ts} +49 -30
  28. package/bin/handsoff-infer.ts +96 -0
  29. package/bin/handsoff-worker.ts +531 -0
  30. package/bin/infer.ts +424 -0
  31. package/bin/keychain.ts +75 -0
  32. package/bin/lattices-app.ts +655 -0
  33. package/bin/lattices-build +125 -0
  34. package/bin/lattices-build-env.ts +77 -0
  35. package/bin/lattices-dev +362 -0
  36. package/bin/lattices.ts +3260 -0
  37. package/bin/project-twin.ts +645 -0
  38. package/docs/agent-execution-plan.md +562 -0
  39. package/docs/agent-layer-guide.md +207 -0
  40. package/docs/agents.md +233 -0
  41. package/docs/ai-chat-ux-review.md +416 -0
  42. package/docs/api.md +1041 -47
  43. package/docs/app.md +96 -13
  44. package/docs/assistant-knowledge.md +130 -0
  45. package/docs/companion-deck.md +209 -0
  46. package/docs/component-extraction-roadmap.md +392 -0
  47. package/docs/concepts.md +13 -12
  48. package/docs/config.md +83 -10
  49. package/docs/gesture-customization-proposal.md +520 -0
  50. package/docs/handsoff-test-scenarios.md +84 -0
  51. package/docs/hyperspace-grid-snappiness.md +210 -0
  52. package/docs/layers.md +176 -28
  53. package/docs/mouse-gestures.md +244 -0
  54. package/docs/ocr.md +21 -9
  55. package/docs/overview.md +42 -23
  56. package/docs/presentation-execution-review.md +491 -0
  57. package/docs/prompts/hands-off-system.md +382 -0
  58. package/docs/prompts/hands-off-turn.md +30 -0
  59. package/docs/prompts/voice-advisor.md +31 -0
  60. package/docs/prompts/voice-fallback.md +23 -0
  61. package/docs/proposals/LAT-001-gesture-visual-customization.md +522 -0
  62. package/docs/proposals/LAT-002-shared-overlay-canvas.md +353 -0
  63. package/docs/proposals/LAT-003-menu-bar-controller-architecture.md +291 -0
  64. package/docs/proposals/LAT-004-interactive-overlay-actors.md +534 -0
  65. package/docs/proposals/LAT-005-action-runtime-product-spine.md +914 -0
  66. package/docs/proposals/LAT-006-followup-gaps.md +103 -0
  67. package/docs/proposals/LAT-006-runs-and-capture-in-lattices.md +566 -0
  68. package/docs/proposals/LAT-007-unified-app-shell.md +128 -0
  69. package/docs/quickstart.md +8 -12
  70. package/docs/reference/dewey.config.ts +74 -0
  71. package/docs/reference/install-agent.md +79 -0
  72. package/docs/release.md +172 -0
  73. package/docs/repo-structure.md +100 -0
  74. package/docs/terminal-kit.md +87 -0
  75. package/docs/tiling-reference.md +224 -0
  76. package/docs/twins.md +138 -0
  77. package/docs/voice-command-protocol.md +278 -0
  78. package/docs/voice-error-model.md +73 -0
  79. package/docs/voice.md +221 -0
  80. package/package.json +69 -16
  81. package/packages/npm/sdk/cua.d.mts +1 -0
  82. package/packages/npm/sdk/cua.d.ts +188 -0
  83. package/packages/npm/sdk/cua.mjs +376 -0
  84. package/app/Lattices.app/Contents/Info.plist +0 -24
  85. package/app/Package.swift +0 -13
  86. package/app/Sources/ActionRow.swift +0 -61
  87. package/app/Sources/App.swift +0 -10
  88. package/app/Sources/AppDelegate.swift +0 -234
  89. package/app/Sources/AppShellView.swift +0 -62
  90. package/app/Sources/AppTypeClassifier.swift +0 -70
  91. package/app/Sources/AppWindowShell.swift +0 -63
  92. package/app/Sources/CheatSheetHUD.swift +0 -332
  93. package/app/Sources/CommandModeState.swift +0 -1362
  94. package/app/Sources/CommandModeView.swift +0 -1405
  95. package/app/Sources/CommandModeWindow.swift +0 -192
  96. package/app/Sources/CommandPaletteView.swift +0 -307
  97. package/app/Sources/CommandPaletteWindow.swift +0 -134
  98. package/app/Sources/DaemonProtocol.swift +0 -101
  99. package/app/Sources/DaemonServer.swift +0 -414
  100. package/app/Sources/DesktopModel.swift +0 -121
  101. package/app/Sources/DesktopModelTypes.swift +0 -71
  102. package/app/Sources/DiagnosticLog.swift +0 -271
  103. package/app/Sources/EventBus.swift +0 -30
  104. package/app/Sources/HotkeyManager.swift +0 -250
  105. package/app/Sources/HotkeyStore.swift +0 -338
  106. package/app/Sources/InventoryManager.swift +0 -35
  107. package/app/Sources/InventoryPath.swift +0 -43
  108. package/app/Sources/KeyRecorderView.swift +0 -210
  109. package/app/Sources/LatticesApi.swift +0 -1125
  110. package/app/Sources/MainView.swift +0 -467
  111. package/app/Sources/MainWindow.swift +0 -83
  112. package/app/Sources/OcrModel.swift +0 -309
  113. package/app/Sources/OcrStore.swift +0 -295
  114. package/app/Sources/OmniSearchState.swift +0 -283
  115. package/app/Sources/OmniSearchView.swift +0 -288
  116. package/app/Sources/OmniSearchWindow.swift +0 -105
  117. package/app/Sources/OrphanRow.swift +0 -129
  118. package/app/Sources/PaletteCommand.swift +0 -419
  119. package/app/Sources/PermissionChecker.swift +0 -125
  120. package/app/Sources/Preferences.swift +0 -92
  121. package/app/Sources/ProcessModel.swift +0 -199
  122. package/app/Sources/ProcessQuery.swift +0 -151
  123. package/app/Sources/Project.swift +0 -28
  124. package/app/Sources/ProjectRow.swift +0 -368
  125. package/app/Sources/ProjectScanner.swift +0 -121
  126. package/app/Sources/ScreenMapState.swift +0 -2387
  127. package/app/Sources/ScreenMapView.swift +0 -2820
  128. package/app/Sources/ScreenMapWindowController.swift +0 -89
  129. package/app/Sources/SessionManager.swift +0 -72
  130. package/app/Sources/SettingsView.swift +0 -1053
  131. package/app/Sources/SettingsWindow.swift +0 -20
  132. package/app/Sources/TabGroupRow.swift +0 -178
  133. package/app/Sources/Terminal.swift +0 -259
  134. package/app/Sources/TerminalQuery.swift +0 -156
  135. package/app/Sources/TerminalSynthesizer.swift +0 -200
  136. package/app/Sources/Theme.swift +0 -163
  137. package/app/Sources/TilePickerView.swift +0 -209
  138. package/app/Sources/TmuxModel.swift +0 -53
  139. package/app/Sources/TmuxQuery.swift +0 -81
  140. package/app/Sources/WindowTiler.swift +0 -1755
  141. package/app/Sources/WorkspaceManager.swift +0 -434
  142. package/bin/lattices-app.js +0 -221
  143. 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("&", "&amp;")
224
+ .replaceAll("<", "&lt;")
225
+ .replaceAll(">", "&gt;")
226
+ .replaceAll('"', "&quot;")
227
+ .replaceAll("'", "&apos;");
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
+ }