linkr-bee-cli 0.0.0-stage → 0.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/LICENSE +202 -0
- package/README.md +66 -2
- package/THIRD_PARTY_LICENSES.md +11359 -0
- package/bin/bee-cli.js +42 -0
- package/lib/download.js +73 -0
- package/lib/ensure.js +90 -0
- package/lib/install.js +20 -0
- package/lib/resolve.js +106 -0
- package/package.json +45 -4
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()
|
package/lib/download.js
ADDED
|
@@ -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.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.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
|
+
}
|