executor 2.0.0-beta.4 → 2.0.0-beta.40

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
@@ -1,27 +1,62 @@
1
1
  # Executor 2 beta
2
2
 
3
- Install with `npm i -g executor@beta`. Requires Node 24.14 or newer.
4
- Run `executor` to start the server and open the dashboard. Use `executor serve`
5
- for headless operation and `executor pair` to get a new browser connection link.
6
-
7
- The package includes the dashboard, framework, runtime compiler, pinned Bun,
8
- Git, and the Motel collector. It does not need a source checkout, Bun, or
9
- 1Password on the user machine. Native runtimes are selected by npm for macOS
10
- (Apple Silicon and Intel), Linux (arm64 and x64), and Windows (x64).
3
+ Install with npm, `npm i -g executor@beta`, on Node 14.18 or newer. Without Node, run `curl -fsSL https://executor.sh/docs/install.sh | sh`
4
+ on macOS or Linux, or `irm https://executor.sh/docs/install.ps1 | iex` in
5
+ PowerShell on Windows. Each release also attaches its archives and `SHA256SUMS`.
6
+ Run `executor` to start the background Executor, or join the one already
7
+ running, and open the dashboard. The background Executor keeps running until
8
+ `executor service stop`; nothing starts it with the OS. `executor` checks npm
9
+ daily for a newer release and suggests `executor upgrade`; set
10
+ `EXECUTOR_NO_UPDATE_CHECK=1` to skip it. Use `executor service start` to start it
11
+ without a browser, `executor serve` to run it in the foreground, and
12
+ `executor pair` to get a new browser connection link.
13
+ `executor upgrade` installs the newest release on your channel the way Executor
14
+ was installed: an install-script copy downloads and checks the release and keeps
15
+ the previous version for `executor upgrade --rollback`; an npm, pnpm, Bun or
16
+ Yarn install runs its package manager. Either way it stops the background
17
+ Executor before replacing its files and starts it again on the new version; a
18
+ release from before the background service, which a rollback can return to, has
19
+ no `executor service`, so run `executor serve` there instead.
20
+
21
+ Executor is one executable, built with Bun, with the files it runs or reads
22
+ beside it: the dashboard, the app framework and compiler references, workerd,
23
+ Git, the native credential-store binding and the Motel collector. It needs no
24
+ source checkout, Node, Bun or 1Password on the user machine. The `executor` npm
25
+ package is a launcher. The first time a version starts, it downloads that
26
+ release's archive for the platform, for macOS (Apple Silicon and Intel), Linux
27
+ (arm64 and x64, glibc) or Windows (x64), from the public GitHub release, checks it
28
+ against the SHA-256 the package records, and keeps it in
29
+ `~/.executor/v2/launcher/<version>` with the version before it. The same archive
30
+ is the release download the install scripts and Homebrew use.
31
+ `EXECUTOR_DOWNLOAD_URL` names a mirror of the releases for all of them.
11
32
 
12
33
  Beta uses `~/.executor/v2/cli` for its data. This path stays the same at the
13
- stable release. It is separate from Executor 1 data. Do not run two servers
14
- against the same directory. `EXECUTOR_DATA_DIR` selects another directory and
15
- `EXECUTOR_PORT` changes the default port of 4312.
34
+ stable release. It is separate from Executor 1 data. One server runs per data
35
+ directory; its `.server.lock` stops a second. `EXECUTOR_DATA_DIR` selects another
36
+ directory, and `EXECUTOR_PORT` or `executor service set port` changes the default
37
+ port of 4312. The desktop app uses the same directory and background Executor.
38
+ Its first launch moves the data of a desktop release from before the background
39
+ service into that directory, once. CLI data already there moves to
40
+ `~/.executor/v2/backups/cli-data-<time>`, which the desktop app names; nothing
41
+ is merged or deleted. The CLI's settings carry over, except its port and the
42
+ `env` variables that belong to its data.
16
43
 
17
44
  On first launch, Executor saves its API and encryption keys in the OS credential
18
45
  store: macOS Keychain, Windows Credential Manager, or Linux Secret Service.
