@lattices/cli 0.3.0 → 0.4.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 (111) hide show
  1. package/README.md +85 -9
  2. package/app/Info.plist +30 -0
  3. package/app/Lattices.app/Contents/Info.plist +8 -2
  4. package/app/Lattices.app/Contents/MacOS/Lattices +0 -0
  5. package/app/Lattices.app/Contents/Resources/AppIcon.icns +0 -0
  6. package/app/Lattices.app/Contents/Resources/tap.wav +0 -0
  7. package/app/Lattices.app/Contents/_CodeSignature/CodeResources +139 -0
  8. package/app/Lattices.entitlements +15 -0
  9. package/app/Package.swift +8 -1
  10. package/app/Resources/tap.wav +0 -0
  11. package/app/Sources/AdvisorLearningStore.swift +90 -0
  12. package/app/Sources/AgentSession.swift +377 -0
  13. package/app/Sources/AppDelegate.swift +45 -12
  14. package/app/Sources/AppShellView.swift +81 -8
  15. package/app/Sources/AudioProvider.swift +386 -0
  16. package/app/Sources/CheatSheetHUD.swift +261 -19
  17. package/app/Sources/DaemonProtocol.swift +13 -0
  18. package/app/Sources/DaemonServer.swift +8 -0
  19. package/app/Sources/DesktopModel.swift +189 -6
  20. package/app/Sources/DesktopModelTypes.swift +2 -0
  21. package/app/Sources/DiagnosticLog.swift +104 -2
  22. package/app/Sources/EventBus.swift +1 -0
  23. package/app/Sources/HUDBottomBar.swift +279 -0
  24. package/app/Sources/HUDController.swift +1158 -0
  25. package/app/Sources/HUDLeftBar.swift +849 -0
  26. package/app/Sources/HUDMinimap.swift +179 -0
  27. package/app/Sources/HUDRightBar.swift +774 -0
  28. package/app/Sources/HUDState.swift +367 -0
  29. package/app/Sources/HUDTopBar.swift +243 -0
  30. package/app/Sources/HandsOffSession.swift +802 -0
  31. package/app/Sources/HomeDashboardView.swift +125 -0
  32. package/app/Sources/HotkeyManager.swift +2 -0
  33. package/app/Sources/HotkeyStore.swift +49 -9
  34. package/app/Sources/IntentEngine.swift +962 -0
  35. package/app/Sources/Intents/CreateLayerIntent.swift +54 -0
  36. package/app/Sources/Intents/DistributeIntent.swift +56 -0
  37. package/app/Sources/Intents/FocusIntent.swift +69 -0
  38. package/app/Sources/Intents/HelpIntent.swift +41 -0
  39. package/app/Sources/Intents/KillIntent.swift +47 -0
  40. package/app/Sources/Intents/LatticeIntent.swift +78 -0
  41. package/app/Sources/Intents/LaunchIntent.swift +67 -0
  42. package/app/Sources/Intents/ListSessionsIntent.swift +32 -0
  43. package/app/Sources/Intents/ListWindowsIntent.swift +30 -0
  44. package/app/Sources/Intents/ScanIntent.swift +52 -0
  45. package/app/Sources/Intents/SearchIntent.swift +190 -0
  46. package/app/Sources/Intents/SwitchLayerIntent.swift +50 -0
  47. package/app/Sources/Intents/TileIntent.swift +61 -0
  48. package/app/Sources/LatticesApi.swift +1275 -30
  49. package/app/Sources/LauncherHUD.swift +348 -0
  50. package/app/Sources/MainView.swift +147 -44
  51. package/app/Sources/MouseFinder.swift +222 -0
  52. package/app/Sources/OcrModel.swift +34 -1
  53. package/app/Sources/OmniSearchState.swift +99 -102
  54. package/app/Sources/OnboardingView.swift +457 -0
  55. package/app/Sources/PermissionChecker.swift +2 -12
  56. package/app/Sources/PiChatDock.swift +454 -0
  57. package/app/Sources/PiChatSession.swift +815 -0
  58. package/app/Sources/PiWorkspaceView.swift +364 -0
  59. package/app/Sources/PlacementSpec.swift +195 -0
  60. package/app/Sources/Preferences.swift +59 -0
  61. package/app/Sources/ProjectScanner.swift +58 -45
  62. package/app/Sources/ScreenMapState.swift +701 -55
  63. package/app/Sources/ScreenMapView.swift +843 -103
  64. package/app/Sources/ScreenMapWindowController.swift +22 -0
  65. package/app/Sources/SessionLayerStore.swift +285 -0
  66. package/app/Sources/SessionManager.swift +4 -1
  67. package/app/Sources/SettingsView.swift +186 -3
  68. package/app/Sources/Theme.swift +9 -8
  69. package/app/Sources/TmuxModel.swift +7 -0
  70. package/app/Sources/TmuxQuery.swift +27 -3
  71. package/app/Sources/VoiceChatView.swift +192 -0
  72. package/app/Sources/VoiceCommandWindow.swift +1594 -0
  73. package/app/Sources/VoiceIntentResolver.swift +671 -0
  74. package/app/Sources/VoxClient.swift +454 -0
  75. package/app/Sources/WindowTiler.swift +348 -87
  76. package/app/Sources/WorkspaceManager.swift +127 -18
  77. package/app/Tests/StageDragTests.swift +333 -0
  78. package/app/Tests/StageJoinTests.swift +313 -0
  79. package/app/Tests/StageManagerTests.swift +280 -0
  80. package/app/Tests/StageTileTests.swift +353 -0
  81. package/assets/AppIcon.icns +0 -0
  82. package/bin/client.ts +16 -0
  83. package/bin/{daemon-client.js → daemon-client.ts} +49 -30
  84. package/bin/handsoff-infer.ts +280 -0
  85. package/bin/handsoff-worker.ts +740 -0
  86. package/bin/lattices-app.ts +338 -0
  87. package/bin/lattices-dev +208 -0
  88. package/bin/{lattices.js → lattices.ts} +777 -140
  89. package/bin/project-twin.ts +645 -0
  90. package/docs/agent-execution-plan.md +562 -0
  91. package/docs/agent-layer-guide.md +207 -0
  92. package/docs/agents.md +142 -0
  93. package/docs/api.md +153 -34
  94. package/docs/app.md +29 -1
  95. package/docs/config.md +5 -1
  96. package/docs/handsoff-test-scenarios.md +84 -0
  97. package/docs/layers.md +20 -20
  98. package/docs/ocr.md +14 -5
  99. package/docs/overview.md +5 -1
  100. package/docs/presentation-execution-review.md +491 -0
  101. package/docs/prompts/hands-off-system.md +374 -0
  102. package/docs/prompts/hands-off-turn.md +30 -0
  103. package/docs/prompts/voice-advisor.md +31 -0
  104. package/docs/prompts/voice-fallback.md +23 -0
  105. package/docs/tiling-reference.md +167 -0
  106. package/docs/twins.md +138 -0
  107. package/docs/voice-command-protocol.md +278 -0
  108. package/docs/voice.md +219 -0
  109. package/package.json +29 -11
  110. package/bin/client.js +0 -4
  111. package/bin/lattices-app.js +0 -221
