@foundingbot/x 0.0.48 → 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.
@@ -0,0 +1,189 @@
1
+ #!/usr/bin/env node
2
+ // The `@foundingbot/x` npm package. Downloads this version's CLI archive from the
3
+ // release into <home>/runtime/versions/<version> (the folder the install
4
+ // scripts and `foundingbot-x update` use), verifies it against the release's SHA256SUMS,
5
+ // then runs it. `dist/bin.mjs` re-enters here for background services
6
+ // installed before 0.0.41, which start the npm package with Node and talk to
7
+ // it over an IPC channel.
8
+ import { spawn, spawnSync } from "node:child_process";
9
+ import crypto from "node:crypto";
10
+ import fs from "node:fs";
11
+ import os from "node:os";
12
+ import path from "node:path";
13
+
14
+ const { version } = JSON.parse(
15
+ fs.readFileSync(path.join(import.meta.dirname, "../package.json"), "utf8"),
16
+ );
17
+
18
+ const SUPPORTED = ["darwin-arm64", "linux-arm64", "linux-x64", "win32-arm64", "win32-x64"];
19
+
20
+ // Founding Bot X: people set FOUNDINGBOT_X_* variables. Our own processes, which
21
+ // mark the environment, pass T3CODE_* ones (see foundingBotXEnvironment.ts).
22
+ const setting = (name) =>
23
+ process.env.T3CODE_FOUNDINGBOT_X_ENV === "1"
24
+ ? process.env[`T3CODE_${name}`]
25
+ : process.env[`FOUNDINGBOT_X_${name}`];
26
+
27
+ function t3Home() {
28
+ const raw = (setting("HOME") || "").trim();
29
+ if (raw === "") return path.join(os.homedir(), ".foundingbot-x");
30
+ if (raw === "~") return os.homedir();
31
+ if (raw.startsWith("~/") || raw.startsWith("~\\")) return path.join(os.homedir(), raw.slice(2));
32
+ return path.resolve(raw);
33
+ }
34
+
35
+ async function get(url) {
36
+ const response = await fetch(url);
37
+ if (response.status === 404) {
38
+ throw new Error(
39
+ `${url} does not exist yet. If ${version} was just released, try again in a few minutes.`,
40
+ );
41
+ }
42
+ if (!response.ok) throw new Error(`GET ${url} returned HTTP ${response.status}`);
43
+ return response;
44
+ }
45
+
46
+ /** Downloads `url` to `file` and returns its sha256, showing progress on a terminal. */
47
+ async function download(url, file) {
48
+ const response = await get(url);
49
+ const total = Number(response.headers.get("content-length")) || 0;
50
+ const hash = crypto.createHash("sha256");
51
+ const out = fs.openSync(file, "w");
52
+ const tty = process.stderr.isTTY;
53
+ let received = 0;
54
+ let shown = -1;
55
+ try {
56
+ for await (const chunk of response.body) {
57
+ hash.update(chunk);
58
+ fs.writeSync(out, chunk);
59
+ received += chunk.byteLength;
60
+ // Redraw once per percent (or per MB without a length), not per chunk.
61
+ const step = total ? Math.floor((received * 100) / total) : Math.floor(received / 1048576);
62
+ if (tty && step !== shown) {
63
+ shown = step;
64
+ const percent = total ? ` ${step}%` : "";
65
+ process.stderr.write(
66
+ `\r\x1b[2Kfoundingbot-x: downloading Founding Bot X ${version}${percent} (${(received / 1048576).toFixed(1)} MB)`,
67
+ );
68
+ }
69
+ }
70
+ } finally {
71
+ fs.closeSync(out);
72
+ if (tty) process.stderr.write("\r\x1b[2K");
73
+ }
74
+ return hash.digest("hex");
75
+ }
76
+
77
+ async function install(dir, key) {
78
+ const archive = `foundingbot-x-${version}-${key}.${process.platform === "win32" ? "zip" : "tar.gz"}`;
79
+ const base = (setting("RELEASE_BASE_URL") || "https://founding.bot/x/releases/download").replace(
80
+ /\/+$/,
81
+ "",
82
+ );
83
+ const sums = await (await get(`${base}/v${version}/SHA256SUMS`)).text();
84
+ const expected = /^([0-9a-f]{64})\s+\*?(.+)$/im;
85
+ const line = sums
86
+ .split(/\r?\n/)
87
+ .map((l) => expected.exec(l.trim()))
88
+ .find((m) => m && m[2] === archive);
89
+ if (!line) throw new Error(`${archive} is not listed in the release's SHA256SUMS`);
90
+
91
+ // Extract beside the final folder and rename it into place, so an
92
+ // interrupted download never leaves a half-installed version behind.
93
+ fs.mkdirSync(path.dirname(dir), { recursive: true });
94
+ const staging = fs.mkdtempSync(`${dir}.staging-`);
95
+ try {
96
+ const file = path.join(staging, archive);
97
+ if ((await download(`${base}/v${version}/${archive}`, file)) !== line[1].toLowerCase()) {
98
+ throw new Error(`checksum mismatch for ${archive}`);
99
+ }
100
+ // Windows' own tar.exe reads zip; a Git-for-Windows tar on PATH cannot.
101
+ const tar =
102
+ process.platform === "win32" ? `${process.env.SystemRoot}\\System32\\tar.exe` : "tar";
103
+ const extract = spawnSync(tar, ["-xf", file, "-C", staging, "--strip-components=1"], {
104
+ stdio: "inherit",
105
+ });
106
+ if (extract.status !== 0) throw new Error(`could not extract ${archive}`);
107
+ fs.rmSync(file);
108
+ fs.writeFileSync(path.join(staging, ".install-complete"), `${version}\n`);
109
+ publish(staging, dir);
110
+ } finally {
111
+ fs.rmSync(staging, { recursive: true, force: true });
112
+ }
113
+ }
114
+
115
+ const isComplete = (dir) => {
116
+ try {
117
+ return fs.readFileSync(path.join(dir, ".install-complete"), "utf8").trim() === version;
118
+ } catch {
119
+ return false;
120
+ }
121
+ };
122
+
123
+ /**
124
+ * Moves a finished staging folder to `dir`. Rename never replaces an existing
125
+ * folder, so a complete install another launcher (or `foundingbot-x update`) published
126
+ * first is kept, never deleted from under a running process. Only a folder
127
+ * without the completion marker, left by an interrupted older installer, is
128
+ * cleared before trying again.
129
+ */
130
+ function publish(staging, dir) {
131
+ try {
132
+ fs.renameSync(staging, dir);
133
+ } catch (error) {
134
+ if (isComplete(dir)) return;
135
+ if (!fs.existsSync(dir)) throw error;
136
+ fs.rmSync(dir, { recursive: true, force: true });
137
+ fs.renameSync(staging, dir);
138
+ }
139
+ }
140
+
141
+ async function main() {
142
+ const key = `${process.platform}-${process.arch}`;
143
+ if (!SUPPORTED.includes(key)) {
144
+ throw new Error(
145
+ `no Founding Bot X CLI build is available for ${key} (supported: ${SUPPORTED.join(", ")}).\n` +
146
+ "The desktop app is at https://founding.bot/x/download",
147
+ );
148
+ }
149
+ const dir = path.join(t3Home(), "runtime", "versions", version);
150
+ if (!isComplete(dir)) await install(dir, key);
151
+
152
+ // A service launcher talks to its server over IPC; pass that channel and
153
+ // signals through. In a terminal, Ctrl-C already reaches the child directly,
154
+ // so forwarding SIGINT too would deliver it twice.
155
+ const ipc = process.send !== undefined;
156
+ const child = spawn(
157
+ path.join(dir, process.platform === "win32" ? "foundingbot-x.exe" : "foundingbot-x"),
158
+ process.argv.slice(2),
159
+ {
160
+ stdio: ipc ? ["inherit", "inherit", "inherit", "ipc"] : "inherit",
161
+ // The executable lives in the home folder, so this is how it knows npm launched it.
162
+ // Founding Bot X: under both names, since the executable reads one or the other.
163
+ env: {
164
+ ...process.env,
165
+ FOUNDINGBOT_X_NPM_LAUNCHER_PATH: import.meta.filename,
166
+ T3CODE_NPM_LAUNCHER_PATH: import.meta.filename,
167
+ },
168
+ },
169
+ );
170
+ if (ipc) {
171
+ process.on("message", (message) => child.connected && child.send(message));
172
+ child.on("message", (message) => process.connected && process.send(message));
173
+ process.on("disconnect", () => child.connected && child.disconnect());
174
+ }
175
+ for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"]) {
176
+ process.on(signal, () => (ipc || signal !== "SIGINT") && child.kill(signal));
177
+ }
178
+ child.on("error", (error) => fail(error));
179
+ child.on("exit", (code, signal) =>
180
+ process.exit(code ?? 128 + (os.constants.signals[signal] || 1)),
181
+ );
182
+ }
183
+
184
+ function fail(error) {
185
+ process.stderr.write(`foundingbot-x: ${error.message}\n`);
186
+ process.exit(1);
187
+ }
188
+
189
+ await main().catch(fail);
package/dist/bin.mjs CHANGED
@@ -1,27 +1,2 @@
1
- import { spawn } from "node:child_process";
2
- import { constants } from "node:os";
3
- import { dirname, join } from "node:path";
4
- import { createRequire } from "node:module";
5
- const require = createRequire(import.meta.url);
6
- const executableName = process.platform === "win32" ? "foundingbot-x.exe" : "foundingbot-x";
7
- const executable = join(dirname(require.resolve("@foundingbot/x-" + process.platform + "-" + process.arch + "/package.json")), executableName);
8
- const ipc = process.send !== undefined;
9
- const child = spawn(executable, process.argv.slice(2), {
10
- stdio: ipc ? ["inherit", "inherit", "inherit", "ipc"] : "inherit",
11
- });
12
- const fail = (error) => {
13
- if (!error) return;
14
- process.stderr.write("foundingbot-x: " + error.message + "\n");
15
- child.kill("SIGTERM");
16
- process.exitCode = 1;
17
- };
18
- if (ipc) {
19
- process.on("message", (message) => { if (child.connected) child.send(message, fail); });
20
- child.on("message", (message) => { if (process.connected) process.send(message, fail); });
21
- process.on("disconnect", () => { if (child.connected) child.disconnect(); });
22
- }
23
- for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"]) {
24
- process.on(signal, () => child.kill(signal));
25
- }
26
- child.on("error", (error) => { fail(error); process.exit(1); });
27
- child.on("exit", (code, signal) => process.exit(code ?? 128 + (constants.signals[signal] || 1)));
1
+ // Background services installed before 0.0.41 start the npm package from this path.
2
+ import "../bin/foundingbot-x.mjs";
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@foundingbot/x",
3
- "version": "0.0.48",
4
- "description": "Founding Bot X CLI. Installs the self-contained executable for this platform.",
3
+ "version": "0.1.0",
4
+ "description": "Founding Bot X CLI. Downloads the self-contained executable for this platform from the release.",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {
7
7
  "type": "git",
@@ -9,17 +9,13 @@
9
9
  "directory": "apps/server"
10
10
  },
