boops-cli 0.0.0-stage → 0.1.8-preview.20261107.7

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,3 +1,3 @@
1
- # Temporary Holding Version
1
+ # Server
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Node.js WebSocket server for the Boops app.
package/bin/t3.mjs ADDED
@@ -0,0 +1,213 @@
1
+ #!/usr/bin/env node
2
+ // Share the installer's runtime cache; pre-0.0.41 services re-enter here over IPC.
3
+ import { spawn, spawnSync } from "node:child_process";
4
+ import crypto from "node:crypto";
5
+ import fs from "node:fs";
6
+ import os from "node:os";
7
+ import path from "node:path";
8
+
9
+ const { version } = JSON.parse(
10
+ fs.readFileSync(path.join(import.meta.dirname, "../package.json"), "utf8"),
11
+ );
12
+
13
+ const SUPPORTED = ["darwin-arm64", "linux-arm64", "linux-x64", "win32-arm64", "win32-x64"];
14
+
15
+ function t3Home() {
16
+ const raw = (process.env.T3CODE_HOME || "").trim();
17
+ if (raw === "") return path.join(os.homedir(), ".boops");
18
+ if (raw === "~") return os.homedir();
19
+ if (raw.startsWith("~/") || raw.startsWith("~\\")) return path.join(os.homedir(), raw.slice(2));
20
+ return path.resolve(raw);
21
+ }
22
+
23
+ async function get(url) {
24
+ const response = await fetch(url);
25
+ if (response.status === 404) {
26
+ throw new Error(
27
+ `${url} does not exist yet. If ${version} was just released, try again in a few minutes.`,
28
+ );
29
+ }
30
+ if (!response.ok) throw new Error(`GET ${url} returned HTTP ${response.status}`);
31
+ return response;
32
+ }
33
+
34
+ async function download(url, file) {
35
+ const response = await get(url);
36
+ const total = Number(response.headers.get("content-length")) || 0;
37
+ const hash = crypto.createHash("sha256");
38
+ const out = fs.openSync(file, "w");
39
+ const tty = process.stderr.isTTY;
40
+ let received = 0;
41
+ let shown = -1;
42
+ try {
43
+ for await (const chunk of response.body) {
44
+ hash.update(chunk);
45
+ fs.writeSync(out, chunk);
46
+ received += chunk.byteLength;
47
+ // Redraw once per percent (or per MB without a length), not per chunk.
48
+ const step = total ? Math.floor((received * 100) / total) : Math.floor(received / 1048576);
49
+ if (tty && step !== shown) {
50
+ shown = step;
51
+ const percent = total ? ` ${step}%` : "";
52
+ process.stderr.write(
53
+ `\r\x1b[2Kboops: downloading Boops ${version}${percent} (${(received / 1048576).toFixed(1)} MB)`,
54
+ );
55
+ }
56
+ }
57
+ } finally {
58
+ fs.closeSync(out);
59
+ if (tty) process.stderr.write("\r\x1b[2K");
60
+ }
61
+ return hash.digest("hex");
62
+ }
63
+
64
+ async function install(dir, key) {
65
+ const archive = `t3-${version}-${key}.${process.platform === "win32" ? "zip" : "tar.gz"}`;
66
+ const base = (
67
+ process.env.T3CODE_RELEASE_BASE_URL || "https://github.com/szokeptr/boops/releases/download"
68
+ ).replace(/\/+$/, "");
69
+ const sums = await (await get(`${base}/v${version}/SHA256SUMS`)).text();
70
+ const expected = /^([0-9a-f]{64})\s+\*?(.+)$/im;
71
+ const line = sums
72
+ .split(/\r?\n/)
73
+ .map((l) => expected.exec(l.trim()))
74
+ .find((m) => m && m[2] === archive);
75
+ if (!line) throw new Error(`${archive} is not listed in the release's SHA256SUMS`);
76
+
77
+ // Stage beside the destination and rename atomically to avoid half-installed versions.
78
+ fs.mkdirSync(path.dirname(dir), { recursive: true });
79
+ const staging = fs.mkdtempSync(`${dir}.staging-`);
80
+ try {
81
+ const file = path.join(staging, archive);
82
+ if ((await download(`${base}/v${version}/${archive}`, file)) !== line[1].toLowerCase()) {
83
+ throw new Error(`checksum mismatch for ${archive}`);
84
+ }
85
+ // Windows' own tar.exe reads zip; a Git-for-Windows tar on PATH cannot.
86
+ const tar =
87
+ process.platform === "win32" ? `${process.env.SystemRoot}\\System32\\tar.exe` : "tar";
88
+ const extract = spawnSync(tar, ["-xf", file, "-C", staging, "--strip-components=1"], {
89
+ stdio: "inherit",
90
+ });
91
+ if (extract.status !== 0) throw new Error(`could not extract ${archive}`);
92
+ fs.rmSync(file);
93
+ const validate = spawnSync(
94
+ path.join(staging, process.platform === "win32" ? "t3.exe" : "t3"),
95
+ ["--version"],
96
+ {
97
+ stdio: ["ignore", "ignore", "pipe"],
98
+ encoding: "utf8",
99
+ timeout: 30_000,
100
+ killSignal: "SIGKILL",
101
+ },
102
+ );
103
+ if (validate.status !== 0) {
104
+ throw executableError(
105
+ key,
106
+ validate.error?.message ||
107
+ validate.stderr?.trim() ||
108
+ `--version exited with ${validate.signal || validate.status}`,
109
+ );
110
+ }
111
+ fs.writeFileSync(path.join(staging, ".install-complete"), `${version}\n`);
112
+ publish(staging, dir);
113
+ } finally {
114
+ fs.rmSync(staging, { recursive: true, force: true });
115
+ }
116
+ }
117
+
118
+ const isComplete = (dir) => {
119
+ try {
120
+ return fs.readFileSync(path.join(dir, ".install-complete"), "utf8").trim() === version;
121
+ } catch {
122
+ return false;
123
+ }
124
+ };
125
+
126
+ /** Preserve concurrently published complete installs; replace only incomplete folders without the completion marker. */
127
+ function publish(staging, dir) {
128
+ try {
129
+ fs.renameSync(staging, dir);
130
+ return;
131
+ } catch (error) {
132
+ if (isComplete(dir)) return;
133
+ if (!fs.existsSync(dir)) throw error;
134
+ }
135
+ const lock = `${dir}.lock`;
136
+ try {
137
+ fs.mkdirSync(lock);
138
+ } catch (error) {
139
+ if (error.code !== "EEXIST") throw error;
140
+ throw new Error(
141
+ `another boops is installing ${version}. If none is running, delete ${lock} and try again.`,
142
+ { cause: error },
143
+ );
144
+ }
145
+ const stale = `${staging}.stale`;
146
+ try {
147
+ // A peer may have published between our marker check and taking the lock.
148
+ if (isComplete(dir)) return;
149
+ // Move it aside rather than deleting in place: once the folder is empty, a
150
+ // peer's first rename can land in it.
151
+ fs.renameSync(dir, stale);
152
+ try {
153
+ fs.renameSync(staging, dir);
154
+ } catch (error) {
155
+ if (!isComplete(dir)) throw error;
156
+ }
157
+ } finally {
158
+ fs.rmSync(lock, { recursive: true, force: true });
159
+ fs.rmSync(stale, { recursive: true, force: true });
160
+ }
161
+ }
162
+
163
+ async function main() {
164
+ const key = `${process.platform}-${process.arch}`;
165
+ if (!SUPPORTED.includes(key)) {
166
+ throw new Error(
167
+ `no Boops CLI build is available for ${key} (supported: ${SUPPORTED.join(", ")}).\n` +
168
+ "The desktop app is at https://github.com/szokeptr/boops/releases",
169
+ );
170
+ }
171
+ const dir = path.join(t3Home(), "runtime", "versions", version);
172
+ if (!isComplete(dir)) await install(dir, key);
173
+
174
+ // Service IPC and signals must reach the child; terminal Ctrl-C already does, so do not forward SIGINT twice.
175
+ const ipc = process.send !== undefined;
176
+ const child = spawn(
177
+ path.join(dir, process.platform === "win32" ? "t3.exe" : "t3"),
178
+ process.argv.slice(2),
179
+ {
180
+ stdio: ipc ? ["inherit", "inherit", "inherit", "ipc"] : "inherit",
181
+ // The executable lives in the T3 home, so this is how it knows npm launched it.
182
+ env: { ...process.env, T3CODE_NPM_LAUNCHER_PATH: import.meta.filename },
183
+ },
184
+ );
185
+ if (ipc) {
186
+ process.on("message", (message) => child.connected && child.send(message));
187
+ child.on("message", (message) => process.connected && process.send(message));
188
+ process.on("disconnect", () => child.connected && child.disconnect());
189
+ }
190
+ for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"]) {
191
+ process.on(signal, () => (ipc || signal !== "SIGINT") && child.kill(signal));
192
+ }
193
+ child.on("error", (error) => fail(executableError(key, error.message)));
194
+ child.on("exit", (code, signal) =>
195
+ process.exit(code ?? 128 + (os.constants.signals[signal] || 1)),
196
+ );
197
+ }
198
+
199
+ function executableError(key, detail) {
200
+ return new Error(
201
+ `could not run Boops ${version} for ${key}: ${detail}` +
202
+ (process.platform === "linux"
203
+ ? "\nThe Linux release requires glibc; musl-based systems such as Alpine are not supported. Use a glibc-based Linux environment."
204
+ : ""),
205
+ );
206
+ }
207
+
208
+ function fail(error) {
209
+ process.stderr.write(`boops: ${error.message}\n`);
210
+ process.exit(1);
211
+ }
212
+
213
+ await main().catch(fail);
package/dist/bin.mjs ADDED
@@ -0,0 +1,2 @@
1
+ // Legacy background services still enter the npm package through this path.
2
+ import "../bin/t3.mjs";
package/package.json CHANGED
@@ -1,6 +1,24 @@
1
1
  {
2
2
  "name": "boops-cli",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.8-preview.20261107.7",
4
+ "description": "Boops CLI. Downloads the self-contained executable for this platform from the GitHub Release.",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/szokeptr/boops",
9
+ "directory": "apps/server"
10
+ },
11
+ "bin": {
12
+ "boops": "bin/t3.mjs"
13
+ },
14
+ "files": [
15
+ "bin",
16
+ "dist"
17
+ ],
18
+ "engines": {
19
+ "node": ">=20.11"
20
+ },
21
+ "t3code": {
22
+ "databaseMigration": 87
23
+ }
24
+ }