automify 0.1.8 → 0.1.10

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/README.md CHANGED
@@ -14,14 +14,14 @@ Computer use surfaces:
14
14
  | -------------- | --------------------------- | ----------------------------------------------------------- |
15
15
  | Browser | `automify.browser()` | Playwright browser with screenshots and actions |
16
16
  | Desktop | `automify.localComputer()` | Native desktop on the current macOS, Windows, or Linux host |
17
- | Docker desktop | `automify.dockerComputer()` | Linux desktop inside a Docker container |
17
+ | Docker desktop | `automify.dockerComputer()` | Linux desktop inside a running Docker container |
18
18
 
19
19
  Command use surfaces:
20
20
 
21
- | Surface | Factory | What it does |
22
- | ---------- | ---------------------- | ---------------------------------------------------- |
23
- | CLI | `automify.cli()` | Terminal automation through model-requested commands |
24
- | Docker CLI | `automify.dockerCli()` | Containerized terminal automation with shared files |
21
+ | Surface | Factory | What it does |
22
+ | ---------- | ---------------------- | ----------------------------------------------------- |
23
+ | CLI | `automify.cli()` | Terminal automation through model-requested commands |
24
+ | Docker CLI | `automify.dockerCli()` | Containerized terminal automation with running Docker |
25
25
 
26
26
  OpenAI and Anthropic models are supported, and any other model can be plugged in with a custom provider adapter.
27
27
 
@@ -120,12 +120,12 @@ const browser = await automify.browser({
120
120
  });
121
121
 
