@that-company/dat 0.1.30 → 0.1.32

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
@@ -28,3 +28,26 @@ curl -fsSL https://thatcompany.ai/install.sh | sh
28
28
  - `DAT_INSTALL_BASE`: full release asset base URL
29
29
  - `DAT_CHANNEL`: release path under `/releases`, default `download/v<package version>`
30
30
  - `DAT_SKIP_INSTALL=1`: skip binary download during npm install
31
+
32
+ ## Release
33
+
34
+ The npm package version must match an already-published `dat` GitHub Release.
35
+ The installer intentionally downloads from `download/v<package version>` instead
36
+ of `latest/download`, so npm installs are reproducible and the package version
37
+ has one meaning.
38
+
39
+ Release order:
40
+
41
+ 1. Publish the `dat` binary release to `theSalted/dat-releases`.
42
+ 2. Set this package version to the same `X.Y.Z`.
43
+ 3. Run `npm run verify:release`.
44
+ 4. Publish with `npm run release`.
45
+ 5. Verify `npm view @that-company/dat version` and a temp global install:
46
+
47
+ ```sh
48
+ npm install -g --prefix /tmp/dat-npm-check @that-company/dat@latest
49
+ /tmp/dat-npm-check/bin/dat --version
50
+ ```
51
+
52
+ The `prepublishOnly` hook runs the same release guard if someone uses
53
+ `npm publish --access public` directly.
package/bin/dat.js CHANGED
@@ -8,7 +8,7 @@ const { join } = require("node:path");
8
8
  const binary = join(__dirname, "..", "vendor", process.platform === "win32" ? "dat.exe" : "dat");
9
9
 
10
10
  if (!existsSync(binary)) {
11
- console.error("dat binary is missing. Reinstall with: npm rebuild -g @thatcompany/dat");
11
+ console.error("dat binary is missing. Reinstall with: npm rebuild -g @that-company/dat");
12
12
  process.exit(1);
13
13
  }
14
14
 
package/package.json CHANGED
@@ -1,12 +1,15 @@
1
1
  {
2
2
  "name": "@that-company/dat",
3
- "version": "0.1.30",
3
+ "version": "0.1.32",
4
4
  "description": "Installer for the D.A.T. CLI",
5
5
  "bin": {
6
6
  "dat": "bin/dat.js"
7
7
  },
8
8
  "scripts": {
9
- "postinstall": "node scripts/install.js"
9
+ "postinstall": "node scripts/install.js",
10
+ "prepublishOnly": "node scripts/prepublish.js",
11
+ "release": "npm run verify:release && npm publish --access public",
12
+ "verify:release": "node scripts/prepublish.js"
10
13
  },
11
14
  "files": [
12
15
  "bin",
@@ -0,0 +1,114 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ const { get, request } = require("node:https");
5
+ const { URL } = require("node:url");
6
+
7
+ const pkg = require("../package.json");
8
+
9
+ const RELEASE_ASSETS = ["dat-darwin-arm64", "dat-darwin-x64", "dat-linux-arm64", "dat-linux-x64"];
10
+ const REPO = process.env.DAT_RELEASE_REPO || "theSalted/dat-releases";
11
+ const CHANNEL = process.env.DAT_CHANNEL || `download/v${pkg.version}`;
12
+ const RELEASE_BASE = trimTrailingSlash(
13
+ process.env.DAT_INSTALL_BASE || `https://github.com/${REPO}/releases/${CHANNEL}`,
14
+ );
15
+
16
+ main().catch((error) => {
17
+ console.error(`dat npm release check failed: ${error instanceof Error ? error.message : String(error)}`);
18
+ process.exit(1);
19
+ });
20
+
21
+ async function main() {
22
+ if (!/^\d+\.\d+\.\d+$/.test(pkg.version)) {
23
+ throw new Error(`package version must be plain X.Y.Z, got ${pkg.version}`);
24
+ }
25
+
26
+ const version = await downloadText(`${RELEASE_BASE}/VERSION`);
27
+ if (version.trim() !== pkg.version) {
28
+ throw new Error(`release VERSION is ${version.trim()}, but package.json is ${pkg.version}`);
29
+ }
30
+
31
+ const sums = await downloadText(`${RELEASE_BASE}/SHA256SUMS`);
32
+ for (const asset of RELEASE_ASSETS) {
33
+ if (!hasChecksumEntry(sums, asset)) {
34
+ throw new Error(`SHA256SUMS has no entry for ${asset}`);
35
+ }
36
+ await assertHeadOk(`${RELEASE_BASE}/${asset}`);
37
+ }
38
+
39
+ console.error(`release check ok: @that-company/dat ${pkg.version} matches ${RELEASE_BASE}`);
40
+ }
41
+
42
+ function hasChecksumEntry(sums, asset) {
43
+ return sums
44
+ .split(/\r?\n/)
45
+ .some((line) => {
46
+ const parts = line.trim().split(/\s+/);
47
+ return parts.length === 2 && parts[1] === asset && /^[0-9a-f]{64}$/i.test(parts[0]);
48
+ });
49
+ }
50
+
51
+ function trimTrailingSlash(value) {
52
+ return value.endsWith("/") ? trimTrailingSlash(value.slice(0, -1)) : value;
53
+ }
54
+
55
+ function downloadText(url, redirects = 0) {
56
+ if (redirects > 5) {
57
+ return Promise.reject(new Error(`too many redirects while downloading ${url}`));
58
+ }
59
+
60
+ return new Promise((resolve, reject) => {
61
+ const req = get(new URL(url), (res) => {
62
+ const status = res.statusCode || 0;
63
+ const location = res.headers.location;
64
+ if (status >= 300 && status < 400 && location) {
65
+ res.resume();
66
+ downloadText(new URL(location, url).toString(), redirects + 1).then(resolve, reject);
67
+ return;
68
+ }
69
+ if (status !== 200) {
70
+ res.resume();
71
+ reject(new Error(`GET ${url} -> HTTP ${status}`));
72
+ return;
73
+ }
74
+ res.setEncoding("utf8");
75
+ let body = "";
76
+ res.on("data", (chunk) => {
77
+ body += chunk;
78
+ });
79
+ res.on("end", () => resolve(body));
80
+ });
81
+ req.on("error", reject);
82
+ req.setTimeout(30_000, () => {
83
+ req.destroy(new Error(`timeout downloading ${url}`));
84
+ });
85
+ });
86
+ }
87
+
88
+ function assertHeadOk(url, redirects = 0) {
89
+ if (redirects > 5) {
90
+ return Promise.reject(new Error(`too many redirects while checking ${url}`));
91
+ }
92
+
93
+ return new Promise((resolve, reject) => {
94
+ const req = request(new URL(url), { method: "HEAD" }, (res) => {
95
+ const status = res.statusCode || 0;
96
+ const location = res.headers.location;
97
+ res.resume();
98
+ if (status >= 300 && status < 400 && location) {
99
+ assertHeadOk(new URL(location, url).toString(), redirects + 1).then(resolve, reject);
100
+ return;
101
+ }
102
+ if (status !== 200) {
103
+ reject(new Error(`HEAD ${url} -> HTTP ${status}`));
104
+ return;
105
+ }
106
+ resolve();
107
+ });
108
+ req.on("error", reject);
109
+ req.setTimeout(30_000, () => {
110
+ req.destroy(new Error(`timeout checking ${url}`));
111
+ });
112
+ req.end();
113
+ });
114
+ }