19
- Linux requires a persistent Secret Service such as GNOME Keyring. There is no
20
- plaintext or in-memory fallback. A locked, unavailable, missing or invalid
21
- credential stops startup; existing installations never receive replacement keys.
22
-
23
- Back up the data directory, including `installation.json`, and the matching OS
24
- credential (`com.usefulsoftware.executor.v2`, account ID from that file).
46
+ Linux uses a persistent Secret Service such as GNOME Keyring. If there is no
47
+ store on a directory's first start, the keys are saved to `keys.json` in the
48
+ data directory (mode `0600`) and the path is printed to stderr. If a store exists
49
+ but access is denied or it is locked, startup stops without a key file and the
50
+ next start asks again. Over SSH, unlock the store first, or set
51
+ `EXECUTOR_KEY_STORAGE=file` before a directory's first start to use `keys.json`
52
+ (`os` requires the store instead). A directory that already uses the OS store stops when it
53
+ is locked or unavailable. A missing or
54
+ invalid credential or key file stops startup; existing installations never
55
+ receive replacement keys.
56
+
57
+ Back up the data directory, including `installation.json` and any `keys.json`,
58
+ and the matching OS credential (`com.usefulsoftware.executor.v2`, account ID
59
+ from that file) when the directory uses one.
25
60
  Copying only the data directory to another machine is not enough to recover it.
26
61
  For a new headless install, supply both `EXECUTOR_API_KEY` (32+ characters) and
27
62
  `EXECUTOR_ENCRYPTION_KEY` (64 hexadecimal characters) through a secret manager.
@@ -38,29 +73,67 @@ version publishes to `latest` only through an explicitly selected stable release
38
73
 
39
74
  ```sh
40
75
  bun install --frozen-lockfile
41
- bun run release:cli
76
+ EXECUTOR_BUN_RUNTIME=/path/to/locked-down/bun bun run release:cli
42
77
  bun run release:wrapper
43
78
  ```
44
79
 
45
80
  Native artifacts are under `.local/releases/<version>-<platform>-<arch>/`.
46
- The npm launcher is under `.local/releases/<version>/wrapper/` and uses exact
47
- native version aliases. These build commands do not publish anything.
48
- The native package bundles the CLI and desktop server together. The app compiler
49
- and framework are prepared for workerd at build time. Native libraries, Git,
50
- Motel and authoring references remain included. No runtime download is needed.
51
- Each compressed native archive must fit within 180 MiB before publication.
52
- This is a release budget with margin, not a documented npm registry limit.
81
+ `release:cli` bundles the runtime twice. `package/` holds the JavaScript runtime
82
+ the desktop app runs with its own Node. `standalone/` holds the release install:
83
+ the `executor` executable (`bun build --compile`) and the files beside it, which
84
+ `executor-<version>-<platform>-<arch>.tgz` packs as the platform's release
85
+ archive. The executable takes `runtime/cli.mjs` in its install directory as
86
+ every module's location, so resources, `require` and `import.meta.resolve` find
87
+ the same files beside it wherever the archive is unpacked, through any launcher
88
+ symlink. Packages that must stay on disk load from
89
+ `runtime/node_modules`; Git is in `runtime/git`.
90
+ The npm launcher is under `.local/releases/<version>/wrapper/`. Its
91
+ package.json records the SHA-256 of each platform archive found under
92
+ `.local/releases/<version>-<platform>-<arch>/`, or under the directory
93
+ `release:wrapper` is given; publishing requires all five. The runtimes are never
94
+ published to npm. Through 2.0.0-beta.36 each was published as a version of the
95
+ `executor` package, such as `2.0.0-beta.36-win32-x64`, which semver ranks above
96
+ the launcher, so a range update such as `bun update -g` replaced the launcher
97
+ with a runtime. Those versions are deprecated so package managers skip them.
98
+ These build commands do not publish anything.
99
+ The app compiler and framework are prepared for workerd at build time, so the
100
+ executable downloads nothing more. Most of each archive is the executable,
101
+ workerd and the bundled Git, which keeps only what the runtime runs (`keptGit`
102
+ in `runtime-bundle.ts`).
53
103
  External source maps stay in the separate CI artifact for release diagnostics.
54
104
 