11
11
  "bin": {
12
- "foundingbot-x": "./bin/foundingbot-x.js"
12
+ "foundingbot-x": "./bin/foundingbot-x.mjs"
13
13
  },
14
14
  "files": [
15
15
  "bin",
16
16
  "dist"
17
17
  ],
18
- "optionalDependencies": {
19
- "@foundingbot/x-darwin-arm64": "0.0.48",
20
- "@foundingbot/x-linux-arm64": "0.0.48",
21
- "@foundingbot/x-linux-x64": "0.0.48",
22
- "@foundingbot/x-win32-arm64": "0.0.48",
23
- "@foundingbot/x-win32-x64": "0.0.48"
18
+ "engines": {
19
+ "node": ">=20.11"
24
20
  }
25
21
  }
@@ -1,33 +0,0 @@
1
- #!/usr/bin/env node
2
- "use strict";
3
- const { spawnSync } = require("node:child_process");
4
- const { constants } = require("node:os");
5
- const { dirname, join } = require("node:path");
6
-
7
- const SUPPORTED = ["darwin-arm64", "linux-arm64", "linux-x64", "win32-arm64", "win32-x64"];
8
- const key = process.platform + "-" + process.arch;
9
-
10
- let packageDir;
11
- try {
12
- packageDir = dirname(require.resolve("@foundingbot/x-" + key + "/package.json"));
13
- } catch {
14
- process.stderr.write(
15
- [
16
- "foundingbot-x: no Founding Bot X CLI build is available for this platform (" + key + ").",
17
- "Supported platforms: " + SUPPORTED.join(", ") + ".",
18
- "If yours is listed, reinstall @foundingbot/x so npm fetches its optional dependency.",
19
- "The desktop app and release archives are at https://founding.bot",
20
- "",
21
- ].join("\n"),
22
- );
23
- process.exit(1);
24
- }
25
-
26
- const executable = join(packageDir, process.platform === "win32" ? "foundingbot-x.exe" : "foundingbot-x");
27
- const result = spawnSync(executable, process.argv.slice(2), { stdio: "inherit" });
28
- if (result.error) {
29
- process.stderr.write("foundingbot-x: failed to start " + executable + ": " + result.error.message + "\n");
30
- process.exit(1);
31
- }
32
- // A child killed by a signal has no status; report it the way a shell would.
33
- process.exit(result.status ?? 128 + (constants.signals[result.signal] || 1));