par3-cli 0.0.0-stage → 0.1.51

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 Par3 app.
package/bin/t3.mjs ADDED
@@ -0,0 +1,161 @@
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(), ".t3");
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[2Kpar3: downloading Par3 ${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/Pho3niX90/par3/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
+ fs.writeFileSync(path.join(staging, ".install-complete"), `${version}\n`);
94
+ publish(staging, dir);
95
+ } finally {
96
+ fs.rmSync(staging, { recursive: true, force: true });
97
+ }
98
+ }
99
+
100
+ const isComplete = (dir) => {
101
+ try {
102
+ return fs.readFileSync(path.join(dir, ".install-complete"), "utf8").trim() === version;
103
+ } catch {
104
+ return false;
105
+ }
106
+ };
107
+
108
+ /** Preserve concurrently published complete installs; replace only incomplete folders without the completion marker. */
109
+ function publish(staging, dir) {
110
+ try {
111
+ fs.renameSync(staging, dir);
112
+ } catch (error) {
113
+ if (isComplete(dir)) return;
114
+ if (!fs.existsSync(dir)) throw error;
115
+ fs.rmSync(dir, { recursive: true, force: true });
116
+ fs.renameSync(staging, dir);
117
+ }
118
+ }
119
+
120
+ async function main() {
121
+ const key = `${process.platform}-${process.arch}`;
122
+ if (!SUPPORTED.includes(key)) {
123
+ throw new Error(
124
+ `no Par3 CLI build is available for ${key} (supported: ${SUPPORTED.join(", ")}).\n` +
125
+ "The desktop app is at https://github.com/Pho3niX90/par3/releases",
126
+ );
127
+ }
128
+ const dir = path.join(t3Home(), "runtime", "versions", version);
129
+ if (!isComplete(dir)) await install(dir, key);
130
+
131
+ // Service IPC and signals must reach the child; terminal Ctrl-C already does, so do not forward SIGINT twice.
132
+ const ipc = process.send !== undefined;
133
+ const child = spawn(
134
+ path.join(dir, process.platform === "win32" ? "t3.exe" : "t3"),
135
+ process.argv.slice(2),
136
+ {
137
+ stdio: ipc ? ["inherit", "inherit", "inherit", "ipc"] : "inherit",
138
+ // The executable lives in the T3 home, so this is how it knows npm launched it.
139
+ env: { ...process.env, T3CODE_NPM_LAUNCHER_PATH: import.meta.filename },
140
+ },
141
+ );
142
+ if (ipc) {
143
+ process.on("message", (message) => child.connected && child.send(message));
144
+ child.on("message", (message) => process.connected && process.send(message));
145
+ process.on("disconnect", () => child.connected && child.disconnect());
146
+ }
147
+ for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"]) {
148
+ process.on(signal, () => (ipc || signal !== "SIGINT") && child.kill(signal));
149
+ }
150
+ child.on("error", (error) => fail(error));
151
+ child.on("exit", (code, signal) =>
152
+ process.exit(code ?? 128 + (os.constants.signals[signal] || 1)),
153
+ );
154
+ }
155
+
156
+ function fail(error) {
157
+ process.stderr.write(`par3: ${error.message}\n`);
158
+ process.exit(1);
159
+ }
160
+
161
+ 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,22 @@
1
1
  {
2
2
  "name": "par3-cli",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.51",
4
+ "description": "Par3 CLI. Downloads the self-contained executable for this platform from the GitHub Release.",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/Pho3niX90/par3.git"
9
+ },
10
+ "bin": {
11
+ "par3": "./bin/t3.mjs",
12
+ "p3": "./bin/t3.mjs",
13
+ "par3-cli": "./bin/t3.mjs"
14
+ },
15
+ "files": [
16
+ "bin",
17
+ "dist"
18
+ ],
19
+ "engines": {
20
+ "node": ">=20.11"
21
+ }
22
+ }