linkbreakers-cli 0.0.0-stage → 1.159.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 CHANGED
@@ -1,3 +1,24 @@
1
- # Temporary Holding Version
1
+ # linkbreakers-cli
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Official command line interface for the [Linkbreakers](https://linkbreakers.com) API, packaged for npm.
4
+
5
+ ```bash
6
+ npm install -g linkbreakers-cli
7
+ linkbreakers version
8
+ ```
9
+
10
+ On install, this package downloads the prebuilt `linkbreakers` binary for your platform (macOS, Linux and Windows on x64 or arm64) from [GitHub Releases](https://github.com/linkbreakers-com/linkbreakers-cli/releases) and verifies its SHA-256 against the release `checksums.txt`. If the download is skipped (for example with `--ignore-scripts`), it happens on first run instead.
11
+
12
+ Looking for the TypeScript SDK? That is the [`linkbreakers`](https://www.npmjs.com/package/linkbreakers) package.
13
+
14
+ ## Quick start
15
+
16
+ ```bash
17
+ export LINKBREAKERS_TOKEN=... # workspace API token from the dashboard
18
+ linkbreakers links create --destination https://example.com --wait-for-qrcode
19
+ linkbreakers raw GET "/v1/visitors?pageSize=50"
20
+ ```
21
+
22
+ Update with `npm install -g linkbreakers-cli@latest`.
23
+
24
+ Full docs: https://linkbreakers.com/cli and https://github.com/linkbreakers-com/linkbreakers-cli
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const { spawnSync } = require('node:child_process');
5
+ const { ensureBinary } = require('../lib/install');
6
+
7
+ ensureBinary()
8
+ .then((bin) => {
9
+ const result = spawnSync(bin, process.argv.slice(2), { stdio: 'inherit', windowsHide: true });
10
+ if (result.error) throw result.error;
11
+ if (result.signal) process.kill(process.pid, result.signal);
12
+ process.exit(result.status ?? 1);
13
+ })
14
+ .catch((err) => {
15
+ console.error(`linkbreakers: ${err.message}`);
16
+ process.exit(1);
17
+ });
package/lib/install.js ADDED
@@ -0,0 +1,129 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('node:crypto');
4
+ const fs = require('node:fs');
5
+ const path = require('node:path');
6
+ const zlib = require('node:zlib');
7
+
8
+ const REPO = 'linkbreakers-com/linkbreakers-cli';
9
+ const VENDOR_DIR = path.join(__dirname, '..', 'vendor');
10
+
11
+ function target(platform = process.platform, arch = process.arch) {
12
+ const os = { darwin: 'darwin', linux: 'linux', win32: 'windows' }[platform];
13
+ const cpu = { x64: 'amd64', arm64: 'arm64' }[arch];
14
+ if (!os || !cpu) {
15
+ throw new Error(`unsupported platform ${platform}/${arch}; download a binary from https://github.com/${REPO}/releases`);
16
+ }
17
+ return { os, cpu, ext: os === 'windows' ? 'zip' : 'tar.gz', exe: os === 'windows' ? 'linkbreakers.exe' : 'linkbreakers' };
18
+ }
19
+
20
+ function binaryPath() {
21
+ return path.join(VENDOR_DIR, target().exe);
22
+ }
23
+
24
+ function packageVersion() {
25
+ return require('../package.json').version;
26
+ }
27
+
28
+ async function download(url) {
29
+ const res = await fetch(url, { headers: { 'User-Agent': 'linkbreakers-cli-npm' } });
30
+ if (!res.ok) throw new Error(`GET ${url} failed: ${res.status} ${res.statusText}`);
31
+ if (new URL(res.url).protocol !== 'https:') throw new Error(`refusing non-HTTPS download from ${res.url}`);
32
+ return Buffer.from(await res.arrayBuffer());
33
+ }
34
+
35
+ function expectedChecksum(checksums, archiveName) {
36
+ for (const line of checksums.split('\n')) {
37
+ const [sum, name] = line.trim().split(/\s+/);
38
+ if (name === archiveName) return sum;
39
+ }
40
+ throw new Error(`${archiveName} is missing from checksums.txt`);
41
+ }
42
+
43
+ function extractFromTarGz(archive, fileName) {
44
+ const tar = zlib.gunzipSync(archive);
45
+ let offset = 0;
46
+ while (offset + 512 <= tar.length) {
47
+ const header = tar.subarray(offset, offset + 512);
48
+ if (header.every((b) => b === 0)) break;
49
+ const name = header.toString('utf8', 0, 100).replace(/\0.*$/s, '');
50
+ const prefix = header.toString('utf8', 345, 500).replace(/\0.*$/s, '');
51
+ const size = parseInt(header.toString('utf8', 124, 136).replace(/\0.*$/s, '').trim() || '0', 8);
52
+ const type = String.fromCharCode(header[156]);
53
+ const fullName = prefix ? `${prefix}/${name}` : name;
54
+ offset += 512;
55
+ if ((type === '0' || type === '\0') && path.posix.basename(fullName) === fileName) {
56
+ return tar.subarray(offset, offset + size);
57
+ }
58
+ offset += Math.ceil(size / 512) * 512;
59
+ }
60
+ throw new Error(`${fileName} not found in archive`);
61
+ }
62
+
63
+ function extractFromZip(archive, fileName) {
64
+ const eocd = archive.lastIndexOf(Buffer.from([0x50, 0x4b, 0x05, 0x06]));
65
+ if (eocd < 0) throw new Error('invalid zip archive');
66
+ const entries = archive.readUInt16LE(eocd + 10);
67
+ let offset = archive.readUInt32LE(eocd + 16);
68
+ for (let i = 0; i < entries; i++) {
69
+ if (archive.readUInt32LE(offset) !== 0x02014b50) throw new Error('invalid zip central directory');
70
+ const method = archive.readUInt16LE(offset + 10);
71
+ const compressedSize = archive.readUInt32LE(offset + 20);
72
+ const nameLength = archive.readUInt16LE(offset + 28);
73
+ const extraLength = archive.readUInt16LE(offset + 30);
74
+ const commentLength = archive.readUInt16LE(offset + 32);
75
+ const localOffset = archive.readUInt32LE(offset + 42);
76
+ const name = archive.toString('utf8', offset + 46, offset + 46 + nameLength);
77
+ offset += 46 + nameLength + extraLength + commentLength;
78
+ if (path.posix.basename(name) !== fileName) continue;
79
+
80
+ const dataStart = localOffset + 30 + archive.readUInt16LE(localOffset + 26) + archive.readUInt16LE(localOffset + 28);
81
+ const data = archive.subarray(dataStart, dataStart + compressedSize);
82
+ if (method === 0) return data;
83
+ if (method === 8) return zlib.inflateRawSync(data);
84
+ throw new Error(`unsupported zip compression method ${method}`);
85
+ }
86
+ throw new Error(`${fileName} not found in archive`);
87
+ }
88
+
89
+ async function install() {
90
+ const version = packageVersion();
91
+ if (!version || version === '0.0.0') {
92
+ throw new Error('this package has no release version; install linkbreakers-cli from npm');
93
+ }
94
+ const t = target();
95
+ const archiveName = `linkbreakers-cli_${version}_${t.os}_${t.cpu}.${t.ext}`;
96
+ const base = `https://github.com/${REPO}/releases/download/v${version}`;
97
+
98
+ const [archive, checksums] = await Promise.all([
99
+ download(`${base}/${archiveName}`),
100
+ download(`${base}/checksums.txt`).then((b) => b.toString('utf8')),
101
+ ]);
102
+
103
+ const actual = crypto.createHash('sha256').update(archive).digest('hex');
104
+ const expected = expectedChecksum(checksums, archiveName);
105
+ if (actual !== expected) {
106
+ throw new Error(`checksum mismatch for ${archiveName}: expected ${expected}, got ${actual}`);
107
+ }
108
+
109
+ const binary = t.ext === 'zip' ? extractFromZip(archive, t.exe) : extractFromTarGz(archive, t.exe);
110
+ fs.mkdirSync(VENDOR_DIR, { recursive: true });
111
+ const dest = path.join(VENDOR_DIR, t.exe);
112
+ const tmp = `${dest}.${process.pid}.tmp`;
113
+ try {
114
+ fs.writeFileSync(tmp, binary, { mode: 0o755 });
115
+ fs.renameSync(tmp, dest);
116
+ } catch (err) {
117
+ fs.rmSync(tmp, { force: true });
118
+ throw err;
119
+ }
120
+ return dest;
121
+ }
122
+
123
+ async function ensureBinary() {
124
+ const bin = binaryPath();
125
+ if (fs.existsSync(bin)) return bin;
126
+ return install();
127
+ }
128
+
129
+ module.exports = { ensureBinary, install, target, expectedChecksum, extractFromTarGz, extractFromZip };
@@ -0,0 +1,8 @@
1
+ 'use strict';
2
+
3
+ const { install } = require('./install');
4
+
5
+ install().catch((err) => {
6
+ console.warn(`linkbreakers-cli: could not download the linkbreakers binary during install (${err.message}).`);
7
+ console.warn('linkbreakers-cli: it will be downloaded on first run instead.');
8
+ });
package/package.json CHANGED
@@ -1,6 +1,46 @@
1
1
  {
2
2
  "name": "linkbreakers-cli",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.159.0",
4
+ "description": "Official CLI for the Linkbreakers API. Installs the prebuilt linkbreakers binary for your platform.",
5
+ "keywords": [
6
+ "linkbreakers",
7
+ "cli",
8
+ "qr-code",
9
+ "lead-capture",
10
+ "links"
11
+ ],
12
+ "homepage": "https://linkbreakers.com/cli",
13
+ "bugs": "https://github.com/linkbreakers-com/linkbreakers-cli/issues",
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/linkbreakers-com/linkbreakers-cli.git",
17
+ "directory": "npm"
18
+ },
19
+ "license": "MIT",
20
+ "author": "Linkbreakers <support@linkbreakers.com>",
21
+ "bin": {
22
+ "linkbreakers": "bin/linkbreakers.js"
23
+ },
24
+ "files": [
25
+ "bin/",
26
+ "lib/install.js",
27
+ "lib/postinstall.js",
28
+ "README.md"
29
+ ],
30
+ "scripts": {
31
+ "postinstall": "node lib/postinstall.js",
32
+ "test": "node --test lib/install.test.js"
33
+ },
34
+ "engines": {
35
+ "node": ">=18"
36
+ },
37
+ "os": [
38
+ "darwin",
39
+ "linux",
40
+ "win32"
41
+ ],
42
+ "cpu": [
43
+ "x64",
44
+ "arm64"
45
+ ]
46
+ }