@chenronggui/hookdock 0.1.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/README.md +10 -0
- package/bin/hookdock.mjs +17 -0
- package/lib/downloader.mjs +103 -0
- package/package.json +32 -0
package/README.md
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# @chenronggui/hookdock
|
|
2
|
+
|
|
3
|
+
Downloads the HookDock Windows x64 installer from the GitHub Release that exactly matches this package version. The CLI verifies the release SHA-256 checksum and never runs the installer.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npx @chenronggui/hookdock@0.1.0 download
|
|
7
|
+
npx @chenronggui/hookdock@0.1.0 download --output ./dist
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
Use `--force` to replace an existing `HookDock-Setup-x64.exe`.
|
package/bin/hookdock.mjs
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import process from "node:process";
|
|
4
|
+
import { downloadInstaller, parseArguments, usage } from "../lib/downloader.mjs";
|
|
5
|
+
|
|
6
|
+
try {
|
|
7
|
+
const options = parseArguments(process.argv.slice(2));
|
|
8
|
+
if (options.help) {
|
|
9
|
+
process.stdout.write(`${usage()}\n`);
|
|
10
|
+
} else {
|
|
11
|
+
const destination = await downloadInstaller(options);
|
|
12
|
+
process.stdout.write(`HookDock ${options.version} downloaded to ${destination}\n`);
|
|
13
|
+
}
|
|
14
|
+
} catch (error) {
|
|
15
|
+
process.stderr.write(`hookdock: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
16
|
+
process.exitCode = 1;
|
|
17
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { createWriteStream, readFileSync } from "node:fs";
|
|
3
|
+
import { access, mkdir, readFile, rename, rm } from "node:fs/promises";
|
|
4
|
+
import { Readable } from "node:stream";
|
|
5
|
+
import { pipeline } from "node:stream/promises";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
|
|
9
|
+
const OWNER = "XIAOGUIGUI";
|
|
10
|
+
const REPOSITORY = "hookdock";
|
|
11
|
+
const ASSET_NAME = "HookDock-Setup-x64.exe";
|
|
12
|
+
const CHECKSUM_NAME = "checksums.txt";
|
|
13
|
+
|
|
14
|
+
export function usage() {
|
|
15
|
+
return [
|
|
16
|
+
"Download the verified HookDock Windows installer.",
|
|
17
|
+
"",
|
|
18
|
+
"Usage:",
|
|
19
|
+
" npx @chenronggui/hookdock download [--output <directory>] [--force]",
|
|
20
|
+
].join("\n");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function parseArguments(argumentsList, version = packageVersion()) {
|
|
24
|
+
if (argumentsList.includes("--help") || argumentsList.includes("-h")) {
|
|
25
|
+
return { help: true, force: false, output: process.cwd(), version };
|
|
26
|
+
}
|
|
27
|
+
if (argumentsList[0] !== "download") {
|
|
28
|
+
throw new Error(`expected the \"download\" command\n\n${usage()}`);
|
|
29
|
+
}
|
|
30
|
+
let output = process.cwd();
|
|
31
|
+
let force = false;
|
|
32
|
+
for (let index = 1; index < argumentsList.length; index += 1) {
|
|
33
|
+
const argument = argumentsList[index];
|
|
34
|
+
if (argument === "--force") {
|
|
35
|
+
force = true;
|
|
36
|
+
} else if (argument === "--output") {
|
|
37
|
+
const directory = argumentsList[index + 1];
|
|
38
|
+
if (!directory) throw new Error("--output requires a directory");
|
|
39
|
+
output = path.resolve(directory);
|
|
40
|
+
index += 1;
|
|
41
|
+
} else {
|
|
42
|
+
throw new Error(`unknown option: ${argument}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return { help: false, force, output, version };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function parseChecksum(contents, assetName = ASSET_NAME) {
|
|
49
|
+
for (const line of contents.split(/\r?\n/)) {
|
|
50
|
+
const match = line.trim().match(/^([a-fA-F0-9]{64})\s+\*?(.+)$/);
|
|
51
|
+
if (match && match[2] === assetName) return match[1].toLowerCase();
|
|
52
|
+
}
|
|
53
|
+
throw new Error(`checksum for ${assetName} was not found`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function downloadInstaller(options) {
|
|
57
|
+
const tag = `v${options.version}`;
|
|
58
|
+
const releaseRoot = `https://github.com/${OWNER}/${REPOSITORY}/releases/download/${tag}`;
|
|
59
|
+
const destinationDirectory = path.resolve(options.output);
|
|
60
|
+
const destination = path.join(destinationDirectory, ASSET_NAME);
|
|
61
|
+
const temporary = `${destination}.download-${process.pid}`;
|
|
62
|
+
await mkdir(destinationDirectory, { recursive: true });
|
|
63
|
+
if (!options.force && await exists(destination)) {
|
|
64
|
+
throw new Error(`${destination} already exists; pass --force to replace it`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const checksumResponse = await fetch(`${releaseRoot}/${CHECKSUM_NAME}`, { redirect: "follow" });
|
|
68
|
+
if (!checksumResponse.ok) throw new Error(`could not fetch checksums (${checksumResponse.status})`);
|
|
69
|
+
const expectedChecksum = parseChecksum(await checksumResponse.text());
|
|
70
|
+
|
|
71
|
+
const installerResponse = await fetch(`${releaseRoot}/${ASSET_NAME}`, { redirect: "follow" });
|
|
72
|
+
if (!installerResponse.ok || !installerResponse.body) {
|
|
73
|
+
throw new Error(`could not download installer (${installerResponse.status})`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
await pipeline(Readable.fromWeb(installerResponse.body), createWriteStream(temporary, { flags: "wx" }));
|
|
78
|
+
const actualChecksum = createHash("sha256").update(await readFile(temporary)).digest("hex");
|
|
79
|
+
if (actualChecksum !== expectedChecksum) {
|
|
80
|
+
throw new Error("downloaded installer failed SHA-256 verification");
|
|
81
|
+
}
|
|
82
|
+
if (options.force) await rm(destination, { force: true });
|
|
83
|
+
await rename(temporary, destination);
|
|
84
|
+
return destination;
|
|
85
|
+
} catch (error) {
|
|
86
|
+
await rm(temporary, { force: true });
|
|
87
|
+
throw error;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function exists(target) {
|
|
92
|
+
try {
|
|
93
|
+
await access(target);
|
|
94
|
+
return true;
|
|
95
|
+
} catch {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function packageVersion() {
|
|
101
|
+
const packagePath = fileURLToPath(new URL("../package.json", import.meta.url));
|
|
102
|
+
return JSON.parse(readFileSync(packagePath, "utf8")).version;
|
|
103
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@chenronggui/hookdock",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Verified downloader for the HookDock Windows installer",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/XIAOGUIGUI/hookdock.git"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://github.com/XIAOGUIGUI/hookdock#readme",
|
|
11
|
+
"bugs": "https://github.com/XIAOGUIGUI/hookdock/issues",
|
|
12
|
+
"type": "module",
|
|
13
|
+
"bin": {
|
|
14
|
+
"hookdock": "bin/hookdock.mjs"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"bin",
|
|
18
|
+
"lib",
|
|
19
|
+
"README.md"
|
|
20
|
+
],
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=18.17"
|
|
23
|
+
},
|
|
24
|
+
"scripts": {
|
|
25
|
+
"test": "node --test",
|
|
26
|
+
"pack:check": "npm pack --dry-run"
|
|
27
|
+
},
|
|
28
|
+
"publishConfig": {
|
|
29
|
+
"access": "public",
|
|
30
|
+
"provenance": true
|
|
31
|
+
}
|
|
32
|
+
}
|