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,42 @@
1
+ import { mkdir, mkdtemp, readFile, writeFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { tmpdir } from "node:os";
4
+
5
+ import { initAutomify } from "../src/index.js";
6
+
7
+ const sharedDir = await mkdtemp(join(tmpdir(), "automify-docker-cli-"));
8
+ const dataDir = join(sharedDir, "data");
9
+ const reportPath = join(dataDir, "report.csv");
10
+ const summaryPath = join(dataDir, "summary.json");
11
+
12
+ await mkdir(dataDir, { recursive: true });
13
+ await writeFile(
14
+ reportPath,
15
+ "region,customer,revenue\n" + "North,Ada Corp,1250\n" + "South,Byron Ltd,980\n" + "North,Lovelace Labs,2230\n"
16
+ );
17
+ await writeFile(summaryPath, "{}\n");
18
+
19
+ const automify = initAutomify({
20
+ provider: {
21
+ type: "openai",
22
+ apiKey: process.env.OPENAI_API_KEY,
23
+ model: process.env.OPENAI_MODEL ?? "gpt-5.5"
24
+ }
25
+ });
26
+
27
+ const cli = automify.dockerCli({
28
+ additionalAptPackages: ["coreutils", "nodejs"],
29
+ shared: { hostPath: sharedDir, containerPath: "/workspace" }
30
+ });
31
+
32
+ try {
33
+ const result = await cli.do(
34
+ "Read data/report.csv, use a Node.js script to calculate revenue by region, update data/summary.json with the result, and report the top region"
35
+ );
36
+ const summary = JSON.parse(await readFile(summaryPath, "utf8"));
37
+ console.log(result.text);
38
+ console.log(summary);
39
+ console.log("Shared output file:", summaryPath);
40
+ } finally {
41
+ await cli.close();
42
+ }
@@ -0,0 +1,18 @@
1
+ import { createLocalDesktopComputer, initAutomify } from "../src/index.js";
2
+
3
+ // Run `npm run install:desktop` once before using createLocalDesktopComputer().
4
+ const automify = initAutomify({
5
+ provider: {
6
+ type: "openai",
7
+ apiKey: process.env.OPENAI_API_KEY,
8
+ model: "gpt-5.5"
9
+ }
10
+ });
11
+
12
+ const desktop = automify.computer({
13
+ computer: await createLocalDesktopComputer()
14
+ });
15
+
16
+ await desktop.do("Open the calendar app and check my next meeting", {
17
+ limits: { steps: 12 }
18
+ });
@@ -0,0 +1,48 @@
1
+ import { initAutomify } from "../src/index.js";
2
+
3
+ const customModelAdapter = {
4
+ async respond(payload, context) {
5
+ // Translate automify's Responses-shaped payload to your provider here.
6
+ // Then translate your provider's response back into this shape.
7
+ console.log("Model request:", {
8
+ model: payload.model,
9
+ tools: payload.tools?.map((tool) => tool.type ?? tool.name),
10
+ previousResponseId: payload.previous_response_id,
11
+ context
12
+ });
13
+
14
+ return {
15
+ id: `custom_${Date.now()}`,
16
+ output: [
17
+ {
18
+ type: "message",
19
+ content: [
20
+ {
21
+ type: "output_text",
22
+ text: "This response came from a custom model adapter."
23
+ }
24
+ ]
25
+ }
26
+ ]
27
+ };
28
+ }
29
+ };
30
+
31
+ const automify = initAutomify({
32
+ provider: {
33
+ type: "custom",
34
+ model: "my-computer-use-model",
35
+ adapter: customModelAdapter
36
+ },
37
+ requestOptions: {
38
+ temperature: 0.2,
39
+ metadata: { example: "custom-model-adapter" }
40
+ }
41
+ });
42
+
43
+ const cli = automify.cli({
44
+ cwd: process.cwd()
45
+ });
46
+
47
+ const result = await cli.do("Explain what model adapter is being used");
48
+ console.log(result.response);
@@ -0,0 +1,37 @@
1
+ import { join } from "node:path";
2
+ import { tmpdir } from "node:os";
3
+
4
+ import { createDockerDesktopComputer, initAutomify } from "../src/index.js";
5
+
6
+ const automify = initAutomify({
7
+ provider: {
8
+ type: "openai",
9
+ apiKey: process.env.OPENAI_API_KEY,
10
+ model: process.env.OPENAI_MODEL ?? "gpt-5.5"
11
+ }
12
+ });
13
+
14
+ const computer = await createDockerDesktopComputer({
15
+ desktop: {
16
+ startupCommand: "xterm"
17
+ }
18
+ });
19
+
20
+ try {
21
+ const desktop = automify.computer({ computer });
22
+ const result = await desktop.do(
23
+ "Use the open terminal to run 'uname -a' and summarize the system information shown on screen.",
24
+ {
25
+ screenshots: {
26
+ initial: join(tmpdir(), "automify-docker-desktop-initial.png"),
27
+ final: join(tmpdir(), "automify-docker-desktop-final.png")
28
+ },
29
+ limits: { steps: 12 }
30
+ }
31
+ );
32
+
33
+ console.log(result.text);
34
+ console.log(result.finalScreenshot);
35
+ } finally {
36
+ await computer.close();
37
+ }
@@ -0,0 +1,28 @@
1
+ import { join } from "node:path";
2
+ import { tmpdir } from "node:os";
3
+
4
+ import { createLocalDesktopComputer, initAutomify } from "../src/index.js";
5
+
6
+ // Run `npm run install:desktop` once before using createLocalDesktopComputer().
7
+ const automify = initAutomify({
8
+ provider: {
9
+ type: "openai",
10
+ apiKey: process.env.OPENAI_API_KEY,
11
+ model: process.env.OPENAI_MODEL ?? "gpt-5.5"
12
+ }
13
+ });
14
+
15
+ const desktop = automify.computer({
16
+ computer: await createLocalDesktopComputer()
17
+ });
18
+
19
+ const instruction =
20
+ "Open the Calendar app installed on this computer, find the next event after today, and summarize it. Do not create or edit events.";
21
+
22
+ await desktop.do(instruction, {
23
+ screenshots: {
24
+ initial: join(tmpdir(), "automify-local-desktop-initial.png"),
25
+ final: join(tmpdir(), "automify-local-desktop-final.png")
26
+ },
27
+ limits: { steps: 12 }
28
+ });
@@ -0,0 +1,26 @@
1
+ import { initAutomify, jsonOutput } from "../src/index.js";
2
+
3
+ const imagePath = process.argv[2];
4
+
5
+ if (!imagePath) {
6
+ throw new Error("Usage: node examples/evaluate-image.js /path/to/image.png");
7
+ }
8
+
9
+ const automify = initAutomify({
10
+ provider: {
11
+ type: "openai",
12
+ apiKey: process.env.OPENAI_API_KEY,
13
+ model: process.env.OPENAI_MODEL ?? "gpt-5.5"
14
+ }
15
+ });
16
+
17
+ const cli = automify.cli();
18
+ const result = await cli.do("Evaluate the supplied image and return a concise visual QA report.", {
19
+ evaluate: [{ path: imagePath, detail: "high" }],
20
+ output: jsonOutput("image_evaluation", {
21
+ summary: "string",
22
+ issues: "array"
23
+ })
24
+ });
25
+
26
+ console.log(result.parsed ?? result.text);
@@ -0,0 +1,42 @@
1
+ import { writeFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { tmpdir } from "node:os";
4
+
5
+ import { createDockerDesktopComputer, filesToData, initAutomify } from "../src/index.js";
6
+
7
+ const inputPath = join(tmpdir(), "automify-input.txt");
8
+ await writeFile(inputPath, "Customer: Ada Lovelace\nTask: prepare a short follow-up note\n");
9
+
10
+ const automify = initAutomify({
11
+ provider: {
12
+ type: "openai",
13
+ apiKey: process.env.OPENAI_API_KEY,
14
+ model: process.env.OPENAI_MODEL ?? "gpt-5.5"
15
+ }
16
+ });
17
+
18
+ const fileData = await filesToData(inputPath);
19
+ const computer = await createDockerDesktopComputer({
20
+ desktop: {
21
+ startupCommand: "xterm"
22
+ },
23
+ sharedFiles: [{ path: inputPath, targetPath: "inputs/customer.txt" }]
24
+ });
25
+
26
+ try {
27
+ const desktop = automify.computer({ computer });
28
+ const result = await desktop.do("Open the shared file from data and summarize it in the terminal.", {
29
+ data: {
30
+ files: fileData,
31
+ shared: computer.sharedFolder
32
+ },
33
+ screenshots: {
34
+ final: join(tmpdir(), "automify-shared-folder-final.png")
35
+ }
36
+ });
37
+
38
+ console.log(result.text);
39
+ console.log(computer.sharedFolder);
40
+ } finally {
41
+ await computer.close();
42
+ }
package/package.json ADDED
@@ -0,0 +1,74 @@
1
+ {
2
+ "name": "automify",
3
+ "version": "0.1.0",
4
+ "description": "AI computer use for browser, CLI, and desktop in Node.js.",
5
+ "homepage": "https://aldovincenti.github.io/automify",
6
+ "bugs": {
7
+ "url": "https://github.com/aldovincenti/automify/issues"
8
+ },
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/aldovincenti/automify.git"
12
+ },
13
+ "type": "module",
14
+ "main": "src/index.js",
15
+ "types": "src/index.d.ts",
16
+ "exports": {
17
+ ".": {
18
+ "types": "./src/index.d.ts",
19
+ "import": "./src/index.js"
20
+ },
21
+ "./zod": {
22
+ "types": "./src/zod.d.ts",
23
+ "import": "./src/zod.js"
24
+ }
25
+ },
26
+ "files": [
27
+ "src",
28
+ "scripts",
29
+ "examples",
30
+ "README.md",
31
+ "LICENSE",
32
+ "CHANGELOG.md",
33
+ "SECURITY.md"
34
+ ],
35
+ "scripts": {
36
+ "postinstall": "node scripts/install-browser.js",
37
+ "install:desktop": "node scripts/install-desktop.js",
38
+ "docs:arguments": "node scripts/generate-argument-reference.js",
39
+ "test": "node --test test/*.test.js",
40
+ "test:e2e": "node --test test/e2e/*.e2e.test.js",
41
+ "test:live": "RUN_OPENAI_E2E=1 node --test test/e2e/live-openai.e2e.test.js",
42
+ "format": "prettier --write .",
43
+ "format:check": "prettier --check ."
44
+ },
45
+ "dependencies": {
46
+ "jimp": "^0.22.10",
47
+ "playwright": ">=1.40.0"
48
+ },
49
+ "peerDependencies": {
50
+ "zod": "^4.0.0"
51
+ },
52
+ "peerDependenciesMeta": {
53
+ "zod": {
54
+ "optional": true
55
+ }
56
+ },
57
+ "engines": {
58
+ "node": ">=18.18"
59
+ },
60
+ "keywords": [
61
+ "automation",
62
+ "computer-use",
63
+ "browser",
64
+ "cli",
65
+ "desktop",
66
+ "playwright",
67
+ "openai",
68
+ "responses"
69
+ ],
70
+ "license": "MIT",
71
+ "devDependencies": {
72
+ "prettier": "^3.8.3"
73
+ }
74
+ }
@@ -0,0 +1,17 @@
1
+ import { writeFile } from "node:fs/promises";
2
+ import { argumentReference } from "../src/lib/argument-reference.js";
3
+
4
+ const rows = argumentReference
5
+ .map((entry) => `| \`${entry.surface}\` | ${entry.preferred.map((name) => `\`${name}\``).join(", ")} | ${entry.notes} |`)
6
+ .join("\n");
7
+
8
+ const markdown = `# Automify Argument Reference
9
+
10
+ This file is generated from \`src/lib/argument-reference.js\`.
11
+
12
+ | Surface | Preferred arguments | Notes |
13
+ | ------- | ------------------- | ----- |
14
+ ${rows}
15
+ `;
16
+
17
+ await writeFile(new URL("../docs/argument-reference.md", import.meta.url), markdown);
@@ -0,0 +1,12 @@
1
+ import { spawnSync } from "node:child_process";
2
+
3
+ if (process.env.AUTOMIFY_SKIP_BROWSER_INSTALL === "1" || process.env.PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD === "1") {
4
+ process.exit(0);
5
+ }
6
+
7
+ const result = spawnSync(process.execPath, ["node_modules/playwright/cli.js", "install", "chromium"], {
8
+ cwd: process.cwd(),
9
+ stdio: "inherit"
10
+ });
11
+
12
+ process.exit(result.status ?? 1);
@@ -0,0 +1,281 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { cpSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
+ import { dirname, join, resolve } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+
6
+ const __dirname = dirname(fileURLToPath(import.meta.url));
7
+ const root = resolve(__dirname, "..");
8
+ const buildRoot = process.env.AUTOMIFY_DESKTOP_BUILD_DIR
9
+ ? resolve(process.env.AUTOMIFY_DESKTOP_BUILD_DIR)
10
+ : join(root, ".automify-desktop");
11
+ const nutSource = join(buildRoot, "nut.js");
12
+ const libnutSource = join(buildRoot, "libnut-core");
13
+ const macPermissionsSource = join(buildRoot, "node-mac-permissions");
14
+ const refs = {
15
+ nut: process.env.AUTOMIFY_DESKTOP_NUT_REF ?? "e413fa1f19a19c4631812e4e1eaf47aa732b5cbe",
16
+ libnutCore: process.env.AUTOMIFY_DESKTOP_LIBNUT_CORE_REF ?? "6bbe5825f1123bcd740117ca932c8b1c6cffb48c",
17
+ macPermissions: process.env.AUTOMIFY_DESKTOP_MAC_PERMISSIONS_REF ?? "6b6ddee993ddce5071b637e42f6ee1434150d0bb"
18
+ };
19
+ const nodeModules = join(root, "node_modules");
20
+ const nutScope = join(nodeModules, "@nut-tree");
21
+ const platformPackageName = `@nut-tree/libnut-${process.platform}`;
22
+ const platformPackageDir = join(nutScope, `libnut-${process.platform}`);
23
+ const macPermissionsPackageDir = join(nutScope, "node-mac-permissions");
24
+
25
+ const runtimeDependencies = [
26
+ "jimp@0.22.10",
27
+ "node-abort-controller@3.1.1",
28
+ "clipboardy@2.3.0",
29
+ "bindings@1.5.0"
30
+ ];
31
+
32
+ console.log("Building official nut.js from source.");
33
+ console.log(`Build directory: ${buildRoot}`);
34
+ console.log(`nut.js ref: ${refs.nut}`);
35
+ console.log(`libnut-core ref: ${refs.libnutCore}`);
36
+ if (process.platform === "darwin") {
37
+ console.log(`node-mac-permissions ref: ${refs.macPermissions}`);
38
+ }
39
+
40
+ checkBuildPrerequisites();
41
+
42
+ mkdirSync(buildRoot, { recursive: true });
43
+ cloneOrPull("https://github.com/nut-tree/libnut-core.git", libnutSource, refs.libnutCore);
44
+ cloneOrPull("https://github.com/nut-tree/nut.js.git", nutSource, refs.nut);
45
+ if (process.platform === "darwin") {
46
+ cloneOrPull("https://github.com/nut-tree/node-mac-permissions.git", macPermissionsSource, refs.macPermissions);
47
+ }
48
+
49
+ patchNutWorkspace();
50
+
51
+ run("npm", ["install"], { cwd: libnutSource });
52
+ buildLibnutCore();
53
+
54
+ run("node", ["patch-packagename.js"], {
55
+ cwd: libnutSource,
56
+ env: { ...process.env, CI: "1" }
57
+ });
58
+
59
+ if (process.platform === "darwin") {
60
+ run("npm", ["install"], { cwd: macPermissionsSource });
61
+ run("npm", ["run", "build:release"], { cwd: macPermissionsSource });
62
+ }
63
+
64
+ patchPlatformLibnutDependency();
65
+
66
+ runPnpm(["install"], { cwd: nutSource });
67
+ runPnpm(["--filter", "@nut-tree/shared", "run", "compile"], { cwd: nutSource });
68
+ runPnpm(["--filter", "@nut-tree/provider-interfaces", "run", "compile"], { cwd: nutSource });
69
+ runPnpm(["--filter", "@nut-tree/default-clipboard-provider", "run", "compile"], { cwd: nutSource });
70
+ runPnpm(["--filter", "@nut-tree/libnut", "run", "compile"], { cwd: nutSource });
71
+ runPnpm(["--filter", "@nut-tree/nut-js", "run", "compile"], { cwd: nutSource });
72
+
73
+ run("npm", ["install", "--no-save", ...runtimeDependencies], { cwd: root });
74
+
75
+ installWorkspacePackage(join(nutSource, "core", "shared"), join(nutScope, "shared"));
76
+ installWorkspacePackage(join(nutSource, "core", "provider-interfaces"), join(nutScope, "provider-interfaces"));
77
+ installWorkspacePackage(join(nutSource, "providers", "clipboardy"), join(nutScope, "default-clipboard-provider"));
78
+ installWorkspacePackage(join(nutSource, "providers", "libnut"), join(nutScope, "libnut"));
79
+ installWorkspacePackage(join(nutSource, "core", "nut.js"), join(nutScope, "nut-js"));
80
+ installWorkspacePackage(libnutSource, platformPackageDir);
81
+ if (process.platform === "darwin") {
82
+ installWorkspacePackage(macPermissionsSource, macPermissionsPackageDir);
83
+ }
84
+
85
+ run("node", ["-e", "import('@nut-tree/nut-js').then(() => console.log('nut.js source build import ok'))"], { cwd: root });
86
+
87
+ function cloneOrPull(repo, target, ref) {
88
+ if (existsSync(join(target, ".git"))) {
89
+ run("git", ["-C", target, "fetch", "--depth", "1", "origin", ref]);
90
+ checkoutFetchedRef(target, ref);
91
+ run("git", ["-C", target, "clean", "-fd"]);
92
+ return;
93
+ }
94
+
95
+ rmSync(target, { recursive: true, force: true });
96
+ run("git", ["clone", "--depth", "1", "--branch", ref, repo, target], { exitOnError: false }).ok ||
97
+ run("git", ["clone", "--depth", "1", repo, target]);
98
+ if (!currentRefMatches(target, ref)) {
99
+ run("git", ["-C", target, "fetch", "--depth", "1", "origin", ref]);
100
+ checkoutFetchedRef(target, ref);
101
+ }
102
+ }
103
+
104
+ function checkoutFetchedRef(target, ref) {
105
+ run("git", ["-C", target, "checkout", "--detach", "FETCH_HEAD"]);
106
+ }
107
+
108
+ function currentRefMatches(target, ref) {
109
+ const result = spawnSync("git", ["-C", target, "rev-parse", "--verify", `${ref}^{commit}`], {
110
+ cwd: root,
111
+ encoding: "utf8"
112
+ });
113
+ return (result.status ?? 1) === 0;
114
+ }
115
+
116
+ function checkBuildPrerequisites() {
117
+ const missing = [];
118
+
119
+ for (const command of ["git", "npm", "cmake"]) {
120
+ if (!commandExists(command)) missing.push(command);
121
+ }
122
+
123
+ if (missing.length === 0) return;
124
+
125
+ console.error(`Missing required desktop build tool(s): ${missing.join(", ")}`);
126
+ console.error("Install the native build prerequisites, then rerun: npm run install:desktop");
127
+
128
+ if (process.platform === "darwin") {
129
+ console.error("macOS: install Xcode Command Line Tools with `xcode-select --install` and install CMake.");
130
+ } else if (process.platform === "linux") {
131
+ console.error("Linux: install CMake, a C/C++ compiler, libxtst-dev, and libpng++-dev.");
132
+ } else if (process.platform === "win32") {
133
+ console.error("Windows: install CMake and Visual Studio C++ Build Tools.");
134
+ }
135
+
136
+ process.exit(1);
137
+ }
138
+
139
+ function commandExists(command) {
140
+ const result = spawnSync(command, ["--version"], {
141
+ cwd: root,
142
+ stdio: "ignore"
143
+ });
144
+ return (result.status ?? 1) === 0;
145
+ }
146
+
147
+ function patchPlatformLibnutDependency() {
148
+ const packagePath = join(nutSource, "providers", "libnut", "package.json");
149
+ const packageJson = JSON.parse(readText(packagePath));
150
+ packageJson.dependencies = {
151
+ [platformPackageName]: `file:${libnutSource}`
152
+ };
153
+ writeText(packagePath, `${JSON.stringify(packageJson, null, 2)}\n`);
154
+ }
155
+
156
+ function buildLibnutCore() {
157
+ const build = run("npm", ["run", "build:release"], {
158
+ cwd: libnutSource,
159
+ exitOnError: false
160
+ });
161
+
162
+ if (build.ok) return;
163
+
164
+ if (!shouldPatchMacosSdk15Build(build)) {
165
+ process.exit(build.status ?? 1);
166
+ }
167
+
168
+ console.warn("libnut-core failed against the macOS SDK 15 screen-capture availability check.");
169
+ console.warn("Applying local compatibility patch for CGDisplayCreateImageForRect and retrying.");
170
+ patchLibnutCoreForMacosSdk15();
171
+ run("npm", ["run", "build:release"], { cwd: libnutSource });
172
+ }
173
+
174
+ function shouldPatchMacosSdk15Build(result) {
175
+ if (process.platform !== "darwin") return false;
176
+ if (process.env.AUTOMIFY_DESKTOP_DISABLE_MACOS_SDK15_PATCH === "1") return false;
177
+ return `${result.stdout}\n${result.stderr}`.includes("CGDisplayCreateImageForRect");
178
+ }
179
+
180
+ function patchLibnutCoreForMacosSdk15() {
181
+ if (process.platform !== "darwin") return;
182
+
183
+ const screenGrabPath = join(libnutSource, "src", "macos", "screengrab.m");
184
+ let source = readText(screenGrabPath);
185
+
186
+ if (source.includes("AutomifyCGDisplayCreateImageForRect")) return;
187
+
188
+ source = source.replace(
189
+ "#import <Cocoa/Cocoa.h>",
190
+ `#import <Cocoa/Cocoa.h>
191
+ #include <dlfcn.h>
192
+
193
+ typedef CGImageRef (*AutomifyCGDisplayCreateImageForRectFn)(CGDirectDisplayID, CGRect);
194
+
195
+ static CGImageRef AutomifyCGDisplayCreateImageForRect(CGDirectDisplayID displayID, CGRect rect) {
196
+ AutomifyCGDisplayCreateImageForRectFn fn = (AutomifyCGDisplayCreateImageForRectFn)dlsym(RTLD_DEFAULT, "CGDisplayCreateImageForRect");
197
+ if (!fn) { return NULL; }
198
+ return fn(displayID, rect);
199
+ }`
200
+ );
201
+ source = source.replace("CGDisplayCreateImageForRect(displayID,", "AutomifyCGDisplayCreateImageForRect(displayID,");
202
+
203
+ writeText(screenGrabPath, source);
204
+ }
205
+
206
+ function patchNutWorkspace() {
207
+ writeText(
208
+ join(nutSource, "pnpm-workspace.yaml"),
209
+ `packages:
210
+ - 'core/*'
211
+ - 'providers/libnut'
212
+ - 'providers/clipboardy'
213
+ `
214
+ );
215
+
216
+ for (const relativePath of [
217
+ ["providers", "clipboardy", "package.json"],
218
+ ["providers", "libnut", "package.json"]
219
+ ]) {
220
+ const packagePath = join(nutSource, ...relativePath);
221
+ const packageJson = JSON.parse(readText(packagePath));
222
+ delete packageJson.peerDependencies;
223
+ writeText(packagePath, `${JSON.stringify(packageJson, null, 2)}\n`);
224
+ }
225
+ }
226
+
227
+ function installWorkspacePackage(source, target) {
228
+ rmSync(target, { recursive: true, force: true });
229
+ mkdirSync(dirname(target), { recursive: true });
230
+ cpSync(source, target, {
231
+ recursive: true,
232
+ filter: (file) => !file.includes(`${source}/node_modules`) && !file.includes(`${source}/coverage`)
233
+ });
234
+ }
235
+
236
+ function run(command, args, options = {}) {
237
+ const executable = process.platform === "win32" && ["npm", "npx"].includes(command) ? `${command}.cmd` : command;
238
+ const result = spawnSync(executable, args, {
239
+ cwd: options.cwd ?? root,
240
+ env: options.env ?? process.env,
241
+ shell: false,
242
+ stdio: options.exitOnError === false ? "pipe" : "inherit",
243
+ encoding: options.exitOnError === false ? "utf8" : undefined
244
+ });
245
+ const commandResult = {
246
+ ok: (result.status ?? 1) === 0,
247
+ status: result.status ?? 1,
248
+ stdout: result.stdout ?? "",
249
+ stderr: result.stderr ?? "",
250
+ error: result.error
251
+ };
252
+
253
+ if (!commandResult.ok) {
254
+ if (options.exitOnError === false) {
255
+ if (commandResult.stdout) process.stdout.write(commandResult.stdout);
256
+ if (commandResult.stderr) process.stderr.write(commandResult.stderr);
257
+ return commandResult;
258
+ }
259
+ if (options.allowMissing && result.error?.code === "ENOENT") return;
260
+ process.exit(commandResult.status);
261
+ }
262
+
263
+ if (options.exitOnError === false) {
264
+ if (commandResult.stdout) process.stdout.write(commandResult.stdout);
265
+ if (commandResult.stderr) process.stderr.write(commandResult.stderr);
266
+ }
267
+
268
+ return commandResult;
269
+ }
270
+
271
+ function runPnpm(args, options = {}) {
272
+ run("npx", ["--yes", "pnpm@8.15.2", ...args], options);
273
+ }
274
+
275
+ function readText(path) {
276
+ return readFileSync(path, "utf8");
277
+ }
278
+
279
+ function writeText(path, text) {
280
+ writeFileSync(path, text);
281
+ }