@rockboxd/cli 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 +45 -0
- package/bin/rockbox.js +26 -0
- package/bin/rockboxd.js +26 -0
- package/install.js +167 -0
- package/package.json +44 -0
package/README.md
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# @rockboxd/cli
|
|
2
|
+
|
|
3
|
+
Installs the [Rockbox Daemon](https://github.com/tsirysndr/rockboxd) command-line
|
|
4
|
+
tools — the `rockbox` CLI and the `rockboxd` daemon — by downloading the latest
|
|
5
|
+
prebuilt binaries from GitHub releases.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npm install -g @rockboxd/cli
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Then:
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
rockboxd # start the daemon
|
|
17
|
+
rockbox --help # control it from the CLI
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## How it works
|
|
21
|
+
|
|
22
|
+
The `postinstall` script resolves the latest release of
|
|
23
|
+
`tsirysndr/rockboxd`, downloads the tarball matching your platform, verifies
|
|
24
|
+
its sha256 checksum against the published `.sha256` asset, and extracts the
|
|
25
|
+
`rockbox` and `rockboxd` binaries into the package's `native/` directory. The
|
|
26
|
+
`rockbox` / `rockboxd` commands are thin Node shims that exec those binaries.
|
|
27
|
+
|
|
28
|
+
## Supported platforms
|
|
29
|
+
|
|
30
|
+
| Platform | Architectures |
|
|
31
|
+
| -------- | --------------- |
|
|
32
|
+
| macOS | arm64, x86_64 |
|
|
33
|
+
| Linux | x86_64, aarch64 |
|
|
34
|
+
| FreeBSD | x86_64 |
|
|
35
|
+
|
|
36
|
+
## Environment variables
|
|
37
|
+
|
|
38
|
+
| Variable | Effect |
|
|
39
|
+
| --------------------------- | ---------------------------------------------------------------- |
|
|
40
|
+
| `ROCKBOX_VERSION` | Pin a specific release tag (e.g. `2026.07.28`) instead of latest |
|
|
41
|
+
| `GITHUB_TOKEN` / `GH_TOKEN` | Authenticate GitHub API requests (avoids rate limits in CI) |
|
|
42
|
+
|
|
43
|
+
## License
|
|
44
|
+
|
|
45
|
+
GPL-2.0, same as Rockbox.
|
package/bin/rockbox.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
const fs = require("fs");
|
|
5
|
+
const path = require("path");
|
|
6
|
+
const { spawn } = require("child_process");
|
|
7
|
+
|
|
8
|
+
const bin = path.join(__dirname, "..", "native", "rockbox");
|
|
9
|
+
if (!fs.existsSync(bin)) {
|
|
10
|
+
console.error(
|
|
11
|
+
"@rockboxd/cli: rockbox binary not found. Reinstall the package " +
|
|
12
|
+
"(the postinstall step downloads it), or run `node install.js` " +
|
|
13
|
+
"inside the package directory."
|
|
14
|
+
);
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const child = spawn(bin, process.argv.slice(2), { stdio: "inherit" });
|
|
19
|
+
child.on("exit", (code, signal) => {
|
|
20
|
+
if (signal) process.kill(process.pid, signal);
|
|
21
|
+
process.exit(code === null ? 1 : code);
|
|
22
|
+
});
|
|
23
|
+
child.on("error", (err) => {
|
|
24
|
+
console.error(`@rockboxd/cli: failed to launch rockbox: ${err.message}`);
|
|
25
|
+
process.exit(1);
|
|
26
|
+
});
|
package/bin/rockboxd.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
const fs = require("fs");
|
|
5
|
+
const path = require("path");
|
|
6
|
+
const { spawn } = require("child_process");
|
|
7
|
+
|
|
8
|
+
const bin = path.join(__dirname, "..", "native", "rockboxd");
|
|
9
|
+
if (!fs.existsSync(bin)) {
|
|
10
|
+
console.error(
|
|
11
|
+
"@rockboxd/cli: rockboxd binary not found. Reinstall the package " +
|
|
12
|
+
"(the postinstall step downloads it), or run `node install.js` " +
|
|
13
|
+
"inside the package directory."
|
|
14
|
+
);
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const child = spawn(bin, process.argv.slice(2), { stdio: "inherit" });
|
|
19
|
+
child.on("exit", (code, signal) => {
|
|
20
|
+
if (signal) process.kill(process.pid, signal);
|
|
21
|
+
process.exit(code === null ? 1 : code);
|
|
22
|
+
});
|
|
23
|
+
child.on("error", (err) => {
|
|
24
|
+
console.error(`@rockboxd/cli: failed to launch rockboxd: ${err.message}`);
|
|
25
|
+
process.exit(1);
|
|
26
|
+
});
|
package/install.js
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Postinstall: download the rockbox CLI binaries from GitHub releases.
|
|
4
|
+
*
|
|
5
|
+
* Resolves the latest release of tsirysndr/rockboxd (or the tag pinned via
|
|
6
|
+
* ROCKBOX_VERSION), picks the tarball matching the current platform, verifies
|
|
7
|
+
* its sha256 against the published .sha256 asset, and extracts the `rockbox`
|
|
8
|
+
* and `rockboxd` binaries into ./native.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
"use strict";
|
|
12
|
+
|
|
13
|
+
const crypto = require("crypto");
|
|
14
|
+
const fs = require("fs");
|
|
15
|
+
const https = require("https");
|
|
16
|
+
const os = require("os");
|
|
17
|
+
const path = require("path");
|
|
18
|
+
const { spawnSync } = require("child_process");
|
|
19
|
+
|
|
20
|
+
const REPO = "tsirysndr/rockboxd";
|
|
21
|
+
const NATIVE_DIR = path.join(__dirname, "native");
|
|
22
|
+
const BINARIES = ["rockbox", "rockboxd"];
|
|
23
|
+
const MAX_REDIRECTS = 10;
|
|
24
|
+
|
|
25
|
+
function platformSlug() {
|
|
26
|
+
const platform = os.platform();
|
|
27
|
+
const arch = os.arch();
|
|
28
|
+
const table = {
|
|
29
|
+
"darwin-arm64": "aarch64-darwin",
|
|
30
|
+
"darwin-x64": "x86_64-darwin",
|
|
31
|
+
"linux-arm64": "aarch64-linux",
|
|
32
|
+
"linux-x64": "amd64-linux",
|
|
33
|
+
"freebsd-x64": "x86_64-freebsd",
|
|
34
|
+
};
|
|
35
|
+
const slug = table[`${platform}-${arch}`];
|
|
36
|
+
if (!slug) {
|
|
37
|
+
console.error(
|
|
38
|
+
`@rockboxd/cli: no prebuilt binaries for ${platform}-${arch}.\n` +
|
|
39
|
+
`See https://github.com/${REPO} for building from source.`
|
|
40
|
+
);
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
return slug;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function requestHeaders() {
|
|
47
|
+
const headers = {
|
|
48
|
+
"User-Agent": "@rockboxd/cli npm installer",
|
|
49
|
+
Accept: "application/octet-stream, application/json",
|
|
50
|
+
};
|
|
51
|
+
const token = process.env.GITHUB_TOKEN || process.env.GH_TOKEN;
|
|
52
|
+
if (token) headers.Authorization = `Bearer ${token}`;
|
|
53
|
+
return headers;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function fetch(url, redirects = 0) {
|
|
57
|
+
return new Promise((resolve, reject) => {
|
|
58
|
+
if (redirects > MAX_REDIRECTS) {
|
|
59
|
+
reject(new Error(`too many redirects fetching ${url}`));
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
https
|
|
63
|
+
.get(url, { headers: requestHeaders() }, (res) => {
|
|
64
|
+
if (
|
|
65
|
+
res.statusCode >= 300 &&
|
|
66
|
+
res.statusCode < 400 &&
|
|
67
|
+
res.headers.location
|
|
68
|
+
) {
|
|
69
|
+
res.resume();
|
|
70
|
+
resolve(fetch(new URL(res.headers.location, url).href, redirects + 1));
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (res.statusCode !== 200) {
|
|
74
|
+
res.resume();
|
|
75
|
+
reject(new Error(`GET ${url} failed with HTTP ${res.statusCode}`));
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const chunks = [];
|
|
79
|
+
res.on("data", (chunk) => chunks.push(chunk));
|
|
80
|
+
res.on("end", () => resolve(Buffer.concat(chunks)));
|
|
81
|
+
res.on("error", reject);
|
|
82
|
+
})
|
|
83
|
+
.on("error", reject);
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function resolveVersion() {
|
|
88
|
+
const pinned = process.env.ROCKBOX_VERSION;
|
|
89
|
+
if (pinned) return pinned.replace(/^v/, "");
|
|
90
|
+
const body = await fetch(
|
|
91
|
+
`https://api.github.com/repos/${REPO}/releases/latest`
|
|
92
|
+
);
|
|
93
|
+
const release = JSON.parse(body.toString("utf8"));
|
|
94
|
+
if (!release.tag_name) {
|
|
95
|
+
throw new Error(`could not resolve latest release of ${REPO}`);
|
|
96
|
+
}
|
|
97
|
+
return release.tag_name;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function verifyChecksum(tarball, checksumUrl, assetName) {
|
|
101
|
+
let expected;
|
|
102
|
+
try {
|
|
103
|
+
const body = await fetch(checksumUrl);
|
|
104
|
+
expected = body.toString("utf8").trim().split(/\s+/)[0].toLowerCase();
|
|
105
|
+
} catch (err) {
|
|
106
|
+
console.warn(
|
|
107
|
+
`@rockboxd/cli: checksum file unavailable (${err.message}), skipping verification`
|
|
108
|
+
);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const actual = crypto.createHash("sha256").update(tarball).digest("hex");
|
|
112
|
+
if (actual !== expected) {
|
|
113
|
+
throw new Error(
|
|
114
|
+
`sha256 mismatch for ${assetName}: expected ${expected}, got ${actual}`
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function extract(tarballPath) {
|
|
120
|
+
fs.mkdirSync(NATIVE_DIR, { recursive: true });
|
|
121
|
+
const result = spawnSync(
|
|
122
|
+
"tar",
|
|
123
|
+
["-xzf", tarballPath, "-C", NATIVE_DIR, ...BINARIES],
|
|
124
|
+
{ stdio: ["ignore", "inherit", "inherit"] }
|
|
125
|
+
);
|
|
126
|
+
if (result.error) throw result.error;
|
|
127
|
+
if (result.status !== 0) {
|
|
128
|
+
throw new Error(`tar exited with status ${result.status}`);
|
|
129
|
+
}
|
|
130
|
+
for (const name of BINARIES) {
|
|
131
|
+
const bin = path.join(NATIVE_DIR, name);
|
|
132
|
+
if (!fs.existsSync(bin)) {
|
|
133
|
+
throw new Error(`archive did not contain the ${name} binary`);
|
|
134
|
+
}
|
|
135
|
+
fs.chmodSync(bin, 0o755);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function main() {
|
|
140
|
+
const slug = platformSlug();
|
|
141
|
+
const version = await resolveVersion();
|
|
142
|
+
const assetName = `rockbox_${version}_${slug}.tar.gz`;
|
|
143
|
+
const base = `https://github.com/${REPO}/releases/download/${version}`;
|
|
144
|
+
|
|
145
|
+
console.log(`@rockboxd/cli: downloading ${assetName} (${version})...`);
|
|
146
|
+
const tarball = await fetch(`${base}/${assetName}`);
|
|
147
|
+
await verifyChecksum(tarball, `${base}/${assetName}.sha256`, assetName);
|
|
148
|
+
|
|
149
|
+
const tmp = path.join(
|
|
150
|
+
os.tmpdir(),
|
|
151
|
+
`rockboxd-cli-${process.pid}-${assetName}`
|
|
152
|
+
);
|
|
153
|
+
fs.writeFileSync(tmp, tarball);
|
|
154
|
+
try {
|
|
155
|
+
extract(tmp);
|
|
156
|
+
} finally {
|
|
157
|
+
fs.rmSync(tmp, { force: true });
|
|
158
|
+
}
|
|
159
|
+
console.log(
|
|
160
|
+
`@rockboxd/cli: installed ${BINARIES.join(", ")} ${version} to ${NATIVE_DIR}`
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
main().catch((err) => {
|
|
165
|
+
console.error(`@rockboxd/cli: install failed: ${err.message}`);
|
|
166
|
+
process.exit(1);
|
|
167
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@rockboxd/cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Rockbox Daemon — installs the rockbox CLI and rockboxd daemon binaries from GitHub releases",
|
|
5
|
+
"license": "GPL-2.0",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/tsirysndr/rockboxd.git"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://github.com/tsirysndr/rockboxd",
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/tsirysndr/rockboxd/issues"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"rockbox",
|
|
16
|
+
"rockboxd",
|
|
17
|
+
"audio",
|
|
18
|
+
"music",
|
|
19
|
+
"player",
|
|
20
|
+
"daemon",
|
|
21
|
+
"cli"
|
|
22
|
+
],
|
|
23
|
+
"bin": {
|
|
24
|
+
"rockbox": "bin/rockbox.js",
|
|
25
|
+
"rockboxd": "bin/rockboxd.js"
|
|
26
|
+
},
|
|
27
|
+
"scripts": {
|
|
28
|
+
"postinstall": "node install.js"
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"install.js",
|
|
32
|
+
"bin/rockbox.js",
|
|
33
|
+
"bin/rockboxd.js",
|
|
34
|
+
"README.md"
|
|
35
|
+
],
|
|
36
|
+
"os": [
|
|
37
|
+
"darwin",
|
|
38
|
+
"linux",
|
|
39
|
+
"freebsd"
|
|
40
|
+
],
|
|
41
|
+
"engines": {
|
|
42
|
+
"node": ">=18"
|
|
43
|
+
}
|
|
44
|
+
}
|