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 +17 -12
- package/package.json +2 -2
- package/scripts/install-browser.js +1 -0
- package/scripts/install-desktop-if-needed.js +42 -0
- package/scripts/install-desktop.js +73 -17
- package/src/lib/argument-reference.js +3 -3
- package/src/lib/desktop-runtime.js +135 -0
- package/src/lib/local-desktop-computer.js +71 -10
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
|
|
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("
|
|
123
|
+
const run = await browser.do("Summarize what you see on the page.", {
|
|
124
124
|
// Optional: structured result shape.
|
|
125
|
-
output: jsonOutput("
|
|
125
|
+
output: jsonOutput("page_summary", { title: "string", summary: "string" })
|
|
126
126
|
});
|
|
127
127
|
|
|
128
|
-
console.log(run.parsed.
|
|
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.
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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:
|
|
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
|
-
|
|
85
|
-
|
|
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
|
|
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)
|
|
175
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
const mouseHeight =
|
|
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;
|