122
122
  try {
123
- const run = await browser.do("Extract the support email.", {
123
+ const run = await browser.do("Summarize what you see on the page.", {
124
124
  // Optional: structured result shape.
125
- output: jsonOutput("support_contact", { email: "string" })
125
+ output: jsonOutput("page_summary", { title: "string", summary: "string" })
126
126
  });
127
127
 
128
- console.log(run.parsed.email);
128
+ console.log(run.parsed.title, run.parsed.summary);
129
129
  } finally {
130
130
  await browser.close();
131
131
  }
@@ -147,7 +147,7 @@ const cli = automify.cli({
147
147
  await cli.do("Run the tests and summarize failures");
148
148
  ```
149
149
 
150
- Use Docker CLI when command execution should happen inside an isolated container:
150
+ Use Docker CLI when command execution should happen inside an isolated container. Docker must be installed and running before you create the adapter:
151
151
 
152
152
  ```js
153
153
  import { mkdir, mkdtemp, readFile, writeFile } from "node:fs/promises";
@@ -208,11 +208,14 @@ winget install --id Microsoft.VisualStudio.2022.BuildTools --exact --override "-
208
208
  winget install --id Kitware.CMake --exact --source winget
209
209
 
210
210
  # macOS: Xcode Command Line Tools plus CMake on PATH.
211
+ # If Homebrew is not installed, install it first:
212
+ /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
213
+
211
214
  xcode-select --install
212
215
  brew install cmake
213
216
 
214
217
  # Debian/Ubuntu Linux.
215
- sudo apt-get install -y build-essential cmake libxtst-dev libpng++-dev
218
+ sudo apt-get install -y git build-essential cmake pkg-config libx11-dev libxtst-dev libpng++-dev
216
219
 
217
220
  # Fedora Linux.
218
221
  sudo dnf install -y gcc-c++ make cmake libXtst-devel libpng-devel
@@ -221,7 +224,9 @@ sudo dnf install -y gcc-c++ make cmake libXtst-devel libpng-devel
221
224
  sudo pacman -S --needed base-devel cmake libxtst libpng
222
225
  ```
223
226
 
224
- On headless Linux hosts, also install `xvfb` unless you manage `DISPLAY` yourself. On macOS and Windows, `cmake --version` must work in the terminal where you run `npx automify-install-desktop`. On Windows, the VS Code CMake Tools extension is not enough by itself, and Visual Studio 2026 is not currently recognized by the native build chain used by nut.js.
227
+ On Linux, install the full package list before running `npx automify-install-desktop`; the installer checks for command-line build tools but does not verify every native library package. On headless Linux hosts, also install `xvfb` unless you manage `DISPLAY` yourself. On macOS, install Homebrew first if `brew` is not available, then install CMake with `brew install cmake`. On macOS and Windows, `cmake --version` must work in the terminal where you run `npx automify-install-desktop`. On Windows, the VS Code CMake Tools extension is not enough by itself, and Visual Studio 2026 is not currently recognized by the native build chain used by nut.js.
228
+
229
+ `npx automify-install-desktop` stores the compiled desktop runtime outside `node_modules` in a long-term cache, so normal `npm update` runs do not remove it. If a later `npm install` or `npm update` detects that a previously installed desktop runtime no longer matches the current platform, CPU architecture, Node ABI, or pinned nut.js/libnut revisions, Automify rebuilds it automatically during `postinstall`. Default cache roots are `%LOCALAPPDATA%\automify\desktop-runtime` on Windows, `~/Library/Caches/automify/desktop-runtime` on macOS, and `${XDG_CACHE_HOME:-~/.cache}/automify/desktop-runtime` on Linux. Override with `AUTOMIFY_DESKTOP_RUNTIME_DIR`; disable auto-rebuild with `AUTOMIFY_SKIP_DESKTOP_AUTO_REBUILD=1`.
225
230
 
226
231
  ```js
227
232
  import { initAutomify } from "automify";
@@ -246,7 +251,7 @@ try {
246
251
  }
247
252
  ```
248
253
 
249
- For isolated Linux desktop computer use, use Docker. `dockerComputer()` can run from a macOS, Windows, or Linux host with Docker, but the desktop it controls inside the container is Linux. Docker desktop does not use `automify-install-desktop`; it needs Docker and an initial app command:
254
+ For isolated Linux desktop computer use, use Docker. `dockerComputer()` can run from a macOS, Windows, or Linux host with Docker installed and running, but the desktop it controls inside the container is Linux. Docker desktop does not use `automify-install-desktop`; it needs a running Docker daemon and an initial app command:
250
255
 
251
256
  ```js
252
257
  import { initAutomify } from "automify";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "automify",
3
- "version": "0.1.8",
3
+ "version": "0.1.10",
4
4
  "description": "AI computer use for browser, CLI, and desktop in Node.js.",
5
5
  "homepage": "https://aldovincenti.github.io/automify",
6
6
  "bugs": {
@@ -36,7 +36,7 @@
36
36
  "SECURITY.md"
37
37
  ],
38
38
  "scripts": {
39
- "postinstall": "node scripts/install-browser.js",
39
+ "postinstall": "node scripts/install-browser.js && node scripts/install-desktop-if-needed.js",
40
40
  "install:desktop": "node scripts/install-desktop.js",
41
41
  "docs:arguments": "node scripts/generate-argument-reference.js",
42
42
  "test": "node --test test/*.test.js",
@@ -9,6 +9,7 @@ if (process.env.AUTOMIFY_SKIP_BROWSER_INSTALL === "1" || process.env.PLAYWRIGHT_
9
9
  }
10
10
 
11
11
  const playwrightCli = join(dirname(require.resolve("playwright")), "cli.js");
12
+ console.log("Automify: installing Playwright browser...");
12
13
  const result = spawnSync(process.execPath, [playwrightCli, "install", "chromium"], {
13
14
  cwd: process.cwd(),
14
15
  stdio: "inherit"
@@ -0,0 +1,42 @@
1
+ #!/usr/bin/env node
2
+ import { spawnSync } from "node:child_process";
3
+ import { existsSync } from "node:fs";
4
+ import { dirname, join, resolve } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ import {
8
+ desktopRuntimeDir,
9
+ desktopRuntimeIsInstalled,
10
+ desktopRuntimeKey,
11
+ findDesktopRuntimeManifests
12
+ } from "../src/lib/desktop-runtime.js";
13
+
14
+ const __dirname = dirname(fileURLToPath(import.meta.url));
15
+ const root = resolve(__dirname, "..");
16
+
17
+ if (process.env.AUTOMIFY_SKIP_DESKTOP_INSTALL === "1" || process.env.AUTOMIFY_SKIP_DESKTOP_AUTO_REBUILD === "1") {
18
+ process.exit(0);
19
+ }
20
+
21
+ if (desktopRuntimeIsInstalled()) {
22
+ process.exit(0);
23
+ }
24
+
25
+ const existingManifests = findDesktopRuntimeManifests();
26
+ const legacyNodeModulesRuntime = existsSync(join(root, "node_modules", "@nut-tree", "nut-js", "package.json"));
27
+
28
+ if (existingManifests.length === 0 && !legacyNodeModulesRuntime) {
29
+ process.exit(0);
30
+ }
31
+
32
+ console.log("Automify desktop runtime was previously installed but is not compatible with this environment.");
33
+ console.log(`Rebuilding desktop runtime cache: ${desktopRuntimeKey()}`);
34
+ console.log(`Runtime directory: ${desktopRuntimeDir()}`);
35
+
36
+ const result = spawnSync(process.execPath, [join(__dirname, "install-desktop.js")], {
37
+ cwd: root,
38
+ env: process.env,
39
+ stdio: "inherit"
40
+ });
41
+
42
+ process.exit(result.status ?? 1);
@@ -4,6 +4,15 @@ import { cpSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } fr
4
4
  import { dirname, join, resolve, sep } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
6
 
7
+ import {
8
+ DESKTOP_RUNTIME_MANIFEST,
9
+ desktopRuntimeDir,
10
+ desktopRuntimeKey,
11
+ desktopRuntimeManifest,
12
+ desktopRuntimeNodeModules,
13
+ desktopRuntimeRefs
14
+ } from "../src/lib/desktop-runtime.js";
15
+
7
16
  const __dirname = dirname(fileURLToPath(import.meta.url));
8
17
  const root = resolve(__dirname, "..");
9
18
  const buildRoot = process.env.AUTOMIFY_DESKTOP_BUILD_DIR
@@ -12,12 +21,10 @@ const buildRoot = process.env.AUTOMIFY_DESKTOP_BUILD_DIR
12
21
  const nutSource = join(buildRoot, "nut.js");
13
22
  const libnutSource = join(buildRoot, "libnut-core");
14
23
  const macPermissionsSource = join(buildRoot, "node-mac-permissions");
15
- const refs = {
16
- nut: process.env.AUTOMIFY_DESKTOP_NUT_REF ?? "e413fa1f19a19c4631812e4e1eaf47aa732b5cbe",
17
- libnutCore: process.env.AUTOMIFY_DESKTOP_LIBNUT_CORE_REF ?? "6bbe5825f1123bcd740117ca932c8b1c6cffb48c",
18
- macPermissions: process.env.AUTOMIFY_DESKTOP_MAC_PERMISSIONS_REF ?? "6b6ddee993ddce5071b637e42f6ee1434150d0bb"
19
- };
20
- const nodeModules = join(root, "node_modules");
24
+ const refs = desktopRuntimeRefs();
25
+ const runtimeDir = desktopRuntimeDir();
26
+ const runtimeNodeModules = desktopRuntimeNodeModules();
27
+ const nodeModules = runtimeNodeModules;
21
28
  const nutScope = join(nodeModules, "@nut-tree");
22
29
  const platformPackageName = `@nut-tree/libnut-${process.platform}`;
23
30
  const platformPackageDir = join(nutScope, `libnut-${process.platform}`);
@@ -27,6 +34,8 @@ const runtimeDependencies = ["jimp@1.6.1", "node-abort-controller@3.1.1", "clipb
27
34
 
28
35
  console.log("Building official nut.js from source.");
29
36
  console.log(`Build directory: ${buildRoot}`);
37
+ console.log(`Runtime directory: ${runtimeDir}`);
38
+ console.log(`Runtime key: ${desktopRuntimeKey()}`);
30
39
  console.log(`nut.js ref: ${refs.nut}`);
31
40
  console.log(`libnut-core ref: ${refs.libnutCore}`);
32
41
  if (process.platform === "darwin") {
@@ -36,6 +45,8 @@ if (process.platform === "darwin") {
36
45
  checkBuildPrerequisites();
37
46
 
38
47
  mkdirSync(buildRoot, { recursive: true });
48
+ mkdirSync(runtimeDir, { recursive: true });
49
+ writeRuntimePackageJson();
39
50
  cloneOrPull("https://github.com/nut-tree/libnut-core.git", libnutSource, refs.libnutCore);
40
51
  cloneOrPull("https://github.com/nut-tree/nut.js.git", nutSource, refs.nut);
41
52
  if (process.platform === "darwin") {
@@ -69,7 +80,7 @@ writeLibnutImportBridge();
69
80
  runPnpm(["--filter", "@nut-tree/nut-js", "run", "compile"], { cwd: nutSource });
70
81
  patchNutJimpCompatibility();
71
82
 
72
- run("npm", ["install", "--no-save", ...runtimeDependencies], { cwd: root });
83
+ run("npm", ["install", "--no-save", ...runtimeDependencies], { cwd: runtimeDir });
73
84
 
74
85
  installWorkspacePackage(join(nutSource, "core", "shared"), join(nutScope, "shared"));
75
86
  installWorkspacePackage(join(nutSource, "core", "provider-interfaces"), join(nutScope, "provider-interfaces"));
@@ -81,9 +92,20 @@ if (process.platform === "darwin") {
81
92
  installWorkspacePackage(macPermissionsSource, macPermissionsPackageDir);
82
93
  }
83
94
 
84
- run("node", ["-e", "import('@nut-tree/nut-js').then(() => console.log('nut.js source build import ok'))"], {
85
- cwd: root
86
- });
95
+ writeRuntimeManifest();
96
+ run(
97
+ "node",
98
+ [
99
+ "-e",
100
+ `const { createRequire } = require("node:module");
101
+ const { join } = require("node:path");
102
+ const runtimeDir = process.argv[1];
103
+ createRequire(join(runtimeDir, "automify-desktop-runtime.cjs"))("@nut-tree/nut-js");
104
+ console.log("nut.js source build import ok");`,
105
+ runtimeDir
106
+ ],
107
+ { cwd: root }
108
+ );
87
109
 
88
110
  function cloneOrPull(repo, target, ref) {
89
111
  if (existsSync(join(target, ".git"))) {
@@ -132,7 +154,8 @@ function checkBuildPrerequisites() {
132
154
  if (process.platform === "darwin") {
133
155
  console.error("macOS: install Xcode Command Line Tools with `xcode-select --install` and install CMake.");
134
156
  } else if (process.platform === "linux") {
135
- console.error("Linux: install CMake, a C/C++ compiler, libxtst-dev, and libpng++-dev.");
157
+ console.error("Linux: install git, build-essential, cmake, pkg-config, libx11-dev, libxtst-dev, and libpng++-dev.");
158
+ console.error("The Linux installer does not verify every native library package before building.");
136
159
  } else if (process.platform === "win32") {
137
160
  console.error("Windows: install CMake and Visual Studio 2022 C++ Build Tools.");
138
161
  console.error("Make sure the `Desktop development with C++` workload is installed.");
@@ -159,22 +182,26 @@ function resolveCommand(command) {
159
182
  function commandCandidates(command) {
160
183
  const candidates = [];
161
184
 
185
+ const npmCli = npmCliCandidate(command);
186
+ if (npmCli) candidates.push(npmCli);
187
+
162
188
  if (process.platform === "win32" && ["npm", "npx"].includes(command)) {
163
189
  candidates.push({ command: `${command}.cmd`, args: [] });
164
190
  }
165
191
 
166
- const npmCli = npmCliCandidate(command);
167
- if (npmCli) candidates.push(npmCli);
168
-
169
192
  candidates.push({ command, args: [] });
170
193
  return candidates;
171
194
  }
172
195
 
173
196
  function npmCliCandidate(command) {
174
- if (!["npm", "npx"].includes(command) || !process.env.npm_execpath) return null;
175
- if (command === "npm") return { command: process.execPath, args: [process.env.npm_execpath] };
197
+ if (!["npm", "npx"].includes(command)) return null;
198
+
199
+ const npmExecPath =
200
+ process.env.npm_execpath ?? join(dirname(process.execPath), "node_modules", "npm", "bin", "npm-cli.js");
201
+ if (!existsSync(npmExecPath)) return null;
202
+ if (command === "npm") return { command: process.execPath, args: [npmExecPath] };
176
203
 
177
- const npxExecPath = join(dirname(process.env.npm_execpath), "npx-cli.js");
204
+ const npxExecPath = join(dirname(npmExecPath), "npx-cli.js");
178
205
  if (!existsSync(npxExecPath)) return null;
179
206
  return { command: process.execPath, args: [npxExecPath] };
180
207
  }
@@ -403,6 +430,35 @@ function installWorkspacePackage(source, target) {
403
430
  });
404
431
  }
405
432
 
433
+ function writeRuntimePackageJson() {
434
+ writeText(
435
+ join(runtimeDir, "package.json"),
436
+ `${JSON.stringify(
437
+ {
438
+ private: true,
439
+ name: "automify-desktop-runtime",
440
+ description: "Persistent native runtime cache for Automify local desktop support."
441
+ },
442
+ null,
443
+ 2
444
+ )}\n`
445
+ );
446
+ }
447
+
448
+ function writeRuntimeManifest() {
449
+ writeText(
450
+ join(runtimeDir, DESKTOP_RUNTIME_MANIFEST),
451
+ `${JSON.stringify(
452
+ {
453
+ ...desktopRuntimeManifest(),
454
+ createdAt: new Date().toISOString()
455
+ },
456
+ null,
457
+ 2
458
+ )}\n`
459
+ );
460
+ }
461
+
406
462
  function run(command, args, options = {}) {
407
463
  const result = runResolvedCommand(command, args, {
408
464
  cwd: options.cwd ?? root,
@@ -51,7 +51,7 @@ export const argumentReference = [
51
51
  "logFile"
52
52
  ],
53
53
  notes:
54
- 'Use additionalAptPackages to apt-install Debian packages before commands run. Use preset: "repo" to mount the current workspace at /workspace and allow common repo commands. Use logFile to capture CLI and Docker container events.'
54
+ 'Requires Docker to be installed and running. Use additionalAptPackages to apt-install Debian packages before commands run. Use preset: "repo" to mount the current workspace at /workspace and allow common repo commands. Use logFile to capture CLI and Docker container events.'
55
55
  },
56
56
  {
57
57
  surface: "automify.dockerComputer()",
@@ -66,7 +66,7 @@ export const argumentReference = [
66
66
  "logFile"
67
67
  ],
68
68
  notes:
69
- "Creates a Docker-backed Linux desktop runner. Pass startupCommand or desktop.startupCommand to launch the initial app. Use additionalAptPackages to apt-install extra Debian packages. Use logFile to capture automation and Docker desktop events. Explicit container names are locked per name until close()."
69
+ "Creates a Docker-backed Linux desktop runner and requires Docker to be installed and running. Pass startupCommand or desktop.startupCommand to launch the initial app. Use additionalAptPackages to apt-install extra Debian packages. Use logFile to capture automation and Docker desktop events. Explicit container names are locked per name until close()."
70
70
  },
71
71
  {
72
72
  surface: "automify.localComputer()",
@@ -93,6 +93,6 @@ export const argumentReference = [
93
93
  "logFile"
94
94
  ],
95
95
  notes:
96
- "container controls Docker and resource limits; startupCommand or desktop.startupCommand is required; shared/sharedFiles control host file access. Use additionalAptPackages to apt-install extra Debian packages and logFile to capture Docker desktop events."
96
+ "Requires Docker to be installed and running. container controls Docker and resource limits; startupCommand or desktop.startupCommand is required; shared/sharedFiles control host file access. Use additionalAptPackages to apt-install extra Debian packages and logFile to capture Docker desktop events."
97
97
  }
98
98
  ];
@@ -0,0 +1,135 @@
1
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join, resolve } from "node:path";
4
+
5
+ export const DESKTOP_RUNTIME_PACKAGE = "@nut-tree/nut-js";
6
+ export const DESKTOP_RUNTIME_MANIFEST = "automify-desktop-runtime.json";
7
+
8
+ export function desktopRuntimeRefs(env = process.env) {
9
+ return {
10
+ nut: env.AUTOMIFY_DESKTOP_NUT_REF ?? "e413fa1f19a19c4631812e4e1eaf47aa732b5cbe",
11
+ libnutCore: env.AUTOMIFY_DESKTOP_LIBNUT_CORE_REF ?? "6bbe5825f1123bcd740117ca932c8b1c6cffb48c",
12
+ macPermissions: env.AUTOMIFY_DESKTOP_MAC_PERMISSIONS_REF ?? "6b6ddee993ddce5071b637e42f6ee1434150d0bb"
13
+ };
14
+ }
15
+
16
+ export function desktopRuntimeCompatibility(env = process.env) {
17
+ const refs = desktopRuntimeRefs(env);
18
+ return {
19
+ platform: process.platform,
20
+ arch: process.arch,
21
+ nodeAbi: process.versions.modules,
22
+ nutRef: refs.nut,
23
+ libnutCoreRef: refs.libnutCore,
24
+ macPermissionsRef: process.platform === "darwin" ? refs.macPermissions : undefined
25
+ };
26
+ }
27
+
28
+ export function desktopRuntimeKey(compatibility = desktopRuntimeCompatibility()) {
29
+ return [
30
+ compatibility.platform,
31
+ compatibility.arch,
32
+ `node-${compatibility.nodeAbi}`,
33
+ `nut-${shortRef(compatibility.nutRef)}`,
34
+ `libnut-${shortRef(compatibility.libnutCoreRef)}`,
35
+ compatibility.macPermissionsRef ? `macperms-${shortRef(compatibility.macPermissionsRef)}` : null
36
+ ]
37
+ .filter(Boolean)
38
+ .join("-");
39
+ }
40
+
41
+ export function defaultDesktopRuntimeRoot(env = process.env) {
42
+ if (env.AUTOMIFY_DESKTOP_RUNTIME_DIR) {
43
+ return resolve(env.AUTOMIFY_DESKTOP_RUNTIME_DIR);
44
+ }
45
+
46
+ if (process.platform === "win32") {
47
+ return join(env.LOCALAPPDATA ?? join(homedir(), "AppData", "Local"), "automify", "desktop-runtime");
48
+ }
49
+
50
+ if (process.platform === "darwin") {
51
+ return join(homedir(), "Library", "Caches", "automify", "desktop-runtime");
52
+ }
53
+
54
+ return join(env.XDG_CACHE_HOME ?? join(homedir(), ".cache"), "automify", "desktop-runtime");
55
+ }
56
+
57
+ export function desktopRuntimeDir(env = process.env) {
58
+ return join(defaultDesktopRuntimeRoot(env), desktopRuntimeKey(desktopRuntimeCompatibility(env)));
59
+ }
60
+
61
+ export function desktopRuntimeNodeModules(env = process.env) {
62
+ return join(desktopRuntimeDir(env), "node_modules");
63
+ }
64
+
65
+ export function desktopRuntimePackageJsonPath(env = process.env) {
66
+ return join(desktopRuntimeNodeModules(env), "@nut-tree", "nut-js", "package.json");
67
+ }
68
+
69
+ export function desktopRuntimeManifestPath(env = process.env) {
70
+ return join(desktopRuntimeDir(env), DESKTOP_RUNTIME_MANIFEST);
71
+ }
72
+
73
+ export function desktopRuntimeManifest(env = process.env) {
74
+ return {
75
+ version: 1,
76
+ package: DESKTOP_RUNTIME_PACKAGE,
77
+ runtimeDir: desktopRuntimeDir(env),
78
+ compatibility: desktopRuntimeCompatibility(env)
79
+ };
80
+ }
81
+
82
+ export function readDesktopRuntimeManifest(env = process.env) {
83
+ const path = desktopRuntimeManifestPath(env);
84
+ if (!existsSync(path)) return null;
85
+ return JSON.parse(readFileSync(path, "utf8"));
86
+ }
87
+
88
+ export function findDesktopRuntimeManifests(env = process.env) {
89
+ const root = defaultDesktopRuntimeRoot(env);
90
+ if (!existsSync(root)) return [];
91
+
92
+ return readdirSync(root, { withFileTypes: true })
93
+ .filter((entry) => entry.isDirectory())
94
+ .map((entry) => {
95
+ const runtimeDir = join(root, entry.name);
96
+ const manifestPath = join(runtimeDir, DESKTOP_RUNTIME_MANIFEST);
97
+ if (!existsSync(manifestPath)) return null;
98
+ try {
99
+ return {
100
+ runtimeDir,
101
+ manifestPath,
102
+ manifest: JSON.parse(readFileSync(manifestPath, "utf8"))
103
+ };
104
+ } catch {
105
+ return null;
106
+ }
107
+ })
108
+ .filter(Boolean);
109
+ }
110
+
111
+ export function desktopRuntimeIsInstalled(env = process.env) {
112
+ const manifest = readDesktopRuntimeManifest(env);
113
+ return desktopRuntimeManifestMatches(manifest, env) && existsSync(desktopRuntimePackageJsonPath(env));
114
+ }
115
+
116
+ export function desktopRuntimeManifestMatches(manifest, env = process.env) {
117
+ if (!manifest || manifest.version !== 1 || manifest.package !== DESKTOP_RUNTIME_PACKAGE) return false;
118
+
119
+ const expected = desktopRuntimeCompatibility(env);
120
+ const actual = manifest.compatibility ?? {};
121
+ return (
122
+ actual.platform === expected.platform &&
123
+ actual.arch === expected.arch &&
124
+ actual.nodeAbi === expected.nodeAbi &&
125
+ actual.nutRef === expected.nutRef &&
126
+ actual.libnutCoreRef === expected.libnutCoreRef &&
127
+ actual.macPermissionsRef === expected.macPermissionsRef
128
+ );
129
+ }
130
+
131
+ function shortRef(ref) {
132
+ return String(ref ?? "unknown")
133
+ .replace(/[^a-zA-Z0-9._-]/g, "_")
134
+ .slice(0, 12);
135
+ }
@@ -1,3 +1,5 @@
1
+ import { createRequire } from "node:module";
2
+ import { readFileSync } from "node:fs";
1
3
  import { readFile, unlink } from "node:fs/promises";
2
4
  import { execFile, spawn } from "node:child_process";
3
5
  import { tmpdir } from "node:os";
@@ -8,6 +10,12 @@ import { promisify } from "node:util";
8
10
  import { AutomifyError } from "./errors.js";
9
11
  import { acquireAdapterLock } from "./adapter-locks.js";
10
12
  import { assertKnownOptions, normalizeLogFile, writeDebugLogFile } from "./runtime.js";
13
+ import {
14
+ DESKTOP_RUNTIME_PACKAGE,
15
+ desktopRuntimeDir,
16
+ desktopRuntimeManifestMatches,
17
+ desktopRuntimeManifestPath
18
+ } from "./desktop-runtime.js";
11
19
 
12
20
  const execFileAsync = promisify(execFile);
13
21
  const LOCAL_DESKTOP_OPTION_KEYS = new Set([
@@ -448,16 +456,39 @@ async function saveNutImageObject(nut, image, options = {}) {
448
456
  }
449
457
 
450
458
  async function importNut() {
459
+ let runtimeError;
451
460
  try {
452
- return await import("@nut-tree/nut-js");
461
+ const runtimeNut = importNutFromDesktopRuntime();
462
+ if (runtimeNut) return runtimeNut;
453
463
  } catch (error) {
464
+ runtimeError = error;
465
+ }
466
+
467
+ try {
468
+ return await import(DESKTOP_RUNTIME_PACKAGE);
469
+ } catch (error) {
470
+ const runtimeSuffix = runtimeError ? ` Cached desktop runtime import failed: ${runtimeError.message}` : "";
454
471
  throw new AutomifyError(
455
- `createLocalDesktopComputer requires the local desktop adapter dependency built from source. ${localDesktopInstallHelp()} After the OS prerequisites are available, run: npx automify-install-desktop`,
472
+ `createLocalDesktopComputer requires the local desktop adapter dependency built from source. ${localDesktopInstallHelp()} After the OS prerequisites are available, run: npx automify-install-desktop.${runtimeSuffix}`,
456
473
  { cause: error }
457
474
  );
458
475
  }
459
476
  }
460
477
 
478
+ function importNutFromDesktopRuntime() {
479
+ const manifestPath = desktopRuntimeManifestPath();
480
+ let manifest;
481
+ try {
482
+ manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
483
+ } catch {
484
+ return null;
485
+ }
486
+ if (!desktopRuntimeManifestMatches(manifest)) return null;
487
+
488
+ const runtimeRequire = createRequire(join(desktopRuntimeDir(), "automify-desktop-runtime.cjs"));
489
+ return runtimeRequire(DESKTOP_RUNTIME_PACKAGE);
490
+ }
491
+
461
492
  function normalizeLocalDesktopEnvironment(environment) {
462
493
  if (environment == null) return undefined;
463
494
  if (typeof environment !== "string" || environment.trim() === "") {
@@ -483,7 +514,7 @@ function localDesktopInstallHelp(platform = process.platform) {
483
514
  return "On Windows, install Visual Studio C++ Build Tools and make sure `cmake --version` works, for example after `winget install Kitware.CMake`.";
484
515
  }
485
516
  if (platform === "linux") {
486
- return "On Linux, install the native build tools for your distro: Debian/Ubuntu need `build-essential cmake libxtst-dev libpng++-dev`; Fedora needs `gcc-c++ make cmake libXtst-devel libpng-devel`; Arch needs `base-devel cmake libxtst libpng`. Headless hosts also need `xvfb` unless DISPLAY is managed externally.";
517
+ return "On Linux, install the native build tools for your distro before running the desktop installer; it does not verify every native library package. Debian/Ubuntu need `git build-essential cmake pkg-config libx11-dev libxtst-dev libpng++-dev`; Fedora needs `gcc-c++ make cmake libXtst-devel libpng-devel`; Arch needs `base-devel cmake libxtst libpng`. Headless hosts also need `xvfb` unless DISPLAY is managed externally.";
487
518
  }
488
519
  return "Install the native build tools and CMake for this OS.";
489
520
  }
@@ -833,15 +864,20 @@ function describePointTransform(x, y, coordinateSpace) {
833
864
 
834
865
  function buildCoordinateSpace(options, screen) {
835
866
  const macOSDisplay = screen.macOSDisplay;
867
+ const environment = screen.environment ?? options.environment ?? defaultDesktopEnvironment();
836
868
  const pixelScale =
837
869
  positiveNumber(options.pixelScale) ??
838
870
  positiveNumber(macOSDisplay?.backingScaleFactor) ??
839
- inferPixelScale(screen, options);
871
+ inferPixelScale({ ...screen, environment }, options);
840
872
  const defaultScale = 1 / pixelScale;
841
- const mouseScaleX = scaleRatio(macOSDisplay?.width, screen.displayWidth) ?? defaultScale;
842
- const mouseScaleY = scaleRatio(macOSDisplay?.height, screen.displayHeight) ?? defaultScale;
843
- const mouseWidth = positiveNumber(macOSDisplay?.width) ?? positiveNumber(screen.displayWidth) * mouseScaleX;
844
- const mouseHeight = positiveNumber(macOSDisplay?.height) ?? positiveNumber(screen.displayHeight) * mouseScaleY;
873
+ const mouseWidth =
874
+ measuredMouseSize("width", { ...screen, environment, macOSDisplay }) ??
875
+ scaledDisplaySize(screen.displayWidth, defaultScale);
876
+ const mouseHeight =
877
+ measuredMouseSize("height", { ...screen, environment, macOSDisplay }) ??
878
+ scaledDisplaySize(screen.displayHeight, defaultScale);
879
+ const mouseScaleX = scaleRatio(mouseWidth, screen.displayWidth) ?? defaultScale;
880
+ const mouseScaleY = scaleRatio(mouseHeight, screen.displayHeight) ?? defaultScale;
845
881
 
846
882
  return {
847
883
  ...screen,
@@ -856,6 +892,20 @@ function buildCoordinateSpace(options, screen) {
856
892
  };
857
893
  }
858
894
 
895
+ function measuredMouseSize(axis, { environment, macOSDisplay, screenWidth, screenHeight }) {
896
+ const macOSValue = positiveNumber(macOSDisplay?.[axis]);
897
+ if (macOSValue) return macOSValue;
898
+ if (environment === "mac") return null;
899
+ return positiveNumber(axis === "width" ? screenWidth : screenHeight);
900
+ }
901
+
902
+ function scaledDisplaySize(size, scale) {
903
+ const numericSize = positiveNumber(size);
904
+ const numericScale = positiveNumber(scale);
905
+ if (!numericSize || !numericScale) return undefined;
906
+ return numericSize * numericScale;
907
+ }
908
+
859
909
  function scaleRatio(target, source) {
860
910
  const numericTarget = positiveNumber(target);
861
911
  const numericSource = positiveNumber(source);
@@ -865,18 +915,29 @@ function scaleRatio(target, source) {
865
915
 
866
916
  function inferPixelScale({ displayWidth, displayHeight, screenWidth, screenHeight }, options = {}) {
867
917
  const environment = options.environment ?? defaultDesktopEnvironment();
868
- if (environment !== "mac") return 1;
869
-
870
918
  const width = positiveNumber(displayWidth) ?? positiveNumber(screenWidth);
871
919
  const height = positiveNumber(displayHeight) ?? positiveNumber(screenHeight);
872
920
  if (!width || !height) return 1;
873
921
 
922
+ if (environment !== "mac") {
923
+ const widthScale = scaleRatio(width, screenWidth);
924
+ const heightScale = scaleRatio(height, screenHeight);
925
+ if (widthScale && heightScale && nearlyEqual(widthScale, heightScale)) {
926
+ return widthScale;
927
+ }
928
+ return 1;
929
+ }
930
+
874
931
  // libnut reports macOS screen size in backing pixels, while CGEvent mouse
875
932
  // coordinates use logical points. Built-in Retina displays are 2x here.
876
933
  if (width >= 2000 || height >= 1400) return 2;
877
934
  return 1;
878
935
  }
879
936
 
937
+ function nearlyEqual(left, right, epsilon = 0.01) {
938
+ return Math.abs(left - right) <= epsilon;
939
+ }
940
+
880
941
  function positiveNumber(value) {
881
942
  const number = Number(value);
882
943
  return Number.isFinite(number) && number > 0 ? number : null;