105
+ Stock Bun lets any local process run its own code inside a compiled executable,
106
+ through `BUN_OPTIONS`, `BUN_BE_BUN`, `BUN_INSPECT` and similar variables that
107
+ Bun reads before the executable's first line. The release executable is built
108
+ on a Bun runtime patched to ignore them, from
109
+ [`bun-runtime/`](bun-runtime/README.md). `EXECUTOR_BUN_RUNTIME` names it; CI and
110
+ publishing builds refuse stock Bun, and a local build without it prints that it
111
+ is a development build. The release workflow fetches the runtime from the
112
+ `bun-runtime-<bun version>-<patch hash>` release, building it first when the
113
+ patch changed.
114
+
115
+ The install scripts are `apps/docs/public/install.sh` and `install.ps1`, served
116
+ at `https://executor.sh/docs/`. The publish job writes the Homebrew formula
117
+ (`scripts/releases/homebrew.ts`) to the tap named by the `HOMEBREW_TAP`
118
+ repository variable once it is set.
119
+
55
120
  The prepared packages retain dependency licenses. Public releases also include
56
121
  checksum-pinned source archives for the bundled Git distribution.
57
122
 
58
123
  ## macOS signing
59
124
 
60
- `node scripts/releases/desktop.ts <version>` is unsigned by default, which is a
61
- development build: Gatekeeper rejects it until someone overrides it by hand.
125
+ `node scripts/releases/desktop.ts <version>` and `node scripts/releases/package.ts`
126
+ are unsigned by default, which is a development build: Gatekeeper rejects it
127
+ until someone overrides it by hand.
62
128
  Add `--sign` to sign with a Developer ID Application identity, or `--notarize`
63
- to sign and submit the result to Apple. Both modes keep electron-builder's
129
+ to sign and submit the result to Apple. A publishing build of the CLI on macOS
130
+ (`EXECUTOR_RELEASE_PUBLISH=true`) fails unless it notarizes. For the CLI, every
131
+ Mach-O file in the install is signed with the hardened runtime, the executable
132
+ last as `com.usefulsoftware.executor.v2.cli` with
133
+ `entitlements.standalone.plist`: JIT for Bun and workerd, and library validation
134
+ left on, so it loads only libraries signed with the same identity. The whole
135
+ install is notarized as one archive; a bare executable cannot hold a stapled
136
+ ticket, so Gatekeeper checks it online. Both modes keep electron-builder's
64
137
  identity auto-discovery off, so a build never picks up a stray keychain identity.
65
138
 
66
139
  Signing accepts these environment variables and keeps plaintext keys off ordinary disk:
package/bin/executor ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import("../bin.mjs");
@@ -0,0 +1 @@
1
+ {"type":"commonjs"}
package/bin.mjs CHANGED
@@ -1,15 +1,245 @@
1
1
  #!/usr/bin/env node
