linkr-bee-cli 0.0.0-stage → 1.1.2

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/bin/bee-cli.js ADDED
@@ -0,0 +1,42 @@
1
+ #!/usr/bin/env node
2
+ 'use strict'
3
+ /**
4
+ * `npx linkr-bee-cli …` / the installed `bee-cli` shim.
5
+ *
6
+ * The arguments go straight to the real program, with the parent's stdio so a
7
+ * TUI or an interactive session behaves as if it had been run directly, and
8
+ * the exit status is the target program's own.
9
+ */
10
+
11
+ const { spawn } = require('node:child_process')
12
+ const { ensureBinary } = require('../lib/ensure')
13
+
14
+ async function main() {
15
+ let binary
16
+ try {
17
+ binary = await ensureBinary()
18
+ } catch (error) {
19
+ process.stderr.write(`bee-cli: ${error.message}\n`)
20
+ process.exit(1)
21
+ }
22
+ const child = spawn(binary, process.argv.slice(2), { stdio: 'inherit' })
23
+ for (const signal of ['SIGINT', 'SIGTERM', 'SIGHUP', 'SIGQUIT']) {
24
+ process.on(signal, () => {
25
+ try {
26
+ child.kill(signal)
27
+ } catch {
28
+ /* the child is already gone */
29
+ }
30
+ })
31
+ }
32
+ child.on('error', (error) => {
33
+ process.stderr.write(`bee-cli: cannot run ${binary}: ${error.message}\n`)
34
+ process.exit(1)
35
+ })
36
+ child.on('exit', (code, signal) => {
37
+ if (signal) process.kill(process.pid, signal)
38
+ else process.exit(code === null ? 1 : code)
39
+ })
40
+ }
41
+
42
+ main()
@@ -0,0 +1,73 @@
1
+ 'use strict'
2
+ /**
3
+ * Getting the release's bytes onto this machine: https, http, and `file:` for
4
+ * a mirror or a test. No dependencies and no shelling out to curl, so the same
5
+ * code runs on every platform Node supports.
6
+ */
7
+
8
+ const fs = require('node:fs')
9
+ const path = require('node:path')
10
+ const http = require('node:http')
11
+ const https = require('node:https')
12
+ const { fileURLToPath } = require('node:url')
13
+
14
+ const REDIRECTS = 5
15
+ const TIMEOUT_MS = 60000
16
+
17
+ /** Fetch `url` into `target`, following redirects and rejecting on HTTP errors. */
18
+ function download(url, target, redirectsLeft = REDIRECTS) {
19
+ if (url.startsWith('file:')) {
20
+ const source = fileURLToPath(url)
21
+ return fs.promises.mkdir(path.dirname(target), { recursive: true }).then(() =>
22
+ fs.promises.copyFile(source, target),
23
+ )
24
+ }
25
+ return new Promise((resolve, reject) => {
26
+ const client = url.startsWith('https:') ? https : http
27
+ const request = client.get(url, { timeout: TIMEOUT_MS, headers: { 'user-agent': 'bee-cli-npm' } }, (response) => {
28
+ const status = response.statusCode || 0
29
+ if (status >= 300 && status < 400 && response.headers.location) {
30
+ response.resume()
31
+ if (redirectsLeft <= 0) {
32
+ reject(new Error(`too many redirects for ${url}`))
33
+ return
34
+ }
35
+ const next = new URL(response.headers.location, url).toString()
36
+ download(next, target, redirectsLeft - 1).then(resolve, reject)
37
+ return
38
+ }
39
+ if (status !== 200) {
40
+ response.resume()
41
+ reject(new Error(`${url} answered HTTP ${status}`))
42
+ return
43
+ }
44
+ fs.promises
45
+ .mkdir(path.dirname(target), { recursive: true })
46
+ .then(() => {
47
+ const out = fs.createWriteStream(target, { mode: 0o755 })
48
+ out.on('error', reject)
49
+ response.on('error', reject)
50
+ response.pipe(out)
51
+ out.on('close', () => resolve(target))
52
+ })
53
+ .catch(reject)
54
+ })
55
+ request.on('timeout', () => request.destroy(new Error(`timed out after ${TIMEOUT_MS} ms: ${url}`)))
56
+ request.on('error', reject)
57
+ })
58
+ }
59
+
60
+ /** The text at `url`, or null when it is not there. */
61
+ async function fetchText(url) {
62
+ try {
63
+ const tmp = path.join(require('node:os').tmpdir(), `bee-cli-${process.pid}-${Date.now()}.txt`)
64
+ await download(url, tmp)
65
+ const text = await fs.promises.readFile(tmp, 'utf8')
66
+ await fs.promises.rm(tmp, { force: true })
67
+ return text
68
+ } catch {
69
+ return null
70
+ }
71
+ }
72
+
73
+ module.exports = { download, fetchText }
package/lib/ensure.js ADDED
@@ -0,0 +1,90 @@
1
+ 'use strict'
2
+ /**
3
+ * Make sure this machine has a runnable `bee-cli`, and answer its path.
4
+ *
5
+ * Order, first win:
6
+ * 1. `$BEE_CLI_BINARY` an explicit path (CI, a checkout, a packager)
7
+ * 2. a cached download `~/.cache/bee-cli/<version>/<target>/bee-cli`
8
+ * 3. the release asset `bee-cli-<target>` beside the SHA256SUMS
9
+ */
10
+
11
+ const fs = require('node:fs')
12
+ const path = require('node:path')
13
+ const {
14
+ assetName,
15
+ hostTarget,
16
+ detectLibc,
17
+ cachePath,
18
+ releaseBase,
19
+ checksumFor,
20
+ sha256File,
21
+ } = require('./resolve')
22
+ const { download, fetchText } = require('./download')
23
+
24
+ const VERSION = require('../package.json').version
25
+
26
+ const BUILD_HINT =
27
+ 'no prebuilt bee-cli for this platform; build it from source: ' +
28
+ 'git clone https://github.com/radxa/linkr-bee && cd linkr-bee/cliproxy-import/tools/linkr-tui && ' +
29
+ 'cargo build --release --no-default-features --features cli, then set BEE_CLI_BINARY to that binary ' +
30
+ '(or BEE_CLI_TARGET to a release target name).'
31
+
32
+ /** Whether a path is a file this process may execute. */
33
+ function isExecutable(file) {
34
+ try {
35
+ fs.accessSync(file, fs.constants.X_OK)
36
+ return fs.statSync(file).isFile()
37
+ } catch {
38
+ return false
39
+ }
40
+ }
41
+
42
+ /** The cached binary for this machine, if a previous run left one. */
43
+ function cachedBinary(version = VERSION, env = process.env) {
44
+ const target = env.BEE_CLI_TARGET || hostTarget(process.platform, process.arch, detectLibc())
45
+ if (!target) return null
46
+ const file = cachePath(version, target, { env, platform: process.platform })
47
+ return isExecutable(file) ? file : null
48
+ }
49
+
50
+ /**
51
+ * Answer a runnable binary, downloading and verifying the release asset when
52
+ * there is no usable cache. Throws with a build hint when the platform has no
53
+ * published asset; a caller that can do without the binary should catch.
54
+ */
55
+ async function ensureBinary({ version = process.env.BEE_CLI_VERSION || VERSION, env = process.env, quiet = false } = {}) {
56
+ if (env.BEE_CLI_BINARY) {
57
+ if (!isExecutable(env.BEE_CLI_BINARY)) throw new Error(`BEE_CLI_BINARY is not an executable file: ${env.BEE_CLI_BINARY}`)
58
+ return env.BEE_CLI_BINARY
59
+ }
60
+ const cached = cachedBinary(version, env)
61
+ if (cached) return cached
62
+
63
+ const target = env.BEE_CLI_TARGET || hostTarget(process.platform, process.arch, detectLibc())
64
+ if (!target) throw new Error(BUILD_HINT)
65
+
66
+ const asset = assetName(target, process.platform)
67
+ const base = releaseBase(version, env)
68
+ const destination = cachePath(version, target, { env, platform: process.platform })
69
+ if (!quiet) process.stderr.write(`bee-cli: fetching ${base}/${asset}\n`)
70
+
71
+ const tmp = `${destination}.${process.pid}.part`
72
+ try {
73
+ await download(`${base}/${asset}`, tmp)
74
+ const expected = env.BEE_CLI_SHA256 || checksumFor(await fetchText(`${base}/SHA256SUMS`), asset)
75
+ if (!expected) throw new Error(`the release has no SHA256SUMS entry for ${asset}; refusing an unverified binary`)
76
+ const actual = await sha256File(tmp)
77
+ if (actual !== expected.toLowerCase()) {
78
+ throw new Error(`checksum mismatch for ${asset}: expected ${expected}, got ${actual}`)
79
+ }
80
+ await fs.promises.chmod(tmp, 0o755)
81
+ await fs.promises.mkdir(path.dirname(destination), { recursive: true })
82
+ await fs.promises.rename(tmp, destination)
83
+ return destination
84
+ } catch (error) {
85
+ await fs.promises.rm(tmp, { force: true })
86
+ throw error
87
+ }
88
+ }
89
+
90
+ module.exports = { ensureBinary, cachedBinary, isExecutable, VERSION }
package/lib/install.js ADDED
@@ -0,0 +1,20 @@
1
+ 'use strict'
2
+ /**
3
+ * Postinstall: warm the cache so the first `bee-cli` run does not wait for a
4
+ * download. Best effort by design — an offline install must not fail, because
5
+ * the binary is not needed until something actually talks to a board.
6
+ */
7
+
8
+ const { ensureBinary } = require('./ensure')
9
+
10
+ async function main() {
11
+ if (process.env.BEE_CLI_SKIP_DOWNLOAD === '1') return
12
+ try {
13
+ const binary = await ensureBinary({ quiet: true })
14
+ if (binary) process.stdout.write(`bee-cli: ${binary}\n`)
15
+ } catch (error) {
16
+ process.stderr.write(`bee-cli: will fetch the binary on first use (${error.message})\n`)
17
+ }
18
+ }
19
+
20
+ if (require.main === module) main()
package/lib/resolve.js ADDED
@@ -0,0 +1,106 @@
1
+ 'use strict'
2
+ /**
3
+ * Where the real `bee-cli` binary lives for this machine, and how this package
4
+ * finds, verifies and caches it.
5
+ *
6
+ * The npm package carries no binaries of its own: the Rust program is built
7
+ * per platform by the project's release workflow, and this package downloads
8
+ * the one matching the host, checks it against the release's SHA256SUMS and
9
+ * keeps it in the user's cache. Everything here is a pure function so the
10
+ * rules can be tested without a network or a board.
11
+ */
12
+
13
+ const os = require('node:os')
14
+ const path = require('node:path')
15
+
16
+ /** The program's own file name on this platform. */
17
+ function executableName(platform = process.platform) {
18
+ return platform === 'win32' ? 'bee-cli.exe' : 'bee-cli'
19
+ }
20
+
21
+ /** The release asset for one Rust target, as the workflow names it. */
22
+ function assetName(target, platform = process.platform) {
23
+ return `bee-cli-${target}${platform === 'win32' || target.includes('windows') ? '.exe' : ''}`
24
+ }
25
+
26
+ /**
27
+ * The Rust target this machine needs.
28
+ *
29
+ * Only targets the project actually publishes are named; anything else is
30
+ * refused here, with the build instructions, rather than by a 404 halfway
31
+ * through a download.
32
+ */
33
+ function hostTarget(platform = process.platform, arch = process.arch, libc = 'gnu') {
34
+ const table = {
35
+ 'darwin arm64': 'aarch64-apple-darwin',
36
+ 'darwin x64': 'x86_64-apple-darwin',
37
+ 'linux x64': libc === 'musl' ? null : 'x86_64-unknown-linux-gnu',
38
+ 'linux arm64': libc === 'musl' ? null : 'aarch64-unknown-linux-gnu',
39
+ 'linux arm': libc === 'musl' ? null : 'armv7-unknown-linux-gnueabihf',
40
+ 'linux ia32': libc === 'musl' ? null : 'i686-unknown-linux-gnu',
41
+ 'win32 x64': 'x86_64-pc-windows-msvc',
42
+ 'win32 arm64': 'aarch64-pc-windows-msvc',
43
+ 'win32 ia32': 'i686-pc-windows-gnu',
44
+ }
45
+ const key = `${platform} ${arch}`
46
+ if (!(key in table)) return null
47
+ return table[key]
48
+ }
49
+
50
+ /** Whether the running Node was built against musl (Alpine and friends). */
51
+ function detectLibc(report = typeof process.report?.getReport === 'function' ? process.report.getReport() : null) {
52
+ if (!report) return 'gnu'
53
+ const glibc = report.header?.glibcVersionRuntime
54
+ return glibc ? 'gnu' : process.platform === 'linux' ? 'musl' : 'gnu'
55
+ }
56
+
57
+ /** Where a downloaded binary is kept: one directory per version and target. */
58
+ function cachePath(version, target, { env = process.env, platform = process.platform } = {}) {
59
+ const root =
60
+ env.BEE_CLI_CACHE_DIR ||
61
+ (platform === 'win32'
62
+ ? path.join(env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local'), 'bee-cli')
63
+ : path.join(env.XDG_CACHE_HOME || path.join(os.homedir(), '.cache'), 'bee-cli'))
64
+ // The file itself keeps the program's own name: it is what argv[0] shows a
65
+ // caller, and `bee-cli --version` should not read `bee-cli-x86_64-…`.
66
+ return path.join(root, version, target, executableName(platform))
67
+ }
68
+
69
+ /** The release the binary comes from, unless the caller named another one. */
70
+ function releaseBase(version, env = process.env) {
71
+ if (env.BEE_CLI_BASE_URL) return env.BEE_CLI_BASE_URL.replace(/\/+$/, '')
72
+ const repo = env.BEE_CLI_REPO || 'radxa/linkr-bee'
73
+ return `https://github.com/${repo}/releases/download/linkr-tui-v${version}`
74
+ }
75
+
76
+ /**
77
+ * The checksum a release's SHA256SUMS gives for one asset.
78
+ *
79
+ * The file is coreutils' format: `<hex> <name>` per line, and the name may
80
+ * carry a `*` marker for binaries. Absent or unreadable is `null`; the caller
81
+ * decides whether that is fatal, because a mirror may publish the disk image
82
+ * only.
83
+ */
84
+ function checksumFor(sums, asset) {
85
+ if (typeof sums !== 'string') return null
86
+ for (const line of sums.split(/\r?\n/)) {
87
+ const match = /^([0-9a-f]{64})\s+\*?(.+)$/i.exec(line.trim())
88
+ if (match && match[2].trim() === asset) return match[1].toLowerCase()
89
+ }
90
+ return null
91
+ }
92
+
93
+ /** sha256 of a file, streamed: the binary is tens of megabytes. */
94
+ async function sha256File(file) {
95
+ const { createHash } = require('node:crypto')
96
+ const { createReadStream } = require('node:fs')
97
+ return new Promise((resolve, reject) => {
98
+ const hash = createHash('sha256')
99
+ const stream = createReadStream(file)
100
+ stream.on('error', reject)
101
+ stream.on('data', (chunk) => hash.update(chunk))
102
+ stream.on('end', () => resolve(hash.digest('hex')))
103
+ })
104
+ }
105
+
106
+ module.exports = { executableName, assetName, hostTarget, detectLibc, cachePath, releaseBase, checksumFor, sha256File }
package/package.json CHANGED
@@ -1,6 +1,47 @@
1
1
  {
2
2
  "name": "linkr-bee-cli",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.1.2",
4
+ "description": "Linkr Bee wireless serial bridge: the command-line client (bee-cli) for talking to a board's UART console over Bluetooth LE, LAN or loopback.",
5
+ "keywords": [
6
+ "bee-cli",
7
+ "linkr-bee",
8
+ "serial",
9
+ "uart",
10
+ "console",
11
+ "bluetooth",
12
+ "ble",
13
+ "embedded",
14
+ "dsh"
15
+ ],
16
+ "homepage": "https://github.com/radxa/linkr-bee/tree/main/tools/linkr-tui/npm",
17
+ "bugs": {
18
+ "url": "https://github.com/radxa/linkr-bee/issues"
19
+ },
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/radxa/linkr-bee.git",
23
+ "directory": "tools/linkr-tui/npm"
24
+ },
25
+ "license": "Apache-2.0",
26
+ "type": "commonjs",
27
+ "bin": {
28
+ "bee-cli": "bin/bee-cli.js"
29
+ },
30
+ "files": [
31
+ "bin",
32
+ "lib",
33
+ "README.md",
34
+ "LICENSE",
35
+ "THIRD_PARTY_LICENSES.md"
36
+ ],
37
+ "engines": {
38
+ "node": ">=18"
39
+ },
40
+ "scripts": {
41
+ "postinstall": "node ./lib/install.js",
42
+ "test": "node --test ./test/*.test.mjs"
43
+ },
44
+ "publishConfig": {
45
+ "access": "public"
46
+ }
47
+ }