@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,252 @@
|
|
|
1
|
+
import {
|
|
2
|
+
hasFlag,
|
|
3
|
+
nonFlagArgs,
|
|
4
|
+
parseFlagValue,
|
|
5
|
+
parseOptionalNumber,
|
|
6
|
+
} from "./helpers.ts";
|
|
7
|
+
import { withDaemon } from "./daemon.ts";
|
|
8
|
+
|
|
9
|
+
export async function captureCommand(subcommand?: string, ...rawArgs: string[]): Promise<void> {
|
|
10
|
+
const sub = subcommand || "window";
|
|
11
|
+
const dashIndex = rawArgs.indexOf("--");
|
|
12
|
+
const commandArgs = dashIndex >= 0 ? rawArgs.slice(0, dashIndex) : rawArgs;
|
|
13
|
+
const childArgs = dashIndex >= 0 ? rawArgs.slice(dashIndex + 1) : [];
|
|
14
|
+
const jsonFlag = hasFlag(commandArgs, "json");
|
|
15
|
+
const positional = nonFlagArgs(commandArgs);
|
|
16
|
+
|
|
17
|
+
if (["stop", "stop-recording", "stopRecording"].includes(sub)) {
|
|
18
|
+
const params: Record<string, unknown> = {};
|
|
19
|
+
const runId = positional[0] || parseFlagValue(commandArgs, "run-id") || parseFlagValue(commandArgs, "runId") || parseFlagValue(commandArgs, "id");
|
|
20
|
+
const stopFile = parseFlagValue(commandArgs, "stop-file") || parseFlagValue(commandArgs, "stopFile");
|
|
21
|
+
const finishedFile = parseFlagValue(commandArgs, "finished-file") || parseFlagValue(commandArgs, "finishedFile");
|
|
22
|
+
const timeoutMs = Number(parseFlagValue(commandArgs, "timeout-ms") || parseFlagValue(commandArgs, "timeoutMs") || 30000);
|
|
23
|
+
if (runId) params.runId = runId;
|
|
24
|
+
if (stopFile) params.stopFile = stopFile;
|
|
25
|
+
if (finishedFile) params.finishedFile = finishedFile;
|
|
26
|
+
if (Number.isFinite(timeoutMs)) params.timeoutMs = timeoutMs;
|
|
27
|
+
params.wait = !hasFlag(commandArgs, "no-wait");
|
|
28
|
+
|
|
29
|
+
await withDaemon(async ({ daemonCall }) => {
|
|
30
|
+
const result = await daemonCall("capture.stopRecording", params, timeoutMs + 5000) as any;
|
|
31
|
+
if (jsonFlag) {
|
|
32
|
+
console.log(JSON.stringify(result, null, 2));
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
console.log(result.finished ? "Recording finished." : "Recording stop requested.");
|
|
36
|
+
if (result.run?.id) console.log(` run: ${result.run.id}`);
|
|
37
|
+
if (result.marker) console.log(` marker: ${result.marker}`);
|
|
38
|
+
});
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const isRecordCommand = [
|
|
43
|
+
"record-command",
|
|
44
|
+
"recordCommand",
|
|
45
|
+
"record-run",
|
|
46
|
+
"recordRun",
|
|
47
|
+
"record-exec",
|
|
48
|
+
"recordExec",
|
|
49
|
+
].includes(sub);
|
|
50
|
+
|
|
51
|
+
if (isRecordCommand) {
|
|
52
|
+
if (!childArgs.length) {
|
|
53
|
+
console.log(`lattices capture record-command — record while running a command
|
|
54
|
+
|
|
55
|
+
Usage:
|
|
56
|
+
lattices capture record-command --app Scout --filename demo.mov -- <command> [...args]
|
|
57
|
+
`);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const params: Record<string, unknown> = { source: "cli" };
|
|
62
|
+
const explicitWid = positional[0] ? Number(positional[0]) : NaN;
|
|
63
|
+
if (Number.isFinite(explicitWid)) params.wid = explicitWid;
|
|
64
|
+
|
|
65
|
+
const session = parseFlagValue(commandArgs, "session");
|
|
66
|
+
const app = parseFlagValue(commandArgs, "app");
|
|
67
|
+
const title = parseFlagValue(commandArgs, "title");
|
|
68
|
+
const filename = parseFlagValue(commandArgs, "filename");
|
|
69
|
+
const runId = parseFlagValue(commandArgs, "run-id") || parseFlagValue(commandArgs, "runId");
|
|
70
|
+
const mode = parseFlagValue(commandArgs, "mode");
|
|
71
|
+
const fps = parseOptionalNumber(commandArgs, "fps");
|
|
72
|
+
const scale = parseOptionalNumber(commandArgs, "scale");
|
|
73
|
+
const timeoutMs = Number(parseFlagValue(commandArgs, "timeout-ms") || parseFlagValue(commandArgs, "timeoutMs") || 30000);
|
|
74
|
+
if (session) params.session = session;
|
|
75
|
+
if (app) params.app = app;
|
|
76
|
+
if (title) params.title = title;
|
|
77
|
+
if (filename) params.filename = filename;
|
|
78
|
+
if (runId) params.runId = runId;
|
|
79
|
+
if (mode) params.mode = mode;
|
|
80
|
+
if (fps !== undefined) params.fps = fps;
|
|
81
|
+
if (scale !== undefined) params.scale = scale;
|
|
82
|
+
|
|
83
|
+
for (const [flag, key] of [["x", "x"], ["y", "y"], ["width", "width"], ["height", "height"], ["w", "w"], ["h", "h"]] as const) {
|
|
84
|
+
const value = parseOptionalNumber(commandArgs, flag);
|
|
85
|
+
if (value !== undefined) params[key] = value;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const recordsRegion = hasFlag(commandArgs, "region") ||
|
|
89
|
+
(params.x !== undefined && params.y !== undefined &&
|
|
90
|
+
(params.width !== undefined || params.w !== undefined) &&
|
|
91
|
+
(params.height !== undefined || params.h !== undefined));
|
|
92
|
+
const method = recordsRegion ? "capture.recordRegion" : "capture.recordWindow";
|
|
93
|
+
|
|
94
|
+
await withDaemon(async ({ daemonCall }) => {
|
|
95
|
+
const start = await daemonCall(method, params, 20000) as any;
|
|
96
|
+
let childExitCode = 0;
|
|
97
|
+
let childError: string | undefined;
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
const proc = Bun.spawn(childArgs, {
|
|
101
|
+
cwd: process.cwd(),
|
|
102
|
+
env: process.env,
|
|
103
|
+
stdin: "inherit",
|
|
104
|
+
stdout: "inherit",
|
|
105
|
+
stderr: "inherit",
|
|
106
|
+
});
|
|
107
|
+
childExitCode = await proc.exited;
|
|
108
|
+
} catch (error) {
|
|
109
|
+
childExitCode = 127;
|
|
110
|
+
childError = (error as Error).message;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const stop = await daemonCall(
|
|
114
|
+
"capture.stopRecording",
|
|
115
|
+
{ runId: start.run?.id, wait: true, timeoutMs },
|
|
116
|
+
timeoutMs + 5000
|
|
117
|
+
) as any;
|
|
118
|
+
|
|
119
|
+
if (jsonFlag) {
|
|
120
|
+
console.log(JSON.stringify({
|
|
121
|
+
ok: childExitCode === 0 && stop.ok !== false,
|
|
122
|
+
child: {
|
|
123
|
+
command: childArgs,
|
|
124
|
+
exitCode: childExitCode,
|
|
125
|
+
error: childError,
|
|
126
|
+
},
|
|
127
|
+
recording: start,
|
|
128
|
+
stopResult: stop,
|
|
129
|
+
}, null, 2));
|
|
130
|
+
} else {
|
|
131
|
+
const artifact = start.artifact || {};
|
|
132
|
+
const run = stop.run || start.run || {};
|
|
133
|
+
console.log(`Recording finished.`);
|
|
134
|
+
console.log(` run: ${run.id || start.run?.id || "?"}`);
|
|
135
|
+
console.log(` artifact: ${artifact.path || "?"}`);
|
|
136
|
+
console.log(` child exit: ${childExitCode}`);
|
|
137
|
+
if (childError) console.log(` child error: ${childError}`);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (childExitCode !== 0 && !hasFlag(commandArgs, "ignore-child-failure")) {
|
|
141
|
+
process.exitCode = childExitCode;
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const isRecord = ["record", "record-window", "recording", "video"].includes(sub);
|
|
148
|
+
const isRecordRegion = ["record-region", "recordRegion", "region-recording"].includes(sub) ||
|
|
149
|
+
(sub === "record" && ["region", "rect"].includes(positional[0] || ""));
|
|
150
|
+
|
|
151
|
+
if (isRecord || isRecordRegion) {
|
|
152
|
+
const params: Record<string, unknown> = { source: "cli" };
|
|
153
|
+
const targetKind = sub === "record" ? positional[0] : undefined;
|
|
154
|
+
const positionalOffset = targetKind === "window" || targetKind === "region" || targetKind === "rect" ? 1 : 0;
|
|
155
|
+
const explicitWid = positional[positionalOffset] ? Number(positional[positionalOffset]) : NaN;
|
|
156
|
+
if (Number.isFinite(explicitWid)) params.wid = explicitWid;
|
|
157
|
+
|
|
158
|
+
const session = parseFlagValue(commandArgs, "session");
|
|
159
|
+
const app = parseFlagValue(commandArgs, "app");
|
|
160
|
+
const title = parseFlagValue(commandArgs, "title");
|
|
161
|
+
const filename = parseFlagValue(commandArgs, "filename");
|
|
162
|
+
const runId = parseFlagValue(commandArgs, "run-id") || parseFlagValue(commandArgs, "runId");
|
|
163
|
+
const mode = parseFlagValue(commandArgs, "mode");
|
|
164
|
+
const fps = parseOptionalNumber(commandArgs, "fps");
|
|
165
|
+
const scale = parseOptionalNumber(commandArgs, "scale");
|
|
166
|
+
const durationMs = parseOptionalNumber(commandArgs, "duration-ms", "durationMs", "duration");
|
|
167
|
+
if (session) params.session = session;
|
|
168
|
+
if (app) params.app = app;
|
|
169
|
+
if (title) params.title = title;
|
|
170
|
+
if (filename) params.filename = filename;
|
|
171
|
+
if (runId) params.runId = runId;
|
|
172
|
+
if (mode) params.mode = mode;
|
|
173
|
+
if (fps !== undefined) params.fps = fps;
|
|
174
|
+
if (scale !== undefined) params.scale = scale;
|
|
175
|
+
|
|
176
|
+
for (const [flag, key] of [["x", "x"], ["y", "y"], ["width", "width"], ["height", "height"], ["w", "w"], ["h", "h"]] as const) {
|
|
177
|
+
const value = parseOptionalNumber(commandArgs, flag);
|
|
178
|
+
if (value !== undefined) params[key] = value;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
await withDaemon(async ({ daemonCall }) => {
|
|
182
|
+
const method = isRecordRegion ? "capture.recordRegion" : "capture.recordWindow";
|
|
183
|
+
const result = await daemonCall(method, params, 20000) as any;
|
|
184
|
+
|
|
185
|
+
if (durationMs !== undefined && durationMs > 0) {
|
|
186
|
+
await new Promise((resolve) => setTimeout(resolve, durationMs));
|
|
187
|
+
const stop = await daemonCall(
|
|
188
|
+
"capture.stopRecording",
|
|
189
|
+
{ runId: result.run?.id, wait: true, timeoutMs: 30000 },
|
|
190
|
+
35000
|
|
191
|
+
) as any;
|
|
192
|
+
result.stopResult = stop;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (jsonFlag) {
|
|
196
|
+
console.log(JSON.stringify(result, null, 2));
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
const artifact = result.artifact || {};
|
|
200
|
+
const run = result.stopResult?.run || result.run || {};
|
|
201
|
+
console.log(`Recording ${result.stopResult ? "finished" : "started"}.`);
|
|
202
|
+
console.log(` run: ${run.id || result.run?.id || "?"}`);
|
|
203
|
+
console.log(` artifact: ${artifact.path || "?"}`);
|
|
204
|
+
if (!result.stopResult) {
|
|
205
|
+
console.log(` stop: lattices capture stop ${result.run?.id || ""}`);
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (!["window", "screenshot", "shot"].includes(sub)) {
|
|
212
|
+
console.log(`lattices capture — capture run artifacts
|
|
213
|
+
|
|
214
|
+
Usage:
|
|
215
|
+
lattices capture window [wid] [--json]
|
|
216
|
+
lattices capture screenshot [wid] [--session name] [--app name]
|
|
217
|
+
lattices capture record window [wid] [--app name] [--duration-ms 5000] [--json]
|
|
218
|
+
lattices capture record region --x N --y N --width N --height N [--duration-ms 5000]
|
|
219
|
+
lattices capture record-command --app Scout --filename demo.mov -- <command> [...args]
|
|
220
|
+
lattices capture stop <run-id>
|
|
221
|
+
`);
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const params: Record<string, unknown> = { source: "cli" };
|
|
226
|
+
const explicitWid = positional[0] ? Number(positional[0]) : NaN;
|
|
227
|
+
if (Number.isFinite(explicitWid)) params.wid = explicitWid;
|
|
228
|
+
const session = parseFlagValue(commandArgs, "session");
|
|
229
|
+
const app = parseFlagValue(commandArgs, "app");
|
|
230
|
+
const title = parseFlagValue(commandArgs, "title");
|
|
231
|
+
const filename = parseFlagValue(commandArgs, "filename");
|
|
232
|
+
const runId = parseFlagValue(commandArgs, "run-id") || parseFlagValue(commandArgs, "runId");
|
|
233
|
+
if (session) params.session = session;
|
|
234
|
+
if (app) params.app = app;
|
|
235
|
+
if (title) params.title = title;
|
|
236
|
+
if (filename) params.filename = filename;
|
|
237
|
+
if (runId) params.runId = runId;
|
|
238
|
+
|
|
239
|
+
await withDaemon(async ({ daemonCall }) => {
|
|
240
|
+
const result = await daemonCall("capture.screenshotWindow", params, 20000) as any;
|
|
241
|
+
if (jsonFlag) {
|
|
242
|
+
console.log(JSON.stringify(result, null, 2));
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
const artifact = result.artifact || {};
|
|
246
|
+
const run = result.run || {};
|
|
247
|
+
const target = result.target || {};
|
|
248
|
+
console.log(`Captured ${target.app || "window"} ${target.wid ? `wid:${target.wid}` : ""}`);
|
|
249
|
+
console.log(` run: ${run.id || "?"}`);
|
|
250
|
+
console.log(` artifact: ${artifact.path || "?"}`);
|
|
251
|
+
});
|
|
252
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export type DaemonClient = typeof import("../daemon-client.ts");
|
|
2
|
+
|
|
3
|
+
export async function withDaemon<T>(
|
|
4
|
+
fn: (client: DaemonClient) => Promise<T>,
|
|
5
|
+
opts?: { message?: string; exitCode?: number }
|
|
6
|
+
): Promise<T> {
|
|
7
|
+
const message = opts?.message ?? "Daemon not running. Start with: lattices app";
|
|
8
|
+
const exitCode = opts?.exitCode ?? 1;
|
|
9
|
+
|
|
10
|
+
const client = await import("../daemon-client.ts");
|
|
11
|
+
if (!(await client.isDaemonRunning())) {
|
|
12
|
+
console.error(message);
|
|
13
|
+
process.exit(exitCode);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
return await fn(client);
|
|
18
|
+
} catch (e: unknown) {
|
|
19
|
+
console.error(`Error: ${(e as Error).message}`);
|
|
20
|
+
process.exit(exitCode);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
export interface ExecOpts {
|
|
4
|
+
encoding?: string;
|
|
5
|
+
stdio?: string | string[];
|
|
6
|
+
cwd?: string;
|
|
7
|
+
[key: string]: any;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function run(cmd: string, opts: ExecOpts = {}): string {
|
|
11
|
+
return execSync(cmd, { encoding: "utf8", ...opts } as any).trim();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function runQuiet(cmd: string): string | null {
|
|
15
|
+
try {
|
|
16
|
+
return run(cmd, { stdio: "pipe" });
|
|
17
|
+
} catch {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function parseFlagValue(args: string[], name: string): string | undefined {
|
|
23
|
+
const prefix = `--${name}=`;
|
|
24
|
+
const exact = `--${name}`;
|
|
25
|
+
for (let i = 0; i < args.length; i++) {
|
|
26
|
+
if (args[i].startsWith(prefix)) return args[i].slice(prefix.length);
|
|
27
|
+
if (args[i] === exact) return args[i + 1];
|
|
28
|
+
}
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function parseOptionalNumber(args: string[], ...names: string[]): number | undefined {
|
|
33
|
+
for (const name of names) {
|
|
34
|
+
const raw = parseFlagValue(args, name);
|
|
35
|
+
if (raw === undefined || raw === "") continue;
|
|
36
|
+
const value = Number(raw);
|
|
37
|
+
if (Number.isFinite(value)) return value;
|
|
38
|
+
}
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function hasFlag(args: string[], name: string): boolean {
|
|
43
|
+
return args.includes(`--${name}`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function nonFlagArgs(args: string[]): string[] {
|
|
47
|
+
const valueFlags = new Set([
|
|
48
|
+
"id", "state", "ttl", "ttlMs", "x", "y", "gap", "placement", "style", "name", "scale",
|
|
49
|
+
"hud-url", "hudUrl", "hud-html", "hudHTML", "hudHtml", "hud-title", "hudTitle",
|
|
50
|
+
"hud-width", "hudWidth", "hud-height", "hudHeight", "width", "height",
|
|
51
|
+
"manifest", "root", "max-depth", "maxDepth", "read-access", "readAccess",
|
|
52
|
+
"pause", "limit", "session", "app", "name", "bundle-id", "bundleId", "bundleIdentifier",
|
|
53
|
+
"path", "app-path", "appPath", "title", "filename", "run-id", "runId",
|
|
54
|
+
"text", "tty", "wid", "treatment", "mode", "phase", "transport", "capture",
|
|
55
|
+
"appearance", "cursor-style", "cursorStyle", "shape", "marker-shape", "markerShape",
|
|
56
|
+
"cursor-shape", "cursorShape", "angle-deg", "angleDeg", "rotation-deg", "rotationDeg",
|
|
57
|
+
"rotation", "angle", "color", "duration-ms", "durationMs",
|
|
58
|
+
"type-interval-ms", "typeIntervalMs", "typing-interval-ms", "typingIntervalMs", "label",
|
|
59
|
+
"caption", "treatment-label", "treatmentLabel", "variant",
|
|
60
|
+
"caption-title", "captionTitle", "caption-body", "captionBody",
|
|
61
|
+
"caption-detail", "captionDetail", "caption-tags", "captionTags",
|
|
62
|
+
"caption-mode", "captionMode", "caption-eyebrow", "captionEyebrow",
|
|
63
|
+
"caption-lead-ms", "captionLeadMs", "caption-sound", "captionSound",
|
|
64
|
+
"caption-placement", "captionPlacement", "caption-margin", "captionMargin",
|
|
65
|
+
"caption-x", "captionX", "caption-y", "captionY",
|
|
66
|
+
"caption-x-ratio", "captionXRatio", "caption-y-ratio", "captionYRatio",
|
|
67
|
+
"caption-left-ratio", "captionLeftRatio", "caption-top-ratio", "captionTopRatio",
|
|
68
|
+
"sound", "sfx",
|
|
69
|
+
"trail", "effect", "path-style", "pathStyle", "motion", "easing", "velocity",
|
|
70
|
+
"trajectory", "curve", "arc", "glow", "bloom", "idle", "settle", "presence",
|
|
71
|
+
"edge", "edge-effect", "edgeEffect", "arrival",
|
|
72
|
+
"fps", "w", "h", "stop-file", "stopFile", "finished-file", "finishedFile",
|
|
73
|
+
"timeout-ms", "timeoutMs", "duration",
|
|
74
|
+
"x", "y", "x-ratio", "xRatio", "y-ratio", "yRatio",
|
|
75
|
+
"relative-x", "relativeX", "relative-y", "relativeY",
|
|
76
|
+
"window-x", "windowX", "window-y", "windowY", "button",
|
|
77
|
+
]);
|
|
78
|
+
const out: string[] = [];
|
|
79
|
+
for (let i = 0; i < args.length; i++) {
|
|
80
|
+
const arg = args[i];
|
|
81
|
+
if (!arg.startsWith("--")) {
|
|
82
|
+
out.push(arg);
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
const flagName = arg.slice(2);
|
|
86
|
+
if (!arg.includes("=") && valueFlags.has(flagName)) i++;
|
|
87
|
+
}
|
|
88
|
+
return out;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function relativeTime(iso: string): string {
|
|
92
|
+
const ms = Date.now() - new Date(iso).getTime();
|
|
93
|
+
const s = Math.floor(ms / 1000);
|
|
94
|
+
if (s < 60) return "just now";
|
|
95
|
+
const m = Math.floor(s / 60);
|
|
96
|
+
if (m < 60) return `${m}m ago`;
|
|
97
|
+
const h = Math.floor(m / 60);
|
|
98
|
+
if (h < 24) return `${h}h ago`;
|
|
99
|
+
const d = Math.floor(h / 24);
|
|
100
|
+
return `${d}d ago`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function pause(ms: number): Promise<void> {
|
|
104
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
105
|
+
}
|
package/bin/cli/layer.ts
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { withDaemon, type DaemonClient } from "./daemon.ts";
|
|
2
|
+
|
|
3
|
+
export async function layerCommand(sub?: string, ...rest: string[]): Promise<void> {
|
|
4
|
+
await withDaemon(async (client) => {
|
|
5
|
+
const { daemonCall } = client;
|
|
6
|
+
|
|
7
|
+
if (sub === "create") {
|
|
8
|
+
await layerCreateCommand(client, rest);
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
if (sub === "snap") {
|
|
12
|
+
await layerSnapCommand(client, rest[0]);
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
if (sub === "session" || sub === "sessions") {
|
|
16
|
+
await layerSessionCommand(client, rest[0]);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
if (sub === "clear") {
|
|
20
|
+
await daemonCall("session.layers.clear");
|
|
21
|
+
console.log("Cleared all session layers.");
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
if (sub === "delete" || sub === "rm") {
|
|
25
|
+
if (!rest[0]) { console.log("Usage: lattices layer delete <name>"); return; }
|
|
26
|
+
await daemonCall("session.layers.delete", { name: rest[0] });
|
|
27
|
+
console.log(`Deleted session layer "${rest[0]}".`);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (sub === undefined || sub === null || sub === "") {
|
|
32
|
+
const result = await daemonCall("layers.list") as any;
|
|
33
|
+
if (!result.layers.length) {
|
|
34
|
+
console.log("No layers configured.");
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
console.log("Layers:\n");
|
|
38
|
+
for (const layer of result.layers) {
|
|
39
|
+
const active = layer.index === result.active ? " \x1b[32m● active\x1b[0m" : "";
|
|
40
|
+
console.log(` [${layer.index}] ${layer.label} (${layer.projectCount} projects)${active}`);
|
|
41
|
+
}
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const idx = parseInt(sub, 10);
|
|
45
|
+
if (!isNaN(idx)) {
|
|
46
|
+
await daemonCall("layer.activate", { index: idx, mode: "launch" });
|
|
47
|
+
console.log(`Activated layer ${idx}`);
|
|
48
|
+
} else {
|
|
49
|
+
await daemonCall("layer.activate", { name: sub, mode: "launch" });
|
|
50
|
+
console.log(`Activated layer "${sub}"`);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ── Layer create: build a session layer from window specs ────────────
|
|
56
|
+
// Usage: lattices layer create <name> [wid:123 wid:456 ...]
|
|
57
|
+
// lattices layer create <name> --json '[{"app":"Chrome","tile":"left"},...]'
|
|
58
|
+
export async function layerCreateCommand(client: DaemonClient, args: string[]): Promise<void> {
|
|
59
|
+
const { daemonCall } = client;
|
|
60
|
+
const name = args[0];
|
|
61
|
+
if (!name) {
|
|
62
|
+
console.log("Usage: lattices layer create <name> [wid:123 ...] [--json '<specs>']");
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const jsonIdx = args.indexOf("--json");
|
|
67
|
+
if (jsonIdx !== -1 && args[jsonIdx + 1]) {
|
|
68
|
+
// JSON mode: parse window specs with tile positions
|
|
69
|
+
const specs = JSON.parse(args[jsonIdx + 1]) as Array<{
|
|
70
|
+
wid?: number; app?: string; title?: string; tile?: string;
|
|
71
|
+
}>;
|
|
72
|
+
|
|
73
|
+
// Collect wids, resolve app-based specs
|
|
74
|
+
const windowIds: number[] = [];
|
|
75
|
+
const windows: Array<{ app: string; contentHint?: string }> = [];
|
|
76
|
+
const tiles: Array<{ wid?: number; app?: string; title?: string; tile: string }> = [];
|
|
77
|
+
|
|
78
|
+
for (const spec of specs) {
|
|
79
|
+
if (spec.wid) {
|
|
80
|
+
windowIds.push(spec.wid);
|
|
81
|
+
if (spec.tile) tiles.push({ wid: spec.wid, tile: spec.tile });
|
|
82
|
+
} else if (spec.app) {
|
|
83
|
+
windows.push({ app: spec.app, contentHint: spec.title });
|
|
84
|
+
if (spec.tile) tiles.push({ app: spec.app, title: spec.title, tile: spec.tile });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
await daemonCall("session.layers.create", {
|
|
89
|
+
name,
|
|
90
|
+
...(windowIds.length ? { windowIds } : {}),
|
|
91
|
+
...(windows.length ? { windows } : {}),
|
|
92
|
+
}) as any;
|
|
93
|
+
|
|
94
|
+
console.log(`Created session layer "${name}" with ${specs.length} window(s).`);
|
|
95
|
+
|
|
96
|
+
// Apply tile positions
|
|
97
|
+
for (const t of tiles) {
|
|
98
|
+
try {
|
|
99
|
+
await daemonCall("window.place", {
|
|
100
|
+
...(t.wid ? { wid: t.wid } : { app: t.app, title: t.title }),
|
|
101
|
+
placement: t.tile,
|
|
102
|
+
});
|
|
103
|
+
} catch { /* window may not be resolved yet */ }
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (tiles.length) console.log(`Tiled ${tiles.length} window(s).`);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Simple wid mode: lattices layer create <name> wid:123 wid:456
|
|
111
|
+
const wids = args.slice(1)
|
|
112
|
+
.filter(a => a.startsWith("wid:"))
|
|
113
|
+
.map(a => parseInt(a.slice(4), 10))
|
|
114
|
+
.filter(n => !isNaN(n));
|
|
115
|
+
|
|
116
|
+
await daemonCall("session.layers.create", {
|
|
117
|
+
name,
|
|
118
|
+
...(wids.length ? { windowIds: wids } : {}),
|
|
119
|
+
}) as any;
|
|
120
|
+
|
|
121
|
+
console.log(`Created session layer "${name}"${wids.length ? ` with ${wids.length} window(s)` : ""}.`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ── Layer snap: snapshot current visible windows into a session layer ─
|
|
125
|
+
export async function layerSnapCommand(client: DaemonClient, name?: string): Promise<void> {
|
|
126
|
+
const { daemonCall } = client;
|
|
127
|
+
const layerName = name || `snap-${new Date().toISOString().slice(11, 19).replace(/:/g, "")}`;
|
|
128
|
+
|
|
129
|
+
// Get all current windows
|
|
130
|
+
const windows = await daemonCall("windows.list") as any[];
|
|
131
|
+
const visibleWids = windows
|
|
132
|
+
.filter((w: any) => !w.isMinimized && w.app !== "lattices")
|
|
133
|
+
.map((w: any) => w.wid);
|
|
134
|
+
|
|
135
|
+
if (!visibleWids.length) {
|
|
136
|
+
console.log("No visible windows to snapshot.");
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
await daemonCall("session.layers.create", {
|
|
141
|
+
name: layerName,
|
|
142
|
+
windowIds: visibleWids,
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
console.log(`Snapped ${visibleWids.length} window(s) → session layer "${layerName}".`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ── Layer session: list or switch session layers ─────────────────────
|
|
149
|
+
export async function layerSessionCommand(client: DaemonClient, nameOrIndex?: string): Promise<void> {
|
|
150
|
+
const { daemonCall } = client;
|
|
151
|
+
const result = await daemonCall("session.layers.list") as any;
|
|
152
|
+
|
|
153
|
+
if (!nameOrIndex) {
|
|
154
|
+
// List session layers
|
|
155
|
+
if (!result.layers.length) {
|
|
156
|
+
console.log("No session layers. Create one with: lattices layer create <name>");
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
console.log("Session layers:\n");
|
|
160
|
+
for (let i = 0; i < result.layers.length; i++) {
|
|
161
|
+
const l = result.layers[i];
|
|
162
|
+
const active = i === result.activeIndex ? " \x1b[32m● active\x1b[0m" : "";
|
|
163
|
+
const winCount = l.windows?.length || 0;
|
|
164
|
+
console.log(` [${i}] ${l.name} (${winCount} windows)${active}`);
|
|
165
|
+
}
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Switch by index or name
|
|
170
|
+
const idx = parseInt(nameOrIndex, 10);
|
|
171
|
+
if (!isNaN(idx)) {
|
|
172
|
+
await daemonCall("session.layers.switch", { index: idx });
|
|
173
|
+
console.log(`Switched to session layer ${idx}.`);
|
|
174
|
+
} else {
|
|
175
|
+
await daemonCall("session.layers.switch", { name: nameOrIndex });
|
|
176
|
+
console.log(`Switched to session layer "${nameOrIndex}".`);
|
|
177
|
+
}
|
|
178
|
+
}
|
package/bin/cli/runs.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { hasFlag, nonFlagArgs, parseFlagValue } from "./helpers.ts";
|
|
2
|
+
import { withDaemon } from "./daemon.ts";
|
|
3
|
+
|
|
4
|
+
function runLine(run: any): string {
|
|
5
|
+
const count = Array.isArray(run.artifacts) ? run.artifacts.length : 0;
|
|
6
|
+
const completed = run.completedAt ? ` completed=${run.completedAt}` : "";
|
|
7
|
+
return ` ${run.id} ${run.state || "?"} artifacts=${count} ${run.title || "Untitled run"}${completed}`;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function runsCommand(rawArgs: string[] = []): Promise<void> {
|
|
11
|
+
const jsonFlag = hasFlag(rawArgs, "json");
|
|
12
|
+
const positional = nonFlagArgs(rawArgs);
|
|
13
|
+
const sub = positional[0];
|
|
14
|
+
|
|
15
|
+
await withDaemon(async ({ daemonCall }) => {
|
|
16
|
+
if (sub && sub !== "list") {
|
|
17
|
+
const run = await daemonCall("runs.get", { id: sub }) as any;
|
|
18
|
+
if (jsonFlag) {
|
|
19
|
+
console.log(JSON.stringify(run, null, 2));
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
console.log(runLine(run));
|
|
23
|
+
console.log(` artifacts: ${run.artifactDirectoryPath}`);
|
|
24
|
+
for (const artifact of run.artifacts || []) {
|
|
25
|
+
console.log(` ${artifact.kind} ${artifact.path}`);
|
|
26
|
+
}
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const limit = Number(parseFlagValue(rawArgs, "limit") || 20);
|
|
31
|
+
const runs = await daemonCall("runs.list", { limit }) as any[];
|
|
32
|
+
if (jsonFlag) {
|
|
33
|
+
console.log(JSON.stringify(runs, null, 2));
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (!runs.length) {
|
|
37
|
+
console.log("No runs yet.");
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
console.log(`Runs (${runs.length}):\n`);
|
|
41
|
+
for (const run of runs) console.log(runLine(run));
|
|
42
|
+
});
|
|
43
|
+
}
|