2
- const name = "executor-" + process.platform + "-" + process.arch;
3
- const supported = ["executor-darwin-arm64","executor-darwin-x64","executor-linux-x64","executor-linux-arm64","executor-win32-x64"];
4
- if (!supported.includes(name)) {
5
- console.error("Executor does not support " + process.platform + "/" + process.arch + ".");
6
- process.exitCode = 1;
7
- } else {
8
- let entry;
9
- try { entry = import.meta.resolve(name + "/bin.mjs"); }
10
- catch {
11
- console.error("Executor's native runtime is missing. Reinstall with: npm i -g executor@beta --include=optional");
12
- process.exitCode = 1;
2
+ // The `executor` npm package's command. The package holds no runtime: on first use of a version it
3
+ // downloads that release's archive for this platform from the public GitHub release, checks it
4
+ // against the SHA-256 this package's package.json records, and unpacks it into
5
+ // ~/.executor/v2/launcher/<version>. It then starts the Executor executable there, passes signals
6
+ // on, and exits as it exits.
7
+ //
8
+ // npm, Bun, pnpm and Yarn resolve `executor` alone, so an update can only ever land on another
9
+ // launcher; the running executable lives outside node_modules, so an update never has to replace a
10
+ // file in use. The executable carries its own runtime, so this file uses only what every Node since
11
+ // 14 has.
12
+ import { spawn, spawnSync } from "child_process";
13
+ import { createHash, randomBytes } from "crypto";
14
+ import {
15
+ createWriteStream,
16
+ existsSync,
17
+ mkdirSync,
18
+ readdirSync,
19
+ renameSync,
20
+ rmSync,
21
+ statSync,
22
+ utimesSync,
23
+ } from "fs";
24
+ import { request as httpRequest } from "http";
25
+ import { request as httpsRequest } from "https";
26
+ import { createRequire } from "module";
27
+ import { homedir } from "os";
28
+ import { join } from "path";
29
+ import { fileURLToPath } from "url";
30
+
31
+ const require = createRequire(import.meta.url);
32
+ const manifest = require("./package.json");
33
+ const version = manifest.version;
34
+ const target = process.platform + "-" + process.arch;
35
+ const digest = (manifest.runtimes || {})[target];
36
+ const channel = version.indexOf("-beta.") === -1 ? "" : "@beta";
37
+ const executableName = process.platform === "win32" ? "executor.exe" : "executor";
38
+ const archive = "executor-" + version + "-" + target + ".tgz";
39
+ // The same mirror setting the install scripts and `executor upgrade` read.
40
+ const downloads = (
41
+ process.env.EXECUTOR_DOWNLOAD_URL ||
42
+ "https://github.com/UsefulSoftwareCo/executor/releases/download"
43
+ ).replace(/\/+$/, "");
44
+ const url = downloads + "/executor@" + version + "/" + archive;
45
+ const root = join(homedir(), ".executor", "v2", "launcher");
46
+ const installed = join(root, version);
47
+ const executable = join(installed, executableName);
48
+
49
+ const fail = (lines) => {
50
+ for (const line of lines) console.error(line);
51
+ process.exit(1);
52
+ };
53
+
54
+ if (typeof digest !== "string")
55
+ fail(["Executor does not support " + process.platform + "/" + process.arch + "."]);
56
+
57
+ /** GET `address`, following redirects; resolves to the response of the final location. */
58
+ const get = (address, redirects) =>
59
+ new Promise((resolve, reject) => {
60
+ const request = (address.startsWith("http:") ? httpRequest : httpsRequest)(
61
+ address,
62
+ { headers: { "user-agent": "executor-launcher/" + version } },
63
+ (response) => {
64
+ const status = response.statusCode || 0;
65
+ if (status >= 300 && status < 400 && response.headers.location) {
66
+ response.resume();
67
+ if (redirects >= 10) return reject(new Error("too many redirects"));
68
+ return resolve(get(new URL(response.headers.location, address).href, redirects + 1));
69
+ }
70
+ if (status !== 200) {
71
+ response.resume();
72
+ return reject(new Error("HTTP " + status));
73
+ }
74
+ resolve(response);
75
+ },
76
+ );
77
+ // A connection that stops sending for a minute is not coming back.
78
+ request.setTimeout(60000, () => request.destroy(new Error("the connection stalled")));
79
+ request.on("error", reject);
80
+ request.end();
81
+ });
82
+
83
+ /** Download the archive to `file`, hashing it as it arrives; resolves to its SHA-256. */
84
+ const download = async (file) => {
85
+ const response = await get(url, 0);
86
+ const total = Number(response.headers["content-length"]) || 0;
87
+ const hash = createHash("sha256");
88
+ const out = createWriteStream(file);
89
+ const tty = process.stderr.isTTY;
90
+ let received = 0;
91
+ let shown = -1;
92
+ const label = "Downloading Executor " + version + " for " + target;
93
+ if (!tty) console.error(label + " (first run of this version)");
94
+ await new Promise((resolve, reject) => {
95
+ response.on("data", (chunk) => {
96
+ hash.update(chunk);
97
+ received += chunk.length;
98
+ const step = total ? Math.floor((received * 100) / total) : Math.floor(received / 1048576);
99
+ if (tty && step !== shown) {
100
+ shown = step;
101
+ const amount = (received / 1048576).toFixed(1) + " MB";
102
+ process.stderr.write(
103
+ "\r\x1b[2K" + label + (total ? " " + step + "%" : "") + " (" + amount + ")",
104
+ );
105
+ }
106
+ });
107
+ response.on("error", reject);
108
+ out.on("error", reject);
109
+ out.on("finish", resolve);
110
+ response.pipe(out);
111
+ });
112
+ if (tty) process.stderr.write("\r\x1b[2K");
113
+ return hash.digest("hex");
114
+ };
115
+
116
+ /**
117
+ * Keep this version and the most recently installed other one, which a background Executor started
118
+ * before an update may still run from, as the install scripts keep `previous`. Downloads that
119
+ * stopped part way are removed after a day. Anything in use, which Windows refuses to delete, stays
120
+ * for a later download to remove.
121
+ */
122
+ const prune = () => {
123
+ const day = 24 * 60 * 60 * 1000;
124
+ const entries = readdirSync(root)
125
+ .filter((name) => name !== version)
126
+ .map((name) => ({ name, time: statSync(join(root, name)).mtimeMs }));
127
+ const versions = entries.filter((entry) => !entry.name.startsWith("."));
128
+ versions.sort((left, right) => right.time - left.time);
129
+ const stale = versions
130
+ .slice(1)
131
+ .concat(entries.filter((entry) => entry.name.startsWith(".") && Date.now() - entry.time > day));
132
+ for (const entry of stale) {
133
+ try {
134
+ rmSync(join(root, entry.name), { recursive: true, force: true });
135
+ } catch {
136
+ // In use; a later download removes it.
137
+ }
13
138
  }
14
- if (entry !== undefined) await import(entry);
15
- }
139
+ };
140
+
141
+ const install = async () => {
142
+ mkdirSync(root, { recursive: true });
143
+ const staging = join(root, ".download-" + process.pid + "-" + randomBytes(4).toString("hex"));
144
+ mkdirSync(staging);
145
+ // `fail` exits at once, without running `finally`.
146
+ const cleanup = () => rmSync(staging, { recursive: true, force: true });
147
+ process.on("exit", cleanup);
148
+ try {
149
+ let actual;
150
+ try {
151
+ actual = await download(join(staging, archive));
152
+ } catch (error) {
153
+ fail([
154
+ "Executor could not download its runtime from " + url + ": " + error.message + ".",
155
+ "Check your connection and try again. EXECUTOR_DOWNLOAD_URL names a mirror of the releases.",
156
+ ]);
157
+ }
158
+ if (actual !== digest)
159
+ fail([
160
+ archive +
161
+ " does not match the SHA-256 this package records (expected " +
162
+ digest +
163
+ ", got " +
164
+ actual +
165
+ ").",
166
+ "Nothing was installed. Check EXECUTOR_DOWNLOAD_URL, or reinstall with: npm i -g executor" +
167
+ channel,
168
+ ]);
169
+ // The system's tar, as the install scripts use: Windows' own bsdtar, since Git's GNU tar on
170
+ // PATH reads a drive letter as a remote host. The archive path is relative for the same reason.
171
+ const tar =
172
+ process.platform === "win32"
173
+ ? join(process.env.SystemRoot || "C:\\Windows", "System32", "tar.exe")
174
+ : "tar";
175
+ // Nothing reaches stdout, which an MCP client starting `executor mcp` reads as the protocol.
176
+ const unpacked = spawnSync(tar, ["-xzf", archive], {
177
+ cwd: staging,
178
+ stdio: ["ignore", "ignore", "inherit"],
179
+ });
180
+ if (unpacked.status !== 0)
181
+ fail([
182
+ "Executor could not unpack " +
183
+ archive +
184
+ " with " +
185
+ tar +
186
+ (unpacked.error ? ": " + unpacked.error.message : "."),
187
+ ]);
188
+ try {
189
+ renameSync(join(staging, "package"), installed);
190
+ } catch (error) {
191
+ // Another `executor` command installed this version first.
192
+ if (!existsSync(executable))
193
+ fail([
194
+ "Executor could not install its runtime in " + installed + ": " + error.message,
195
+ "Remove " + installed + " and try again.",
196
+ ]);
197
+ }
198
+ // The version's time orders it for pruning; the archive's own times are from 1985.
199
+ const now = new Date();
200
+ utimesSync(installed, now, now);
201
+ } finally {
202
+ process.removeListener("exit", cleanup);
203
+ cleanup();
204
+ }
205
+ try {
206
+ prune();
207
+ } catch {
208
+ // Pruning never stops Executor from starting.
209
+ }
210
+ };
211
+
212
+ if (!existsSync(executable)) await install();
213
+
214
+ const child = spawn(executable, process.argv.slice(2), {
215
+ stdio: "inherit",
216
+ env: Object.assign({}, process.env, {
217
+ // How Executor was installed, for `executor upgrade` and its update notice.
218
+ EXECUTOR_LAUNCHER: fileURLToPath(import.meta.url),
219
+ EXECUTOR_LAUNCHER_NODE: process.versions.node,
220
+ }),
221
+ });
222
+
223
+ // A terminal's Ctrl-C already reaches the whole process group, and on Windows every process on
224
+ // the console. A service manager signals only this process, so the signal is passed on. Windows
225
+ // has no signals to pass: killing the child there would end it at once, before its shutdown.
226
+ const signals =
227
+ process.platform === "win32" ? ["SIGINT", "SIGBREAK"] : ["SIGINT", "SIGTERM", "SIGHUP"];
228
+ const forward = (signal) => {
229
+ if (process.platform !== "win32" && child.exitCode === null && child.signalCode === null)
230
+ child.kill(signal);
231
+ };
232
+ for (const signal of signals) process.on(signal, forward);
233
+
234
+ child.on("error", (error) =>
235
+ fail([
236
+ "Executor could not start " + executable + ": " + error.message,
237
+ "Remove " + installed + " to download it again.",
238
+ ]),
239
+ );
240
+ child.on("exit", (code, signal) => {
241
+ for (const name of signals) process.removeListener(name, forward);
242
+ // Ending by the same signal tells the shell what stopped Executor.
243
+ if (signal !== null) process.kill(process.pid, signal);
244
+ else process.exit(code === null ? 1 : code);
245
+ });
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "executor",
3
- "version": "2.0.0-beta.4",
3
+ "version": "2.0.0-beta.40",
4
4
  "private": false,
5
5
  "description": "Run Executor locally and connect your agents to your apps.",
6
- "homepage": "https://v2.executor.sh",
6
+ "homepage": "https://executor.sh",
7
7
  "license": "MIT",
8
8
  "repository": {
9
9
  "type": "git",
@@ -11,13 +11,15 @@
11
11
  },
12
12
  "type": "module",
13
13
  "engines": {
14
- "node": ">=24.14.0"
14
+ "node": ">=14.18.0"
15
15
  },
16
16
  "bin": {
17
17
  "executor": "bin.mjs"
18
18
  },
19
19
  "files": [
20
20
  "bin.mjs",
21
+ "bin/executor",
22
+ "bin/package.json",
21
23
  "README.md",
22
24
  "LICENSE"
23
25
  ],
@@ -25,11 +27,11 @@
25
27
  "access": "public",
26
28
  "tag": "beta"
27
29
  },
