executor 2.0.0-beta.36 → 2.0.0-beta.38

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 (3) hide show
  1. package/README.md +26 -19
  2. package/bin.mjs +199 -22
  3. package/package.json +7 -7
package/README.md CHANGED
@@ -1,7 +1,6 @@
1
1
  # Executor 2 beta
2
2
 
3
- Install with npm, `npm i -g executor@beta --include=optional`, on Node 14.18
4
- or newer. Without Node, run `curl -fsSL https://executor.sh/docs/install.sh | sh`
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`
5
4
  on macOS or Linux, or `irm https://executor.sh/docs/install.ps1 | iex` in
6
5
  PowerShell on Windows. Each release also attaches its archives and `SHA256SUMS`.
7
6
  Run `executor` to start the background Executor, or join the one already
@@ -23,10 +22,13 @@ Executor is one executable, built with Bun, with the files it runs or reads
23
22
  beside it: the dashboard, the app framework and compiler references, workerd,
24
23
  Git, the native credential-store binding and the Motel collector. It needs no
25
24
  source checkout, Node, Bun or 1Password on the user machine. The `executor` npm
26
- package is a launcher that starts the executable from the platform package npm
27
- installs as an optional dependency, for macOS (Apple Silicon and Intel), Linux
28
- (arm64 and x64, glibc) and Windows (x64). The same archive is the release
29
- download the install scripts and Homebrew use.
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.
30
32
 
31
33
  Beta uses `~/.executor/v2/cli` for its data. This path stays the same at the
32
34
  stable release. It is separate from Executor 1 data. One server runs per data
@@ -79,20 +81,25 @@ Native artifacts are under `.local/releases/<version>-<platform>-<arch>/`.
79
81
  `release:cli` bundles the runtime twice. `package/` holds the JavaScript runtime
80
82
  the desktop app runs with its own Node. `standalone/` holds the release install:
81
83
  the `executor` executable (`bun build --compile`) and the files beside it, which
82
- `executor-<version>-<platform>-<arch>.tgz` packs as the platform's npm package
83
- and release archive. The executable takes `runtime/cli.mjs` in its install
84
- directory as every module's location, so resources, `require` and
85
- `import.meta.resolve` find the same files beside it as from the npm package,
86
- through any launcher symlink. Packages that must stay on disk load from
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
87
89
  `runtime/node_modules`; Git is in `runtime/git`.
88
- The npm launcher is under `.local/releases/<version>/wrapper/` and uses exact
89
- native version aliases. These build commands do not publish anything.
90
- The app compiler and framework are prepared for workerd at build time. No
91
- runtime download is needed. Each compressed native archive must fit within
92
- 178 MiB before publication (`npmArchiveBudgetBytes` in `config.ts`). npm
93
- documents no limit; the budget stays below the largest archive it has accepted
94
- from Executor. Most of each archive is the executable, workerd and the bundled
95
- Git, which keeps only what the runtime runs (`keptGit` in `runtime-bundle.ts`).
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`).
96
103
  External source maps stay in the separate CI artifact for release diagnostics.
97
104
 
98
105
  Stock Bun lets any local process run its own code inside a compiled executable,
package/bin.mjs CHANGED
@@ -1,39 +1,216 @@
1
1
  #!/usr/bin/env node
2
- // The `executor` npm package's command. It starts the Executor executable from the platform
3
- // package npm installed as an optional dependency, passes signals on, and exits as it exits. The
4
- // executable carries its own runtime, so this file uses only what every Node since 14 has.
5
- import { spawn } from "child_process";
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";
6
26
  import { createRequire } from "module";
7
- import { dirname, join } from "path";
27
+ import { homedir } from "os";
28
+ import { join } from "path";
8
29
  import { fileURLToPath } from "url";
9
30
 
10
31
  const require = createRequire(import.meta.url);
11
32
  const manifest = require("./package.json");
12
- const name = "executor-" + process.platform + "-" + process.arch;
13
- const channel = manifest.version.indexOf("-beta.") === -1 ? "" : "@beta";
14
- const reinstall = "npm i -g executor" + channel + " --include=optional";
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);
15
48
 
16
49
  const fail = (lines) => {
17
50
  for (const line of lines) console.error(line);
18
51
  process.exit(1);
19
52
  };
20
53
 
21
- if (!Object.prototype.hasOwnProperty.call(manifest.optionalDependencies || {}, name))
54
+ if (typeof digest !== "string")
22
55
  fail(["Executor does not support " + process.platform + "/" + process.arch + "."]);
23
56
 
24
- let root;
25
- try {
26
- root = dirname(require.resolve(name + "/package.json"));
27
- } catch {
28
- // npm skips optional dependencies with --omit=optional, or when a lockfile made on another
29
- // platform did not list this one.
30
- fail([
31
- "Executor's native runtime is missing: " + name + " is not installed.",
32
- "Reinstall with optional dependencies: " + reinstall,
33
- ]);
34
- }
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
+ }
138
+ }
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();
35
213
 
36
- const executable = join(root, process.platform === "win32" ? "executor.exe" : "executor");
37
214
  const child = spawn(executable, process.argv.slice(2), {
38
215
  stdio: "inherit",
39
216
  env: Object.assign({}, process.env, {
@@ -57,7 +234,7 @@ for (const signal of signals) process.on(signal, forward);
57
234
  child.on("error", (error) =>
58
235
  fail([
59
236
  "Executor could not start " + executable + ": " + error.message,
60
- "Reinstall with: " + reinstall,
237
+ "Remove " + installed + " to download it again.",
61
238
  ]),
62
239
  );
63
240
  child.on("exit", (code, signal) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "executor",
3
- "version": "2.0.0-beta.36",
3
+ "version": "2.0.0-beta.38",
4
4
  "private": false,
5
5
  "description": "Run Executor locally and connect your agents to your apps.",
6
6
  "homepage": "https://executor.sh",
@@ -27,11 +27,11 @@
27
27
  "access": "public",
28
28
  "tag": "beta"
29
29
  },
30
- "optionalDependencies": {
31
- "executor-darwin-arm64": "npm:executor@2.0.0-beta.36-darwin-arm64",
32
- "executor-darwin-x64": "npm:executor@2.0.0-beta.36-darwin-x64",
33
- "executor-linux-x64": "npm:executor@2.0.0-beta.36-linux-x64",
34
- "executor-linux-arm64": "npm:executor@2.0.0-beta.36-linux-arm64",
35
- "executor-win32-x64": "npm:executor@2.0.0-beta.36-win32-x64"
30
+ "runtimes": {
31
+ "darwin-arm64": "eb01447cfdb59c03aec9e16e5989d717ddc33bded40c2b864e2aa1b0482b73e0",
32
+ "darwin-x64": "126fb19eb8a421507a199ded24bec5fc79ee345d76f17b8fb571f5639bd31ba4",
33
+ "linux-x64": "01f232a7cd13aa5116092fc8767741c12d566502fdfc53ab851ab9b3ead8e413",
34
+ "linux-arm64": "172784c097fccf30d4d3f238b72ba048bc724c0d5d263feac189cbb67d73b3f9",
35
+ "win32-x64": "2e9726e74d6b2414a29971bd7603056675542a9cc2edeba35eef388b5a954936"
36
36
  }
37
37
  }