automify 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/LICENSE +21 -0
  3. package/README.md +401 -0
  4. package/SECURITY.md +17 -0
  5. package/examples/anthropic-provider.js +18 -0
  6. package/examples/browser-basic.js +30 -0
  7. package/examples/browser-with-safety.js +38 -0
  8. package/examples/claude-model-adapter.js +141 -0
  9. package/examples/cli-basic.js +20 -0
  10. package/examples/cli-docker.js +42 -0
  11. package/examples/custom-computer.js +18 -0
  12. package/examples/custom-model-adapter.js +48 -0
  13. package/examples/desktop-docker.js +37 -0
  14. package/examples/desktop-local.js +28 -0
  15. package/examples/evaluate-image.js +26 -0
  16. package/examples/files-and-shared-folder.js +42 -0
  17. package/package.json +74 -0
  18. package/scripts/generate-argument-reference.js +17 -0
  19. package/scripts/install-browser.js +12 -0
  20. package/scripts/install-desktop.js +281 -0
  21. package/src/index.d.ts +1049 -0
  22. package/src/index.js +83 -0
  23. package/src/lib/adapter-locks.js +93 -0
  24. package/src/lib/adapter-toolkit.js +239 -0
  25. package/src/lib/anthropic-model-adapter.js +451 -0
  26. package/src/lib/argument-reference.js +98 -0
  27. package/src/lib/automify.js +938 -0
  28. package/src/lib/browser-automify.js +89 -0
  29. package/src/lib/cli-automify.js +520 -0
  30. package/src/lib/computer-automify.js +103 -0
  31. package/src/lib/docker-cli-automify.js +517 -0
  32. package/src/lib/docker-desktop-computer.js +725 -0
  33. package/src/lib/errors.js +24 -0
  34. package/src/lib/file-data.js +140 -0
  35. package/src/lib/init.js +217 -0
  36. package/src/lib/local-desktop-computer.js +963 -0
  37. package/src/lib/model-adapter.js +32 -0
  38. package/src/lib/openai-responses-client.js +162 -0
  39. package/src/lib/output.js +57 -0
  40. package/src/lib/playwright-computer.js +363 -0
  41. package/src/lib/presets.js +141 -0
  42. package/src/lib/result.js +95 -0
  43. package/src/lib/runtime.js +471 -0
  44. package/src/lib/virtual-shared-folder.js +109 -0
  45. package/src/lib/zod-output.js +26 -0
  46. package/src/zod.d.ts +12 -0
  47. package/src/zod.js +5 -0
