@solcreek/dew 0.5.1 → 0.7.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.
package/bin/dew CHANGED
@@ -1,25 +1,215 @@
1
1
  #!/usr/bin/env node
2
- const { execFileSync } = require("child_process");
2
+ "use strict";
3
+
4
+ // Thin dispatcher for @solcreek/dew.
5
+ //
6
+ // First run downloads the matching native binary from the GitHub Release
7
+ // for this package version, verifies SHA256 against checksums.txt, and
8
+ // caches in DEW_CACHE_DIR. Subsequent runs spawn the cached binary directly.
9
+ //
10
+ // We don't ship the binary inside the npm package — it lives in
11
+ // goreleaser-produced tarballs on GH Releases. This keeps the npm tarball
12
+ // tiny (no binary bytes touched by npm install), avoids per-platform
13
+ // publish setups, and lets brew/install.sh/direct download all consume
14
+ // the same exact bytes from one canonical source.
15
+ //
16
+ // Override: DEW_BINARY=/path/to/dew for local builds and testing.
17
+ // Override: DEW_CACHE_DIR=/path overrides the cache location.
18
+ // Set DEW_VERIFY_COSIGN=1 to hard-require cosign signature verification.
19
+
20
+ const fs = require("fs");
3
21
  const path = require("path");
4
22
  const os = require("os");
5
- const { existsSync } = require("fs");
23
+ const https = require("https");
24
+ const crypto = require("crypto");
25
+ const zlib = require("zlib");
26
+ const { spawnSync } = require("child_process");
27
+ const { pipeline } = require("stream/promises");
28
+ const { createWriteStream, createReadStream } = require("fs");
6
29
 
7
- const binDir = path.join(__dirname);
8
- let binary;
30
+ const pkg = require("../package.json");
31
+ const VERSION = pkg.version;
32
+ const REPO = "solcreek/dew";
9
33
 
10
- if (os.platform() === "win32") {
11
- binary = path.join(binDir, "dew.exe");
12
- } else {
13
- binary = path.join(binDir, "dew-bin");
14
- }
34
+ const SUPPORTED = {
35
+ "darwin-arm64": { goos: "darwin", goarch: "arm64", binName: "dew" },
36
+ "darwin-x64": { goos: "darwin", goarch: "amd64", binName: "dew" },
37
+ "linux-x64": { goos: "linux", goarch: "amd64", binName: "dew" },
38
+ "linux-arm64": { goos: "linux", goarch: "arm64", binName: "dew" },
39
+ "win32-x64": { goos: "windows", goarch: "amd64", binName: "dew.exe" },
40
+ };
15
41
 
