@humanmd/cli 0.1.0-alpha.5

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 ADDED
@@ -0,0 +1,13 @@
1
+ # @humanmd/cli
2
+
3
+ Native npm wrapper for the HMD command-line tool.
4
+
5
+ The package installs the platform-specific `hmd` binary from GitHub release artifacts generated by `cargo-dist`. JavaScript is only used as distribution glue; it does not parse or render HMD documents.
6
+
7
+ ```bash
8
+ npm install -g @humanmd/cli
9
+ hmd --help
10
+ npx @humanmd/cli validate examples/todo.hmd
11
+ ```
12
+
13
+ For local development, set `HMD_CLI_BINARY=/path/to/hmd` or run `npm test` from this package.
package/bin/hmd.js ADDED
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env node
2
+ import { spawnSync } from "node:child_process";
3
+ import { existsSync } from "node:fs";
4
+ import { dirname, join } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ const packageRoot = dirname(dirname(fileURLToPath(import.meta.url)));
8
+ const binaryName = process.platform === "win32" ? "hmd.exe" : "hmd";
9
+ const candidates = [
10
+ process.env.HMD_CLI_BINARY,
11
+ join(packageRoot, "vendor", binaryName),
12
+ join(packageRoot, "vendor", "hmd"),
13
+ ].filter(Boolean);
14
+
15
+ const binary = candidates.find((candidate) => existsSync(candidate));
16
+
17
+ if (!binary) {
18
+ console.error(
19
+ "error: hmd native binary not found. Reinstall @humanmd/cli or set HMD_CLI_BINARY."
20
+ );
21
+ process.exit(1);
22
+ }
23
+
24
+ const child = spawnSync(binary, process.argv.slice(2), {
25
+ stdio: "inherit",
26
+ windowsHide: false,
27
+ });
28
+
29
+ if (child.error) {
30
+ console.error(`error: failed to run ${binary}: ${child.error.message}`);
31
+ process.exit(1);
32
+ }
33
+
34
+ if (child.signal) {
35
+ process.kill(process.pid, child.signal);
36
+ }
37
+
38
+ process.exit(child.status ?? 1);
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "@humanmd/cli",
3
+ "version": "0.1.0-alpha.5",
4
+ "description": "Native HMD CLI wrapper for npm.",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/VrianCao/hmd.git",
9
+ "directory": "packages/npm-cli"
10
+ },
11
+ "publishConfig": {
12
+ "access": "public"
13
+ },
14
+ "type": "module",
15
+ "bin": {
16
+ "hmd": "bin/hmd.js"
17
+ },
18
+ "files": [
19
+ "bin",
20
+ "scripts",
21
+ "README.md"
22
+ ],
23
+ "scripts": {
24
+ "postinstall": "node scripts/postinstall.js",
25
+ "test": "node test/smoke.mjs",
26
+ "pack:check": "npm pack --dry-run"
27
+ },
28
+ "engines": {
29
+ "node": ">=18"
30
+ }
31
+ }
@@ -0,0 +1,206 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import {
3
+ chmodSync,
4
+ copyFileSync,
5
+ existsSync,
6
+ mkdirSync,
7
+ mkdtempSync,
8
+ readFileSync,
9
+ readdirSync,
10
+ rmSync,
11
+ statSync,
12
+ writeFileSync,
13
+ } from "node:fs";
14
+ import { get } from "node:https";
15
+ import { tmpdir } from "node:os";
16
+ import { basename, dirname, join } from "node:path";
17
+ import { fileURLToPath } from "node:url";
18
+
19
+ const packageRoot = dirname(dirname(fileURLToPath(import.meta.url)));
20
+ const vendorDir = join(packageRoot, "vendor");
21
+ const binaryName = process.platform === "win32" ? "hmd.exe" : "hmd";
22
+ const destination = join(vendorDir, binaryName);
23
+
24
+ if (process.env.HMD_SKIP_DOWNLOAD === "1") {
25
+ console.log("Skipping hmd binary download because HMD_SKIP_DOWNLOAD=1.");
26
+ process.exit(0);
27
+ }
28
+
29
+ mkdirSync(vendorDir, { recursive: true });
30
+
31
+ if (process.env.HMD_CLI_BINARY) {
32
+ copyNativeBinary(process.env.HMD_CLI_BINARY, destination);
33
+ process.exit(0);
34
+ }
35
+
36
+ if (existsSync(destination)) {
37
+ process.exit(0);
38
+ }
39
+
40
+ const packageJson = JSON.parse(readFileSync(join(packageRoot, "package.json"), "utf8"));
41
+ const repo = process.env.HMD_CLI_REPOSITORY || "VrianCao/hmd";
42
+ const version = process.env.HMD_CLI_VERSION || packageJson.version;
43
+ const tag =
44
+ process.env.HMD_CLI_RELEASE_TAG || (version.startsWith("v") ? version : `v${version}`);
45
+ const target = targetTriple();
46
+ const assetUrl = process.env.HMD_CLI_ASSET_URL || (await findReleaseAsset(repo, tag, target));
47
+ const workDir = mkdtempSync(join(tmpdir(), "hmd-cli-"));
48
+
49
+ try {
50
+ const archivePath = join(workDir, basename(new URL(assetUrl).pathname));
51
+ await download(assetUrl, archivePath);
52
+ const extractDir = join(workDir, "extract");
53
+ mkdirSync(extractDir, { recursive: true });
54
+ extractArchive(archivePath, extractDir);
55
+ const extractedBinary = findBinary(extractDir);
56
+
57
+ if (!extractedBinary) {
58
+ throw new Error(`downloaded artifact does not contain ${binaryName}`);
59
+ }
60
+
61
+ copyNativeBinary(extractedBinary, destination);
62
+ } finally {
63
+ rmSync(workDir, { recursive: true, force: true });
64
+ }
65
+
66
+ function targetTriple() {
67
+ const key = `${process.platform}-${process.arch}`;
68
+ const targets = {
69
+ "darwin-x64": "x86_64-apple-darwin",
70
+ "darwin-arm64": "aarch64-apple-darwin",
71
+ "linux-x64": "x86_64-unknown-linux-gnu",
72
+ "linux-arm64": "aarch64-unknown-linux-gnu",
73
+ "win32-x64": "x86_64-pc-windows-msvc",
74
+ };
75
+ const target = targets[key];
76
+
77
+ if (!target) {
78
+ throw new Error(`unsupported platform for @humanmd/cli: ${key}`);
79
+ }
80
+
81
+ return target;
82
+ }
83
+
84
+ async function findReleaseAsset(repo, tag, target) {
85
+ const releaseUrl = `https://api.github.com/repos/${repo}/releases/tags/${tag}`;
86
+ const release = JSON.parse(await fetchText(releaseUrl));
87
+ const asset = release.assets
88
+ ?.filter((candidate) => candidate.name.includes(target))
89
+ .filter((candidate) => /\.(tar\.xz|tar\.gz|tgz|zip)$/.test(candidate.name))
90
+ .find((candidate) => /(^|[-_])hmd([-_]|$)|hmd-cli/.test(candidate.name));
91
+
92
+ if (!asset) {
93
+ throw new Error(`no @humanmd/cli release artifact for ${target} in ${repo}@${tag}`);
94
+ }
95
+
96
+ return asset.browser_download_url;
97
+ }
98
+
99
+ function fetchText(url) {
100
+ return new Promise((resolve, reject) => {
101
+ get(
102
+ url,
103
+ {
104
+ headers: {
105
+ Accept: "application/vnd.github+json",
106
+ "User-Agent": "@humanmd/cli postinstall",
107
+ },
108
+ },
109
+ (response) => {
110
+ if (isRedirect(response.statusCode)) {
111
+ fetchText(response.headers.location).then(resolve, reject);
112
+ return;
113
+ }
114
+
115
+ if (response.statusCode < 200 || response.statusCode >= 300) {
116
+ reject(new Error(`GET ${url} failed with HTTP ${response.statusCode}`));
117
+ response.resume();
118
+ return;
119
+ }
120
+
121
+ const chunks = [];
122
+ response.setEncoding("utf8");
123
+ response.on("data", (chunk) => chunks.push(chunk));
124
+ response.on("end", () => resolve(chunks.join("")));
125
+ }
126
+ ).on("error", reject);
127
+ });
128
+ }
129
+
130
+ function download(url, path) {
131
+ return new Promise((resolve, reject) => {
132
+ get(
133
+ url,
134
+ {
135
+ headers: {
136
+ "User-Agent": "@humanmd/cli postinstall",
137
+ },
138
+ },
139
+ (response) => {
140
+ if (isRedirect(response.statusCode)) {
141
+ download(response.headers.location, path).then(resolve, reject);
142
+ return;
143
+ }
144
+
145
+ if (response.statusCode < 200 || response.statusCode >= 300) {
146
+ reject(new Error(`GET ${url} failed with HTTP ${response.statusCode}`));
147
+ response.resume();
148
+ return;
149
+ }
150
+
151
+ const chunks = [];
152
+ response.on("data", (chunk) => chunks.push(chunk));
153
+ response.on("end", () => {
154
+ writeFileSync(path, Buffer.concat(chunks));
155
+ resolve();
156
+ });
157
+ }
158
+ ).on("error", reject);
159
+ });
160
+ }
161
+
162
+ function isRedirect(statusCode) {
163
+ return [301, 302, 303, 307, 308].includes(statusCode);
164
+ }
165
+
166
+ function extractArchive(archivePath, extractDir) {
167
+ if (archivePath.endsWith(".zip")) {
168
+ if (process.platform === "win32") {
169
+ execFileSync("powershell", [
170
+ "-NoProfile",
171
+ "-Command",
172
+ `Expand-Archive -LiteralPath '${archivePath}' -DestinationPath '${extractDir}' -Force`,
173
+ ]);
174
+ return;
175
+ }
176
+
177
+ execFileSync("unzip", ["-q", archivePath, "-d", extractDir]);
178
+ return;
179
+ }
180
+
181
+ execFileSync("tar", ["-xf", archivePath, "-C", extractDir]);
182
+ }
183
+
184
+ function findBinary(dir) {
185
+ for (const entry of readdirSync(dir)) {
186
+ const path = join(dir, entry);
187
+ const stat = statSync(path);
188
+
189
+ if (stat.isDirectory()) {
190
+ const nested = findBinary(path);
191
+ if (nested) {
192
+ return nested;
193
+ }
194
+ } else if (entry === binaryName || (process.platform !== "win32" && entry === "hmd")) {
195
+ return path;
196
+ }
197
+ }
198
+
199
+ return null;
200
+ }
201
+
202
+ function copyNativeBinary(source, dest) {
203
+ copyFileSync(source, dest);
204
+ chmodSync(dest, 0o755);
205
+ console.log(`Installed hmd binary to ${dest}.`);
206
+ }