gitpreflight 0.1.4

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.
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env node
2
+
3
+ "use strict";
4
+
5
+ const { spawn } = require("node:child_process");
6
+ const { ensureGitPreflightBinary } = require("../lib/installer");
7
+
8
+ async function main() {
9
+ const args = process.argv.slice(2);
10
+ const { binPath } = await ensureGitPreflightBinary({ reason: "run" });
11
+
12
+ const child = spawn(binPath, args, {
13
+ stdio: "inherit",
14
+ env: process.env
15
+ });
16
+
17
+ child.on("exit", (code, signal) => {
18
+ if (typeof code === "number") process.exit(code);
19
+ process.exitCode = 1;
20
+ if (signal) {
21
+ process.stderr.write(`GitPreflight exited due to signal: ${signal}\n`);
22
+ }
23
+ });
24
+
25
+ child.on("error", (err) => {
26
+ process.stderr.write(`Failed to start GitPreflight: ${err?.message ?? String(err)}\n`);
27
+ process.exit(2);
28
+ });
29
+ }
30
+
31
+ Promise.resolve(main()).catch((err) => {
32
+ process.stderr.write(`GitPreflight installer error: ${err?.message ?? String(err)}\n`);
33
+ process.exit(2);
34
+ });
@@ -0,0 +1,42 @@
1
+ #!/usr/bin/env node
2
+
3
+ "use strict";
4
+
5
+ const { ensureGitPreflightBinary } = require("../lib/installer");
6
+
7
+ async function main() {
8
+ const interactive = Boolean(process.stdout.isTTY) && Boolean(process.stderr.isTTY) && process.env.CI !== "1" && process.env.CI !== "true";
9
+
10
+ if (process.env.GITPREFLIGHT_SKIP_DOWNLOAD === "1" || process.env.GITPREFLIGHT_SKIP_DOWNLOAD === "true") {
11
+ if (interactive) {
12
+ process.stderr.write("GitPreflight: run `gitpreflight install` to choose setup mode (global, local, or repo).\n");
13
+ }
14
+ return;
15
+ }
16
+
17
+ // Avoid noisy downloads during local development (workspace version is usually 0.0.0).
18
+ if (!process.env.GITPREFLIGHT_INSTALL_VERSION) {
19
+ try {
20
+ const version = require("../package.json").version;
21
+ if (version === "0.0.0") {
22
+ if (interactive) {
23
+ process.stderr.write("GitPreflight: run `gitpreflight install` to choose setup mode (global, local, or repo).\n");
24
+ }
25
+ return;
26
+ }
27
+ } catch {
28
+ return;
29
+ }
30
+ }
31
+
32
+ await ensureGitPreflightBinary({ reason: "postinstall" });
33
+
34
+ if (interactive) {
35
+ process.stderr.write("GitPreflight installed. Next: run `gitpreflight install` to configure hooks.\n");
36
+ }
37
+ }
38
+
39
+ Promise.resolve(main()).catch((err) => {
40
+ // Never fail install for transient network issues.
41
+ process.stderr.write(`GitPreflight: postinstall download skipped (${err?.message ?? String(err)})\n`);
42
+ });
@@ -0,0 +1,243 @@
1
+ "use strict";
2
+
3
+ const fs = require("node:fs");
4
+ const fsp = require("node:fs/promises");
5
+ const path = require("node:path");
6
+ const os = require("node:os");
7
+ const http = require("node:http");
8
+ const https = require("node:https");
9
+ const crypto = require("node:crypto");
10
+
11
+ const DEFAULT_GITHUB_REPO = "un/gitpreflight";
12
+
13
+ function isTruthy(v) {
14
+ return v === "1" || v === "true" || v === "yes";
15
+ }
16
+
17
+ function getPackageVersion() {
18
+ try {
19
+ return require("../package.json").version;
20
+ } catch {
21
+ return "0.0.0";
22
+ }
23
+ }
24
+
25
+ function resolveTarget() {
26
+ const platform = process.platform;
27
+ const arch = process.arch;
28
+
29
+ if (platform !== "darwin" && platform !== "linux") {
30
+ throw new Error(`Unsupported platform: ${platform}. GitPreflight supports macOS and Linux.`);
31
+ }
32
+ if (arch !== "x64" && arch !== "arm64") {
33
+ throw new Error(`Unsupported architecture: ${arch}. GitPreflight supports x64 and arm64.`);
34
+ }
35
+
36
+ return { platform, arch };
37
+ }
38
+
39
+ function resolveCachePaths(version) {
40
+ const home = os.homedir();
41
+ const base = process.env.GITPREFLIGHT_HOME && process.env.GITPREFLIGHT_HOME.trim()
42
+ ? process.env.GITPREFLIGHT_HOME.trim()
43
+ : path.join(home, ".gitpreflight");
44
+
45
+ const dir = path.join(base, "bin", version);
46
+ const binPath = path.join(dir, "gitpreflight");
47
+ return { dir, binPath };
48
+ }
49
+
50
+ function request(url) {
51
+ return new Promise((resolve, reject) => {
52
+ const u = new URL(url);
53
+ const client = u.protocol === "https:" ? https : u.protocol === "http:" ? http : null;
54
+ if (!client) {
55
+ reject(new Error(`Unsupported URL protocol: ${u.protocol}`));
56
+ return;
57
+ }
58
+
59
+ const req = client.get(
60
+ u,
61
+ {
62
+ headers: {
63
+ "user-agent": "gitpreflight-installer",
64
+ accept: "application/octet-stream"
65
+ }
66
+ },
67
+ (res) => {
68
+ resolve(res);
69
+ }
70
+ );
71
+
72
+ req.setTimeout(30_000, () => {
73
+ req.destroy(new Error(`Timeout downloading ${url}`));
74
+ });
75
+
76
+ req.on("error", reject);
77
+ });
78
+ }
79
+
80
+ async function downloadText(url, maxRedirects = 5) {
81
+ const res = await request(url);
82
+
83
+ if (
84
+ (res.statusCode === 301 || res.statusCode === 302 || res.statusCode === 307 || res.statusCode === 308) &&
85
+ res.headers.location &&
86
+ maxRedirects > 0
87
+ ) {
88
+ res.resume();
89
+ const next = new URL(res.headers.location, url).toString();
90
+ return downloadText(next, maxRedirects - 1);
91
+ }
92
+
93
+ if (!res.statusCode || res.statusCode < 200 || res.statusCode >= 300) {
94
+ const body = await streamToString(res);
95
+ throw new Error(`HTTP ${res.statusCode ?? "?"} downloading ${url}${body ? `: ${body.trim()}` : ""}`);
96
+ }
97
+
98
+ return streamToString(res);
99
+ }
100
+
101
+ async function downloadFile(url, destPath, maxRedirects = 5) {
102
+ const res = await request(url);
103
+
104
+ if (
105
+ (res.statusCode === 301 || res.statusCode === 302 || res.statusCode === 307 || res.statusCode === 308) &&
106
+ res.headers.location &&
107
+ maxRedirects > 0
108
+ ) {
109
+ res.resume();
110
+ const next = new URL(res.headers.location, url).toString();
111
+ return downloadFile(next, destPath, maxRedirects - 1);
112
+ }
113
+
114
+ if (!res.statusCode || res.statusCode < 200 || res.statusCode >= 300) {
115
+ const body = await streamToString(res);
116
+ throw new Error(`HTTP ${res.statusCode ?? "?"} downloading ${url}${body ? `: ${body.trim()}` : ""}`);
117
+ }
118
+
119
+ await fsp.mkdir(path.dirname(destPath), { recursive: true });
120
+
121
+ await new Promise((resolve, reject) => {
122
+ const out = fs.createWriteStream(destPath);
123
+ res.pipe(out);
124
+ res.on("error", reject);
125
+ out.on("error", reject);
126
+ out.on("finish", resolve);
127
+ });
128
+ }
129
+
130
+ function streamToString(stream) {
131
+ return new Promise((resolve, reject) => {
132
+ let data = "";
133
+ stream.setEncoding("utf8");
134
+ stream.on("data", (chunk) => {
135
+ data += chunk;
136
+ });
137
+ stream.on("error", reject);
138
+ stream.on("end", () => resolve(data));
139
+ });
140
+ }
141
+
142
+ async function sha256File(absPath) {
143
+ const h = crypto.createHash("sha256");
144
+ await new Promise((resolve, reject) => {
145
+ const s = fs.createReadStream(absPath);
146
+ s.on("data", (chunk) => h.update(chunk));
147
+ s.on("error", reject);
148
+ s.on("end", resolve);
149
+ });
150
+ return h.digest("hex");
151
+ }
152
+
153
+ function parseChecksums(text) {
154
+ const lines = text.split(/\r?\n/);
155
+ const map = new Map();
156
+ for (const line of lines) {
157
+ const t = line.trim();
158
+ if (!t) continue;
159
+ const m = t.match(/^([a-f0-9]{64})\s{2}(.+)$/i);
160
+ if (!m) continue;
161
+ map.set(m[2].trim(), m[1].toLowerCase());
162
+ }
163
+ return map;
164
+ }
165
+
166
+ async function fileExists(p) {
167
+ try {
168
+ await fsp.access(p);
169
+ return true;
170
+ } catch {
171
+ return false;
172
+ }
173
+ }
174
+
175
+ async function chmod755(p) {
176
+ try {
177
+ await fsp.chmod(p, 0o755);
178
+ } catch {
179
+ // best-effort
180
+ }
181
+ }
182
+
183
+ async function ensureGitPreflightBinary(opts) {
184
+ const version = (process.env.GITPREFLIGHT_INSTALL_VERSION && process.env.GITPREFLIGHT_INSTALL_VERSION.trim()) || getPackageVersion();
185
+ const repo = (process.env.GITPREFLIGHT_GITHUB_REPO && process.env.GITPREFLIGHT_GITHUB_REPO.trim()) || DEFAULT_GITHUB_REPO;
186
+
187
+ const baseUrlOverride = process.env.GITPREFLIGHT_INSTALL_BASE_URL && process.env.GITPREFLIGHT_INSTALL_BASE_URL.trim()
188
+ ? process.env.GITPREFLIGHT_INSTALL_BASE_URL.trim().replace(/\/+$/, "")
189
+ : null;
190
+
191
+ if (!version) {
192
+ throw new Error("Could not determine GitPreflight version to install");
193
+ }
194
+
195
+ if (version === "0.0.0" && !baseUrlOverride) {
196
+ throw new Error(
197
+ "GitPreflight is running from a source checkout (version 0.0.0). " +
198
+ "Build from source, or set GITPREFLIGHT_INSTALL_VERSION to a released version."
199
+ );
200
+ }
201
+
202
+ const { platform, arch } = resolveTarget();
203
+ const assetName = `gitpreflight-v${version}-${platform}-${arch}`;
204
+ const base = baseUrlOverride ?? `https://github.com/${repo}/releases/download/v${version}`;
205
+ const assetUrl = `${base}/${assetName}`;
206
+ const checksumsUrl = `${base}/checksums.txt`;
207
+
208
+ const { dir, binPath } = resolveCachePaths(version);
209
+ if (await fileExists(binPath)) {
210
+ return { binPath, version, platform, arch, assetName, repo, cached: true };
211
+ }
212
+
213
+ await fsp.mkdir(dir, { recursive: true });
214
+ const tmpPath = path.join(dir, `gitpreflight.tmp-${process.pid}-${Date.now()}`);
215
+
216
+ if (!isTruthy(process.env.GITPREFLIGHT_QUIET_INSTALL) && opts?.reason !== "postinstall") {
217
+ process.stderr.write(`GitPreflight: downloading ${assetName}...\n`);
218
+ }
219
+
220
+ await downloadFile(assetUrl, tmpPath);
221
+
222
+ const checksumsText = await downloadText(checksumsUrl);
223
+ const checksums = parseChecksums(checksumsText);
224
+ const expected = checksums.get(assetName);
225
+ if (!expected) {
226
+ throw new Error(`Missing checksum for ${assetName} in checksums.txt`);
227
+ }
228
+
229
+ const actual = await sha256File(tmpPath);
230
+ if (actual !== expected) {
231
+ throw new Error(`Checksum mismatch for ${assetName} (expected ${expected}, got ${actual})`);
232
+ }
233
+
234
+ await chmod755(tmpPath);
235
+ await fsp.rename(tmpPath, binPath);
236
+ await chmod755(binPath);
237
+
238
+ return { binPath, version, platform, arch, assetName, repo, cached: false };
239
+ }
240
+
241
+ module.exports = {
242
+ ensureGitPreflightBinary
243
+ };
package/package.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "gitpreflight",
3
+ "version": "0.1.4",
4
+ "description": "GitPreflight CLI installer/wrapper (downloads the platform binary)",
5
+ "bin": {
6
+ "gitpreflight": "bin/gitpreflight.js"
7
+ },
8
+ "files": [
9
+ "bin",
10
+ "lib"
11
+ ],
12
+ "scripts": {
13
+ "postinstall": "node bin/postinstall.js"
14
+ },
15
+ "engines": {
16
+ "node": ">=18"
17
+ }
18
+ }