nur-cli 0.43.1 → 0.43.3

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 (2) hide show
  1. package/bin.js +42 -5
  2. package/package.json +1 -1
package/bin.js CHANGED
@@ -17,11 +17,13 @@ const http = require("http");
17
17
  const fs = require("fs");
18
18
  const path = require("path");
19
19
  const os = require("os");
20
+ const crypto = require("crypto");
20
21
  const { spawnSync } = require("child_process");
21
22
 
22
23
  const REPO = "nuroctane/nur-cli";
23
- // Falls back to this pinned version when `latest/download` is unreachable.
24
- const FALLBACK_VERSION = "0.40.1";
24
+ // Falls back to this package's own release when `latest/download` is
25
+ // unreachable. publish-npm.yml stamps the release tag into package.json.
26
+ const FALLBACK_VERSION = require("./package.json").version;
25
27
 
26
28
  function fail(msg) {
27
29
  process.stderr.write(`nur-cli: ${msg}\n`);
@@ -65,7 +67,9 @@ function fetchBuffer(url, redirects) {
65
67
  }
66
68
  if (status !== 200) {
67
69
  res.resume();
68
- return reject(new Error(`HTTP ${status} for ${url}`));
70
+ const err = new Error(`HTTP ${status} for ${url}`);
71
+ err.status = status;
72
+ return reject(err);
69
73
  }
70
74
  const chunks = [];
71
75
  res.on("data", (c) => chunks.push(c));
@@ -123,14 +127,47 @@ async function downloadAndInstall() {
123
127
  if (buf.length < 1000000) {
124
128
  fail(`downloaded asset too small (${buf.length} bytes) - aborting`);
125
129
  }
130
+ await verifyChecksum(picked, buf);
126
131
 
132
+ // Write beside the target, then rename: an interrupted download never leaves
133
+ // a truncated `nur` behind.
127
134
  const dest = installedBinaryPath();
128
- fs.writeFileSync(dest, buf);
129
- if (asset.execBit) fs.chmodSync(dest, 0o755);
135
+ const tmp = `${dest}.tmp-${process.pid}`;
136
+ fs.writeFileSync(tmp, buf);
137
+ if (asset.execBit) fs.chmodSync(tmp, 0o755);
138
+ try {
139
+ fs.renameSync(tmp, dest);
140
+ } catch (e) {
141
+ fs.rmSync(tmp, { force: true });
142
+ if (e.code === "EPERM" || e.code === "EBUSY") {
143
+ fail(`${dest} is in use - close every running nur, then run this again`);
144
+ }
145
+ throw e;
146
+ }
130
147
  process.stdout.write(`nur-cli: installed ${dest}\n`);
131
148
  return dest;
132
149
  }
133
150
 
151
+ /** Check `buf` against the `<asset>.sha256` the release publishes beside it. */
152
+ async function verifyChecksum(url, buf) {
153
+ const name = url.split("/").pop();
154
+ let record;
155
+ try {
156
+ record = (await fetchBuffer(`${url}.sha256`)).toString("utf8");
157
+ } catch (e) {
158
+ if (e.status === 404) return; // a release from before checksums were published
159
+ fail(`could not fetch the checksum for ${name} (${e.message})`);
160
+ }
161
+ const expected = (record.trim().split(/\s+/)[0] || "").toLowerCase();
162
+ if (!/^[0-9a-f]{64}$/.test(expected)) {
163
+ fail(`${name}.sha256 is not a sha256 record - aborting`);
164
+ }
165
+ const actual = crypto.createHash("sha256").update(buf).digest("hex");
166
+ if (actual !== expected) {
167
+ fail(`${name} does not match its published sha256 (${actual} != ${expected}) - aborting`);
168
+ }
169
+ }
170
+
134
171
  function runNurInstall(bin) {
135
172
  // Full one-stop install: PATH wiring, prereqs, ecosystem packs, browser.
136
173
  // The binary owns all of it - same as double-clicking a release EXE.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nur-cli",
3
- "version": "0.43.1",
3
+ "version": "0.43.3",
4
4
  "description": "NurCLI - fully loaded multi-provider coding agent (TUI, vision, tools, sandbox, skills). One command installs the native binary: npx nur-cli",
5
5
  "license": "MIT",
6
6
  "repository": {