28
- "optionalDependencies": {
29
- "executor-darwin-arm64": "npm:executor@2.0.0-beta.4-darwin-arm64",
30
- "executor-darwin-x64": "npm:executor@2.0.0-beta.4-darwin-x64",
31
- "executor-linux-x64": "npm:executor@2.0.0-beta.4-linux-x64",
32
- "executor-linux-arm64": "npm:executor@2.0.0-beta.4-linux-arm64",
33
- "executor-win32-x64": "npm:executor@2.0.0-beta.4-win32-x64"
30
+ "runtimes": {
31
+ "darwin-arm64": "85ae709121e94e10216e3a8e7b6e547fa2fb1f40077507d3e6c2fa21776da07a",
32
+ "darwin-x64": "6ec9505e1aeae66bb13370577a1446fbabdaf7ad7a3947f377de0513e44c8bc5",
33
+ "linux-x64": "ee4e11927b3067e80948f1ffb9187ad52f9bb39fee2ae7c22fa39933d8aa0cac",
34
+ "linux-arm64": "23791582608c449cd070668cebe024df2dbb11b914a32aec4abbd2abb7c0e080",
35
+ "win32-x64": "cd7802407951ed968aa150f301a0727763697f13fcf4f512f62e2eac15e7ab19"
34
36
  }
35
37
  }