@@ -0,0 +1,338 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import { 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
+
10
+ const __dirname = import.meta.dir;
11
+ const appDir = resolve(__dirname, "../app");
12
+ const bundlePath = resolve(appDir, "Lattices.app");
13
+ const binaryDir = resolve(bundlePath, "Contents/MacOS");
14
+ const binaryPath = resolve(binaryDir, "Lattices");
15
+ const entitlementsPath = resolve(__dirname, "../app/Lattices.entitlements");
16
+ const resourcesDir = resolve(bundlePath, "Contents/Resources");
17
+ const iconPath = resolve(__dirname, "../assets/AppIcon.icns");
18
+ const tapSoundPath = resolve(__dirname, "../app/Resources/tap.wav");
19
+
20
+ const REPO = "arach/lattices";
21
+ const RELEASE_APP_ASSET_NAMES = ["Lattices.dmg"];
22
+ const RELEASE_BINARY_ASSET_NAMES = ["Lattices-macos-arm64", "LatticeApp-macos-arm64"];
23
+ type ReleaseAsset = { name: string; browser_download_url: string };
24
+
25
+ // ── Helpers ──────────────────────────────────────────────────────────
26
+
27
+ function isRunning(): boolean {
28
+ try {
29
+ execSync("pgrep -x Lattices", { stdio: "pipe" });
30
+ return true;
31
+ } catch {
32
+ return false;
33
+ }
34
+ }
35
+
36
+ function quit(): boolean {
37
+ try {
38
+ execSync("pkill -x Lattices", { stdio: "pipe" });
39
+ // Wait briefly for process to exit
40
+ try { execSync("sleep 0.5", { stdio: "pipe" }); } catch {}
41
+ // Force kill if still running
42
+ if (isRunning()) {
43
+ execSync("pkill -9 -x Lattices", { stdio: "pipe" });
44
+ }
45
+ return true;
46
+ } catch {
47
+ return false;
48
+ }
49
+ }
50
+
51
+ function hasSwift(): boolean {
52
+ try {
53
+ execSync("which swift", { stdio: "pipe" });
54
+ return true;
55
+ } catch {
56
+ return false;
57
+ }
58
+ }
59
+
60
+ function packageVersion(): string {
61
+ try {
62
+ const pkg = JSON.parse(readFileSync(resolve(__dirname, "../package.json"), "utf8"));
63
+ return typeof pkg.version === "string" ? pkg.version : "0.1.0";
64
+ } catch {
65
+ return "0.1.0";
66
+ }
67
+ }
68
+
69
+ function launch(extraArgs: string[] = []): void {
70
+ if (isRunning()) {
71
+ console.log("lattices app is already running.");
72
+ return;
73
+ }
74
+ const args = [bundlePath];
75
+ if (extraArgs.length) args.push("--args", ...extraArgs);
76
+ spawn("open", args, { detached: true, stdio: "ignore" }).unref();
77
+ console.log("lattices app launched.");
78
+ }
79
+
80
+ function resolveSigningIdentity(): string | null {
81
+ try {
82
+ const identities = execSync("security find-identity -v -p codesigning", { stdio: "pipe" }).toString();
83
+ return identities.match(/"(Developer ID Application:[^"]+)"/)?.[1]
84
+ || identities.match(/"(Apple Development:[^"]+)"/)?.[1]
85
+ || null;
86
+ } catch {
87
+ return null;
88
+ }
89
+ }
90
+
91
+ function signBundle(): void {
92
+ const identity = resolveSigningIdentity();
93
+ const entFlag = existsSync(entitlementsPath) ? ` --entitlements '${entitlementsPath}'` : "";
94
+
95
+ if (identity) {
96
+ console.log(`Signing with: ${identity}`);
97
+ try {
98
+ execSync(
99
+ `codesign --force --sign '${identity}'${entFlag} --identifier com.arach.lattices '${bundlePath}'`,
100
+ { stdio: "pipe" }
101
+ );
102
+ return;
103
+ } catch {
104
+ console.log(`Warning: signing with '${identity}' failed — falling back to ad-hoc.`);
105
+ }
106
+ } else {
107
+ console.log("Warning: no local signing identity found — falling back to ad-hoc.");
108
+ }
109
+
110
+ execSync(
111
+ `codesign --force --sign -${entFlag} --identifier com.arach.lattices '${bundlePath}'`,
112
+ { stdio: "pipe" }
113
+ );
114
+ }
115
+
116
+ function writeInfoPlist(): void {
117
+ mkdirSync(resolve(bundlePath, "Contents"), { recursive: true });
118
+ const version = packageVersion();
119
+ const plist = `<?xml version="1.0" encoding="UTF-8"?>
120
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
121
+ <plist version="1.0">
122
+ <dict>
123
+ <key>CFBundleIdentifier</key>
124
+ <string>com.arach.lattices</string>
125
+ <key>CFBundleName</key>
126
+ <string>Lattices</string>
127
+ <key>CFBundleDisplayName</key>
128
+ <string>Lattices</string>
129
+ <key>CFBundleExecutable</key>
130
+ <string>Lattices</string>
131
+ <key>CFBundleIconFile</key>
132
+ <string>AppIcon</string>
133
+ <key>CFBundlePackageType</key>
134
+ <string>APPL</string>
135
+ <key>CFBundleVersion</key>
136
+ <string>${version}</string>
137
+ <key>CFBundleShortVersionString</key>
138
+ <string>${version}</string>
139
+ <key>LSMinimumSystemVersion</key>
140
+ <string>13.0</string>
141
+ <key>LSUIElement</key>
142
+ <true/>
143
+ <key>NSHighResolutionCapable</key>
144
+ <true/>
145
+ <key>NSSupportsAutomaticTermination</key>
146
+ <true/>
147
+ </dict>
148
+ </plist>
149
+ `;
150
+ writeFileSync(resolve(bundlePath, "Contents/Info.plist"), plist);
151
+ }
152
+
153
+ function syncBundleResources(): void {
154
+ mkdirSync(resourcesDir, { recursive: true });
155
+ if (existsSync(iconPath)) {
156
+ execSync(`cp '${iconPath}' '${resolve(resourcesDir, "AppIcon.icns")}'`);
157
+ }
158
+ if (existsSync(tapSoundPath)) {
159
+ execSync(`cp '${tapSoundPath}' '${resolve(resourcesDir, "tap.wav")}'`);
160
+ }
161
+ }
162
+
163
+ // ── Build from source (current arch only) ────────────────────────────
164
+
165
+ function buildFromSource(): boolean {
166
+ console.log("Building lattices app from source...");
167
+ try {
168
+ execSync("swift build -c release", {
169
+ cwd: appDir,
170
+ stdio: "inherit",
171
+ });
172
+ } catch {
173
+ return false;
174
+ }
175
+
176
+ const builtPath = resolve(appDir, ".build/release/Lattices");
177
+ if (!existsSync(builtPath)) return false;
178
+
179
+ mkdirSync(binaryDir, { recursive: true });
180
+ execSync(`cp '${builtPath}' '${binaryPath}'`);
181
+ writeInfoPlist();
182
+ syncBundleResources();
183
+
184
+ // Re-sign the bundle so macOS TCC recognizes a stable identity across rebuilds.
185
+ // Prefer a real local signing identity; only fall back to ad-hoc when necessary.
186
+ try {
187
+ signBundle();
188
+ } catch {
189
+ // Non-fatal — app still works, just permissions won't persist across rebuilds
190
+ console.log("Warning: code signing failed — permissions may not persist across rebuilds.");
191
+ }
192
+ // Update bundle timestamp so Finder shows the correct modified date
193
+ try { execSync(`touch '${bundlePath}'`, { stdio: "pipe" }); } catch {}
194
+ console.log("Build complete.");
195
+ return true;
196
+ }
197
+
198
+ // ── Download from GitHub releases ────────────────────────────────────
199
+
200
+ function httpsGet(url: string): Promise<IncomingMessage> {
201
+ return new Promise((resolve, reject) => {
202
+ get(url, { headers: { "User-Agent": "lattices" } }, (res) => {
203
+ if (res.statusCode! >= 300 && res.statusCode! < 400 && res.headers.location) {
204
+ return httpsGet(res.headers.location).then(resolve, reject);
205
+ }
206
+ if (res.statusCode !== 200) {
207
+ reject(new Error(`HTTP ${res.statusCode}`));
208
+ res.resume();
209
+ return;
210
+ }
211
+ resolve(res);
212
+ }).on("error", reject);
213
+ });
214
+ }
215
+
216
+ async function downloadToFile(url: string, destination: string): Promise<void> {
217
+ const res = await httpsGet(url);
218
+ const ws = createWriteStream(destination);
219
+ await new Promise<void>((resolve, reject) => {
220
+ res.pipe(ws);
221
+ ws.on("finish", resolve);
222
+ ws.on("error", reject);
223
+ });
224
+ }
225
+
226
+ function installBundleFromDmg(dmgPath: string): void {
227
+ const mountPoint = mkdtempSync(join(tmpdir(), "lattices-mount-"));
228
+ try {
229
+ execSync(`hdiutil attach -nobrowse -readonly -mountpoint '${mountPoint}' '${dmgPath}'`, { stdio: "pipe" });
230
+ const mountedBundle = resolve(mountPoint, "Lattices.app");
231
+ if (!existsSync(mountedBundle)) {
232
+ throw new Error("Lattices.app not found in mounted disk image");
233
+ }
234
+ rmSync(bundlePath, { recursive: true, force: true });
235
+ execSync(`cp -R '${mountedBundle}' '${bundlePath}'`);
236
+ } finally {
237
+ try {
238
+ execSync(`hdiutil detach '${mountPoint}' -quiet`, { stdio: "pipe" });
239
+ } catch {}
240
+ rmSync(mountPoint, { recursive: true, force: true });
241
+ }
242
+ }
243
+
244
+ async function download(): Promise<boolean> {
245
+ console.log("Downloading pre-built lattices app...");
246
+
247
+ try {
248
+ const apiUrl = `https://api.github.com/repos/${REPO}/releases/latest`;
249
+ const apiRes = await httpsGet(apiUrl);
250
+ const chunks: Buffer[] = [];
251
+ for await (const chunk of apiRes) chunks.push(chunk as Buffer);
252
+ const release = JSON.parse(Buffer.concat(chunks).toString());
253
+
254
+ const assets: ReleaseAsset[] = Array.isArray(release.assets) ? release.assets : [];
255
+ const appAsset = assets.find((a) =>
256
+ RELEASE_APP_ASSET_NAMES.includes(a.name) || (a.name.endsWith(".dmg") && a.name.startsWith("Lattices"))
257
+ );
258
+ if (appAsset) {
259
+ const tempDir = mkdtempSync(join(tmpdir(), "lattices-download-"));
260
+ const dmgPath = resolve(tempDir, appAsset.name);
261
+ try {
262
+ await downloadToFile(appAsset.browser_download_url, dmgPath);
263
+ installBundleFromDmg(dmgPath);
264
+ } finally {
265
+ rmSync(tempDir, { recursive: true, force: true });
266
+ }
267
+ console.log("Download complete.");
268
+ return true;
269
+ }
270
+
271
+ const binaryAsset = assets.find((a) => RELEASE_BINARY_ASSET_NAMES.includes(a.name));
272
+ if (!binaryAsset) throw new Error("App bundle not found in release assets");
273
+
274
+ mkdirSync(binaryDir, { recursive: true });
275
+ await downloadToFile(binaryAsset.browser_download_url, binaryPath);
276
+ chmodSync(binaryPath, 0o755);
277
+ writeInfoPlist();
278
+ syncBundleResources();
279
+ console.log("Download complete.");
280
+ return true;
281
+ } catch (e) {
282
+ console.log(`Download failed: ${(e as Error).message}`);
283
+ return false;
284
+ }
285
+ }
286
+
287
+ // ── Commands ─────────────────────────────────────────────────────────
288
+
289
+ async function ensureBinary(): Promise<void> {
290
+ if (existsSync(binaryPath)) return;
291
+
292
+ const downloaded = await download();
293
+ if (downloaded) return;
294
+
295
+ console.error(
296
+ "Could not find a bundled lattices app or download one.\n" +
297
+ "Options:\n" +
298
+ " \u2022 Reinstall or update @lattices/cli\n" +
299
+ " \u2022 Developers can build from source with: lattices-app build\n" +
300
+ " \u2022 Download manually from: https://github.com/" + REPO + "/releases"
301
+ );
302
+ process.exit(1);
303
+ }
304
+
305
+ const cmd = process.argv[2];
306
+ const flags = process.argv.slice(3);
307
+ const launchFlags: string[] = [];
308
+ if (flags.includes("--diagnostics") || flags.includes("-d")) launchFlags.push("--diagnostics");
309
+ if (flags.includes("--screen-map") || flags.includes("-m")) launchFlags.push("--screen-map");
310
+
311
+ if (cmd === "build") {
312
+ if (!hasSwift()) {
313
+ console.error("Swift is required. Install with: xcode-select --install");
314
+ process.exit(1);
315
+ }
316
+ buildFromSource();
317
+ } else if (cmd === "quit") {
318
+ if (quit()) {
319
+ console.log("lattices app stopped.");
320
+ } else {
321
+ console.log("lattices app is not running.");
322
+ }
323
+ } else if (cmd === "restart") {
324
+ // Quit → rebuild → relaunch
325
+ quit();
326
+ if (!hasSwift()) {
327
+ console.error("Swift is required. Install with: xcode-select --install");
328
+ process.exit(1);
329
+ }
330
+ if (!buildFromSource()) {
331
+ console.error("Build failed.");
332
+ process.exit(1);
333
+ }
334
+ launch(launchFlags);
335
+ } else {
336
+ await ensureBinary();
337
+ launch(launchFlags);
338
+ }
@@ -0,0 +1,208 @@
1
+ #!/usr/bin/env bash
2
+ # lattices-dev — convenience commands for Lattices development
3
+
4
+ set -euo pipefail
5
+
6
+ SCRIPT_PATH="$(readlink -f "$0" 2>/dev/null || python3 -c "import os,sys; print(os.path.realpath(sys.argv[1]))" "$0")"
7
+ APP_DIR="$(cd "$(dirname "$SCRIPT_PATH")/../app" && pwd)"
8
+ ROOT="$(cd "$(dirname "$SCRIPT_PATH")/.." && pwd)"
9
+ LOG_FILE="$HOME/.lattices/lattices.log"
10
+ BINARY="$APP_DIR/.build/release/Lattices"
11
+ BUNDLE="$APP_DIR/Lattices.app"
12
+ BUNDLE_BIN="$BUNDLE/Contents/MacOS/Lattices"
13
+ RESOURCES_DIR="$BUNDLE/Contents/Resources"
14
+ ENTITLEMENTS="$APP_DIR/Lattices.entitlements"
15
+ ICON="$ROOT/assets/AppIcon.icns"
16
+ TAP_SOUND="$APP_DIR/Resources/tap.wav"
17
+ VERSION="$(node -p "require('$ROOT/package.json').version" 2>/dev/null || echo '0.1.0')"
18
+
19
+ red() { printf "\033[31m%s\033[0m\n" "$*"; }
20
+ green() { printf "\033[32m%s\033[0m\n" "$*"; }
21
+ dim() { printf "\033[2m%s\033[0m\n" "$*"; }
22
+
23
+ select_sign_identity() {
24
+ local identities identity=""
25
+ identities="$(security find-identity -v -p codesigning 2>/dev/null || true)"
26
+ identity="$(printf '%s\n' "$identities" | sed -n 's/.*"\(Developer ID Application:[^"]*\)".*/\1/p' | head -n 1)"
27
+ if [ -z "$identity" ]; then
28
+ identity="$(printf '%s\n' "$identities" | sed -n 's/.*"\(Apple Development:[^"]*\)".*/\1/p' | head -n 1)"
29
+ fi
30
+ printf '%s' "$identity"
31
+ }
32
+
33
+ sign_bundle() {
34
+ local identity sign_status=0
35
+ local -a ent_flags=()
36
+
37
+ if [ -f "$ENTITLEMENTS" ]; then
38
+ ent_flags=(--entitlements "$ENTITLEMENTS")
39
+ fi
40
+
41
+ identity="$(select_sign_identity)"
42
+ if [ -n "$identity" ]; then
43
+ dim "Signing with: $identity"
44
+ if ! codesign --force --sign "$identity" "${ent_flags[@]}" --identifier com.arach.lattices "$BUNDLE"; then
45
+ red "Signing with '$identity' failed. Falling back to ad-hoc."
46
+ sign_status=1
47
+ fi
48
+ else
49
+ sign_status=1
50
+ fi
51
+
52
+ if [ "$sign_status" -ne 0 ]; then
53
+ dim "No usable signing identity found. Using ad-hoc signature."
54
+ codesign --force --sign - "${ent_flags[@]}" --identifier com.arach.lattices "$BUNDLE"
55
+ fi
56
+ }
57
+
58
+ write_info_plist() {
59
+ mkdir -p "$BUNDLE/Contents"
60
+ cat > "$BUNDLE/Contents/Info.plist" <<PLIST
61
+ <?xml version="1.0" encoding="UTF-8"?>
62
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
63
+ <plist version="1.0">
64
+ <dict>
65
+ <key>CFBundleIdentifier</key>
66
+ <string>com.arach.lattices</string>
67
+ <key>CFBundleName</key>
68
+ <string>Lattices</string>
69
+ <key>CFBundleDisplayName</key>
70
+ <string>Lattices</string>
71
+ <key>CFBundleExecutable</key>
72
+ <string>Lattices</string>
73
+ <key>CFBundleIconFile</key>
74
+ <string>AppIcon</string>
75
+ <key>CFBundlePackageType</key>
76
+ <string>APPL</string>
77
+ <key>CFBundleVersion</key>
78
+ <string>$VERSION</string>
79
+ <key>CFBundleShortVersionString</key>
80
+ <string>$VERSION</string>
81
+ <key>LSMinimumSystemVersion</key>
82
+ <string>13.0</string>
83
+ <key>LSUIElement</key>
84
+ <true/>
85
+ <key>NSHighResolutionCapable</key>
86
+ <true/>
87
+ <key>NSSupportsAutomaticTermination</key>
88
+ <true/>
89
+ </dict>
90
+ </plist>
91
+ PLIST
92
+ }
93
+
94
+ cmd_build() {
95
+ echo "Building release..."
96
+ cd "$APP_DIR" && swift build -c release
97
+ # Refresh the bundle so dev builds and published bundles are complete.
98
+ rm -rf "$BUNDLE/Contents/MacOS" "$RESOURCES_DIR"
99
+ mkdir -p "$(dirname "$BUNDLE_BIN")" "$RESOURCES_DIR"
100
+ cp "$BINARY" "$BUNDLE_BIN"
101
+ if [ -f "$ICON" ]; then
102
+ cp "$ICON" "$RESOURCES_DIR/AppIcon.icns"
103
+ fi
104
+ if [ -f "$TAP_SOUND" ]; then
105
+ cp "$TAP_SOUND" "$RESOURCES_DIR/tap.wav"
106
+ fi
107
+ write_info_plist
108
+ # Re-sign so TCC permissions persist across rebuilds
109
+ sign_bundle
110
+ green "Build complete."
111
+ }
112
+
113
+ cmd_restart() {
114
+ echo "Restarting Lattices..."
115
+ pkill -x Lattices 2>/dev/null && sleep 1 || true
116
+ cmd_build
117
+ open "$BUNDLE"
118
+ green "Lattices restarted."
119
+ }
120
+
121
+ cmd_quit() {
122
+ if pkill -x Lattices 2>/dev/null; then
123
+ green "Lattices stopped."
124
+ else
125
+ dim "Lattices is not running."
126
+ fi
127
+ }
128
+
129
+ cmd_launch() {
130
+ if pgrep -x Lattices >/dev/null 2>&1; then
131
+ dim "Lattices is already running."
132
+ else
133
+ open "$BUNDLE"
134
+ green "Lattices launched."
135
+ fi
136
+ }
137
+
138
+ cmd_logs() {
139
+ if [ -f "$LOG_FILE" ]; then
140
+ tail -f "$LOG_FILE"
141
+ else
142
+ red "No log file at $LOG_FILE"
143
+ fi
144
+ }
145
+
146
+ cmd_log() {
147
+ # Show last N lines (default 30)
148
+ local n="${1:-30}"
149
+ if [ -f "$LOG_FILE" ]; then
150
+ tail -n "$n" "$LOG_FILE"
151
+ else
152
+ red "No log file at $LOG_FILE"
153
+ fi
154
+ }
155
+
156
+ cmd_clear_logs() {
157
+ if [ -f "$LOG_FILE" ]; then
158
+ > "$LOG_FILE"
159
+ green "Logs cleared."
160
+ else
161
+ dim "No log file to clear."
162
+ fi
163
+ }
164
+
165
+ cmd_status() {
166
+ if pgrep -x Lattices >/dev/null 2>&1; then
167
+ local pid=$(pgrep -x Lattices)
168
+ green "Lattices running (pid $pid)"
169
+ else
170
+ dim "Lattices is not running."
171
+ fi
172
+ if [ -f "$LOG_FILE" ]; then
173
+ local lines=$(wc -l < "$LOG_FILE" | tr -d ' ')
174
+ local size=$(du -h "$LOG_FILE" | cut -f1 | tr -d ' ')
175
+ dim "Log: $lines lines, $size"
176
+ fi
177
+ if [ -f "$HOME/.lattices/advisor-learning.jsonl" ]; then
178
+ local entries=$(wc -l < "$HOME/.lattices/advisor-learning.jsonl" | tr -d ' ')
179
+ dim "Advisor learning: $entries entries"
180
+ fi
181
+ }
182
+
183
+ cmd_help() {
184
+ echo "lattices-dev — Lattices development commands"
185
+ echo ""
186
+ echo " restart Quit + rebuild + relaunch"
187
+ echo " build Build release binary"
188
+ echo " quit Stop the running app"
189
+ echo " launch Start the app (if not running)"
190
+ echo " logs Tail the log file (live)"
191
+ echo " log [N] Show last N log lines (default 30)"
192
+ echo " clear-logs Clear the log file"
193
+ echo " status Show running state and stats"
194
+ echo " help Show this help"
195
+ }
196
+
197
+ case "${1:-help}" in
198
+ restart) cmd_restart ;;
199
+ build) cmd_build ;;
200
+ quit|stop) cmd_quit ;;
201
+ launch|start) cmd_launch ;;
202
+ logs) cmd_logs ;;
203
+ log) cmd_log "${2:-30}" ;;
204
+ clear-logs) cmd_clear_logs ;;
205
+ status) cmd_status ;;
206
+ help|--help|-h) cmd_help ;;
207
+ *) red "Unknown command: $1"; cmd_help; exit 1 ;;
208
+ esac