@waxhq/wax 0.1.0-alpha.3
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 +36 -0
- package/package.json +37 -0
- package/postinstall.js +266 -0
- package/run.js +33 -0
package/README.md
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# @waxhq/wax
|
|
2
|
+
|
|
3
|
+
Optional alpha npm wrapper for the wax design-system analysis CLI.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm install -g @waxhq/wax@alpha
|
|
7
|
+
wax --help
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
npx @waxhq/wax@alpha --help
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
During `postinstall`, this package downloads the host `wax` binary from the matching GitHub Release, verifies its `sha256`, validates the archive shape, and exposes `wax` through npm.
|
|
15
|
+
|
|
16
|
+
Supported hosts:
|
|
17
|
+
|
|
18
|
+
- macOS arm64 and x64
|
|
19
|
+
- Linux arm64 and x64
|
|
20
|
+
|
|
21
|
+
The curl installer remains the primary alpha install path while the npm wrapper is validated across supported hosts:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
curl -fsSL https://raw.githubusercontent.com/Daio-io/wax/main/scripts/install.sh | bash
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Release maintainers must publish prereleases under the `alpha` dist-tag and configure npm trusted publishing for `Daio-io/wax` + `release.yml` before relying on CI publish. The checked-in package version stays on a snapshot placeholder; release CI rewrites it from the Git tag before publishing.
|
|
28
|
+
|
|
29
|
+
For local smoke tests, set `WAX_CLI_VERSION` to the release you want to install; otherwise the wrapper resolves the checked-in snapshot placeholder.
|
|
30
|
+
|
|
31
|
+
Local/test environment variables:
|
|
32
|
+
|
|
33
|
+
- `WAX_CLI_RELEASE_BASE_URL`: override the release asset base URL, primarily for `file://` test mirrors.
|
|
34
|
+
- `WAX_CLI_VERSION`: override the package version used to select release assets.
|
|
35
|
+
- `WAX_CLI_REPO`: override the GitHub repository used for release downloads.
|
|
36
|
+
- `WAX_CLI_SKIP_DOWNLOAD=1`: skip the postinstall binary download.
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@waxhq/wax",
|
|
3
|
+
"version": "0.1.0-alpha.3",
|
|
4
|
+
"description": "npm wrapper for the wax design-system analysis CLI",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/Daio-io/wax.git",
|
|
9
|
+
"directory": "packages/cli"
|
|
10
|
+
},
|
|
11
|
+
"bin": {
|
|
12
|
+
"wax": "./run.js"
|
|
13
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"postinstall": "node postinstall.js",
|
|
16
|
+
"test": "node --test *.test.js"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"README.md",
|
|
20
|
+
"postinstall.js",
|
|
21
|
+
"run.js"
|
|
22
|
+
],
|
|
23
|
+
"os": [
|
|
24
|
+
"darwin",
|
|
25
|
+
"linux"
|
|
26
|
+
],
|
|
27
|
+
"cpu": [
|
|
28
|
+
"x64",
|
|
29
|
+
"arm64"
|
|
30
|
+
],
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"access": "public"
|
|
33
|
+
},
|
|
34
|
+
"engines": {
|
|
35
|
+
"node": ">=18"
|
|
36
|
+
}
|
|
37
|
+
}
|
package/postinstall.js
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const crypto = require("node:crypto");
|
|
4
|
+
const fs = require("node:fs");
|
|
5
|
+
const https = require("node:https");
|
|
6
|
+
const os = require("node:os");
|
|
7
|
+
const path = require("node:path");
|
|
8
|
+
const { pipeline } = require("node:stream");
|
|
9
|
+
const { spawnSync } = require("node:child_process");
|
|
10
|
+
|
|
11
|
+
const PACKAGE_ROOT = __dirname;
|
|
12
|
+
const PACKAGE_JSON = require("./package.json");
|
|
13
|
+
const DEFAULT_REPO = "Daio-io/wax";
|
|
14
|
+
const MAX_REDIRECTS = 5;
|
|
15
|
+
const DOWNLOAD_TIMEOUT_MS = 60_000;
|
|
16
|
+
|
|
17
|
+
function fail(message) {
|
|
18
|
+
console.error(`wax postinstall error: ${message}`);
|
|
19
|
+
console.error("");
|
|
20
|
+
console.error("Try reinstalling with:");
|
|
21
|
+
console.error(" npm install -g @waxhq/wax");
|
|
22
|
+
console.error("");
|
|
23
|
+
console.error("Or install via curl:");
|
|
24
|
+
console.error(" curl -fsSL https://raw.githubusercontent.com/Daio-io/wax/main/scripts/install.sh | bash");
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function targetTriple() {
|
|
29
|
+
const osPart = {
|
|
30
|
+
darwin: "apple-darwin",
|
|
31
|
+
linux: "unknown-linux-gnu",
|
|
32
|
+
}[process.platform];
|
|
33
|
+
|
|
34
|
+
const archPart = {
|
|
35
|
+
arm64: "aarch64",
|
|
36
|
+
x64: "x86_64",
|
|
37
|
+
}[process.arch];
|
|
38
|
+
|
|
39
|
+
if (!osPart || !archPart) {
|
|
40
|
+
fail(
|
|
41
|
+
`unsupported host ${process.platform}/${process.arch}. Supported hosts: darwin/linux on x64 or arm64.`
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return `${archPart}-${osPart}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function versionTag() {
|
|
49
|
+
const version = process.env.WAX_CLI_VERSION || PACKAGE_JSON.version;
|
|
50
|
+
return `v${version.replace(/^v/, "")}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function releaseBaseUrl(repo, tag) {
|
|
54
|
+
if (process.env.WAX_CLI_RELEASE_BASE_URL) {
|
|
55
|
+
return process.env.WAX_CLI_RELEASE_BASE_URL.replace(/\/+$/, "");
|
|
56
|
+
}
|
|
57
|
+
return `https://github.com/${repo}/releases/download/${tag}`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function download(url, destination, redirectsLeft = MAX_REDIRECTS) {
|
|
61
|
+
return new Promise((resolve, reject) => {
|
|
62
|
+
const parsedUrl = new URL(url);
|
|
63
|
+
|
|
64
|
+
if (parsedUrl.protocol === "file:") {
|
|
65
|
+
fs.copyFile(parsedUrl, destination, (error) => {
|
|
66
|
+
if (error) {
|
|
67
|
+
reject(error);
|
|
68
|
+
} else {
|
|
69
|
+
resolve();
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (parsedUrl.protocol !== "https:") {
|
|
76
|
+
reject(new Error(`unsupported download URL protocol: ${parsedUrl.protocol}`));
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const request = https.get(
|
|
81
|
+
url,
|
|
82
|
+
{
|
|
83
|
+
headers: {
|
|
84
|
+
"user-agent": `@waxhq/wax/${PACKAGE_JSON.version}`,
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
(response) => {
|
|
88
|
+
const status = response.statusCode || 0;
|
|
89
|
+
const location = response.headers.location;
|
|
90
|
+
|
|
91
|
+
if (status >= 300 && status < 400 && location) {
|
|
92
|
+
response.resume();
|
|
93
|
+
if (redirectsLeft <= 0) {
|
|
94
|
+
reject(new Error(`too many redirects while downloading ${url}`));
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const redirectedUrl = new URL(location, url).toString();
|
|
98
|
+
download(redirectedUrl, destination, redirectsLeft - 1).then(resolve, reject);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (status < 200 || status >= 300) {
|
|
103
|
+
response.resume();
|
|
104
|
+
reject(new Error(`download failed (${status}) for ${url}`));
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const file = fs.createWriteStream(destination, { mode: 0o600 });
|
|
109
|
+
pipeline(response, file, (error) => {
|
|
110
|
+
if (error) {
|
|
111
|
+
reject(new Error(`download stream failed for ${url}: ${error.message}`));
|
|
112
|
+
} else {
|
|
113
|
+
resolve();
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
request.setTimeout(DOWNLOAD_TIMEOUT_MS, () => {
|
|
120
|
+
request.destroy(new Error(`download timed out after ${DOWNLOAD_TIMEOUT_MS / 1000}s for ${url}`));
|
|
121
|
+
});
|
|
122
|
+
request.on("error", reject);
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function sha256(filePath) {
|
|
127
|
+
const hash = crypto.createHash("sha256");
|
|
128
|
+
const file = fs.readFileSync(filePath);
|
|
129
|
+
hash.update(file);
|
|
130
|
+
return hash.digest("hex");
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function expectedSha256(checksumText, archiveName) {
|
|
134
|
+
for (const line of checksumText.split(/\r?\n/)) {
|
|
135
|
+
const match = line.match(/^([a-fA-F0-9]{64})\s+\*?(.+)$/);
|
|
136
|
+
if (match && path.basename(match[2].trim()) === archiveName) {
|
|
137
|
+
return match[1].toLowerCase();
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
throw new Error(`checksum file did not contain a sha256 for ${archiveName}`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function runTar(args, cwd) {
|
|
145
|
+
const result = spawnSync("tar", args, {
|
|
146
|
+
cwd,
|
|
147
|
+
encoding: "utf8",
|
|
148
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
if (result.error && result.error.code === "ENOENT") {
|
|
152
|
+
fail("required command not found: tar");
|
|
153
|
+
}
|
|
154
|
+
if (result.status !== 0) {
|
|
155
|
+
const detail = result.stderr.trim() || (result.error && result.error.message) || `exit status ${result.status}`;
|
|
156
|
+
fail(`tar ${args.join(" ")} failed: ${detail}`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return result.stdout;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function archiveEntries(archivePath, cwd) {
|
|
163
|
+
return runTar(["-tzf", archivePath], cwd)
|
|
164
|
+
.trim()
|
|
165
|
+
.split("\n")
|
|
166
|
+
.filter(Boolean);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function archiveListing(archivePath, cwd) {
|
|
170
|
+
return runTar(["-tvzf", archivePath], cwd)
|
|
171
|
+
.trim()
|
|
172
|
+
.split("\n")
|
|
173
|
+
.filter(Boolean);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function validateArchive(archivePath, cwd, expectedDir, expectedMember) {
|
|
177
|
+
const entries = archiveEntries(archivePath, cwd);
|
|
178
|
+
if (!entries.includes(expectedMember)) {
|
|
179
|
+
throw new Error(`archive is missing expected entry: ${expectedMember}`);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const unexpected = entries.filter((entry) => entry !== `${expectedDir}/` && entry !== expectedMember);
|
|
183
|
+
if (unexpected.length > 0) {
|
|
184
|
+
throw new Error(`archive contains unexpected entries: ${unexpected.join(", ")}`);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const listing = archiveListing(archivePath, cwd);
|
|
188
|
+
if (listing.length !== entries.length) {
|
|
189
|
+
throw new Error("archive listing did not match archive entries");
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
for (const [index, entry] of entries.entries()) {
|
|
193
|
+
const type = listing[index][0];
|
|
194
|
+
if (entry === `${expectedDir}/` && type !== "d") {
|
|
195
|
+
throw new Error(`archive entry is not a directory: ${entry}`);
|
|
196
|
+
}
|
|
197
|
+
if (entry === expectedMember && type !== "-") {
|
|
198
|
+
throw new Error(`archive entry is not a regular file: ${entry}`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async function install() {
|
|
204
|
+
if (process.env.WAX_CLI_SKIP_DOWNLOAD === "1") {
|
|
205
|
+
console.log("Skipping wax binary download because WAX_CLI_SKIP_DOWNLOAD=1");
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const repo = process.env.WAX_CLI_REPO || DEFAULT_REPO;
|
|
210
|
+
const tag = versionTag();
|
|
211
|
+
const version = tag.replace(/^v/, "");
|
|
212
|
+
const target = targetTriple();
|
|
213
|
+
const archiveName = `wax-${version}-${target}.tar.gz`;
|
|
214
|
+
const archiveUrl = `${releaseBaseUrl(repo, tag)}/${archiveName}`;
|
|
215
|
+
const checksumUrl = `${archiveUrl}.sha256`;
|
|
216
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "wax-cli-"));
|
|
217
|
+
const archivePath = path.join(tmpDir, archiveName);
|
|
218
|
+
const checksumPath = `${archivePath}.sha256`;
|
|
219
|
+
const extractDir = path.join(tmpDir, "extract");
|
|
220
|
+
const expectedDir = `wax-${version}-${target}`;
|
|
221
|
+
const expectedMember = `${expectedDir}/wax`;
|
|
222
|
+
const installDir = path.join(PACKAGE_ROOT, "bin");
|
|
223
|
+
const installPath = path.join(installDir, "wax");
|
|
224
|
+
|
|
225
|
+
try {
|
|
226
|
+
console.log(`Installing wax ${version} for ${target}`);
|
|
227
|
+
console.log(`Download: ${archiveUrl}`);
|
|
228
|
+
|
|
229
|
+
await download(archiveUrl, archivePath);
|
|
230
|
+
await download(checksumUrl, checksumPath);
|
|
231
|
+
|
|
232
|
+
const expected = expectedSha256(fs.readFileSync(checksumPath, "utf8"), archiveName);
|
|
233
|
+
const actual = sha256(archivePath);
|
|
234
|
+
if (actual !== expected) {
|
|
235
|
+
fail(`checksum mismatch for ${archiveName}; expected ${expected}, got ${actual}`);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
validateArchive(archivePath, tmpDir, expectedDir, expectedMember);
|
|
239
|
+
|
|
240
|
+
fs.mkdirSync(extractDir, { recursive: true });
|
|
241
|
+
runTar(["-xzf", archivePath, "-C", extractDir, expectedMember], tmpDir);
|
|
242
|
+
const extractedBinary = path.join(extractDir, expectedMember);
|
|
243
|
+
const extractedStat = fs.lstatSync(extractedBinary);
|
|
244
|
+
if (!extractedStat.isFile()) {
|
|
245
|
+
fail(`archive entry is not a regular file: ${expectedMember}`);
|
|
246
|
+
}
|
|
247
|
+
fs.rmSync(installDir, { recursive: true, force: true });
|
|
248
|
+
fs.mkdirSync(installDir, { recursive: true });
|
|
249
|
+
fs.copyFileSync(extractedBinary, installPath);
|
|
250
|
+
fs.chmodSync(installPath, 0o755);
|
|
251
|
+
|
|
252
|
+
console.log(`Installed wax to ${installPath}`);
|
|
253
|
+
} finally {
|
|
254
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (require.main === module) {
|
|
259
|
+
install().catch((error) => fail(error.message));
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
module.exports = {
|
|
263
|
+
download,
|
|
264
|
+
expectedSha256,
|
|
265
|
+
validateArchive,
|
|
266
|
+
};
|
package/run.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require("node:fs");
|
|
4
|
+
const path = require("node:path");
|
|
5
|
+
const { spawnSync } = require("node:child_process");
|
|
6
|
+
|
|
7
|
+
const binaryPath = path.join(__dirname, "bin", process.platform === "win32" ? "wax.exe" : "wax");
|
|
8
|
+
|
|
9
|
+
if (!fs.existsSync(binaryPath)) {
|
|
10
|
+
console.error("wax binary is missing from this @waxhq/wax installation.");
|
|
11
|
+
console.error("");
|
|
12
|
+
console.error("Reinstall the package to download the host binary:");
|
|
13
|
+
console.error(" npm install -g @waxhq/wax");
|
|
14
|
+
console.error("");
|
|
15
|
+
console.error("If npm lifecycle scripts were disabled, reinstall without --ignore-scripts.");
|
|
16
|
+
console.error("If WAX_CLI_SKIP_DOWNLOAD=1 was intentional, provide packages/cli/bin/wax before running.");
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const result = spawnSync(binaryPath, process.argv.slice(2), {
|
|
21
|
+
stdio: "inherit",
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
if (result.error) {
|
|
25
|
+
console.error(`failed to run wax: ${result.error.message}`);
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (result.signal) {
|
|
30
|
+
process.kill(process.pid, result.signal);
|
|
31
|
+
} else {
|
|
32
|
+
process.exit(result.status === null ? 1 : result.status);
|
|
33
|
+
}
|