16
- if (!existsSync(binary)) {
17
- console.error("dew: binary not found. Run: npm rebuild @solcreek/dew");
42
+ function fatal(msg) {
43
+ process.stderr.write(`dew: ${msg}\n`);
18
44
  process.exit(1);
19
45
  }
20
46
 
21
- try {
22
- execFileSync(binary, process.argv.slice(2), { stdio: "inherit" });
23
- } catch (e) {
24
- process.exit(e.status || 1);
47
+ function cacheDir() {
48
+ if (process.env.DEW_CACHE_DIR) return process.env.DEW_CACHE_DIR;
49
+ const xdg = process.env.XDG_DATA_HOME;
50
+ const base = xdg || path.join(os.homedir(), ".local", "share");
51
+ return path.join(base, "dew", "bin", VERSION);
52
+ }
53
+
54
+ function get(url) {
55
+ return new Promise((resolve, reject) => {
56
+ https.get(url, (res) => {
57
+ if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
58
+ res.resume();
59
+ return resolve(get(res.headers.location));
60
+ }
61
+ if (res.statusCode !== 200) {
62
+ return reject(new Error(`HTTP ${res.statusCode} ${url}`));
63
+ }
64
+ resolve(res);
65
+ }).on("error", reject);
66
+ });
67
+ }
68
+
69
+ async function downloadTo(url, destPath) {
70
+ const res = await get(url);
71
+ await pipeline(res, createWriteStream(destPath));
72
+ }
73
+
74
+ async function fetchText(url) {
75
+ const res = await get(url);
76
+ let body = "";
77
+ for await (const chunk of res) body += chunk;
78
+ return body;
79
+ }
80
+
81
+ function sha256OfFile(p) {
82
+ const h = crypto.createHash("sha256");
83
+ return new Promise((resolve, reject) => {
84
+ const s = createReadStream(p);
85
+ s.on("data", (d) => h.update(d));
86
+ s.on("end", () => resolve(h.digest("hex")));
87
+ s.on("error", reject);
88
+ });
89
+ }
90
+
91
+ // Extract a single named file from a tar.gz with no native deps.
92
+ // Tar layout: 512-byte header, payload padded to 512.
93
+ function extractTarGz(archivePath, outDir, binName) {
94
+ const buf = zlib.gunzipSync(fs.readFileSync(archivePath));
95
+ let off = 0;
96
+ while (off + 512 <= buf.length) {
97
+ const header = buf.subarray(off, off + 512);
98
+ const nameRaw = header.subarray(0, 100).toString("utf8").replace(/\0.*$/, "");
99
+ if (!nameRaw) { off += 512; continue; }
100
+ const sizeOctal = header.subarray(124, 136).toString("utf8").replace(/[\0 ]+$/, "");
101
+ const size = parseInt(sizeOctal, 8) || 0;
102
+ const typeflag = String.fromCharCode(header[156]) || "0";
103
+ const dataStart = off + 512;
104
+ const dataEnd = dataStart + size;
105
+ if ((typeflag === "0" || typeflag === "") && path.basename(nameRaw) === binName) {
106
+ const dest = path.join(outDir, binName);
107
+ fs.writeFileSync(dest, buf.subarray(dataStart, dataEnd));
108
+ fs.chmodSync(dest, 0o755);
109
+ return true;
110
+ }
111
+ off = dataEnd + (size % 512 === 0 ? 0 : 512 - (size % 512));
112
+ }
113
+ return false;
25
114
  }
115
+
116
+ async function maybeVerifyCosign(dir, sumsBody) {
117
+ const verifyMode = process.env.DEW_VERIFY_COSIGN || "";
118
+ const cosignAvailable = spawnSync("cosign", ["version"], { stdio: "ignore" }).status === 0;
119
+ if (verifyMode !== "1" && !cosignAvailable) return;
120
+
121
+ const base = `https://github.com/${REPO}/releases/download/v${VERSION}`;
122
+ const sumsPath = path.join(dir, "checksums.txt");
123
+ const sigPath = path.join(dir, "checksums.txt.sig");
124
+ const pemPath = path.join(dir, "checksums.txt.pem");
125
+ fs.writeFileSync(sumsPath, sumsBody);
126
+ try {
127
+ await downloadTo(`${base}/checksums.txt.sig`, sigPath);
128
+ await downloadTo(`${base}/checksums.txt.pem`, pemPath);
129
+ } catch (e) {
130
+ if (verifyMode === "1") fatal(`cosign sig fetch failed: ${e.message}`);
131
+ process.stderr.write("dew: cosign verification skipped (signature absent)\n");
132
+ return;
133
+ }
134
+ const r = spawnSync("cosign", [
135
+ "verify-blob",
136
+ "--certificate", pemPath,
137
+ "--signature", sigPath,
138
+ "--certificate-identity-regexp",
139
+ `^https://github.com/${REPO}/\\.github/workflows/release\\.yml@refs/tags/v[0-9]+\\.[0-9]+\\.[0-9]+`,
140
+ "--certificate-oidc-issuer", "https://token.actions.githubusercontent.com",
141
+ sumsPath,
142
+ ], { stdio: verifyMode === "1" ? "inherit" : "pipe" });
143
+ if (r.status !== 0) {
144
+ if (verifyMode === "1") fatal("cosign verification failed");
145
+ process.stderr.write("dew: cosign verification failed (continuing — set DEW_VERIFY_COSIGN=1 to enforce)\n");
146
+ return;
147
+ }
148
+ process.stderr.write("dew: cosign verified\n");
149
+ }
150
+
151
+ async function ensureBinary() {
152
+ const triple = `${process.platform}-${process.arch}`;
153
+ const spec = SUPPORTED[triple];
154
+ if (!spec) {
155
+ fatal(
156
+ `${process.platform}/${process.arch} is not a supported platform.\n` +
157
+ `Supported: ${Object.keys(SUPPORTED).join(", ")}.\n` +
158
+ `If you have built dew from source, set DEW_BINARY=/path/to/dew.`,
159
+ );
160
+ }
161
+
162
+ const dir = cacheDir();
163
+ const binPath = path.join(dir, spec.binName);
164
+ if (fs.existsSync(binPath)) return binPath;
165
+
166
+ fs.mkdirSync(dir, { recursive: true });
167
+ const tarName = `dew_${VERSION}_${spec.goos}_${spec.goarch}.tar.gz`;
168
+ const base = `https://github.com/${REPO}/releases/download/v${VERSION}`;
169
+ const tarUrl = `${base}/${tarName}`;
170
+ const sumUrl = `${base}/checksums.txt`;
171
+ const tarPath = path.join(dir, tarName);
172
+
173
+ process.stderr.write(`dew: downloading ${tarName}\n`);
174
+ try {
175
+ await downloadTo(tarUrl, tarPath);
176
+ } catch (e) {
177
+ fatal(`download failed: ${e.message}\n Set DEW_BINARY=/path/to/dew to bypass.`);
178
+ }
179
+
180
+ process.stderr.write(`dew: verifying checksum\n`);
181
+ let sums;
182
+ try { sums = await fetchText(sumUrl); }
183
+ catch (e) { fatal(`could not fetch checksums.txt: ${e.message}`); }
184
+ const line = sums.split("\n").find((l) => l.endsWith(` ${tarName}`));
185
+ if (!line) fatal(`checksums.txt has no entry for ${tarName}`);
186
+ const expected = line.split(/\s+/)[0];
187
+ const actual = await sha256OfFile(tarPath);
188
+ if (actual !== expected) {
189
+ fs.unlinkSync(tarPath);
190
+ fatal(`checksum mismatch for ${tarName}\n expected ${expected}\n got ${actual}`);
191
+ }
192
+
193
+ await maybeVerifyCosign(dir, sums);
194
+
195
+ process.stderr.write(`dew: extracting\n`);
196
+ if (!extractTarGz(tarPath, dir, spec.binName)) {
197
+ fatal(`tarball missing ${spec.binName}`);
198
+ }
199
+ fs.unlinkSync(tarPath);
200
+ return binPath;
201
+ }
202
+
203
+ (async () => {
204
+ const override = process.env.DEW_BINARY;
205
+ let bin;
206
+ if (override) {
207
+ if (!fs.existsSync(override)) fatal(`DEW_BINARY=${override} does not exist`);
208
+ bin = override;
209
+ } else {
210
+ bin = await ensureBinary();
211
+ }
212
+ const result = spawnSync(bin, process.argv.slice(2), { stdio: "inherit" });
213
+ if (result.error) fatal(result.error.message);
214
+ process.exit(result.status === null ? 1 : result.status);
215
+ })().catch((e) => fatal(e.message));
package/package.json CHANGED
@@ -1,16 +1,23 @@
1
1
  {
2
2
  "name": "@solcreek/dew",
3
- "version": "0.5.1",
3
+ "version": "0.7.0",
4
4
  "description": "Ultra-lightweight VM + deploy tool. One Go binary for local dev and production.",
5
5
  "license": "MIT",
6
6
  "repository": {
7
7
  "type": "git",
8
- "url": "https://github.com/solcreek/dew"
8
+ "url": "git+https://github.com/solcreek/dew.git"
9
9
  },
10
10
  "bin": {
11
11
  "dew": "bin/dew"
12
12
  },
13
+ "files": [
14
+ "bin/dew",
15
+ "README.md"
16
+ ],
13
17
  "scripts": {
14
- "postinstall": "node scripts/postinstall.js"
18
+ "test": "node --test test/*.test.mjs"
19
+ },
20
+ "engines": {
21
+ "node": ">=18"
15
22
  }
16
23
  }
@@ -1,118 +0,0 @@
1
- const { execSync } = require("child_process");
2
- const { existsSync, writeFileSync, mkdirSync, chmodSync } = require("fs");
3
- const path = require("path");
4
- const os = require("os");
5
-
6
- const binDir = path.join(__dirname, "..", "bin");
7
- const binary = path.join(binDir, "dew-bin");
8
-
9
- if (!existsSync(binDir)) {
10
- mkdirSync(binDir, { recursive: true });
11
- }
12
-
13
- if (!existsSync(binary)) {
14
- const platform = os.platform();
15
- const arch = os.arch() === "arm64" ? "arm64" : "amd64";
16
- const repo = "solcreek/dew";
17
- const pkg = require("../package.json");
18
- const tag = `v${pkg.version}`;
19
-
20
- let asset;
21
- if (platform === "darwin") {
22
- asset = `dew-darwin-${arch}`;
23
- } else if (platform === "linux") {
24
- asset = `dew-linux-${arch}`;
25
- } else if (platform === "win32") {
26
- asset = `dew-windows-x86_64.exe`;
27
- } else {
28
- console.log(`dew: unsupported platform ${platform}`);
29
- process.exit(0);
30
- }
31
-
32
- const url = `https://github.com/${repo}/releases/download/${tag}/${asset}`;
33
- console.log(`dew: downloading ${asset}...`);
34
-
35
- try {
36
- execSync(`curl -fsSL -o "${binary}" "${url}"`, { stdio: "pipe" });
37
- chmodSync(binary, 0o755);
38
- console.log("dew: installed");
39
- } catch (e) {
40
- console.log(`dew: download failed from ${url}`);
41
- console.log(" GitHub Release may not exist yet.");
42
- console.log(" Install manually: https://github.com/solcreek/dew/releases");
43
- process.exit(0);
44
- }
45
- }
46
-
47
- // Show how to invoke dew based on how the user installed the package.
48
- // npx → `npx @solcreek/dew ...` (the binary is in a per-run cache)
49
- // local → `npx dew ...` or `./node_modules/.bin/dew ...`
50
- // global → `dew ...` (on PATH)
51
- //
52
- // We can't reliably detect "is this a global install" from inside
53
- // postinstall, so we surface all three forms once.
54
- function printInvocationHint() {
55
- // Skip in CI / non-interactive shells unless DEW_INSTALL_HINT is set
56
- if (process.env.CI && !process.env.DEW_INSTALL_HINT) return;
57
-
58
- const npmConfig = process.env.npm_config_global === "true";
59
- console.log("");
60
- if (npmConfig) {
61
- console.log("dew: installed globally — run `dew --help` from any terminal.");
62
- } else {
63
- console.log("dew: installed locally. Choose how to invoke:");
64
- console.log(" • One-off: npx @solcreek/dew --help");
65
- console.log(" • Local pkg: npx dew --help (inside this project)");
66
- console.log(" • Global: npm i -g @solcreek/dew → then `dew --help`");
67
- }
68
- console.log("");
69
- }
70
- printInvocationHint();
71
-
72
- // macOS: check whether the downloaded binary already has a Developer ID
73
- // signature. Release binaries (≥v0.5.0) are notarized + Developer-ID-signed
74
- // in CI, so we MUST NOT re-sign them — `codesign --force -s -` would strip
75
- // the Developer ID and replace it with an ad-hoc signature, which macOS
76
- // rejects for the virtualization entitlement.
77
- //
78
- // We only fall back to ad-hoc signing if the binary lacks any usable
79
- // signature (e.g. a custom build from source, or a hypothetical fork).
80
- if (os.platform() === "darwin" && existsSync(binary) && !binary.endsWith(".exe")) {
81
- let hasDeveloperID = false;
82
- try {
83
- const info = execSync(`codesign -dv "${binary}" 2>&1`, { encoding: "utf8" });
84
- hasDeveloperID = /Developer ID Application/.test(info) && !/Signature=adhoc/.test(info);
85
- } catch (_) {
86
- // not signed at all
87
- }
88
-
89
- if (hasDeveloperID) {
90
- console.log("dew: Developer ID signature detected — leaving binary untouched");
91
- } else {
92
- console.log("dew: binary is unsigned, falling back to ad-hoc signing");
93
- console.log("dew: NOTE — ad-hoc signing means VM commands won't work.");
94
- console.log("dew: download a release binary from https://github.com/solcreek/dew/releases/latest for full functionality.");
95
- const entitlements = path.join(__dirname, "entitlements.plist");
96
- if (!existsSync(entitlements)) {
97
- writeFileSync(
98
- entitlements,
99
- `<?xml version="1.0" encoding="UTF-8"?>
100
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
101
- <plist version="1.0">
102
- <dict>
103
- <key>com.apple.security.virtualization</key>
104
- <true/>
105
- </dict>
106
- </plist>`
107
- );
108
- }
109
- try {
110
- execSync(
111
- `codesign --entitlements "${entitlements}" --force -s - "${binary}"`,
112
- { stdio: "pipe" }
113
- );
114
- } catch (e) {
115
- console.log("dew: ⚠️ ad-hoc codesign also failed (sandboxed terminal?)");
116
- }
117
- }
118
- }