@@ -0,0 +1,725 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { execFile } from "node:child_process";
3
+ import { promisify } from "node:util";
4
+
5
+ import { AutomifyError } from "./errors.js";
6
+ import { acquireAdapterLock } from "./adapter-locks.js";
7
+ import { applyDockerDesktopPreset } from "./presets.js";
8
+ import { assertKnownOptions, debugLog, normalizeLogFile, writeDebugLogFile } from "./runtime.js";
9
+ import { prepareVirtualSharedFolder } from "./virtual-shared-folder.js";
10
+
11
+ const execFileAsync = promisify(execFile);
12
+
13
+ const DEFAULT_IMAGE = "debian:bookworm-slim";
14
+ const DEFAULT_ENVIRONMENT = "linux";
15
+ const DEFAULT_DISPLAY = ":99";
16
+ const DEFAULT_WIDTH = 1440;
17
+ const DEFAULT_HEIGHT = 900;
18
+ const DEFAULT_DEPTH = 24;
19
+ const DEFAULT_DESKTOP_PACKAGES = [
20
+ "xvfb",
21
+ "openbox",
22
+ "xterm",
23
+ "x11-utils",
24
+ "xdotool",
25
+ "imagemagick",
26
+ "scrot",
27
+ "ca-certificates"
28
+ ];
29
+ const DEFAULT_INSTRUCTIONS = [
30
+ "You are controlling an isolated Linux desktop running in a virtual display.",
31
+ "Orient from the screenshot first: identify the active app, visible window, focused field, current page, and the specific target required by the task before acting.",
32
+ "Use deterministic entry points. For a website or web app, use the browser address bar only when a browser is clearly focused; otherwise use a visible app icon, terminal command, launcher/search mechanism, or provided startup app. For app content, use visible search, filters, or navigation controls.",
33
+ "Do not open or use Alt+Tab or other cyclic app/window switchers unless the task explicitly asks to switch to the previous app. Cyclic switching is unreliable because the window order is unknown.",
34
+ "Do not click as a probe. Click only when the screenshot shows a specific visible target and the purpose of that click is clear from the task or current UI. Prefer named controls, fields, menu items, visible app icons, terminal prompts, and address/search fields over unlabeled areas.",
35
+ "Treat the environment as ephemeral: do not depend on host desktop state, personal accounts, or files outside the shared workspace unless the task provides them.",
36
+ "If the target is not visible, choose a deterministic recovery path: direct URL, terminal command, launcher/search, in-app search, visible navigation, or a screenshot/wait when loading is visible. Do not repeat nearly identical clicks after no visible change.",
37
+ "After any action that launches an app, navigates, submits input, changes windows, or might trigger loading, use the next screenshot to decide the next step. Stop when the requested result is known; do not keep interacting to confirm unnecessarily."
38
+ ].join("\n");
39
+ const VIRTUAL_DESKTOP_OPTION_KEYS = new Set([
40
+ "preset",
41
+ "container",
42
+ "dockerCommand",
43
+ "image",
44
+ "containerName",
45
+ "existingContainer",
46
+ "keepContainer",
47
+ "start",
48
+ "display",
49
+ "viewport",
50
+ "displayWidth",
51
+ "displayHeight",
52
+ "displayDepth",
53
+ "environment",
54
+ "instructions",
55
+ "desktop",
56
+ "startupCommand",
57
+ "windowManagerCommand",
58
+ "autoRemove",
59
+ "sandbox",
60
+ "installDependencies",
61
+ "desktopPackages",
62
+ "additionalAptPackages",
63
+ "network",
64
+ "cpus",
65
+ "memory",
66
+ "memorySwap",
67
+ "cpuShares",
68
+ "cpusetCpus",
69
+ "packageManager",
70
+ "readOnly",
71
+ "pidsLimit",
72
+ "shmSize",
73
+ "tmpfsTmp",
74
+ "tmpfsRun",
75
+ "volumes",
76
+ "env",
77
+ "shared",
78
+ "sharedFolder",
79
+ "sharedFiles",
80
+ "files",
81
+ "waitMs",
82
+ "startupTimeoutMs",
83
+ "dockerTimeoutMs",
84
+ "screenshotMaxBuffer",
85
+ "logsMaxBuffer",
86
+ "screenshotSettleMs",
87
+ "execFile",
88
+ "silent",
89
+ "debug",
90
+ "logFile",
91
+ "onUnknownAction"
92
+ ]);
93
+ export const DOCKER_DESKTOP_COMPUTER_OPTION_KEYS = VIRTUAL_DESKTOP_OPTION_KEYS;
94
+ const VIRTUAL_DESKTOP_CONTAINER_KEYS = new Set([
95
+ "docker",
96
+ "dockerCommand",
97
+ "image",
98
+ "name",
99
+ "existing",
100
+ "keep",
101
+ "autoRemove",
102
+ "sandbox",
103
+ "readOnly",
104
+ "network",
105
+ "cpus",
106
+ "memory",
107
+ "memorySwap",
108
+ "cpuShares",
109
+ "cpusetCpus",
110
+ "pidsLimit",
111
+ "shmSize",
112
+ "tmpfsTmp",
113
+ "tmpfsRun",
114
+ "volumes",
115
+ "env",
116
+ "timeoutMs",
117
+ "cwd",
118
+ "workdir",
119
+ "packages",
120
+ "additionalAptPackages",
121
+ "installDependencies"
122
+ ]);
123
+ const VIRTUAL_DESKTOP_DESKTOP_KEYS = new Set([
124
+ "startupCommand",
125
+ "windowManagerCommand",
126
+ "packages",
127
+ "additionalAptPackages",
128
+ "installDependencies",
129
+ "packageManager"
130
+ ]);
131
+
132
+ export async function createDockerDesktopComputer(options = {}) {
133
+ options = normalizeVirtualDesktopOptions(options);
134
+ const releaseLock = await acquireDockerDesktopLock(options);
135
+ debugVirtualDesktop(options, "create", {
136
+ image: options.image ?? DEFAULT_IMAGE,
137
+ containerName: options.containerName ?? null,
138
+ start: options.start !== false
139
+ });
140
+ try {
141
+ const session = new DockerDesktopSession(options);
142
+ await session.prepareSharedFolder();
143
+ if (options.start !== false) {
144
+ await session.start();
145
+ }
146
+
147
+ const computer = {
148
+ displayWidth: session.width,
149
+ displayHeight: session.height,
150
+ environment: options.environment ?? DEFAULT_ENVIRONMENT,
151
+ instructions: options.instructions ?? DEFAULT_INSTRUCTIONS,
152
+
153
+ async execute(action) {
154
+ await session.execute(action);
155
+ },
156
+
157
+ async screenshot(context) {
158
+ return session.screenshot(context);
159
+ },
160
+
161
+ async close() {
162
+ try {
163
+ await session.close();
164
+ } finally {
165
+ await releaseLock?.();
166
+ }
167
+ },
168
+
169
+ session
170
+ };
171
+ computer.sharedFolder = session.sharedFolder?.data;
172
+ return computer;
173
+ } catch (error) {
174
+ await releaseLock?.();
175
+ throw error;
176
+ }
177
+ }
178
+
179
+ export class DockerDesktopSession {
180
+ constructor(options = {}) {
181
+ options = normalizeVirtualDesktopOptions(options);
182
+ this.options = options;
183
+ this.docker = options.dockerCommand ?? "docker";
184
+ this.execFile = options.execFile ?? execFileAsync;
185
+ this.image = options.image ?? DEFAULT_IMAGE;
186
+ this.name = options.containerName ?? `automify-desktop-${randomUUID()}`;
187
+ this.display = normalizeDisplay(options.display ?? DEFAULT_DISPLAY);
188
+ this.width = positiveInteger(options.displayWidth) ?? DEFAULT_WIDTH;
189
+ this.height = positiveInteger(options.displayHeight) ?? DEFAULT_HEIGHT;
190
+ this.depth = positiveInteger(options.displayDepth) ?? DEFAULT_DEPTH;
191
+ this.installDependencies = options.installDependencies ?? isBaseLinuxImage(this.image);
192
+ this.sharedFolder = null;
193
+ this.started = false;
194
+ this.created = false;
195
+ }
196
+
197
+ async prepareSharedFolder() {
198
+ if (this.sharedFolder) return this.sharedFolder;
199
+ this.sharedFolder = await prepareVirtualSharedFolder(this.options, {
200
+ prefix: "automify-docker-desktop-"
201
+ });
202
+ return this.sharedFolder;
203
+ }
204
+
205
+ async start() {
206
+ if (this.started) return;
207
+ await this.prepareSharedFolder();
208
+ if (this.options.existingContainer) {
209
+ debugVirtualDesktop(this.options, "use_existing_container", { containerName: this.name });
210
+ this.started = true;
211
+ return;
212
+ }
213
+
214
+ debugVirtualDesktop(this.options, "container_start", {
215
+ containerName: this.name,
216
+ image: this.image,
217
+ display: this.display,
218
+ width: this.width,
219
+ height: this.height,
220
+ installDependencies: this.installDependencies
221
+ });
222
+ const args = [
223
+ "run",
224
+ "-d",
225
+ "--name",
226
+ this.name,
227
+ "--network",
228
+ dockerNetwork(this.options.network),
229
+ "--pids-limit",
230
+ String(positiveInteger(this.options.pidsLimit) ?? 512),
231
+ "--shm-size",
232
+ String(this.options.shmSize ?? "1g"),
233
+ "--tmpfs",
234
+ String(this.options.tmpfsTmp ?? "/tmp:exec,nosuid,nodev,size=512m"),
235
+ "--tmpfs",
236
+ String(this.options.tmpfsRun ?? "/run:nosuid,nodev,size=64m"),
237
+ "-e",
238
+ `DISPLAY=${this.display}`
239
+ ];
240
+ appendDockerResourceArgs(args, this.options);
241
+
242
+ if (this.options.autoRemove === true) {
243
+ args.splice(2, 0, "--rm");
244
+ }
245
+ if (this.options.sandbox !== false && !this.installDependencies) {
246
+ args.push("--cap-drop", "ALL", "--security-opt", "no-new-privileges");
247
+ }
248
+ if (this.options.readOnly !== false && !this.installDependencies) {
249
+ args.push("--read-only");
250
+ }
251
+ for (const volume of this.options.volumes ?? []) {
252
+ args.push("-v", String(volume));
253
+ }
254
+ if (this.sharedFolder) {
255
+ args.push("-v", this.sharedFolder.volume);
256
+ }
257
+ for (const env of this.options.env ?? []) {
258
+ args.push("-e", String(env));
259
+ }
260
+
261
+ args.push(this.image, "sh", "-lc", this.startupScript());
262
+ await this.runDocker(args, "start Docker desktop container");
263
+ debugVirtualDesktop(this.options, "container_created", { containerName: this.name });
264
+ this.created = true;
265
+ this.started = true;
266
+ try {
267
+ await this.waitForReady();
268
+ debugVirtualDesktop(this.options, "desktop_ready", { containerName: this.name, display: this.display });
269
+ } catch (error) {
270
+ const diagnostics = await this.startupDiagnostics();
271
+ await this.close();
272
+ throw new AutomifyError(
273
+ [
274
+ "Virtual Linux desktop did not become ready before startupTimeoutMs.",
275
+ diagnostics ? `Docker diagnostics:\n${diagnostics}` : null
276
+ ]
277
+ .filter(Boolean)
278
+ .join("\n"),
279
+ { cause: error }
280
+ );
281
+ }
282
+ }
283
+
284
+ startupScript() {
285
+ const windowManager = this.options.windowManagerCommand ?? "openbox";
286
+ const app = this.options.startupCommand;
287
+ const xvfb = [
288
+ "Xvfb",
289
+ shellQuote(this.display),
290
+ "-screen",
291
+ "0",
292
+ shellQuote(`${this.width}x${this.height}x${this.depth}`),
293
+ "-nolisten",
294
+ "tcp"
295
+ ].join(" ");
296
+ const parts = [
297
+ this.dependencyInstallScript(),
298
+ `${xvfb} &`,
299
+ "XVFB_PID=$!",
300
+ "sleep 0.2",
301
+ `${windowManager} >/tmp/automify-window-manager.log 2>&1 &`
302
+ ];
303
+ parts.push(`${app} >/tmp/automify-startup.log 2>&1 &`);
304
+ parts.push("wait $XVFB_PID");
305
+ return parts.join("\n");
306
+ }
307
+
308
+ dependencyInstallScript() {
309
+ if (!this.installDependencies) return ":";
310
+
311
+ const packages = uniquePackages([
312
+ ...(this.options.desktopPackages ?? DEFAULT_DESKTOP_PACKAGES),
313
+ ...(this.options.additionalAptPackages ?? [])
314
+ ])
315
+ .map((pkg) => shellQuote(pkg))
316
+ .join(" ");
317
+ const packageManager = this.options.packageManager ?? "apt";
318
+
319
+ if (packageManager !== "apt") {
320
+ return String(packageManager);
321
+ }
322
+
323
+ return [
324
+ "export DEBIAN_FRONTEND=noninteractive",
325
+ "apt-get update",
326
+ packages ? `apt-get install -y --no-install-recommends ${packages}` : ":",
327
+ "rm -rf /var/lib/apt/lists/*"
328
+ ].join("\n");
329
+ }
330
+
331
+ async waitForReady() {
332
+ const timeoutMs = positiveInteger(this.options.startupTimeoutMs) ?? (this.installDependencies ? 120_000 : 10_000);
333
+ const startedAt = Date.now();
334
+ let lastError;
335
+
336
+ while (Date.now() - startedAt < timeoutMs) {
337
+ try {
338
+ await this.exec(["sh", "-lc", "xdpyinfo >/dev/null 2>&1"]);
339
+ return;
340
+ } catch (error) {
341
+ lastError = error;
342
+ await sleep(150);
343
+ }
344
+ }
345
+
346
+ throw lastError ?? new AutomifyError("Virtual Linux desktop readiness check timed out.");
347
+ }
348
+
349
+ async execute(action) {
350
+ await this.start();
351
+ if (!action || typeof action !== "object") {
352
+ throw new AutomifyError("Docker desktop action must be an object.");
353
+ }
354
+ debugVirtualDesktop(this.options, "action", { action });
355
+
356
+ switch (action.type) {
357
+ case "click":
358
+ await this.exec(["xdotool", "mousemove", x(action.x), y(action.y), "click", button(action.button)]);
359
+ break;
360
+ case "double_click":
361
+ await this.exec([
362
+ "xdotool",
363
+ "mousemove",
364
+ x(action.x),
365
+ y(action.y),
366
+ "click",
367
+ "--repeat",
368
+ "2",
369
+ button(action.button)
370
+ ]);
371
+ break;
372
+ case "scroll":
373
+ await this.scroll(action);
374
+ break;
375
+ case "keypress":
376
+ await this.exec(["xdotool", "key", ...keys(action.keys ?? [action.key])]);
377
+ break;
378
+ case "type":
379
+ await this.exec(["xdotool", "type", "--clearmodifiers", "--", String(action.text ?? "")]);
380
+ break;
381
+ case "wait":
382
+ await sleep(Math.max(0, Number(action.ms ?? action.duration_ms ?? this.options.waitMs ?? 1000) || 0));
383
+ break;
384
+ case "screenshot":
385
+ break;
386
+ case "move":
387
+ await this.exec(["xdotool", "mousemove", x(action.x), y(action.y)]);
388
+ break;
389
+ case "drag":
390
+ await this.drag(action);
391
+ break;
392
+ default:
393
+ if (typeof this.options.onUnknownAction === "function") {
394
+ await this.options.onUnknownAction(action);
395
+ break;
396
+ }
397
+ throw new AutomifyError(`Unsupported Docker desktop action: ${action.type}`);
398
+ }
399
+ }
400
+
401
+ async scroll(action) {
402
+ const scrollY = Number(action.scroll_y ?? action.delta_y ?? action.deltaY ?? 0);
403
+ const scrollX = Number(action.scroll_x ?? action.delta_x ?? action.deltaX ?? 0);
404
+ const amount = Math.max(1, Math.ceil(Math.max(Math.abs(scrollY), Math.abs(scrollX)) / 120));
405
+ const scrollButton = Math.abs(scrollX) > Math.abs(scrollY) ? (scrollX > 0 ? "7" : "6") : scrollY > 0 ? "5" : "4";
406
+ await this.exec([
407
+ "xdotool",
408
+ "mousemove",
409
+ x(action.x),
410
+ y(action.y),
411
+ "click",
412
+ "--repeat",
413
+ String(amount),
414
+ scrollButton
415
+ ]);
416
+ }
417
+
418
+ async drag(action) {
419
+ const path = action.path?.length ? action.path : [action, action];
420
+ const start = path[0];
421
+ const end = path.at(-1);
422
+ await this.exec(["xdotool", "mousemove", x(start.x), y(start.y), "mousedown", "1"]);
423
+ for (const point of path.slice(1)) {
424
+ await this.exec(["xdotool", "mousemove", x(point.x), y(point.y)]);
425
+ }
426
+ await this.exec(["xdotool", "mousemove", x(end.x), y(end.y), "mouseup", "1"]);
427
+ }
428
+
429
+ async screenshot(context) {
430
+ await this.start();
431
+ const startedAt = Date.now();
432
+ if (context?.initial || context?.final) {
433
+ await sleep(this.options.screenshotSettleMs ?? 300);
434
+ }
435
+ const { stdout } = await this.execFile(
436
+ this.docker,
437
+ [
438
+ "exec",
439
+ "-e",
440
+ `DISPLAY=${this.display}`,
441
+ this.name,
442
+ "sh",
443
+ "-lc",
444
+ "scrot -o - 2>/dev/null || import -window root -screen png:-"
445
+ ],
446
+ {
447
+ encoding: "buffer",
448
+ maxBuffer: this.options.screenshotMaxBuffer ?? 20 * 1024 * 1024
449
+ }
450
+ );
451
+ debugVirtualDesktop(this.options, "screenshot", {
452
+ phase: context?.final ? "final" : context?.initial ? "initial" : "step",
453
+ bytes: stdout?.byteLength,
454
+ durationMs: Date.now() - startedAt
455
+ });
456
+ return stdout;
457
+ }
458
+
459
+ async exec(args, options = {}) {
460
+ return this.execFile(this.docker, ["exec", "-e", `DISPLAY=${this.display}`, this.name, ...args], options);
461
+ }
462
+
463
+ async runDocker(args, label) {
464
+ debugVirtualDesktop(this.options, "docker", { label, args: summarizeDockerArgs(args) });
465
+ try {
466
+ return await this.execFile(this.docker, args, {
467
+ timeout: this.options.dockerTimeoutMs ?? 30_000
468
+ });
469
+ } catch (error) {
470
+ throw new AutomifyError(`Unable to ${label}. Ensure Docker is running and image ${this.image} exists.`, {
471
+ cause: error
472
+ });
473
+ }
474
+ }
475
+
476
+ async startupDiagnostics() {
477
+ const sections = [];
478
+
479
+ try {
480
+ const { stdout } = await this.execFile(this.docker, [
481
+ "inspect",
482
+ "--format",
483
+ "{{.State.Status}} exit={{.State.ExitCode}} oom={{.State.OOMKilled}}",
484
+ this.name
485
+ ]);
486
+ sections.push(`state: ${String(stdout).trim()}`);
487
+ } catch (error) {
488
+ sections.push(`state: unavailable (${shortError(error)})`);
489
+ }
490
+
491
+ try {
492
+ const { stdout, stderr } = await this.execFile(this.docker, ["logs", "--tail", "80", this.name], {
493
+ maxBuffer: this.options.logsMaxBuffer ?? 256 * 1024
494
+ });
495
+ const logs = `${stdout ?? ""}${stderr ?? ""}`.trim();
496
+ if (logs) {
497
+ sections.push(`logs:\n${logs}`);
498
+ }
499
+ } catch (error) {
500
+ sections.push(`logs: unavailable (${shortError(error)})`);
501
+ }
502
+
503
+ return sections.join("\n");
504
+ }
505
+
506
+ async close() {
507
+ if (this.created && !this.options.existingContainer && !this.options.keepContainer) {
508
+ debugVirtualDesktop(this.options, "container_close", { containerName: this.name });
509
+ await this.execFile(this.docker, ["rm", "-f", this.name]).catch(() => {});
510
+ }
511
+ await this.sharedFolder?.close();
512
+ this.started = false;
513
+ this.created = false;
514
+ }
515
+ }
516
+
517
+ export const createVirtualDesktopComputer = createDockerDesktopComputer;
518
+ export const DockerVirtualDesktopSession = DockerDesktopSession;
519
+
520
+ async function acquireDockerDesktopLock(options) {
521
+ if (!options.containerName) return null;
522
+ return acquireAdapterLock(`docker-desktop:${options.containerName}`, {
523
+ label: `Docker desktop container ${JSON.stringify(options.containerName)}`
524
+ });
525
+ }
526
+
527
+ function normalizeVirtualDesktopOptions(options = {}) {
528
+ assertKnownOptions("Docker desktop adapter", options, VIRTUAL_DESKTOP_OPTION_KEYS);
529
+ assertKnownOptions("Docker desktop container", options.container, VIRTUAL_DESKTOP_CONTAINER_KEYS);
530
+ assertKnownOptions("Docker desktop desktop", options.desktop, VIRTUAL_DESKTOP_DESKTOP_KEYS);
531
+ options = applyDockerDesktopPreset(options);
532
+ const container = options.container ?? {};
533
+ const viewport = options.viewport ?? {};
534
+ const desktop = options.desktop ?? {};
535
+ const normalized = {
536
+ ...options,
537
+ debug: options.debug ?? false,
538
+ logFile: normalizeLogFile(options.logFile, "Docker desktop adapter logFile"),
539
+ dockerCommand: options.dockerCommand ?? container.dockerCommand ?? container.docker,
540
+ image: options.image ?? container.image,
541
+ containerName: options.containerName ?? container.name,
542
+ existingContainer: options.existingContainer ?? container.existing,
543
+ keepContainer: options.keepContainer ?? container.keep,
544
+ autoRemove: options.autoRemove ?? container.autoRemove,
545
+ sandbox: options.sandbox ?? container.sandbox,
546
+ readOnly: options.readOnly ?? container.readOnly,
547
+ network: options.network ?? container.network,
548
+ cpus: options.cpus ?? container.cpus,
549
+ memory: options.memory ?? container.memory,
550
+ memorySwap: options.memorySwap ?? container.memorySwap,
551
+ cpuShares: options.cpuShares ?? container.cpuShares,
552
+ cpusetCpus: options.cpusetCpus ?? container.cpusetCpus,
553
+ pidsLimit: options.pidsLimit ?? container.pidsLimit,
554
+ shmSize: options.shmSize ?? container.shmSize,
555
+ tmpfsTmp: options.tmpfsTmp ?? container.tmpfsTmp,
556
+ tmpfsRun: options.tmpfsRun ?? container.tmpfsRun,
557
+ volumes: options.volumes ?? container.volumes,
558
+ env: options.env ?? container.env,
559
+ dockerTimeoutMs: options.dockerTimeoutMs ?? container.timeoutMs,
560
+ displayWidth: options.displayWidth ?? viewport.width,
561
+ displayHeight: options.displayHeight ?? viewport.height,
562
+ displayDepth: options.displayDepth ?? viewport.depth,
563
+ startupCommand: options.startupCommand ?? desktop.startupCommand,
564
+ windowManagerCommand: options.windowManagerCommand ?? desktop.windowManagerCommand,
565
+ desktopPackages: options.desktopPackages ?? desktop.packages ?? container.packages,
566
+ additionalAptPackages:
567
+ options.additionalAptPackages ?? desktop.additionalAptPackages ?? container.additionalAptPackages,
568
+ installDependencies: options.installDependencies ?? desktop.installDependencies ?? container.installDependencies,
569
+ packageManager: options.packageManager ?? desktop.packageManager,
570
+ sharedFolder: options.sharedFolder ?? options.shared,
571
+ files: options.files ?? options.sharedFiles
572
+ };
573
+ validateDockerDesktopStartupCommand(normalized);
574
+ return normalized;
575
+ }
576
+
577
+ function validateDockerDesktopStartupCommand(options) {
578
+ if (typeof options.startupCommand === "string" && options.startupCommand.trim()) return;
579
+ throw new AutomifyError(
580
+ "Docker desktop startupCommand is required. Pass a non-empty startupCommand or desktop.startupCommand."
581
+ );
582
+ }
583
+
584
+ function debugVirtualDesktop(options, message, details) {
585
+ writeDebugLogFile(options.logFile, "automify:docker-desktop", message, details, { silent: options.silent });
586
+ debugLog(options.debug, "automify:docker-desktop", message, details, { silent: options.silent });
587
+ }
588
+
589
+ function summarizeDockerArgs(args) {
590
+ return args.map((arg) => (String(arg).length > 160 ? `${String(arg).slice(0, 157)}...` : arg));
591
+ }
592
+
593
+ export function dockerDesktopDockerfile() {
594
+ return `FROM ${DEFAULT_IMAGE}
595
+ ENV DEBIAN_FRONTEND=noninteractive
596
+ RUN apt-get update \\
597
+ && apt-get install -y --no-install-recommends \\
598
+ xvfb openbox xterm x11-utils xdotool imagemagick scrot ca-certificates \\
599
+ && rm -rf /var/lib/apt/lists/*
600
+ `;
601
+ }
602
+
603
+ export function defaultDockerDesktopImage() {
604
+ return DEFAULT_IMAGE;
605
+ }
606
+
607
+ export const virtualDesktopDockerfile = dockerDesktopDockerfile;
608
+ export const defaultVirtualDesktopImage = defaultDockerDesktopImage;
609
+
610
+ function keys(values) {
611
+ const normalized = values.map((value) => key(value)).filter(Boolean);
612
+ if (normalized.length === 0) {
613
+ throw new AutomifyError("keypress action did not include any keys.");
614
+ }
615
+ return normalized;
616
+ }
617
+
618
+ function key(value) {
619
+ const raw = String(value ?? "").trim();
620
+ const lower = raw.toLowerCase().replace(/\s+/g, "_");
621
+ const aliases = {
622
+ alt: "Alt",
623
+ backspace: "BackSpace",
624
+ cmd: "Super",
625
+ command: "Super",
626
+ control: "Control",
627
+ ctrl: "Control",
628
+ delete: "Delete",
629
+ down: "Down",
630
+ enter: "Return",
631
+ esc: "Escape",
632
+ escape: "Escape",
633
+ left: "Left",
634
+ meta: "Super",
635
+ option: "Alt",
636
+ return: "Return",
637
+ right: "Right",
638
+ shift: "Shift",
639
+ space: "space",
640
+ tab: "Tab",
641
+ up: "Up"
642
+ };
643
+ return aliases[lower] ?? raw;
644
+ }
645
+
646
+ function button(value) {
647
+ if (value === "right") return "3";
648
+ if (value === "middle") return "2";
649
+ return "1";
650
+ }
651
+
652
+ function x(value) {
653
+ return String(Math.max(0, Math.round(Number(value) || 0)));
654
+ }
655
+
656
+ function y(value) {
657
+ return String(Math.max(0, Math.round(Number(value) || 0)));
658
+ }
659
+
660
+ function normalizeDisplay(display) {
661
+ const value = String(display || DEFAULT_DISPLAY).trim();
662
+ return value.startsWith(":") ? value : `:${value}`;
663
+ }
664
+
665
+ function dockerNetwork(value) {
666
+ if (value === false || value === "none") return "none";
667
+ if (typeof value === "string" && value.trim()) return value.trim();
668
+ return "bridge";
669
+ }
670
+
671
+ function isBaseLinuxImage(image) {
672
+ const normalized = String(image || "").toLowerCase();
673
+ return ["ubuntu:", "debian:"].some((prefix) => normalized.startsWith(prefix));
674
+ }
675
+
676
+ function shortError(error) {
677
+ return String(error?.stderr || error?.message || error)
678
+ .trim()
679
+ .split("\n")[0];
680
+ }
681
+
682
+ function positiveInteger(value) {
683
+ const number = Number(value);
684
+ if (!Number.isFinite(number) || number <= 0) return null;
685
+ return Math.floor(number);
686
+ }
687
+
688
+ function appendDockerResourceArgs(args, options) {
689
+ const cpus = positiveNumber(options.cpus);
690
+ if (cpus != null) {
691
+ args.push("--cpus", String(cpus));
692
+ }
693
+ appendNonEmptyArg(args, "--memory", options.memory);
694
+ appendNonEmptyArg(args, "--memory-swap", options.memorySwap);
695
+
696
+ const cpuShares = positiveInteger(options.cpuShares);
697
+ if (cpuShares != null) {
698
+ args.push("--cpu-shares", String(cpuShares));
699
+ }
700
+ appendNonEmptyArg(args, "--cpuset-cpus", options.cpusetCpus);
701
+ }
702
+
703
+ function appendNonEmptyArg(args, flag, value) {
704
+ if (value == null || value === false) return;
705
+ const normalized = String(value).trim();
706
+ if (normalized) args.push(flag, normalized);
707
+ }
708
+
709
+ function positiveNumber(value) {
710
+ const number = Number(value);
711
+ if (!Number.isFinite(number) || number <= 0) return null;
712
+ return number;
713
+ }
714
+
715
+ function shellQuote(value) {
716
+ return `'${String(value).replaceAll("'", "'\\''")}'`;
717
+ }
718
+
719
+ function uniquePackages(packages) {
720
+ return [...new Set(packages.map((pkg) => String(pkg).trim()).filter(Boolean))];
721
+ }
722
+
723
+ function sleep(ms) {
724
+ return new Promise((resolve) => setTimeout(resolve, ms));
725
+ }